summaryrefslogtreecommitdiffstats
path: root/src/lib-test/test-subprocess.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib-test/test-subprocess.c')
-rw-r--r--src/lib-test/test-subprocess.c392
1 files changed, 392 insertions, 0 deletions
diff --git a/src/lib-test/test-subprocess.c b/src/lib-test/test-subprocess.c
new file mode 100644
index 0000000..eeaae80
--- /dev/null
+++ b/src/lib-test/test-subprocess.c
@@ -0,0 +1,392 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "hostpid.h"
+#include "array.h"
+#include "ioloop.h"
+#include "sleep.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+struct test_subprocess {
+ pid_t pid;
+};
+
+volatile sig_atomic_t test_subprocess_is_child = 0;
+static bool test_subprocess_lib_init = FALSE;
+static volatile bool test_subprocess_notification_signal_received[SIGUSR1 + 1];
+static struct event *test_subprocess_event = NULL;
+static ARRAY(struct test_subprocess *) test_subprocesses = ARRAY_INIT;
+static void (*test_subprocess_cleanup_callback)(void) = NULL;
+
+static void
+test_subprocess_signal(const siginfo_t *si ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ io_loop_stop(current_ioloop);
+}
+
+static void test_subprocess_free_all(void)
+{
+ struct test_subprocess *subp;
+
+ array_foreach_elem(&test_subprocesses, subp)
+ i_free(subp);
+ array_free(&test_subprocesses);
+}
+
+static void ATTR_NORETURN
+test_subprocess_child(int (*func)(void *context), void *context,
+ bool continue_test)
+{
+ int ret;
+
+ if (!continue_test)
+ test_forked_end();
+
+ hostpid_init();
+
+ lib_signals_deinit();
+ lib_signals_init();
+
+ lib_signals_set_handler(SIGTERM,
+ LIBSIG_FLAG_DELAYED | LIBSIG_FLAG_IOLOOP_AUTOMOVE,
+ test_subprocess_signal, NULL);
+ lib_signals_set_handler(SIGQUIT,
+ LIBSIG_FLAG_DELAYED | LIBSIG_FLAG_IOLOOP_AUTOMOVE,
+ test_subprocess_signal, NULL);
+ lib_signals_set_handler(SIGINT,
+ LIBSIG_FLAG_DELAYED | LIBSIG_FLAG_IOLOOP_AUTOMOVE,
+ test_subprocess_signal, NULL);
+
+ ret = func(context);
+
+ /* Prevent race condition */
+ lib_signals_clear_handlers_and_ignore(SIGTERM);
+
+ event_unref(&test_subprocess_event);
+ lib_signals_deinit();
+
+ if (!continue_test) {
+ lib_deinit();
+ lib_exit(ret);
+ }
+ test_exit((test_has_failed() ? 1 : 0));
+}
+
+#undef test_subprocess_fork
+void test_subprocess_fork(int (*func)(void *context), void *context,
+ bool continue_test)
+{
+ struct test_subprocess *subprocess;
+
+ subprocess = i_new(struct test_subprocess, 1);
+
+ lib_signals_ioloop_detach();
+
+ /* avoid races: fork the child process with test_subprocess_is_child
+ set to 1 in case it immediately receives a signal. */
+ test_subprocess_is_child = 1;
+ if ((subprocess->pid = fork()) == (pid_t)-1)
+ i_fatal("test: sub-process: fork() failed: %m");
+ if (subprocess->pid == 0) {
+ /* cannot include this in the list to avoid accidental
+ * kill of PID 0, so just free it here explicitly. */
+ i_free(subprocess);
+ test_subprocess_free_all();
+
+ test_subprocess_child(func, context, continue_test);
+ i_unreached();
+ }
+ test_subprocess_is_child = 0;
+
+ array_push_back(&test_subprocesses, &subprocess);
+ lib_signals_ioloop_attach();
+}
+
+static void test_subprocess_verify_exit_status(int status)
+{
+ test_out_quiet("sub-process ended properly",
+ WIFEXITED(status) && WEXITSTATUS(status) == 0);
+ if (WIFEXITED(status)) {
+ if (WEXITSTATUS(status) != 0) {
+ e_warning(test_subprocess_event,
+ "Sub-process exited with status %d",
+ WEXITSTATUS(status));
+ }
+ } else if (WIFSIGNALED(status)) {
+ e_warning(test_subprocess_event,
+ "Sub-process forcibly terminated with signal %d",
+ WTERMSIG(status));
+ } else if (WIFSTOPPED(status)) {
+ e_warning(test_subprocess_event,
+ "Sub-process stopped with signal %d",
+ WSTOPSIG(status));
+ } else {
+ e_warning(test_subprocess_event,
+ "Sub-process terminated abnormally with status %d",
+ status);
+ }
+}
+
+static void test_subprocess_kill_forced(struct test_subprocess *subp)
+{
+ i_assert(subp->pid > 0);
+ (void)kill(subp->pid, SIGKILL);
+ (void)waitpid(subp->pid, NULL, 0);
+}
+
+void test_subprocess_kill_all(unsigned int timeout_secs)
+{
+ struct test_subprocess **subps;
+ unsigned int subps_count, subps_left, i;
+
+ subps = array_get_modifiable(&test_subprocesses, &subps_count);
+
+ /* Request children to terminate gently */
+ for (i = 0; i < subps_count; i++) {
+ if (subps[i] == NULL || subps[i]->pid == (pid_t)-1)
+ continue;
+
+ e_debug(test_subprocess_event,
+ "Terminating sub-process [%u]", i);
+ if (kill(subps[i]->pid, SIGTERM) < 0) {
+ e_error(test_subprocess_event,
+ "Failed to kill sub-process [%u] with SIGTERM: "
+ "%m", i);
+ }
+ }
+
+ /* Wait for children */
+ subps_left = subps_count;
+ while (subps_left > 0) {
+ int status;
+ pid_t wret = (pid_t)-1;
+
+ alarm(timeout_secs);
+ wret = waitpid(-1, &status, 0);
+ alarm(0);
+
+ test_assert(wret > 0);
+ if (wret < 0 && errno == EINTR)
+ e_warning(test_subprocess_event,
+ "Wait for sub-processes timed out");
+ if (wret > 0)
+ test_subprocess_verify_exit_status(status);
+
+ if (wret == 0)
+ break;
+ if (wret < 0) {
+ if (errno == ECHILD)
+ continue;
+ e_warning(test_subprocess_event,
+ "Wait for sub-processes failed: %m");
+ break;
+ }
+ for (i = 0; i < subps_count; i++) {
+ if (subps[i] == NULL || subps[i]->pid != wret)
+ continue;
+ e_debug(test_subprocess_event,
+ "Terminated sub-process [%u]", i);
+ i_free(subps[i]);
+ subps_left--;
+ }
+ }
+
+ /* Kill disobedient ones with fire */
+ for (i = 0; i < subps_count; i++) {
+ if (subps[i] == NULL || subps[i]->pid == (pid_t)-1)
+ continue;
+ e_warning(test_subprocess_event,
+ "Forcibly killed sub-process [%u]", i);
+ test_subprocess_kill_forced(subps[i]);
+ i_assert(subps_left > 0);
+ i_free(subps[i]);
+ subps_left--;
+ }
+ i_assert(subps_left == 0);
+
+ array_clear(&test_subprocesses);
+}
+
+static void test_subprocess_kill_all_forced(void)
+{
+ struct test_subprocess **subps;
+ unsigned int subps_count, i;
+
+ if (!array_is_created(&test_subprocesses))
+ return;
+
+ /* This is also called from signal handler context, so no debug logging
+ here.
+ */
+
+ subps = array_get_modifiable(&test_subprocesses, &subps_count);
+
+ if (subps_count == 0)
+ return;
+
+ for (i = 0; i < subps_count; i++) {
+ if (subps[i] == NULL || subps[i]->pid == (pid_t)-1)
+ continue;
+ test_subprocess_kill_forced(subps[i]);
+ subps[i]->pid = (pid_t)-1;
+ }
+}
+
+/*
+ * Main
+ */
+
+volatile sig_atomic_t terminating = 0;
+
+static void test_subprocess_cleanup(void)
+{
+ if (test_subprocess_is_child != 0) {
+ /* Child processes must not execute the cleanups */
+ return;
+ }
+
+ /* We get here when the test ended normally, badly failed, crashed,
+ terminated, or executed exit() unexpectedly. The cleanups performed
+ here are important and must be executed at all times. */
+
+ /* While any unfreed memory will be handled by the system, lingering
+ child processes will not be handled so well. So, we need to make sure
+ here that we don't leave any pesky child processes alive. */
+ test_subprocess_kill_all_forced();
+
+ /* Perform any additional important cleanup specific to the test. */
+ if (test_subprocess_cleanup_callback != NULL)
+ test_subprocess_cleanup_callback();
+}
+
+static void
+test_subprocess_alarm(const siginfo_t *si ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ /* We use alarm() to implement a simple timeout on waitpid(), which will
+ exit with EINTR when SIGALRM is received. This handler overrides the
+ default SIGALRM handler, so that the process is not killed and no
+ messages are printed to terminal.
+ */
+}
+
+static void
+test_subprocess_terminate(const siginfo_t *si, void *context ATTR_UNUSED)
+{
+ int signo = si->si_signo;
+
+ if (terminating != 0)
+ raise(signo);
+ terminating = 1;
+
+ /* Perform important cleanups */
+ test_subprocess_cleanup();
+
+ (void)signal(signo, SIG_DFL);
+ if (signo == SIGTERM)
+ _exit(0);
+ else
+ raise(signo);
+}
+
+static void test_atexit(void)
+{
+ /* NOTICE: This is also called by children, so be careful. */
+
+ /* Perform important cleanups */
+ test_subprocess_cleanup();
+}
+
+void test_subprocess_set_cleanup_callback(void (*callback)(void))
+{
+ test_subprocess_cleanup_callback = callback;
+}
+
+void test_subprocess_notify_signal_send(int signo, pid_t pid)
+{
+ if (kill(pid, signo) < 0)
+ i_fatal("kill(%ld, SIGHUP) failed: %m", (long)pid);
+}
+
+void test_subprocess_notify_signal_send_parent(int signo)
+{
+ test_subprocess_notify_signal_send(signo, getppid());
+}
+
+void test_subprocess_notify_signal_reset(int signo)
+{
+ i_assert(signo >= 0 &&
+ (unsigned int)signo < N_ELEMENTS(test_subprocess_notification_signal_received));
+ test_subprocess_notification_signal_received[signo] = FALSE;
+}
+
+void test_subprocess_notify_signal_wait(int signo, unsigned int timeout_msecs)
+{
+ unsigned int i, count = timeout_msecs / 10;
+
+ for (i = 0; i < count; i++) {
+ if (test_subprocess_notification_signal_received[signo])
+ return;
+ i_sleep_msecs(10);
+ }
+ i_fatal("Didn't receive wait notification signal from server");
+}
+
+static void
+test_subprocess_notification_signal(const siginfo_t *si,
+ void *context ATTR_UNUSED)
+{
+ int signo = si->si_signo;
+
+ i_assert(signo >= 0 &&
+ (unsigned int)signo < N_ELEMENTS(test_subprocess_notification_signal_received));
+ test_subprocess_notification_signal_received[signo] = TRUE;
+}
+
+void test_subprocesses_init(bool debug)
+{
+ if (!lib_is_initialized()) {
+ lib_init();
+ test_subprocess_lib_init = TRUE;
+ }
+ lib_signals_init();
+
+ atexit(test_atexit);
+ lib_signals_ignore(SIGPIPE, TRUE);
+ lib_signals_set_handler(SIGALRM, 0, test_subprocess_alarm, NULL);
+ lib_signals_set_handler(SIGTERM, 0, test_subprocess_terminate, NULL);
+ lib_signals_set_handler(SIGQUIT, 0, test_subprocess_terminate, NULL);
+ lib_signals_set_handler(SIGINT, 0, test_subprocess_terminate, NULL);
+ lib_signals_set_handler(SIGSEGV, 0, test_subprocess_terminate, NULL);
+ lib_signals_set_handler(SIGABRT, 0, test_subprocess_terminate, NULL);
+ lib_signals_set_handler(SIGHUP, LIBSIG_FLAG_RESTART,
+ test_subprocess_notification_signal, NULL);
+ lib_signals_set_handler(SIGUSR1, LIBSIG_FLAG_RESTART,
+ test_subprocess_notification_signal, NULL);
+
+ i_array_init(&test_subprocesses, 8);
+
+ test_subprocess_event = event_create(NULL);
+ event_set_forced_debug(test_subprocess_event, debug);
+ event_set_append_log_prefix(test_subprocess_event, "test: ");
+}
+
+void test_subprocesses_deinit(void)
+{
+ test_subprocess_cleanup();
+ test_subprocess_free_all();
+ array_free(&test_subprocesses);
+
+ event_unref(&test_subprocess_event);
+ lib_signals_deinit();
+
+ if (test_subprocess_lib_init)
+ lib_deinit();
+}