notify: add a new --fork verb that implements a minimal receiver side for sd_notify() messages

This commit is contained in:
Lennart Poettering
2025-02-14 14:43:22 +01:00
parent 30999dd5cf
commit 4389e4c2ae
2 changed files with 327 additions and 37 deletions

View File

@@ -26,7 +26,10 @@
<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>
@@ -237,6 +240,54 @@
<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>

View 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-recv.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,7 @@ 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, "hq", options, NULL)) >= 0) {
switch (c) {
@@ -305,6 +320,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,58 +336,271 @@ 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.");
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;
switch (arg_action) {
for (i = optind; i < argc; i++)
if (streq(argv[i], ";"))
break;
case ACTION_NOTIFY: {
if (arg_fdname && fdset_isempty(arg_fds))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No file descriptors passed, but --fdname= set, 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.");
size_t n_arg_env;
arg_exec = strv_copy_n(argv + i + 1, argc - i - 1);
if (!arg_exec)
return log_oom();
if (do_exec) {
int i;
n_arg_env = i - optind;
} else
n_arg_env = argc - optind;
for (i = optind; i < argc; i++)
if (streq(argv[i], ";"))
break;
have_env = have_env || n_arg_env > 0;
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 (!have_env && arg_action != ACTION_BOOTED) {
if (do_exec)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No notify message specified while --exec, refusing.");
arg_exec = strv_copy_n(argv + i + 1, argc - i - 1);
if (!arg_exec)
return log_oom();
/* No argument at all? */
help();
return -EINVAL;
n_arg_env = i - optind;
} else
n_arg_env = argc - optind;
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.");
/* 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));
break;
}
if (have_env && arg_action == ACTION_BOOTED)
log_warning("Notify message specified along with --booted, ignoring.");
case ACTION_BOOTED:
if (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();
break;
case ACTION_FORK:
if (optind >= argc)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--fork requires a command to be specified, refusing.");
break;
default:
assert_not_reached();
}
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_NOTIFY)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--ready, --reloading, --stopping, --pid=, --status=, --fd= may not be combined with --fork or --booted, 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)
return 0;
_cleanup_free_ char *escaped = cescape(text);
log_debug("Received notification message without READY=1, ignoring: %s", strna(escaped));
return 0;
}
log_debug("Received READY=1, exiting.");
return sd_event_exit(sd_event_source_get_event(s), EXIT_SUCCESS);
}
static int on_child(sd_event_source *s, const siginfo_t *si, void *userdata) {
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 no 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.");
return sd_event_exit(sd_event_source_get_event(s), ret);
}
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 },
/* except_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) {
/* Let's explicitly close the fds we just opened. Not because it was necessary (we should be
* setting O_CLOEXEC after all on all of them), but mostly to make debugging nice */
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[]) {
_cleanup_free_ char *status = NULL, *main_pid = NULL, *main_pidfd_id = NULL, *msg = NULL,
*monotonic_usec = NULL, *fdn = NULL;
@@ -379,6 +615,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)