mirror of
https://github.com/systemd/systemd.git
synced 2025-01-11 09:18:07 +03:00
systemctl: Add --wait option to wait until started units terminate again
Fixes #3830
This commit is contained in:
parent
c49b50113e
commit
93a0884126
@ -363,7 +363,20 @@
|
|||||||
to finish. If this is not specified, the job will be
|
to finish. If this is not specified, the job will be
|
||||||
verified, enqueued and <command>systemctl</command> will
|
verified, enqueued and <command>systemctl</command> will
|
||||||
wait until the unit's start-up is completed. By passing this
|
wait until the unit's start-up is completed. By passing this
|
||||||
argument, it is only verified and enqueued.</para>
|
argument, it is only verified and enqueued. This option may not be
|
||||||
|
combined with <option>--wait</option>.</para>
|
||||||
|
</listitem>
|
||||||
|
</varlistentry>
|
||||||
|
|
||||||
|
<varlistentry>
|
||||||
|
<term><option>--wait</option></term>
|
||||||
|
|
||||||
|
<listitem>
|
||||||
|
<para>Synchronously wait for started units to terminate again.
|
||||||
|
This option may not be combined with <option>--no-block</option>.
|
||||||
|
Note that this will wait forever if any given unit never terminates
|
||||||
|
(by itself or by getting stopped explicitly); particularly services
|
||||||
|
which use <literal>RemainAfterExit=yes</literal>.</para>
|
||||||
</listitem>
|
</listitem>
|
||||||
</varlistentry>
|
</varlistentry>
|
||||||
|
|
||||||
|
@ -118,6 +118,7 @@ static enum dependency {
|
|||||||
} arg_dependency = DEPENDENCY_FORWARD;
|
} arg_dependency = DEPENDENCY_FORWARD;
|
||||||
static const char *arg_job_mode = "replace";
|
static const char *arg_job_mode = "replace";
|
||||||
static UnitFileScope arg_scope = UNIT_FILE_SYSTEM;
|
static UnitFileScope arg_scope = UNIT_FILE_SYSTEM;
|
||||||
|
static bool arg_wait = false;
|
||||||
static bool arg_no_block = false;
|
static bool arg_no_block = false;
|
||||||
static bool arg_no_legend = false;
|
static bool arg_no_legend = false;
|
||||||
static bool arg_no_pager = false;
|
static bool arg_no_pager = false;
|
||||||
@ -2679,13 +2680,86 @@ static const char *method_to_verb(const char *method) {
|
|||||||
return "n/a";
|
return "n/a";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
sd_bus_slot *match;
|
||||||
|
sd_event *event;
|
||||||
|
Set *unit_paths;
|
||||||
|
bool any_failed;
|
||||||
|
} WaitContext;
|
||||||
|
|
||||||
|
static void wait_context_free(WaitContext *c) {
|
||||||
|
c->match = sd_bus_slot_unref(c->match);
|
||||||
|
c->event = sd_event_unref(c->event);
|
||||||
|
c->unit_paths = set_free(c->unit_paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int on_properties_changed(sd_bus_message *m, void *userdata, sd_bus_error *error) {
|
||||||
|
WaitContext *c = userdata;
|
||||||
|
const char *path;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
path = sd_bus_message_get_path(m);
|
||||||
|
if (!set_contains(c->unit_paths, path))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/* Check if ActiveState changed to inactive/failed */
|
||||||
|
/* (s interface, a{sv} changed_properties, as invalidated_properties) */
|
||||||
|
r = sd_bus_message_skip(m, "s");
|
||||||
|
if (r < 0)
|
||||||
|
return bus_log_parse_error(r);
|
||||||
|
r = sd_bus_message_enter_container(m, SD_BUS_TYPE_ARRAY, "{sv}");
|
||||||
|
if (r < 0)
|
||||||
|
return bus_log_parse_error(r);
|
||||||
|
|
||||||
|
while ((r = sd_bus_message_enter_container(m, SD_BUS_TYPE_DICT_ENTRY, "sv")) > 0) {
|
||||||
|
const char *s;
|
||||||
|
bool is_failed;
|
||||||
|
|
||||||
|
r = sd_bus_message_read(m, "s", &s);
|
||||||
|
if (r < 0)
|
||||||
|
return bus_log_parse_error(r);
|
||||||
|
if (streq(s, "ActiveState")) {
|
||||||
|
r = sd_bus_message_enter_container(m, SD_BUS_TYPE_VARIANT, "s");
|
||||||
|
if (r < 0)
|
||||||
|
return bus_log_parse_error(r);
|
||||||
|
r = sd_bus_message_read(m, "s", &s);
|
||||||
|
if (r < 0)
|
||||||
|
return bus_log_parse_error(r);
|
||||||
|
is_failed = streq(s, "failed");
|
||||||
|
if (streq(s, "inactive") || is_failed) {
|
||||||
|
log_debug("%s became %s, dropping from --wait tracking", path, s);
|
||||||
|
set_remove(c->unit_paths, path);
|
||||||
|
c->any_failed |= is_failed;
|
||||||
|
} else
|
||||||
|
log_debug("ActiveState on %s changed to %s", path, s);
|
||||||
|
break; /* no need to dissect the rest of the message */
|
||||||
|
} else {
|
||||||
|
/* other property */
|
||||||
|
r = sd_bus_message_skip(m, "v");
|
||||||
|
if (r < 0)
|
||||||
|
return bus_log_parse_error(r);
|
||||||
|
}
|
||||||
|
r = sd_bus_message_exit_container(m);
|
||||||
|
if (r < 0)
|
||||||
|
return bus_log_parse_error(r);
|
||||||
|
}
|
||||||
|
if (r < 0)
|
||||||
|
return bus_log_parse_error(r);
|
||||||
|
|
||||||
|
if (set_isempty(c->unit_paths))
|
||||||
|
sd_event_exit(c->event, EXIT_SUCCESS);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int start_unit_one(
|
static int start_unit_one(
|
||||||
sd_bus *bus,
|
sd_bus *bus,
|
||||||
const char *method,
|
const char *method,
|
||||||
const char *name,
|
const char *name,
|
||||||
const char *mode,
|
const char *mode,
|
||||||
sd_bus_error *error,
|
sd_bus_error *error,
|
||||||
BusWaitForJobs *w) {
|
BusWaitForJobs *w,
|
||||||
|
WaitContext *wait_context) {
|
||||||
|
|
||||||
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
|
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
|
||||||
const char *path;
|
const char *path;
|
||||||
@ -2696,6 +2770,40 @@ static int start_unit_one(
|
|||||||
assert(mode);
|
assert(mode);
|
||||||
assert(error);
|
assert(error);
|
||||||
|
|
||||||
|
if (wait_context) {
|
||||||
|
_cleanup_free_ char *unit_path = NULL;
|
||||||
|
const char* mt;
|
||||||
|
|
||||||
|
log_debug("Watching for property changes of %s", name);
|
||||||
|
r = sd_bus_call_method(
|
||||||
|
bus,
|
||||||
|
"org.freedesktop.systemd1",
|
||||||
|
"/org/freedesktop/systemd1",
|
||||||
|
"org.freedesktop.systemd1.Manager",
|
||||||
|
"RefUnit",
|
||||||
|
error,
|
||||||
|
NULL,
|
||||||
|
"s", name);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to RefUnit %s: %s", name, bus_error_message(error, r));
|
||||||
|
|
||||||
|
unit_path = unit_dbus_path_from_name(name);
|
||||||
|
if (!unit_path)
|
||||||
|
return log_oom();
|
||||||
|
|
||||||
|
r = set_put_strdup(wait_context->unit_paths, unit_path);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to add unit path %s to set: %m", unit_path);
|
||||||
|
|
||||||
|
mt = strjoina("type='signal',"
|
||||||
|
"interface='org.freedesktop.DBus.Properties',"
|
||||||
|
"path='", unit_path, "',"
|
||||||
|
"member='PropertiesChanged'");
|
||||||
|
r = sd_bus_add_match(bus, &wait_context->match, mt, on_properties_changed, wait_context);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to add match for PropertiesChanged signal: %m");
|
||||||
|
}
|
||||||
|
|
||||||
log_debug("Calling manager for %s on %s, %s", method, name, mode);
|
log_debug("Calling manager for %s on %s, %s", method, name, mode);
|
||||||
|
|
||||||
r = sd_bus_call_method(
|
r = sd_bus_call_method(
|
||||||
@ -2841,10 +2949,18 @@ static int start_unit(int argc, char *argv[], void *userdata) {
|
|||||||
const char *method, *mode, *one_name, *suffix = NULL;
|
const char *method, *mode, *one_name, *suffix = NULL;
|
||||||
_cleanup_strv_free_ char **names = NULL;
|
_cleanup_strv_free_ char **names = NULL;
|
||||||
sd_bus *bus;
|
sd_bus *bus;
|
||||||
|
_cleanup_(wait_context_free) WaitContext wait_context = {};
|
||||||
char **name;
|
char **name;
|
||||||
int r = 0;
|
int r = 0;
|
||||||
|
|
||||||
r = acquire_bus(BUS_MANAGER, &bus);
|
if (arg_wait && !strstr(argv[0], "start")) {
|
||||||
|
log_error("--wait may only be used with a command that starts units.");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* we cannot do sender tracking on the private bus, so we need the full
|
||||||
|
* one for RefUnit to implement --wait */
|
||||||
|
r = acquire_bus(arg_wait ? BUS_FULL : BUS_MANAGER, &bus);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return r;
|
return r;
|
||||||
|
|
||||||
@ -2888,11 +3004,36 @@ static int start_unit(int argc, char *argv[], void *userdata) {
|
|||||||
return log_error_errno(r, "Could not watch jobs: %m");
|
return log_error_errno(r, "Could not watch jobs: %m");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg_wait) {
|
||||||
|
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||||
|
|
||||||
|
wait_context.unit_paths = set_new(&string_hash_ops);
|
||||||
|
if (!wait_context.unit_paths)
|
||||||
|
return log_oom();
|
||||||
|
|
||||||
|
r = sd_bus_call_method(
|
||||||
|
bus,
|
||||||
|
"org.freedesktop.systemd1",
|
||||||
|
"/org/freedesktop/systemd1",
|
||||||
|
"org.freedesktop.systemd1.Manager",
|
||||||
|
"Subscribe",
|
||||||
|
&error,
|
||||||
|
NULL, NULL);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to enable subscription: %s", bus_error_message(&error, r));
|
||||||
|
r = sd_event_default(&wait_context.event);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to allocate event loop: %m");
|
||||||
|
r = sd_bus_attach_event(bus, wait_context.event, 0);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to attach bus to event loop: %m");
|
||||||
|
}
|
||||||
|
|
||||||
STRV_FOREACH(name, names) {
|
STRV_FOREACH(name, names) {
|
||||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||||
int q;
|
int q;
|
||||||
|
|
||||||
q = start_unit_one(bus, method, *name, mode, &error, w);
|
q = start_unit_one(bus, method, *name, mode, &error, w, arg_wait ? &wait_context : NULL);
|
||||||
if (r >= 0 && q < 0)
|
if (r >= 0 && q < 0)
|
||||||
r = translate_bus_error_to_exit_status(q, &error);
|
r = translate_bus_error_to_exit_status(q, &error);
|
||||||
}
|
}
|
||||||
@ -2924,6 +3065,15 @@ static int start_unit(int argc, char *argv[], void *userdata) {
|
|||||||
check_triggering_units(bus, *name);
|
check_triggering_units(bus, *name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (r >= 0 && arg_wait) {
|
||||||
|
int q;
|
||||||
|
q = sd_event_loop(wait_context.event);
|
||||||
|
if (q < 0)
|
||||||
|
return log_error_errno(q, "Failed to run event loop: %m");
|
||||||
|
if (wait_context.any_failed)
|
||||||
|
r = EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6587,6 +6737,7 @@ static void systemctl_help(void) {
|
|||||||
" -s --signal=SIGNAL Which signal to send\n"
|
" -s --signal=SIGNAL Which signal to send\n"
|
||||||
" --now Start or stop unit in addition to enabling or disabling it\n"
|
" --now Start or stop unit in addition to enabling or disabling it\n"
|
||||||
" -q --quiet Suppress output\n"
|
" -q --quiet Suppress output\n"
|
||||||
|
" --wait For (re)start, wait until service stopped again\n"
|
||||||
" --no-block Do not wait until operation finished\n"
|
" --no-block Do not wait until operation finished\n"
|
||||||
" --no-wall Don't send wall message before halt/power-off/reboot\n"
|
" --no-wall Don't send wall message before halt/power-off/reboot\n"
|
||||||
" --no-reload Don't reload daemon after en-/dis-abling unit files\n"
|
" --no-reload Don't reload daemon after en-/dis-abling unit files\n"
|
||||||
@ -6857,6 +7008,7 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
|
|||||||
ARG_FIRMWARE_SETUP,
|
ARG_FIRMWARE_SETUP,
|
||||||
ARG_NOW,
|
ARG_NOW,
|
||||||
ARG_MESSAGE,
|
ARG_MESSAGE,
|
||||||
|
ARG_WAIT,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const struct option options[] = {
|
static const struct option options[] = {
|
||||||
@ -6880,6 +7032,7 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
|
|||||||
{ "user", no_argument, NULL, ARG_USER },
|
{ "user", no_argument, NULL, ARG_USER },
|
||||||
{ "system", no_argument, NULL, ARG_SYSTEM },
|
{ "system", no_argument, NULL, ARG_SYSTEM },
|
||||||
{ "global", no_argument, NULL, ARG_GLOBAL },
|
{ "global", no_argument, NULL, ARG_GLOBAL },
|
||||||
|
{ "wait", no_argument, NULL, ARG_WAIT },
|
||||||
{ "no-block", no_argument, NULL, ARG_NO_BLOCK },
|
{ "no-block", no_argument, NULL, ARG_NO_BLOCK },
|
||||||
{ "no-legend", no_argument, NULL, ARG_NO_LEGEND },
|
{ "no-legend", no_argument, NULL, ARG_NO_LEGEND },
|
||||||
{ "no-pager", no_argument, NULL, ARG_NO_PAGER },
|
{ "no-pager", no_argument, NULL, ARG_NO_PAGER },
|
||||||
@ -7060,6 +7213,10 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
|
|||||||
arg_scope = UNIT_FILE_GLOBAL;
|
arg_scope = UNIT_FILE_GLOBAL;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ARG_WAIT:
|
||||||
|
arg_wait = true;
|
||||||
|
break;
|
||||||
|
|
||||||
case ARG_NO_BLOCK:
|
case ARG_NO_BLOCK:
|
||||||
arg_no_block = true;
|
arg_no_block = true;
|
||||||
break;
|
break;
|
||||||
@ -7235,6 +7392,11 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
|
|||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg_wait && arg_no_block) {
|
||||||
|
log_error("--wait may not be combined with --no-block.");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,4 +49,32 @@ systemctl stop --job-mode=replace-irreversibly unstoppable.service || exit 1
|
|||||||
# Shutdown of the container/VM will hang if not.
|
# Shutdown of the container/VM will hang if not.
|
||||||
systemctl start unstoppable.service || exit 1
|
systemctl start unstoppable.service || exit 1
|
||||||
|
|
||||||
|
# Test waiting for a started unit(s) to terminate again
|
||||||
|
cat <<EOF > /run/systemd/system/wait2.service
|
||||||
|
[Unit]
|
||||||
|
Description=Wait for 2 seconds
|
||||||
|
[Service]
|
||||||
|
ExecStart=/bin/sh -ec 'sleep 2'
|
||||||
|
EOF
|
||||||
|
cat <<EOF > /run/systemd/system/wait5fail.service
|
||||||
|
[Unit]
|
||||||
|
Description=Wait for 5 seconds and fail
|
||||||
|
[Service]
|
||||||
|
ExecStart=/bin/sh -ec 'sleep 5; false'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# wait2 succeeds
|
||||||
|
START_SEC=$(date -u '+%s')
|
||||||
|
systemctl start --wait wait2.service || exit 1
|
||||||
|
END_SEC=$(date -u '+%s')
|
||||||
|
ELAPSED=$(($END_SEC-$START_SEC))
|
||||||
|
[[ "$ELAPSED" -ge 2 ]] && [[ "$ELAPSED" -le 3 ]] || exit 1
|
||||||
|
|
||||||
|
# wait5fail fails, so systemctl should fail
|
||||||
|
START_SEC=$(date -u '+%s')
|
||||||
|
! systemctl start --wait wait2.service wait5fail.service || exit 1
|
||||||
|
END_SEC=$(date -u '+%s')
|
||||||
|
ELAPSED=$(($END_SEC-$START_SEC))
|
||||||
|
[[ "$ELAPSED" -ge 5 ]] && [[ "$ELAPSED" -le 7 ]] || exit 1
|
||||||
|
|
||||||
touch /testok
|
touch /testok
|
||||||
|
Loading…
Reference in New Issue
Block a user