diff --git a/src/shared/main-func.c b/src/shared/main-func.c index 4aa6d10433..a90fb458bd 100644 --- a/src/shared/main-func.c +++ b/src/shared/main-func.c @@ -3,9 +3,12 @@ #include #include "sd-daemon.h" +#include "sd-event.h" +#include "sd-future.h" #include "argv-util.h" #include "ask-password-agent.h" +#include "log.h" #include "main-func.h" #include "pager.h" #include "polkit-agent.h" @@ -34,3 +37,72 @@ int exit_failure_if_negative(int result) { int exit_failure_if_nonzero(int result) { return result < 0 ? EXIT_FAILURE : result; } + +typedef struct MainFiberContext { + int argc; + char **argv; + main_fiber_func_t func; + + int result; + bool exit_requested; +} MainFiberContext; + +static int main_fiber_trampoline(void *userdata) { + MainFiberContext *ctx = ASSERT_PTR(userdata); + sd_event *event = ASSERT_PTR(sd_fiber_get_event()); + + ctx->result = ctx->func(ctx->argc, ctx->argv); + + /* impl's result is an errno, the exit code an exit status that impl's exit handlers see: keep + * them apart. */ + ctx->exit_requested = sd_event_get_exit_code(event, /* ret= */ NULL) >= 0; + if (!ctx->exit_requested) + (void) sd_event_exit(event, 0); + + return ctx->result; +} + +int run_main_fiber(int argc, char *argv[], main_fiber_func_t func) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + MainFiberContext ctx = { + .argc = argc, + .argv = argv, + .func = func, + }; + int r; + + assert(func); + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + /* The fiber gets a pointer into this frame and is only unwound by the loop we run below, which + * requires that we own that loop and that sd_fiber_new() arms the fiber's exit event source. */ + if (sd_fiber_is_running() || sd_event_get_state(event) != SD_EVENT_INITIAL) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), + "Refusing to run a main fiber on a busy event loop."); + + /* Fire-and-forget: the loop unwinds the fiber through its exit event source, and a suspended + * fiber is one we couldn't drop a reference to anyway. */ + r = sd_fiber_new(event, program_invocation_short_name, main_fiber_trampoline, &ctx, + /* destroy= */ NULL, /* ret= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to spawn main fiber: %m"); + + r = sd_event_loop(event); + if (sd_event_get_state(event) != SD_EVENT_FINISHED) + /* Exit sources never ran, so the fiber may still be suspended with no working loop left + * to unwind it. Leak both. + * + * TODO: detach the loop from the default slot, it stays reachable with the abandoned + * fiber's sources armed on it, pointing into this frame. */ + return log_error_errno(r, "Event loop failed: %m"); + + /* An impl cancelled by the exit request just echoes -ECANCELED back at us, so let the requested + * status speak. Anything else is impl's own verdict, and its successes stay out of the status. */ + if (ctx.exit_requested && (ctx.result >= 0 || ctx.result == -ECANCELED)) + return r; + + return ctx.result < 0 ? ctx.result : 0; +} diff --git a/src/shared/main-func.h b/src/shared/main-func.h index 00d671f0ce..fdc6e8c843 100644 --- a/src/shared/main-func.h +++ b/src/shared/main-func.h @@ -34,3 +34,13 @@ int exit_failure_if_nonzero(int result) _const_; * Note: "true" means failure! */ #define DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(impl) \ _DEFINE_MAIN_FUNCTION(, impl(argc, argv), exit_failure_if_nonzero) + +typedef int (*main_fiber_func_t)(int argc, char *argv[]); + +/* Spawn impl as a fiber on the default event loop and run the loop until it returns. impl follows the + * DEFINE_MAIN_FUNCTION convention: negative is failure, anything else success. sd_event_exit() picks an + * explicit exit status, cancels impl, and wins over its result. Signal policy belongs to impl. */ +int run_main_fiber(int argc, char *argv[], main_fiber_func_t func); + +#define DEFINE_MAIN_FUNCTION_FIBER(impl) \ + _DEFINE_MAIN_FUNCTION(, run_main_fiber(argc, argv, impl), exit_failure_if_nonzero) diff --git a/src/test/meson.build b/src/test/meson.build index e41233a3c8..cbd96e1dc2 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -147,6 +147,8 @@ simple_tests += files( 'test-login-util.c', 'test-machine-util.c', 'test-macro.c', + 'test-main-func-fiber.c', + 'test-main-func.c', 'test-memfd-util.c', 'test-memory-util.c', 'test-mempool.c', diff --git a/src/test/test-main-func-fiber.c b/src/test/test-main-func-fiber.c new file mode 100644 index 0000000000..3a9ba15e16 --- /dev/null +++ b/src/test/test-main-func-fiber.c @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "main-func.h" +#include "tests.h" + +static int run(int argc, char *argv[]) { + ASSERT_GE(argc, 1); + ASSERT_NOT_NULL(argv); + ASSERT_NOT_NULL(argv[0]); + ASSERT_NULL(argv[argc]); + + /* A positive result means success, so this has to exit zero. */ + return 23; +} + +DEFINE_MAIN_FUNCTION_FIBER(run); diff --git a/src/test/test-main-func.c b/src/test/test-main-func.c new file mode 100644 index 0000000000..a92ddee1ea --- /dev/null +++ b/src/test/test-main-func.c @@ -0,0 +1,148 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "sd-event.h" +#include "sd-future.h" + +#include "main-func.h" +#include "tests.h" + +static char *test_argv[] = { + (char*) "alpha", + (char*) "beta", + NULL, +}; + +static void assert_test_argv(int argc, char *argv[]) { + ASSERT_EQ(argc, 2); + ASSERT_PTR_EQ(argv, test_argv); + ASSERT_STREQ(argv[0], "alpha"); + ASSERT_STREQ(argv[1], "beta"); + ASSERT_NULL(argv[2]); +} + +static int run_main_fiber_checked(main_fiber_func_t func) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + r = run_main_fiber(2, test_argv, func); + + /* The main fiber, and with it the event loop it pins, has to be gone by the time run_main_fiber() + * returns, so allocating the default event again must hand out a fresh one. */ + ASSERT_OK_POSITIVE(sd_event_default(&event)); + + return r; +} + +static int return_argc(int argc, char *argv[]) { + assert_test_argv(argc, argv); + + return argc; +} + +static int return_cancelled(int argc, char *argv[]) { + assert_test_argv(argc, argv); + + return -ECANCELED; +} + +TEST(result) { + /* Positive results mean success and stay out of the exit status. */ + ASSERT_OK_ZERO(run_main_fiber_checked(return_argc)); + ASSERT_ERROR(run_main_fiber_checked(return_cancelled), ECANCELED); +} + +static int nested_run(int argc, char *argv[]) { + assert_test_argv(argc, argv); + + /* Attaching another main fiber to the running loop would leave it behind once we return. */ + ASSERT_ERROR(run_main_fiber(argc, argv, return_argc), EBUSY); + return 0; +} + +TEST(nested) { + ASSERT_OK_ZERO(run_main_fiber_checked(nested_run)); +} + +static int signal_to_raise; + +static int raise_signal(int argc, char *argv[]) { + assert_test_argv(argc, argv); + + ASSERT_OK(sd_event_set_signal_exit(sd_fiber_get_event(), true)); + + ASSERT_OK_ERRNO(raise(signal_to_raise)); + ASSERT_ERROR(sd_fiber_suspend(), ECANCELED); + return -ECANCELED; +} + +TEST(signal) { + FOREACH_ARGUMENT(signal_to_raise, SIGINT, SIGTERM) + ASSERT_OK_ZERO(run_main_fiber_checked(raise_signal)); +} + +static int fail_event(sd_event_source *s, void *userdata) { + ASSERT_NOT_NULL(s); + ASSERT_NULL(userdata); + + return -EINVAL; +} + +static bool fiber_unwound; + +static void mark_fiber_unwound(bool *marker) { + ASSERT_NOT_NULL(marker); + fiber_unwound = true; +} + +static int event_loop_error(int argc, char *argv[]) { + _cleanup_(sd_event_source_unrefp) sd_event_source *defer = NULL; + _unused_ _cleanup_(mark_fiber_unwound) bool unwind_marker = false; + + assert_test_argv(argc, argv); + + ASSERT_OK(sd_event_add_defer(sd_fiber_get_event(), &defer, fail_event, /* userdata= */ NULL)); + ASSERT_OK(sd_event_source_set_exit_on_failure(defer, true)); + ASSERT_ERROR(sd_fiber_suspend(), ECANCELED); + return -ECANCELED; +} + +TEST(event_loop_error) { + fiber_unwound = false; + + ASSERT_ERROR(run_main_fiber_checked(event_loop_error), EINVAL); + ASSERT_TRUE(fiber_unwound); +} + +static int exit_event(int argc, char *argv[]) { + assert_test_argv(argc, argv); + + ASSERT_OK(sd_event_exit(sd_fiber_get_event(), 23)); + ASSERT_ERROR(sd_fiber_suspend(), ECANCELED); + return -ECANCELED; +} + +static int exit_event_and_return(int argc, char *argv[]) { + assert_test_argv(argc, argv); + + ASSERT_OK(sd_event_exit(sd_fiber_get_event(), 23)); + return 0; +} + +static int exit_event_and_fail(int argc, char *argv[]) { + assert_test_argv(argc, argv); + + ASSERT_OK(sd_event_exit(sd_fiber_get_event(), 0)); + return -ENOANO; +} + +TEST(event_exit_code) { + ASSERT_EQ(run_main_fiber_checked(exit_event), 23); + ASSERT_EQ(run_main_fiber_checked(exit_event_and_return), 23); + + /* Requesting an exit doesn't cancel a running impl, so its failure still wins. */ + ASSERT_ERROR(run_main_fiber_checked(exit_event_and_fail), ENOANO); +} + +DEFINE_TEST_MAIN(LOG_DEBUG);