From: Lennart Poettering Date: Fri, 14 Feb 2025 13:43:22 +0000 (+0100) Subject: notify: add a new --fork verb that implements a minimal receiver side for sd_notify... X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=9c274eff66582eb213e075ce107bd12f285814e1;p=thirdparty%2Fsystemd.git notify: add a new --fork verb that implements a minimal receiver side for sd_notify() messages --- diff --git a/man/systemd-notify.xml b/man/systemd-notify.xml index 9a66721a613..25b5e2d041f 100644 --- a/man/systemd-notify.xml +++ b/man/systemd-notify.xml @@ -26,7 +26,10 @@ systemd-notify OPTIONS VARIABLE=VALUE - systemd-notify --exec OPTIONS VARIABLE=VALUE ; CMDLINE + systemd-notify --exec OPTIONS VARIABLE=VALUE ; -- CMDLINE + + + systemd-notify --fork OPTIONS -- CMDLINE @@ -237,6 +240,54 @@ + + + + Instead of sending a notification message, fork off a command line and wait until a + READY=1 message is received from it. In other words: this makes + systemd-notify the receiver of notification messages instead of the sender, + swapping roles. This is useful to quickly fork off a process that implements the + sd_notify() protocol from a shell script. The invoked command line will have + standard input and standard output connected to /dev/null, but standard error + will be inherited from the invoking process. The numeric process ID is written to standard output by + systemd-notify (unless is specified), which may be used + to later terminate the forked off process. + + Note that processes forked off like this will likely remain running after + systemd-notify already returned, which hence will result in them being reparented + to the closest process reaper process, i.e. typically the per-user or system service manager. + + Note that this option should not be used to invoke full services ad-hoc, use + systemd-run for that. + + Also note that when invoked with this switch systemd-notify will exit + successfully under two distinction conditions: + + + systemd-notify received a READY=1 + notification from the child it just forked off. + + The child process exited cleanly (with exit status zero) before sending + READY=1. + + + Example use:# PID=$(systemd-notify --fork -- mycommand) +… +kill "$PID" +unset PID + + + + + + + + + Turn off output of the numeric process ID when is used. + + + + diff --git a/src/notify/notify.c b/src/notify/notify.c index afa5923a729..881778f2808 100644 --- a/src/notify/notify.c +++ b/src/notify/notify.c @@ -7,18 +7,24 @@ #include #include "sd-daemon.h" +#include "sd-event.h" #include "alloc-util.h" #include "build.h" #include "env-util.h" +#include "escape.h" +#include "event-util.h" +#include "exit-status.h" #include "fd-util.h" #include "fdset.h" #include "format-util.h" #include "log.h" #include "main-func.h" +#include "notify-util.h" #include "parse-util.h" #include "pretty-print.h" #include "process-util.h" +#include "socket-util.h" #include "string-util.h" #include "strv.h" #include "terminal-util.h" @@ -28,6 +34,7 @@ static enum { ACTION_NOTIFY, ACTION_BOOTED, + ACTION_FORK, } arg_action = ACTION_NOTIFY; static bool arg_ready = false; static bool arg_reloading = false; @@ -41,6 +48,7 @@ static char **arg_env = NULL; static char **arg_exec = NULL; static FDSet *arg_fds = NULL; static char *arg_fdname = NULL; +static bool arg_quiet = false; STATIC_DESTRUCTOR_REGISTER(arg_pid, pidref_done); STATIC_DESTRUCTOR_REGISTER(arg_env, strv_freep); @@ -57,7 +65,8 @@ static int help(void) { return log_oom(); printf("%s [OPTIONS...] [VARIABLE=VALUE...]\n" - "%s [OPTIONS...] --exec [VARIABLE=VALUE...] ; CMDLINE...\n" + "%s [OPTIONS...] --exec [VARIABLE=VALUE...] ; -- CMDLINE...\n" + "%s [OPTIONS...] --fork -- CMDLINE...\n" "\n%sNotify the init system about service status updates.%s\n\n" " -h --help Show this help\n" " --version Show package version\n" @@ -73,9 +82,12 @@ static int help(void) { " --exec Execute command line separated by ';' once done\n" " --fd=FD Pass specified file descriptor with along with message\n" " --fdname=NAME Name to assign to passed file descriptor(s)\n" + " --fork Receive notifications from child rather than sending them\n" + " -q --quiet Do not show PID of child when forking\n" "\nSee the %s for details.\n", program_invocation_short_name, program_invocation_short_name, + program_invocation_short_name, ansi_highlight(), ansi_normal(), link); @@ -165,6 +177,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_EXEC, ARG_FD, ARG_FDNAME, + ARG_FORK, }; static const struct option options[] = { @@ -181,6 +194,8 @@ static int parse_argv(int argc, char *argv[]) { { "exec", no_argument, NULL, ARG_EXEC }, { "fd", required_argument, NULL, ARG_FD }, { "fdname", required_argument, NULL, ARG_FDNAME }, + { "fork", no_argument, NULL, ARG_FORK }, + { "quiet", no_argument, NULL, 'q' }, {} }; @@ -191,7 +206,9 @@ static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) { + while ((c = getopt_long(argc, argv, + (arg_action == ACTION_FORK || do_exec) ? "+hq" : "hq", + options, NULL)) >= 0) { switch (c) { @@ -305,6 +322,14 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_FORK: + arg_action = ACTION_FORK; + break; + + case 'q': + arg_quiet = true; + break; + case '?': return -EINVAL; @@ -313,56 +338,254 @@ static int parse_argv(int argc, char *argv[]) { } } - if (arg_fdname && fdset_isempty(arg_fds)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No file descriptors passed, but --fdname= set, refusing."); + if (arg_action == ACTION_NOTIFY) { + if (arg_fdname && fdset_isempty(arg_fds)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No file descriptors passed, but --fdname= set, refusing."); - bool have_env = arg_ready || arg_stopping || arg_reloading || arg_status || pidref_is_set(&arg_pid) || !fdset_isempty(arg_fds); - size_t n_arg_env; + bool have_env = arg_ready || arg_stopping || arg_reloading || arg_status || pidref_is_set(&arg_pid) || !fdset_isempty(arg_fds); + size_t n_arg_env; - if (do_exec) { - int i; + if (do_exec) { + int i; - for (i = optind; i < argc; i++) - if (streq(argv[i], ";")) - break; + for (i = optind; i < argc; i++) + if (streq(argv[i], ";")) + break; - if (i >= argc) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "If --exec is used argument list must contain ';' separator, refusing."); - if (i+1 == argc) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty command line specified after ';' separator, refusing."); + if (i >= argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "If --exec is used argument list must contain ';' separator, refusing."); + if (i+1 == argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty command line specified after ';' separator, refusing."); - arg_exec = strv_copy_n(argv + i + 1, argc - i - 1); - if (!arg_exec) - return log_oom(); + arg_exec = strv_copy_n(argv + i + 1, argc - i - 1); + if (!arg_exec) + return log_oom(); - n_arg_env = i - optind; - } else - n_arg_env = argc - optind; + n_arg_env = i - optind; + } else + n_arg_env = argc - optind; - have_env = have_env || n_arg_env > 0; + have_env = have_env || n_arg_env > 0; + if (!have_env) { + if (do_exec) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No notify message specified while --exec, refusing."); - if (!have_env && arg_action != ACTION_BOOTED) { - if (do_exec) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No notify message specified while --exec, refusing."); + /* No argument at all? */ + help(); + return -EINVAL; + } - /* No argument at all? */ - help(); - return -EINVAL; + if (n_arg_env > 0) { + arg_env = strv_copy_n(argv + optind, n_arg_env); + if (!arg_env) + return log_oom(); + } + if (!fdset_isempty(passed)) + log_warning("Warning: %u more file descriptors passed than referenced with --fd=.", fdset_size(passed)); } - if (have_env && arg_action == ACTION_BOOTED) - log_warning("Notify message specified along with --booted, ignoring."); + if (arg_action == ACTION_BOOTED && argc > optind) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--booted takes no parameters, refusing."); - if (n_arg_env > 0) { - arg_env = strv_copy_n(argv + optind, n_arg_env); - if (!arg_env) - return log_oom(); + if (arg_action == ACTION_FORK && optind >= argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--fork requires a command to be specified, refusing."); + + return 1; +} + +static int on_notify_socket(sd_event_source *s, int fd, unsigned event, void *userdata) { + PidRef *child = ASSERT_PTR(userdata); + int r; + + assert(s); + assert(fd >= 0); + + _cleanup_free_ char *text = NULL; + _cleanup_(pidref_done) PidRef pidref = PIDREF_NULL; + r = notify_recv(fd, &text, /* ret_ucred= */ NULL, &pidref); + if (r == -EAGAIN) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to receive notification message: %m"); + + if (!pidref_equal(child, &pidref)) { + log_warning("Received notification message from unexpected process " PID_FMT " (expected " PID_FMT "), ignoring.", + pidref.pid, child->pid); + return 0; + } + + const char *p = find_line_startswith(text, "READY=1"); + if (!p || !IN_SET(*p, '\n', 0)) { + if (DEBUG_LOGGING) { + _cleanup_free_ char *escaped = cescape(text); + log_debug("Received notification message without READY=1, ignoring. (%s)", strna(escaped)); + } + } else { + log_debug("Recieved READY=1, exiting."); + assert_se(sd_event_exit(sd_event_source_get_event(s), EXIT_SUCCESS) >= 0); } - if (!fdset_isempty(passed)) - log_warning("Warning: %u more file descriptors passed than referenced with --fd=.", fdset_size(passed)); + return 0; +} - return 1; +static int on_child(sd_event_source *s, const siginfo_t *si, void *userdata) { + int r; + + assert(s); + assert(si); + + int ret; + if (si->si_code == CLD_EXITED) { + if (si->si_status != EXIT_SUCCESS) + log_debug("Child failed with exit status %i.", si->si_status); + else + log_debug("Child exited successfully. (But not READY=1 message was sent!)"); + + /* NB: we propagate success here if the child exited cleanly but never sent us READY=1. We + * are not a service manager after all, where this would be a protocol violation. We are just + * a shell tool to fork off stuff in the background, where I think it makes sense to allow + * clean early exit of forked off processes. */ + ret = si->si_status; + + } else if (IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED)) + ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), + "Child terminated by signal %s.", signal_to_string(si->si_status)); + else + ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), + "Child terminated due to unknown reason."); + + r = sd_event_exit(sd_event_source_get_event(s), ret); + if (r < 0) + return log_error_errno(r, "Failed to request event loop exit: %m"); + + return 0; +} + +static int action_fork(char *const *_command) { + + static const int forward_signals[] = { + SIGHUP, + SIGTERM, + SIGINT, + SIGQUIT, + SIGTSTP, + SIGCONT, + SIGUSR1, + SIGUSR2, + }; + + int r; + + assert(!strv_isempty(_command)); + + /* Make a copy, since pidref_safe_fork_full() will change argv[] further down. */ + _cleanup_strv_free_ char **command = strv_copy(_command); + if (!command) + return log_oom(); + + _cleanup_close_ int socket_fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (socket_fd < 0) + return log_error_errno(errno, "Failed to allocate AF_UNIX socket for notifications: %m"); + + r = setsockopt_int(socket_fd, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + return log_error_errno(r, "Failed to enable SO_PASSCRED: %m"); + + r = setsockopt_int(socket_fd, SOL_SOCKET, SO_PASSPIDFD, true); + if (r < 0) + log_debug_errno(r, "Failed to enable SO_PASSPIDFD, ignoring: %m"); + + /* Pick an address via auto-bind */ + union sockaddr_union sa = { + .sa.sa_family = AF_UNIX, + }; + if (bind(socket_fd, &sa.sa, offsetof(union sockaddr_union, un.sun_path)) < 0) + return log_error_errno(errno, "Failed to bind AF_UNIX socket: %m"); + + _cleanup_free_ char *addr_string = NULL; + r = getsockname_pretty(socket_fd, &addr_string); + if (r < 0) + return log_error_errno(r, "Failed to get socket name: %m"); + + _cleanup_free_ char *c = strv_join(command, " "); + if (!c) + return log_oom(); + + _cleanup_(pidref_done) PidRef child = PIDREF_NULL; + r = pidref_safe_fork_full( + "(notify)", + /* stdio_fds= */ (const int[]) { -EBADF, -EBADF, STDERR_FILENO }, + /* execpt_fds= */ NULL, + /* n_except_fds= */ 0, + /* flags= */ FORK_REARRANGE_STDIO, + &child); + if (r < 0) + return log_error_errno(r, "Failed to fork child in order to execute '%s': %m", c); + if (r == 0) { + socket_fd = safe_close(socket_fd); + pidref_done(&child); + + if (setenv("NOTIFY_SOCKET", addr_string, /* overwrite= */ true) < 0) { + log_error_errno(errno, "Failed to set $NOTIFY_SOCKET: %m"); + _exit(EXIT_MEMORY); + } + + log_debug("Executing: %s", c); + execvp(command[0], command); + log_error_errno(errno, "Failed to execute '%s': %m", c); + _exit(EXIT_EXEC); + } + + if (!arg_quiet) { + printf(PID_FMT "\n", child.pid); + fflush(stdout); + } + + BLOCK_SIGNALS(SIGCHLD); + + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + r = sd_event_new(&event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *socket_event_source = NULL; + r = sd_event_add_io(event, &socket_event_source, socket_fd, EPOLLIN, on_notify_socket, &child); + if (r < 0) + return log_error_errno(r, "Failed to allocate IO source: %m"); + + /* If we receive both the sd_notify() message and a SIGCHLD always process sd_notify() first, it's + * the more interesting, "positive" information. */ + r = sd_event_source_set_priority(socket_event_source, SD_EVENT_PRIORITY_NORMAL - 10); + if (r < 0) + return log_error_errno(r, "Failed to change child event source priority: %m"); + + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *child_event_source = NULL; + r = event_add_child_pidref(event, &child_event_source, &child, WEXITED, on_child, /* userdata= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to allocate child source: %m"); + + /* Handle SIGCHLD before propagating the other signals below */ + r = sd_event_source_set_priority(child_event_source, SD_EVENT_PRIORITY_NORMAL - 5); + if (r < 0) + return log_error_errno(r, "Failed to change child event source priority: %m"); + + sd_event_source **forward_signal_sources = NULL; + size_t n_forward_signal_sources = 0; + CLEANUP_ARRAY(forward_signal_sources, n_forward_signal_sources, event_source_unref_many); + + r = event_forward_signals( + event, + child_event_source, + forward_signals, ELEMENTSOF(forward_signals), + &forward_signal_sources, &n_forward_signal_sources); + if (r < 0) + return log_error_errno(r, "Failed to set up signal forwarding: %m"); + + r = sd_event_loop(event); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + return r; } static int run(int argc, char* argv[]) { @@ -379,6 +602,9 @@ static int run(int argc, char* argv[]) { if (r <= 0) return r; + if (arg_action == ACTION_FORK) + return action_fork(argv + optind); + if (arg_action == ACTION_BOOTED) { r = sd_booted(); if (r < 0)