main-func: add DEFINE_MAIN_FUNCTION_FIBER
Provide a Tokio-like #[tokio::main] entry point for binaries that want to run their main function inside a fiber on a default sd_event loop. DEFINE_MAIN_FUNCTION_FIBER(impl) mirrors DEFINE_MAIN_FUNCTION but routes impl through a new run_main_fiber() helper. impl keeps the run() convention while the loop's exit code keeps the exit status convention: impl's result never becomes an exit code, an explicit exit status wins over a successful impl, and impl's own failure wins over the exit status unless it is the -ECANCELED of a fiber that the exit request cancelled. Signal policy stays with impl so existing handlers such as sd_event_set_signal_exit() keep working. The fiber is created fire-and-forget: as long as the loop reaches SD_EVENT_FINISHED it unwinds the fiber through its exit event source, so we never hold a reference we cannot drop because the fiber is still suspended. A loop that fails internally never dispatches its exit sources, so the fiber may stay suspended with no working loop left to unwind it; we leak both for now and leave a TODO to keep the broken loop out of the default slot. Since the fiber also refers to run_main_fiber()'s stack frame, refuse to run on a loop we don't own, or from a fiber, as sd_fiber_new() leaves the exit event source disabled there and nothing would unwind the fiber. Cover argument forwarding, result propagation, explicit event exits, nested invocations, signal-source registration, event-loop errors, and fiber stack unwinding, and check after every run that the default event loop was released again. Also build and run a separate test executable that expands DEFINE_MAIN_FUNCTION_FIBER itself and verifies that a positive implementation result maps to a successful process exit.
This commit is contained in:
@@ -3,9 +3,12 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
@@ -0,0 +1,148 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
#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);
|
||||
Reference in New Issue
Block a user