mirror of
https://github.com/morgan9e/systemd
synced 2026-04-14 00:14:32 +09:00
notify: add a new --fork verb that implements a minimal receiver side for sd_notify() messages
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user