root/daemons/execd/cts-exec-helper.c

/* [previous][next][first][last][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. interval_cb
  2. notify_cb
  3. param_key_val_cb
  4. test_exit
  5. test_shutdown
  6. read_events
  7. timeout_err
  8. connection_events
  9. try_connect
  10. start_test
  11. generate_params
  12. build_arg_context
  13. main

   1 /*
   2  * Copyright 2012-2023 the Pacemaker project contributors
   3  *
   4  * The version control history for this file may have further details.
   5  *
   6  * This source code is licensed under the GNU Lesser General Public License
   7  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
   8  */
   9 
  10 #include <crm_internal.h>
  11 
  12 #include <glib.h>
  13 #include <unistd.h>
  14 
  15 #include <crm/crm.h>
  16 #include <crm/services.h>
  17 #include <crm/common/cmdline_internal.h>
  18 #include <crm/common/mainloop.h>
  19 
  20 #include <crm/pengine/status.h>
  21 #include <crm/pengine/internal.h>
  22 #include <crm/cib.h>
  23 #include <crm/cib/internal.h>
  24 #include <crm/lrmd.h>
  25 
  26 #define SUMMARY "cts-exec-helper - inject commands into the Pacemaker executor and watch for events"
  27 
  28 static int exec_call_id = 0;
  29 static gboolean start_test(gpointer user_data);
  30 static void try_connect(void);
  31 
  32 static char *key = NULL;
  33 static char *val = NULL;
  34 
  35 static struct {
  36     int verbose;
  37     int quiet;
  38     guint interval_ms;
  39     int timeout;
  40     int start_delay;
  41     int cancel_call_id;
  42     gboolean no_wait;
  43     gboolean is_running;
  44     gboolean no_connect;
  45     int exec_call_opts;
  46     const char *api_call;
  47     const char *rsc_id;
  48     const char *provider;
  49     const char *class;
  50     const char *type;
  51     const char *action;
  52     const char *listen;
  53     gboolean use_tls;
  54     lrmd_key_value_t *params;
  55 } options;
  56 
  57 static gboolean
  58 interval_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     /* [previous][next][first][last][top][bottom][index][help] */
  59     options.interval_ms = crm_parse_interval_spec(optarg);
  60     return errno == 0;
  61 }
  62 
  63 static gboolean
  64 notify_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     /* [previous][next][first][last][top][bottom][index][help] */
  65     if (pcmk__str_any_of(option_name, "--notify-orig", "-n", NULL)) {
  66         options.exec_call_opts = lrmd_opt_notify_orig_only;
  67     } else if (pcmk__str_any_of(option_name, "--notify-changes", "-o", NULL)) {
  68         options.exec_call_opts = lrmd_opt_notify_changes_only;
  69     }
  70 
  71     return TRUE;
  72 }
  73 
  74 static gboolean
  75 param_key_val_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
     /* [previous][next][first][last][top][bottom][index][help] */
  76     if (pcmk__str_any_of(option_name, "--param-key", "-k", NULL)) {
  77         pcmk__str_update(&key, optarg);
  78     } else if (pcmk__str_any_of(option_name, "--param-val", "-v", NULL)) {
  79         pcmk__str_update(&val, optarg);
  80     }
  81 
  82     if (key != NULL && val != NULL) {
  83         options.params = lrmd_key_value_add(options.params, key, val);
  84         pcmk__str_update(&key, NULL);
  85         pcmk__str_update(&val, NULL);
  86     }
  87 
  88     return TRUE;
  89 }
  90 
  91 static GOptionEntry basic_entries[] = {
  92     { "api-call", 'c', 0, G_OPTION_ARG_STRING, &options.api_call,
  93       "Directly relates to executor API functions",
  94       NULL },
  95 
  96     { "is-running", 'R', 0, G_OPTION_ARG_NONE, &options.is_running,
  97       "Determine if a resource is registered and running",
  98       NULL },
  99 
 100     { "listen", 'l', 0, G_OPTION_ARG_STRING, &options.listen,
 101       "Listen for a specific event string",
 102       NULL },
 103 
 104     { "no-wait", 'w', 0, G_OPTION_ARG_NONE, &options.no_wait,
 105       "Make api call and do not wait for result",
 106       NULL },
 107 
 108     { "notify-changes", 'o', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, notify_cb,
 109       "Only notify client changes to recurring operations",
 110       NULL },
 111 
 112     { "notify-orig", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, notify_cb,
 113       "Only notify this client of the results of an API action",
 114       NULL },
 115 
 116     { "tls", 'S', 0, G_OPTION_ARG_NONE, &options.use_tls,
 117       "Use TLS backend for local connection",
 118       NULL },
 119 
 120     { NULL }
 121 };
 122 
 123 static GOptionEntry api_call_entries[] = {
 124     { "action", 'a', 0, G_OPTION_ARG_STRING, &options.action,
 125       NULL, NULL },
 126 
 127     { "cancel-call-id", 'x', 0, G_OPTION_ARG_INT, &options.cancel_call_id,
 128       NULL, NULL },
 129 
 130     { "class", 'C', 0, G_OPTION_ARG_STRING, &options.class,
 131       NULL, NULL },
 132 
 133     { "interval", 'i', 0, G_OPTION_ARG_CALLBACK, interval_cb,
 134       NULL, NULL },
 135 
 136     { "param-key", 'k', 0, G_OPTION_ARG_CALLBACK, param_key_val_cb,
 137       NULL, NULL },
 138 
 139     { "param-val", 'v', 0, G_OPTION_ARG_CALLBACK, param_key_val_cb,
 140       NULL, NULL },
 141 
 142     { "provider", 'P', 0, G_OPTION_ARG_STRING, &options.provider,
 143       NULL, NULL },
 144 
 145     { "rsc-id", 'r', 0, G_OPTION_ARG_STRING, &options.rsc_id,
 146       NULL, NULL },
 147 
 148     { "start-delay", 's', 0, G_OPTION_ARG_INT, &options.start_delay,
 149       NULL, NULL },
 150 
 151     { "timeout", 't', 0, G_OPTION_ARG_INT, &options.timeout,
 152       NULL, NULL },
 153 
 154     { "type", 'T', 0, G_OPTION_ARG_STRING, &options.type,
 155       NULL, NULL },
 156 
 157     { NULL }
 158 };
 159 
 160 static GMainLoop *mainloop = NULL;
 161 static lrmd_t *lrmd_conn = NULL;
 162 
 163 static char event_buf_v0[1024];
 164 
 165 static crm_exit_t
 166 test_exit(crm_exit_t exit_code)
     /* [previous][next][first][last][top][bottom][index][help] */
 167 {
 168     lrmd_api_delete(lrmd_conn);
 169     return crm_exit(exit_code);
 170 }
 171 
 172 #define print_result(fmt, args...)  \
 173     if (!options.quiet) {           \
 174         printf(fmt "\n" , ##args);  \
 175     }
 176 
 177 #define report_event(event)                                             \
 178     snprintf(event_buf_v0, sizeof(event_buf_v0), "NEW_EVENT event_type:%s rsc_id:%s action:%s rc:%s op_status:%s", \
 179              lrmd_event_type2str(event->type),                          \
 180              event->rsc_id,                                             \
 181              event->op_type ? event->op_type : "none",                  \
 182              services_ocf_exitcode_str(event->rc),                      \
 183              pcmk_exec_status_str(event->op_status));                   \
 184     crm_info("%s", event_buf_v0);
 185 
 186 static void
 187 test_shutdown(int nsig)
     /* [previous][next][first][last][top][bottom][index][help] */
 188 {
 189     lrmd_api_delete(lrmd_conn);
 190     lrmd_conn = NULL;
 191 }
 192 
 193 static void
 194 read_events(lrmd_event_data_t * event)
     /* [previous][next][first][last][top][bottom][index][help] */
 195 {
 196     report_event(event);
 197     if (options.listen) {
 198         if (pcmk__str_eq(options.listen, event_buf_v0, pcmk__str_casei)) {
 199             print_result("LISTEN EVENT SUCCESSFUL");
 200             test_exit(CRM_EX_OK);
 201         }
 202     }
 203 
 204     if (exec_call_id && (event->call_id == exec_call_id)) {
 205         if (event->op_status == 0 && event->rc == 0) {
 206             print_result("API-CALL SUCCESSFUL for 'exec'");
 207         } else {
 208             print_result("API-CALL FAILURE for 'exec', rc:%d lrmd_op_status:%s",
 209                          event->rc, pcmk_exec_status_str(event->op_status));
 210             test_exit(CRM_EX_ERROR);
 211         }
 212 
 213         if (!options.listen) {
 214             test_exit(CRM_EX_OK);
 215         }
 216     }
 217 }
 218 
 219 static gboolean
 220 timeout_err(gpointer data)
     /* [previous][next][first][last][top][bottom][index][help] */
 221 {
 222     print_result("LISTEN EVENT FAILURE - timeout occurred, never found");
 223     test_exit(CRM_EX_TIMEOUT);
 224     return FALSE;
 225 }
 226 
 227 static void
 228 connection_events(lrmd_event_data_t * event)
     /* [previous][next][first][last][top][bottom][index][help] */
 229 {
 230     int rc = event->connection_rc;
 231 
 232     if (event->type != lrmd_event_connect) {
 233         /* ignore */
 234         return;
 235     }
 236 
 237     if (!rc) {
 238         crm_info("Executor client connection established");
 239         start_test(NULL);
 240         return;
 241     } else {
 242         sleep(1);
 243         try_connect();
 244         crm_notice("Executor client connection failed");
 245     }
 246 }
 247 
 248 static void
 249 try_connect(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 250 {
 251     int tries = 10;
 252     static int num_tries = 0;
 253     int rc = 0;
 254 
 255     lrmd_conn->cmds->set_callback(lrmd_conn, connection_events);
 256     for (; num_tries < tries; num_tries++) {
 257         rc = lrmd_conn->cmds->connect_async(lrmd_conn, crm_system_name, 3000);
 258 
 259         if (!rc) {
 260             return;             /* we'll hear back in async callback */
 261         }
 262         sleep(1);
 263     }
 264 
 265     print_result("API CONNECTION FAILURE");
 266     test_exit(CRM_EX_ERROR);
 267 }
 268 
 269 static gboolean
 270 start_test(gpointer user_data)
     /* [previous][next][first][last][top][bottom][index][help] */
 271 {
 272     int rc = 0;
 273 
 274     if (!options.no_connect) {
 275         if (!lrmd_conn->cmds->is_connected(lrmd_conn)) {
 276             try_connect();
 277             /* async connect -- this function will get called back into */
 278             return 0;
 279         }
 280     }
 281     lrmd_conn->cmds->set_callback(lrmd_conn, read_events);
 282 
 283     if (options.timeout) {
 284         g_timeout_add(options.timeout, timeout_err, NULL);
 285     }
 286 
 287     if (!options.api_call) {
 288         return 0;
 289     }
 290 
 291     if (pcmk__str_eq(options.api_call, "exec", pcmk__str_casei)) {
 292         rc = lrmd_conn->cmds->exec(lrmd_conn,
 293                                    options.rsc_id,
 294                                    options.action,
 295                                    NULL,
 296                                    options.interval_ms,
 297                                    options.timeout,
 298                                    options.start_delay,
 299                                    options.exec_call_opts,
 300                                    options.params);
 301 
 302         if (rc > 0) {
 303             exec_call_id = rc;
 304             print_result("API-CALL 'exec' action pending, waiting on response");
 305         }
 306 
 307     } else if (pcmk__str_eq(options.api_call, "register_rsc", pcmk__str_casei)) {
 308         rc = lrmd_conn->cmds->register_rsc(lrmd_conn,
 309                                            options.rsc_id,
 310                                            options.class, options.provider, options.type, 0);
 311     } else if (pcmk__str_eq(options.api_call, "get_rsc_info", pcmk__str_casei)) {
 312         lrmd_rsc_info_t *rsc_info;
 313 
 314         rsc_info = lrmd_conn->cmds->get_rsc_info(lrmd_conn, options.rsc_id, 0);
 315 
 316         if (rsc_info) {
 317             print_result("RSC_INFO: id:%s class:%s provider:%s type:%s",
 318                          rsc_info->id, rsc_info->standard,
 319                          (rsc_info->provider? rsc_info->provider : "<none>"),
 320                          rsc_info->type);
 321             lrmd_free_rsc_info(rsc_info);
 322             rc = pcmk_ok;
 323         } else {
 324             rc = -1;
 325         }
 326     } else if (pcmk__str_eq(options.api_call, "unregister_rsc", pcmk__str_casei)) {
 327         rc = lrmd_conn->cmds->unregister_rsc(lrmd_conn, options.rsc_id, 0);
 328     } else if (pcmk__str_eq(options.api_call, "cancel", pcmk__str_casei)) {
 329         rc = lrmd_conn->cmds->cancel(lrmd_conn, options.rsc_id, options.action,
 330                                      options.interval_ms);
 331     } else if (pcmk__str_eq(options.api_call, "metadata", pcmk__str_casei)) {
 332         char *output = NULL;
 333 
 334         rc = lrmd_conn->cmds->get_metadata(lrmd_conn,
 335                                            options.class,
 336                                            options.provider, options.type, &output, 0);
 337         if (rc == pcmk_ok) {
 338             print_result("%s", output);
 339             free(output);
 340         }
 341     } else if (pcmk__str_eq(options.api_call, "list_agents", pcmk__str_casei)) {
 342         lrmd_list_t *list = NULL;
 343         lrmd_list_t *iter = NULL;
 344 
 345         rc = lrmd_conn->cmds->list_agents(lrmd_conn, &list, options.class, options.provider);
 346 
 347         if (rc > 0) {
 348             print_result("%d agents found", rc);
 349             for (iter = list; iter != NULL; iter = iter->next) {
 350                 print_result("%s", iter->val);
 351             }
 352             lrmd_list_freeall(list);
 353             rc = 0;
 354         } else {
 355             print_result("API_CALL FAILURE - no agents found");
 356             rc = -1;
 357         }
 358     } else if (pcmk__str_eq(options.api_call, "list_ocf_providers", pcmk__str_casei)) {
 359         lrmd_list_t *list = NULL;
 360         lrmd_list_t *iter = NULL;
 361 
 362         rc = lrmd_conn->cmds->list_ocf_providers(lrmd_conn, options.type, &list);
 363 
 364         if (rc > 0) {
 365             print_result("%d providers found", rc);
 366             for (iter = list; iter != NULL; iter = iter->next) {
 367                 print_result("%s", iter->val);
 368             }
 369             lrmd_list_freeall(list);
 370             rc = 0;
 371         } else {
 372             print_result("API_CALL FAILURE - no providers found");
 373             rc = -1;
 374         }
 375 
 376     } else if (pcmk__str_eq(options.api_call, "list_standards", pcmk__str_casei)) {
 377         lrmd_list_t *list = NULL;
 378         lrmd_list_t *iter = NULL;
 379 
 380         rc = lrmd_conn->cmds->list_standards(lrmd_conn, &list);
 381 
 382         if (rc > 0) {
 383             print_result("%d standards found", rc);
 384             for (iter = list; iter != NULL; iter = iter->next) {
 385                 print_result("%s", iter->val);
 386             }
 387             lrmd_list_freeall(list);
 388             rc = 0;
 389         } else {
 390             print_result("API_CALL FAILURE - no providers found");
 391             rc = -1;
 392         }
 393 
 394     } else if (pcmk__str_eq(options.api_call, "get_recurring_ops", pcmk__str_casei)) {
 395         GList *op_list = NULL;
 396         GList *op_item = NULL;
 397         rc = lrmd_conn->cmds->get_recurring_ops(lrmd_conn, options.rsc_id, 0, 0,
 398                                                 &op_list);
 399 
 400         for (op_item = op_list; op_item != NULL; op_item = op_item->next) {
 401             lrmd_op_info_t *op_info = op_item->data;
 402 
 403             print_result("RECURRING_OP: %s_%s_%s timeout=%sms",
 404                          op_info->rsc_id, op_info->action,
 405                          op_info->interval_ms_s, op_info->timeout_ms_s);
 406             lrmd_free_op_info(op_info);
 407         }
 408         g_list_free(op_list);
 409 
 410     } else if (options.api_call) {
 411         print_result("API-CALL FAILURE unknown action '%s'", options.action);
 412         test_exit(CRM_EX_ERROR);
 413     }
 414 
 415     if (rc < 0) {
 416         print_result("API-CALL FAILURE for '%s' api_rc:%d",
 417                      options.api_call, rc);
 418         test_exit(CRM_EX_ERROR);
 419     }
 420 
 421     if (options.api_call && rc == pcmk_ok) {
 422         print_result("API-CALL SUCCESSFUL for '%s'", options.api_call);
 423         if (!options.listen) {
 424             test_exit(CRM_EX_OK);
 425         }
 426     }
 427 
 428     if (options.no_wait) {
 429         /* just make the call and exit regardless of anything else. */
 430         test_exit(CRM_EX_OK);
 431     }
 432 
 433     return 0;
 434 }
 435 
 436 /*!
 437  * \internal
 438  * \brief Generate resource parameters from CIB if none explicitly given
 439  *
 440  * \return Standard Pacemaker return code
 441  */
 442 static int
 443 generate_params(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 444 {
 445     int rc = pcmk_rc_ok;
 446     pe_working_set_t *data_set = NULL;
 447     xmlNode *cib_xml_copy = NULL;
 448     pe_resource_t *rsc = NULL;
 449     GHashTable *params = NULL;
 450     GHashTable *meta = NULL;
 451     GHashTableIter iter;
 452     char *key = NULL;
 453     char *value = NULL;
 454 
 455     if (options.params != NULL) {
 456         return pcmk_rc_ok; // User specified parameters explicitly
 457     }
 458 
 459     // Retrieve and update CIB
 460     rc = cib__signon_query(NULL, NULL, &cib_xml_copy);
 461     if (rc != pcmk_rc_ok) {
 462         return rc;
 463     }
 464     if (!cli_config_update(&cib_xml_copy, NULL, FALSE)) {
 465         crm_err("Could not update CIB");
 466         return pcmk_rc_cib_corrupt;
 467     }
 468 
 469     // Calculate cluster status
 470     data_set = pe_new_working_set();
 471     if (data_set == NULL) {
 472         crm_crit("Could not allocate working set");
 473         return ENOMEM;
 474     }
 475     pe__set_working_set_flags(data_set, pe_flag_no_counts|pe_flag_no_compat);
 476     data_set->input = cib_xml_copy;
 477     data_set->now = crm_time_new(NULL);
 478     cluster_status(data_set);
 479 
 480     // Find resource in CIB
 481     rsc = pe_find_resource_with_flags(data_set->resources, options.rsc_id,
 482                                       pe_find_renamed|pe_find_any);
 483     if (rsc == NULL) {
 484         crm_err("Resource does not exist in config");
 485         pe_free_working_set(data_set);
 486         return EINVAL;
 487     }
 488 
 489     // Add resource instance parameters to options.params
 490     params = pe_rsc_params(rsc, NULL, data_set);
 491     if (params != NULL) {
 492         g_hash_table_iter_init(&iter, params);
 493         while (g_hash_table_iter_next(&iter, (gpointer *) &key,
 494                                       (gpointer *) &value)) {
 495             options.params = lrmd_key_value_add(options.params, key, value);
 496         }
 497     }
 498 
 499     // Add resource meta-attributes to options.params
 500     meta = pcmk__strkey_table(free, free);
 501     get_meta_attributes(meta, rsc, NULL, data_set);
 502     g_hash_table_iter_init(&iter, meta);
 503     while (g_hash_table_iter_next(&iter, (gpointer *) &key,
 504                                   (gpointer *) &value)) {
 505         char *crm_name = crm_meta_name(key);
 506 
 507         options.params = lrmd_key_value_add(options.params, crm_name, value);
 508         free(crm_name);
 509     }
 510     g_hash_table_destroy(meta);
 511 
 512     pe_free_working_set(data_set);
 513     return rc;
 514 }
 515 
 516 static GOptionContext *
 517 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
     /* [previous][next][first][last][top][bottom][index][help] */
 518     GOptionContext *context = NULL;
 519 
 520     context = pcmk__build_arg_context(args, NULL, group, NULL);
 521 
 522     pcmk__add_main_args(context, basic_entries);
 523     pcmk__add_arg_group(context, "api-call", "API Call Options:",
 524                         "Parameters for api-call option", api_call_entries);
 525 
 526     return context;
 527 }
 528 
 529 int
 530 main(int argc, char **argv)
     /* [previous][next][first][last][top][bottom][index][help] */
 531 {
 532     GError *error = NULL;
 533     crm_exit_t exit_code = CRM_EX_OK;
 534     crm_trigger_t *trig = NULL;
 535 
 536     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
 537     /* Typically we'd pass all the single character options that take an argument
 538      * as the second parameter here (and there's a bunch of those in this tool).
 539      * However, we control how this program is called so we can just not call it
 540      * in a way where the preprocessing ever matters.
 541      */
 542     gchar **processed_args = pcmk__cmdline_preproc(argv, NULL);
 543     GOptionContext *context = build_arg_context(args, NULL);
 544 
 545     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
 546         exit_code = CRM_EX_USAGE;
 547         goto done;
 548     }
 549 
 550     /* We have to use crm_log_init here to set up the logging because there's
 551      * different handling for daemons vs. command line programs, and
 552      * pcmk__cli_init_logging is set up to only handle the latter.
 553      */
 554     crm_log_init(NULL, LOG_INFO, TRUE, (args->verbosity? TRUE : FALSE), argc,
 555                  argv, FALSE);
 556 
 557     for (int i = 0; i < args->verbosity; i++) {
 558         crm_bump_log_level(argc, argv);
 559     }
 560 
 561     if (!options.listen && pcmk__strcase_any_of(options.api_call, "metadata", "list_agents",
 562                                                 "list_standards", "list_ocf_providers", NULL)) {
 563         options.no_connect = TRUE;
 564     }
 565 
 566     if (options.is_running) {
 567         int rc = pcmk_rc_ok;
 568 
 569         if (options.rsc_id == NULL) {
 570             exit_code = CRM_EX_USAGE;
 571             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
 572                         "--is-running requires --rsc-id");
 573             goto done;
 574         }
 575 
 576         options.interval_ms = 0;
 577         if (options.timeout == 0) {
 578             options.timeout = 30000;
 579         }
 580 
 581         rc = generate_params();
 582         if (rc != pcmk_rc_ok) {
 583             exit_code = pcmk_rc2exitc(rc);
 584             g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
 585                         "Can not determine resource status: "
 586                         "unable to get parameters from CIB");
 587             goto done;
 588         }
 589         options.api_call = "exec";
 590         options.action = "monitor";
 591         options.exec_call_opts = lrmd_opt_notify_orig_only;
 592     }
 593 
 594     if (!options.api_call && !options.listen) {
 595         exit_code = CRM_EX_USAGE;
 596         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
 597                     "Must specify at least one of --api-call, --listen, "
 598                     "or --is-running");
 599         goto done;
 600     }
 601 
 602     if (options.use_tls) {
 603         lrmd_conn = lrmd_remote_api_new(NULL, "localhost", 0);
 604     } else {
 605         lrmd_conn = lrmd_api_new();
 606     }
 607     trig = mainloop_add_trigger(G_PRIORITY_HIGH, start_test, NULL);
 608     mainloop_set_trigger(trig);
 609     mainloop_add_signal(SIGTERM, test_shutdown);
 610 
 611     crm_info("Starting");
 612     mainloop = g_main_loop_new(NULL, FALSE);
 613     g_main_loop_run(mainloop);
 614 
 615 done:
 616     g_strfreev(processed_args);
 617     pcmk__free_arg_context(context);
 618 
 619     free(key);
 620     free(val);
 621 
 622     pcmk__output_and_clear_error(&error, NULL);
 623     return test_exit(exit_code);
 624 }

/* [previous][next][first][last][top][bottom][index][help] */