main-func: add DEFINE_MAIN_FUNCTION_FIBER (#42238)
Provides 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, which:
- allocates the default sd_event,
- enables exit-on-idle so the loop terminates once all event sources
drain (structured concurrency: impl is expected to cancel and reap
whatever it spawned before returning),
- spawns impl as a fiber named after program_invocation_short_name,
- installs SIGINT/SIGTERM handlers that cancel the main fiber (and
disable themselves so the loop can go idle once the fiber unwinds),
- runs sd_event_loop and propagates impl's return value via
sd_future_result, normalising -ECANCELED to 0 since that just
represents an orderly signal-driven shutdown.
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