]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
notify: add a new --fork verb that implements a minimal receiver side for sd_notify...
authorLennart Poettering <lennart@poettering.net>
Fri, 14 Feb 2025 13:43:22 +0000 (14:43 +0100)
committerLennart Poettering <lennart@poettering.net>
Tue, 18 Feb 2025 12:17:54 +0000 (13:17 +0100)
man/systemd-notify.xml
src/notify/notify.c

index 9a66721a613577d57e6ca06a23e5398f9bef078c..25b5e2d041ff42700907e25db64590a2b10c36fb 100644 (file)
       <command>systemd-notify</command> <arg choice="opt" rep="repeat">OPTIONS</arg> <arg choice="opt" rep="repeat">VARIABLE=VALUE</arg>
     </cmdsynopsis>
     <cmdsynopsis>
-      <command>systemd-notify</command> <arg>--exec</arg> <arg choice="opt" rep="repeat">OPTIONS</arg> <arg choice="opt" rep="repeat">VARIABLE=VALUE</arg> <arg>;</arg> <arg rep="repeat">CMDLINE</arg>
+      <command>systemd-notify</command> <arg choice="plain">--exec</arg> <arg choice="opt" rep="repeat">OPTIONS</arg> <arg choice="opt" rep="repeat">VARIABLE=VALUE</arg> <arg choice="plain">; --</arg> <arg choice="req" rep="repeat">CMDLINE</arg>
+    </cmdsynopsis>
+    <cmdsynopsis>
+      <command>systemd-notify</command> <arg choice="plain">--fork</arg> <arg choice="opt" rep="repeat">OPTIONS</arg> <arg choice="plain">--</arg> <arg choice="req" rep="repeat">CMDLINE</arg>
     </cmdsynopsis>
   </refsynopsisdiv>
 
         <xi:include href="version-info.xml" xpointer="v254"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><option>--fork</option></term>
+
+        <listitem><para>Instead of sending a notification message, fork off a command line and wait until a
+        <literal>READY=1</literal> message is received from it. In other words: this makes
+        <command>systemd-notify</command> the receiver of notification messages instead of the sender,
+        swapping roles. This is useful to quickly fork off a process that implements the
+        <function>sd_notify()</function> protocol from a shell script. The invoked command line will have
+        standard input and standard output connected to <filename>/dev/null</filename>, but standard error
+        will be inherited from the invoking process. The numeric process ID is written to standard output by
+        <command>systemd-notify</command> (unless <option>--quiet</option> is specified), which may be used
+        to later terminate the forked off process.</para>
+
+        <para>Note that processes forked off like this will likely remain running after
+        <command>systemd-notify</command> 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.</para>
+
+        <para>Note that this option should not be used to invoke full services ad-hoc, use
+        <command>systemd-run</command> for that.</para>
+
+        <para>Also note that when invoked with this switch <command>systemd-notify</command> will exit
+        successfully under two distinction conditions:
+
+        <orderedlist>
+          <listitem><para><command>systemd-notify</command> received a <literal>READY=1</literal>
+          notification from the child it just forked off.</para></listitem>
+
+          <listitem><para>The child process exited cleanly (with exit status zero) before sending
+          <literal>READY=1</literal>.</para></listitem>
+        </orderedlist></para>
+
+        <para>Example use:<programlisting># PID=$(systemd-notify --fork -- mycommand)
+…
+kill "$PID"
+unset PID</programlisting></para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--quiet</option></term>
+        <term><option>-q</option></term>
+
+        <listitem><para>Turn off output of the numeric process ID when <option>--fork</option> is used.</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
       <xi:include href="standard-options.xml" xpointer="help" />
       <xi:include href="standard-options.xml" xpointer="version" />
     </variablelist>
index afa5923a729ffe435ad9ebd9e59d12a6daf1420f..881778f2808a20cad1955a5ec0e25bba4a3a6dd0 100644 (file)
@@ -7,18 +7,24 @@
 #include <unistd.h>
 
 #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)