/* Copyright (c) 2001-2004, Roger Dingledine. * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. * Copyright (c) 2007-2018, The Tor Project, Inc. */ /* See LICENSE for licensing information */ #include "orconfig.h" #define UTIL_PRIVATE #include "common/util.h" #include "common/util_process.h" #include "common/crypto.h" #include "common/torlog.h" #include "test/test.h" #ifndef BUILDDIR #define BUILDDIR "." #endif #ifdef _WIN32 #define notify_pending_waitpid_callbacks() STMT_NIL #define TEST_CHILD "test-child.exe" #define EOL "\r\n" #else #define TEST_CHILD (BUILDDIR "/src/test/test-child") #define EOL "\n" #endif /* defined(_WIN32) */ #ifdef _WIN32 /* I've assumed Windows doesn't have the gap between fork and exec * that causes the race condition on unix-like platforms */ #define MATCH_PROCESS_STATUS(s1,s2) ((s1) == (s2)) #else /* !(defined(_WIN32)) */ /* work around a race condition of the timing of SIGCHLD handler updates * to the process_handle's fields, and checks of those fields * * TODO: Once we can signal failure to exec, change PROCESS_STATUS_RUNNING to * PROCESS_STATUS_ERROR (and similarly with *_OR_NOTRUNNING) */ #define PROCESS_STATUS_RUNNING_OR_NOTRUNNING (PROCESS_STATUS_RUNNING+1) #define IS_RUNNING_OR_NOTRUNNING(s) \ ((s) == PROCESS_STATUS_RUNNING || (s) == PROCESS_STATUS_NOTRUNNING) /* well, this is ugly */ #define MATCH_PROCESS_STATUS(s1,s2) \ ( (s1) == (s2) \ ||((s1) == PROCESS_STATUS_RUNNING_OR_NOTRUNNING \ && IS_RUNNING_OR_NOTRUNNING(s2)) \ ||((s2) == PROCESS_STATUS_RUNNING_OR_NOTRUNNING \ && IS_RUNNING_OR_NOTRUNNING(s1))) #endif /* defined(_WIN32) */ /** Helper function for testing tor_spawn_background */ static void run_util_spawn_background(const char *argv[], const char *expected_out, const char *expected_err, int expected_exit, int expected_status) { int retval, exit_code; ssize_t pos; process_handle_t *process_handle=NULL; char stdout_buf[100], stderr_buf[100]; int status; /* Start the program */ #ifdef _WIN32 status = tor_spawn_background(NULL, argv, NULL, &process_handle); #else status = tor_spawn_background(argv[0], argv, NULL, &process_handle); #endif notify_pending_waitpid_callbacks(); /* the race condition doesn't affect status, * because status isn't updated by the SIGCHLD handler, * but we still need to handle PROCESS_STATUS_RUNNING_OR_NOTRUNNING */ tt_assert(MATCH_PROCESS_STATUS(expected_status, status)); if (status == PROCESS_STATUS_ERROR) { tt_ptr_op(process_handle, OP_EQ, NULL); return; } tt_ptr_op(process_handle, OP_NE, NULL); /* When a spawned process forks, fails, then exits very quickly, * (this typically occurs when exec fails) * there is a race condition between the SIGCHLD handler * updating the process_handle's fields, and this test * checking the process status in those fields. * The SIGCHLD update can occur before or after the code below executes. * This causes intermittent failures in spawn_background_fail(), * typically when the machine is under load. * We use PROCESS_STATUS_RUNNING_OR_NOTRUNNING to avoid this issue. */ /* the race condition affects the change in * process_handle->status from RUNNING to NOTRUNNING */ tt_assert(MATCH_PROCESS_STATUS(expected_status, process_handle->status)); #ifndef _WIN32 notify_pending_waitpid_callbacks(); /* the race condition affects the change in * process_handle->waitpid_cb to NULL, * so we skip the check if expected_status is ambiguous, * that is, PROCESS_STATUS_RUNNING_OR_NOTRUNNING */ tt_assert(process_handle->waitpid_cb != NULL || expected_status == PROCESS_STATUS_RUNNING_OR_NOTRUNNING); #endif /* !defined(_WIN32) */ #ifdef _WIN32 tt_assert(process_handle->stdout_pipe != INVALID_HANDLE_VALUE); tt_assert(process_handle->stderr_pipe != INVALID_HANDLE_VALUE); tt_assert(process_handle->stdin_pipe != INVALID_HANDLE_VALUE); #else tt_assert(process_handle->stdout_pipe >= 0); tt_assert(process_handle->stderr_pipe >= 0); tt_assert(process_handle->stdin_pipe >= 0); #endif /* defined(_WIN32) */ /* Check stdout */ pos = tor_read_all_from_process_stdout(process_handle, stdout_buf, sizeof(stdout_buf) - 1); tt_assert(pos >= 0); stdout_buf[pos] = '\0'; tt_int_op(strlen(expected_out),OP_EQ, pos); tt_str_op(expected_out,OP_EQ, stdout_buf); notify_pending_waitpid_callbacks(); /* Check it terminated correctly */ retval = tor_get_exit_code(process_handle, 1, &exit_code); tt_int_op(PROCESS_EXIT_EXITED,OP_EQ, retval); tt_int_op(expected_exit,OP_EQ, exit_code); // TODO: Make test-child exit with something other than 0 #ifndef _WIN32 notify_pending_waitpid_callbacks(); tt_ptr_op(process_handle->waitpid_cb, OP_EQ, NULL); #endif /* Check stderr */ pos = tor_read_all_from_process_stderr(process_handle, stderr_buf, sizeof(stderr_buf) - 1); tt_assert(pos >= 0); stderr_buf[pos] = '\0'; tt_str_op(expected_err,OP_EQ, stderr_buf); tt_int_op(strlen(expected_err),OP_EQ, pos); notify_pending_waitpid_callbacks(); done: if (process_handle) tor_process_handle_destroy(process_handle, 1); } /** Check that we can launch a process and read the output */ static void test_util_spawn_background_ok(void *ptr) { const char *argv[] = {TEST_CHILD, "--test", NULL}; const char *expected_out = "OUT"EOL "--test"EOL "SLEEPING"EOL "DONE" EOL; const char *expected_err = "ERR"EOL; (void)ptr; run_util_spawn_background(argv, expected_out, expected_err, 0, PROCESS_STATUS_RUNNING); } /** Check that failing to find the executable works as expected */ static void test_util_spawn_background_fail(void *ptr) { const char *argv[] = {BUILDDIR "/src/test/no-such-file", "--test", NULL}; const char *expected_err = ""; char expected_out[1024]; char code[32]; #ifdef _WIN32 const int expected_status = PROCESS_STATUS_ERROR; #else /* TODO: Once we can signal failure to exec, set this to be * PROCESS_STATUS_RUNNING_OR_ERROR */ const int expected_status = PROCESS_STATUS_RUNNING_OR_NOTRUNNING; #endif /* defined(_WIN32) */ memset(expected_out, 0xf0, sizeof(expected_out)); memset(code, 0xf0, sizeof(code)); (void)ptr; tor_snprintf(code, sizeof(code), "%x/%x", 9 /* CHILD_STATE_FAILEXEC */ , ENOENT); tor_snprintf(expected_out, sizeof(expected_out), "ERR: Failed to spawn background process - code %s\n", code); run_util_spawn_background(argv, expected_out, expected_err, 255, expected_status); } /** Test that reading from a handle returns a partial read rather than * blocking */ static void test_util_spawn_background_partial_read_impl(int exit_early) { const int expected_exit = 0; const int expected_status = PROCESS_STATUS_RUNNING; int retval, exit_code; ssize_t pos = -1; process_handle_t *process_handle=NULL; int status; char stdout_buf[100], stderr_buf[100]; const char *argv[] = {TEST_CHILD, "--test", NULL}; const char *expected_out[] = { "OUT" EOL "--test" EOL "SLEEPING" EOL, "DONE" EOL, NULL }; const char *expected_err = "ERR" EOL; #ifndef _WIN32 int eof = 0; #endif int expected_out_ctr; if (exit_early) { argv[1] = "--hang"; expected_out[0] = "OUT"EOL "--hang"EOL "SLEEPING" EOL; } /* Start the program */ #ifdef _WIN32 status = tor_spawn_background(NULL, argv, NULL, &process_handle); #else status = tor_spawn_background(argv[0], argv, NULL, &process_handle); #endif tt_int_op(expected_status,OP_EQ, status); tt_assert(process_handle); tt_int_op(expected_status,OP_EQ, process_handle->status); /* Check stdout */ for (expected_out_ctr = 0; expected_out[expected_out_ctr] != NULL;) { #ifdef _WIN32 pos = tor_read_all_handle(process_handle->stdout_pipe, stdout_buf, sizeof(stdout_buf) - 1, NULL); #else /* Check that we didn't read the end of file last time */ tt_assert(!eof); pos = tor_read_all_handle(process_handle->stdout_pipe, stdout_buf, sizeof(stdout_buf) - 1, NULL, &eof); #endif /* defined(_WIN32) */ log_info(LD_GENERAL, "tor_read_all_handle() returned %d", (int)pos); /* We would have blocked, keep on trying */ if (0 == pos) continue; tt_assert(pos > 0); stdout_buf[pos] = '\0'; tt_str_op(expected_out[expected_out_ctr],OP_EQ, stdout_buf); tt_int_op(strlen(expected_out[expected_out_ctr]),OP_EQ, pos); expected_out_ctr++; } if (exit_early) { tor_process_handle_destroy(process_handle, 1); process_handle = NULL; goto done; } /* The process should have exited without writing more */ #ifdef _WIN32 pos = tor_read_all_handle(process_handle->stdout_pipe, stdout_buf, sizeof(stdout_buf) - 1, process_handle); tt_int_op(0,OP_EQ, pos); #else /* !(defined(_WIN32)) */ if (!eof) { /* We should have got all the data, but maybe not the EOF flag */ pos = tor_read_all_handle(process_handle->stdout_pipe, stdout_buf, sizeof(stdout_buf) - 1, process_handle, &eof); tt_int_op(0,OP_EQ, pos); tt_assert(eof); } /* Otherwise, we got the EOF on the last read */ #endif /* defined(_WIN32) */ /* Check it terminated correctly */ retval = tor_get_exit_code(process_handle, 1, &exit_code); tt_int_op(PROCESS_EXIT_EXITED,OP_EQ, retval); tt_int_op(expected_exit,OP_EQ, exit_code); // TODO: Make test-child exit with something other than 0 /* Check stderr */ pos = tor_read_all_from_process_stderr(process_handle, stderr_buf, sizeof(stderr_buf) - 1); tt_assert(pos >= 0); stderr_buf[pos] = '\0'; tt_str_op(expected_err,OP_EQ, stderr_buf); tt_int_op(strlen(expected_err),OP_EQ, pos); done: tor_process_handle_destroy(process_handle, 1); } static void test_util_spawn_background_partial_read(void *arg) { (void)arg; test_util_spawn_background_partial_read_impl(0); } static void test_util_spawn_background_exit_early(void *arg) { (void)arg; test_util_spawn_background_partial_read_impl(1); } static void test_util_spawn_background_waitpid_notify(void *arg) { int retval, exit_code; process_handle_t *process_handle=NULL; int status; int ms_timer; const char *argv[] = {TEST_CHILD, "--fast", NULL}; (void) arg; #ifdef _WIN32 status = tor_spawn_background(NULL, argv, NULL, &process_handle); #else status = tor_spawn_background(argv[0], argv, NULL, &process_handle); #endif tt_int_op(status, OP_EQ, PROCESS_STATUS_RUNNING); tt_ptr_op(process_handle, OP_NE, NULL); /* We're not going to look at the stdout/stderr output this time. Instead, * we're testing whether notify_pending_waitpid_calbacks() can report the * process exit (on unix) and/or whether tor_get_exit_code() can notice it * (on windows) */ #ifndef _WIN32 ms_timer = 30*1000; tt_ptr_op(process_handle->waitpid_cb, OP_NE, NULL); while (process_handle->waitpid_cb && ms_timer > 0) { tor_sleep_msec(100); ms_timer -= 100; notify_pending_waitpid_callbacks(); } tt_int_op(ms_timer, OP_GT, 0); tt_ptr_op(process_handle->waitpid_cb, OP_EQ, NULL); #endif /* !defined(_WIN32) */ ms_timer = 30*1000; while (((retval = tor_get_exit_code(process_handle, 0, &exit_code)) == PROCESS_EXIT_RUNNING) && ms_timer > 0) { tor_sleep_msec(100); ms_timer -= 100; } tt_int_op(ms_timer, OP_GT, 0); tt_int_op(retval, OP_EQ, PROCESS_EXIT_EXITED); done: tor_process_handle_destroy(process_handle, 1); } #undef TEST_CHILD #undef EOL #undef MATCH_PROCESS_STATUS #ifndef _WIN32 #undef PROCESS_STATUS_RUNNING_OR_NOTRUNNING #undef IS_RUNNING_OR_NOTRUNNING #endif #define UTIL_TEST(name, flags) \ { #name, test_util_ ## name, flags, NULL, NULL } struct testcase_t slow_util_tests[] = { UTIL_TEST(spawn_background_ok, 0), UTIL_TEST(spawn_background_fail, 0), UTIL_TEST(spawn_background_partial_read, 0), UTIL_TEST(spawn_background_exit_early, 0), UTIL_TEST(spawn_background_waitpid_notify, 0), END_OF_TESTCASES };