From 7732cecb1e48f94e1f11d4be1d51975987781099 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Thu, 22 May 2025 20:24:08 +0200 Subject: [PATCH 1/7] core/unit: correct indentation Follow-up for 54e1f676a2d3fa8c39beac84fe5027d9276d491d --- src/core/unit.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/unit.c b/src/core/unit.c index 1d23d24cc8..2906675202 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -6332,7 +6332,7 @@ void unit_next_freezer_state(Unit *u, FreezerAction action, FreezerState *ret_ne * if its parent is frozen. So we instead "demote" a normal freeze into a freeze * initiated by parent if the parent is frozen */ if (IN_SET(current, FREEZER_RUNNING, FREEZER_THAWING, - FREEZER_FREEZING_BY_PARENT, FREEZER_FROZEN_BY_PARENT)) /* Should usually be refused by unit_freezer_action */ + FREEZER_FREEZING_BY_PARENT, FREEZER_FROZEN_BY_PARENT)) /* Should usually be refused by unit_freezer_action */ next = current; else if (current == FREEZER_FREEZING) { if (IN_SET(parent, FREEZER_RUNNING, FREEZER_THAWING)) From 7707eb8fc2fa672c8938cb1a2270772174b9492f Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Fri, 16 May 2025 18:17:18 +0200 Subject: [PATCH 2/7] core/socket: use ERROR_IS_NEG_* at one more place --- src/core/socket.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core/socket.c b/src/core/socket.c index e5cff0de76..1765e6f9c6 100644 --- a/src/core/socket.c +++ b/src/core/socket.c @@ -2412,12 +2412,11 @@ static void socket_enter_running(Socket *s, int cfd_in) { if (s->max_connections_per_source > 0) { r = socket_acquire_peer(s, cfd, &p); - if (r < 0) { - if (ERRNO_IS_DISCONNECT(r)) - return; + if (ERRNO_IS_NEG_DISCONNECT(r)) + return; + if (r < 0) /* We didn't have enough resources to acquire peer information, let's fail. */ goto fail; - } if (r > 0 && p->n_ref > s->max_connections_per_source) { _cleanup_free_ char *t = NULL; From 286c61732f546771cc7ac6b556e83d64260a676e Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Mon, 30 Jun 2025 12:47:21 +0200 Subject: [PATCH 3/7] units/initrd-cleanup.service: drop --job-mode=replace-irreversibly hack This partially reverts d766c75acdd825abe1bcfd45406ae2e609a008b6. The offending commit tries to block systemd-udevd.service from being activated during switch-root, but it is a dirty hack and causes problems with e.g. Ctrl-Alt-Delete handling which actually need to start a conflicting target. Let's revert this here, and the original issue will be resolved in a cleaner fashion in later commits. --- units/initrd-cleanup.service | 1 - 1 file changed, 1 deletion(-) diff --git a/units/initrd-cleanup.service b/units/initrd-cleanup.service index 9c7914e341..08ab503962 100644 --- a/units/initrd-cleanup.service +++ b/units/initrd-cleanup.service @@ -18,4 +18,3 @@ After=initrd-root-fs.target initrd-fs.target initrd.target [Service] Type=oneshot ExecStart=systemctl --no-block isolate initrd-switch-root.target -ExecStart=systemctl --no-block --job-mode=replace-irreversibly start initrd-switch-root.service From 1b9400f2eea3f6484a7b02255d9c74b4757b5392 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sun, 18 May 2025 19:54:51 +0200 Subject: [PATCH 4/7] core/transaction: add job mode "lenient" as an even weaker version of _FAIL --- man/systemctl.xml | 11 +++++++---- src/basic/unit-def.c | 1 + src/basic/unit-def.h | 1 + src/core/transaction.c | 29 +++++++++++++++++++++-------- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/man/systemctl.xml b/man/systemctl.xml index 037410cd21..08cfb5421b 100644 --- a/man/systemctl.xml +++ b/man/systemctl.xml @@ -2197,6 +2197,7 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err When queuing a new job, this option controls how to deal with already queued jobs. It takes one of fail, + lenient, replace, replace-irreversibly, isolate, @@ -2209,10 +2210,12 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err isolate command is used which implies the isolate job mode. - If fail is specified and a requested - operation conflicts with a pending job (more specifically: - causes an already pending start job to be reversed into a stop - job or vice versa), cause the operation to fail. + If fail is specified and a requested operation on weak dependencies + conflicts with a pending job (more specifically: causes an already pending start job to be reversed + into a stop job or vice versa), cause the operation to fail. + + If lenient is specified and a requested operation conflicts with any + active/activating unit, cause the operation to fail. If replace (the default) is specified, any conflicting pending job will be replaced, as diff --git a/src/basic/unit-def.c b/src/basic/unit-def.c index 89c7f93790..45911fb190 100644 --- a/src/basic/unit-def.c +++ b/src/basic/unit-def.c @@ -341,6 +341,7 @@ DEFINE_STRING_TABLE_LOOKUP(notify_access, NotifyAccess); static const char* const job_mode_table[_JOB_MODE_MAX] = { [JOB_FAIL] = "fail", + [JOB_LENIENT] = "lenient", [JOB_REPLACE] = "replace", [JOB_REPLACE_IRREVERSIBLY] = "replace-irreversibly", [JOB_ISOLATE] = "isolate", diff --git a/src/basic/unit-def.h b/src/basic/unit-def.h index a8779769e3..f82d072ed5 100644 --- a/src/basic/unit-def.h +++ b/src/basic/unit-def.h @@ -282,6 +282,7 @@ typedef enum NotifyAccess { typedef enum JobMode { JOB_FAIL, /* Fail if a conflicting job is already queued */ + JOB_LENIENT, /* Fail if any conflicting unit is active (even weaker than JOB_FAIL) */ JOB_REPLACE, /* Replace an existing conflicting job */ JOB_REPLACE_IRREVERSIBLY, /* Like JOB_REPLACE + produce irreversible jobs */ JOB_ISOLATE, /* Start a unit, and stop all others */ diff --git a/src/core/transaction.c b/src/core/transaction.c index 1fe3c950e2..1e5b88af4d 100644 --- a/src/core/transaction.c +++ b/src/core/transaction.c @@ -565,7 +565,7 @@ static int transaction_is_destructive(Transaction *tr, JobMode mode, sd_bus_erro assert(!j->transaction_prev); assert(!j->transaction_next); - if (j->unit->job && (mode == JOB_FAIL || j->unit->job->irreversible) && + if (j->unit->job && (IN_SET(mode, JOB_FAIL, JOB_LENIENT) || j->unit->job->irreversible) && job_type_is_conflicting(j->unit->job->type, j->type)) return sd_bus_error_setf(e, BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE, "Transaction for %s/%s is destructive (%s has '%s' job queued, but '%s' is included in transaction).", @@ -576,7 +576,7 @@ static int transaction_is_destructive(Transaction *tr, JobMode mode, sd_bus_erro return 0; } -static void transaction_minimize_impact(Transaction *tr) { +static int transaction_minimize_impact(Transaction *tr, JobMode mode, sd_bus_error *e) { Job *head; assert(tr); @@ -584,13 +584,16 @@ static void transaction_minimize_impact(Transaction *tr) { /* Drops all unnecessary jobs that reverse already active jobs * or that stop a running service. */ + if (!IN_SET(mode, JOB_FAIL, JOB_LENIENT)) + return 0; + rescan: HASHMAP_FOREACH(head, tr->jobs) { LIST_FOREACH(transaction, j, head) { bool stops_running_service, changes_existing_job; /* If it matters, we shouldn't drop it */ - if (j->matters_to_anchor) + if (j->matters_to_anchor && mode != JOB_LENIENT) continue; /* Would this stop a running service? @@ -607,6 +610,13 @@ rescan: if (!stops_running_service && !changes_existing_job) continue; + if (j->matters_to_anchor) { + assert(mode == JOB_LENIENT); + return sd_bus_error_setf(e, BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE, + "%s/%s would stop a running unit or change existing job, bailing", + j->unit->id, job_type_to_string(j->type)); + } + if (stops_running_service) log_unit_debug(j->unit, "%s/%s would stop a running service.", @@ -626,6 +636,8 @@ rescan: goto rescan; } } + + return 0; } static int transaction_apply( @@ -734,11 +746,12 @@ int transaction_activate( /* First step: figure out which jobs matter */ transaction_find_jobs_that_matter_to_anchor(tr->anchor_job, generation++); - /* Second step: Try not to stop any running services if - * we don't have to. Don't try to reverse running - * jobs if we don't have to. */ - if (mode == JOB_FAIL) - transaction_minimize_impact(tr); + /* Second step: Try not to stop any running services if we don't have to. Don't try to reverse + * running jobs if we don't have to. */ + r = transaction_minimize_impact(tr, mode, e); + if (r < 0) + return r; /* Note that we don't log here, because for JOB_LENIENT conflicts are very much expected + and shouldn't appear to be fatal for the unit. Only inform the caller via bus error. */ /* Third step: Drop redundant jobs */ transaction_drop_redundant(tr); From 1b4ab5a209c4874e01cf9577c740b82567458b09 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Fri, 16 May 2025 18:10:46 +0200 Subject: [PATCH 5/7] core/socket: introduce DeferTrigger= and DeferTriggerMaxSec= Alternative to b50f6dbe574b0421db7dbf200ad951186382277d The commit naively returned early from socket_enter_running(), which however is quite problematic, as the socket will be woken up over and over again without doing a thing, until we eventually hit Poll/TriggerLimit*=. On top of that it requires hacks to hold the start job for initrd-switch-root.service up. Overall I doubt that is the right approach. Let's instead hook this into our job engine, and try to activate the service again when some other units are stopped. If all installed jobs have been run yet we're still seeing the conflict or the manually selected timeout is reached, fail the socket as before. --- man/org.freedesktop.systemd1.xml | 18 +++- man/systemd.socket.xml | 31 ++++++ src/basic/unit-def.c | 1 + src/basic/unit-def.h | 1 + src/core/dbus-socket.c | 10 ++ src/core/job.c | 4 + src/core/load-fragment-gperf.gperf.in | 2 + src/core/load-fragment.c | 1 + src/core/load-fragment.h | 1 + src/core/manager.c | 29 ++++++ src/core/manager.h | 4 + src/core/socket.c | 136 ++++++++++++++++++++++++-- src/core/socket.h | 14 +++ src/core/unit.c | 27 +++++ src/core/unit.h | 9 ++ src/shared/bus-unit-util.c | 6 +- 16 files changed, 282 insertions(+), 12 deletions(-) diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index 95f9a4f449..3cf0e18709 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -4962,6 +4962,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { readonly t PollLimitIntervalUSec = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u PollLimitBurst = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s DeferTrigger = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly t DeferTriggerMaxUSec = ...; readonly u UID = ...; readonly u GID = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") @@ -5626,6 +5630,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + + + @@ -6244,6 +6252,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + + + @@ -12112,8 +12124,10 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ ProtectHostnameEx, PassPIDFD, AcceptFileDescriptors, - DelegateNamespaces, and - RemoveSubgroup() were added in version 258. + DelegateNamespaces, + RemoveSubgroup(), + DeferTrigger, and + DeferTriggerMaxUSec were added in version 258. Mount Unit Objects diff --git a/man/systemd.socket.xml b/man/systemd.socket.xml index 91552d691f..213ceffcdb 100644 --- a/man/systemd.socket.xml +++ b/man/systemd.socket.xml @@ -956,6 +956,37 @@ + + DeferTrigger= + + Takes a boolean argument, or patient. May only be used when Accept=no. + If enabled, job mode lenient instead of replace is used when + triggering the service, which means currently activating/running units that conflict with the service + won't be disturbed/brought down. Furthermore, if a conflict exists, the socket unit will wait for + current job queue to complete and potentially defer the activation by then. An upper limit of total time + to wait can be configured via DeferTriggerMaxSec=. If set to , + the socket unit will fail if all jobs have finished or the timeout has been reached but the conflict remains. + If , always wait until DeferTriggerMaxSec= elapses. + Defaults to no. + + This setting is particularly useful if the socket unit should stay active across switch-root/soft-reboot + operations while the triggered service is stopped. + + + + + + DeferTriggerMaxSec= + + Configures the maximum time to defer the triggering when DeferTrigger= + is enabled. If the service cannot be activated within the specified time, the socket will be considered + failed and get terminated. Takes a unit-less value in seconds, or a time span value such as "5min 20s". + Pass 0 or infinity to disable the timeout logic (the default). + + + + + PassFileDescriptorsToExec= diff --git a/src/basic/unit-def.c b/src/basic/unit-def.c index 45911fb190..8bc5f56532 100644 --- a/src/basic/unit-def.c +++ b/src/basic/unit-def.c @@ -250,6 +250,7 @@ static const char* const socket_state_table[_SOCKET_STATE_MAX] = { [SOCKET_START_CHOWN] = "start-chown", [SOCKET_START_POST] = "start-post", [SOCKET_LISTENING] = "listening", + [SOCKET_DEFERRED] = "deferred", [SOCKET_RUNNING] = "running", [SOCKET_STOP_PRE] = "stop-pre", [SOCKET_STOP_PRE_SIGTERM] = "stop-pre-sigterm", diff --git a/src/basic/unit-def.h b/src/basic/unit-def.h index f82d072ed5..fd55950ea6 100644 --- a/src/basic/unit-def.h +++ b/src/basic/unit-def.h @@ -169,6 +169,7 @@ typedef enum SocketState { SOCKET_START_CHOWN, SOCKET_START_POST, SOCKET_LISTENING, + SOCKET_DEFERRED, SOCKET_RUNNING, SOCKET_STOP_PRE, SOCKET_STOP_PRE_SIGTERM, diff --git a/src/core/dbus-socket.c b/src/core/dbus-socket.c index de96f00a15..02039dd6db 100644 --- a/src/core/dbus-socket.c +++ b/src/core/dbus-socket.c @@ -21,6 +21,7 @@ static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_result, socket_result, SocketRe static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_bind_ipv6_only, socket_address_bind_ipv6_only, SocketAddressBindIPv6Only); static BUS_DEFINE_PROPERTY_GET(property_get_fdname, "s", Socket, socket_fdname); static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_timestamping, socket_timestamping, SocketTimestamping); +static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_defer_trigger, socket_defer_trigger, SocketDeferTrigger); static int property_get_listen( sd_bus *bus, @@ -115,6 +116,8 @@ const sd_bus_vtable bus_socket_vtable[] = { SD_BUS_PROPERTY("TriggerLimitBurst", "u", bus_property_get_unsigned, offsetof(Socket, trigger_limit.burst), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("PollLimitIntervalUSec", "t", bus_property_get_usec, offsetof(Socket, poll_limit.interval), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("PollLimitBurst", "u", bus_property_get_unsigned, offsetof(Socket, poll_limit.burst), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("DeferTrigger", "s", property_get_defer_trigger, offsetof(Socket, defer_trigger), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("DeferTriggerMaxUSec", "t", bus_property_get_usec, offsetof(Socket, defer_trigger_max_usec), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("UID", "u", bus_property_get_uid, offsetof(Unit, ref_uid), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("GID", "u", bus_property_get_gid, offsetof(Unit, ref_gid), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("PassFileDescriptorsToExec", "b", bus_property_get_bool, offsetof(Socket, pass_fds_to_exec), SD_BUS_VTABLE_PROPERTY_CONST), @@ -148,6 +151,7 @@ static BUS_DEFINE_SET_TRANSIENT_STRING_WITH_CHECK(ifname, ifname_valid); static BUS_DEFINE_SET_TRANSIENT_TO_STRING_ALLOC(ip_tos, "i", int32_t, int, "%" PRIi32, ip_tos_to_string_alloc); static BUS_DEFINE_SET_TRANSIENT_TO_STRING(socket_protocol, "i", int32_t, int, "%" PRIi32, socket_protocol_to_string); static BUS_DEFINE_SET_TRANSIENT_PARSE(socket_timestamping, SocketTimestamping, socket_timestamping_from_string_harder); +static BUS_DEFINE_SET_TRANSIENT_PARSE(socket_defer_trigger, SocketDeferTrigger, socket_defer_trigger_from_string); static int bus_socket_set_transient_property( Socket *s, @@ -274,6 +278,12 @@ static int bus_socket_set_transient_property( if (streq(name, "PollLimitIntervalUSec")) return bus_set_transient_usec(u, name, &s->poll_limit.interval, message, flags, error); + if (streq(name, "DeferTrigger")) + return bus_set_transient_socket_defer_trigger(u, name, &s->defer_trigger, message, flags, error); + + if (streq(name, "DeferTriggerMaxUSec")) + return bus_set_transient_usec_fix_0(u, name, &s->defer_trigger_max_usec, message, flags, error); + if (streq(name, "SmackLabel")) return bus_set_transient_string(u, name, &s->smack, message, flags, error); diff --git a/src/core/job.c b/src/core/job.c index a60025cffc..bac12542dc 100644 --- a/src/core/job.c +++ b/src/core/job.c @@ -1108,6 +1108,10 @@ finish: unit_submit_to_stop_when_bound_queue(u); unit_submit_to_stop_when_unneeded_queue(u); + /* All jobs might have finished, let's see */ + if (u->manager->may_dispatch_stop_notify_queue == 0) + u->manager->may_dispatch_stop_notify_queue = -1; + manager_check_finished(u->manager); return 0; diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index aacc76d053..96fe04aaf4 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -528,6 +528,8 @@ Socket.TriggerLimitIntervalSec, config_parse_sec, Socket.TriggerLimitBurst, config_parse_unsigned, 0, offsetof(Socket, trigger_limit.burst) Socket.PollLimitIntervalSec, config_parse_sec, 0, offsetof(Socket, poll_limit.interval) Socket.PollLimitBurst, config_parse_unsigned, 0, offsetof(Socket, poll_limit.burst) +Socket.DeferTrigger, config_parse_socket_defer_trigger, 0, offsetof(Socket, defer_trigger) +Socket.DeferTriggerMaxSec, config_parse_sec_fix_0, 0, offsetof(Socket, defer_trigger_max_usec) {% if ENABLE_SMACK %} Socket.SmackLabel, config_parse_unit_string_printf, 0, offsetof(Socket, smack) Socket.SmackLabelIPIn, config_parse_unit_string_printf, 0, offsetof(Socket, smack_ip_in) diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index bed01029dc..a6cb3c7e88 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -159,6 +159,7 @@ DEFINE_CONFIG_PARSE_PTR(config_parse_exec_mount_propagation_flag, mount_propagat DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_numa_policy, mpol, int, -1); DEFINE_CONFIG_PARSE_ENUM(config_parse_status_unit_format, status_unit_format, StatusUnitFormat); DEFINE_CONFIG_PARSE_ENUM_FULL(config_parse_socket_timestamping, socket_timestamping_from_string_harder, SocketTimestamping); +DEFINE_CONFIG_PARSE_ENUM(config_parse_socket_defer_trigger, socket_defer_trigger, SocketDeferTrigger); bool contains_instance_specifier_superset(const char *s) { const char *p, *q; diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index d87bff0996..e2fe7dce1b 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -148,6 +148,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_timeout_abort); CONFIG_PARSER_PROTOTYPE(config_parse_swap_priority); CONFIG_PARSER_PROTOTYPE(config_parse_mount_images); CONFIG_PARSER_PROTOTYPE(config_parse_socket_timestamping); +CONFIG_PARSER_PROTOTYPE(config_parse_socket_defer_trigger); CONFIG_PARSER_PROTOTYPE(config_parse_extension_images); CONFIG_PARSER_PROTOTYPE(config_parse_bpf_foreign_program); CONFIG_PARSER_PROTOTYPE(config_parse_cgroup_socket_bind); diff --git a/src/core/manager.c b/src/core/manager.c index 000f19d6f6..aa43c9d79b 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -1597,6 +1597,32 @@ static unsigned manager_dispatch_stop_when_bound_queue(Manager *m) { return n; } +static unsigned manager_dispatch_stop_notify_queue(Manager *m) { + unsigned n = 0; + + assert(m); + + if (m->may_dispatch_stop_notify_queue < 0) + m->may_dispatch_stop_notify_queue = hashmap_isempty(m->jobs); + + if (!m->may_dispatch_stop_notify_queue) + return 0; + + m->may_dispatch_stop_notify_queue = false; + + LIST_FOREACH(stop_notify_queue, u, m->stop_notify_queue) { + assert(u->in_stop_notify_queue); + + assert(UNIT_VTABLE(u)->stop_notify); + if (UNIT_VTABLE(u)->stop_notify(u)) { + assert(!u->in_stop_notify_queue); + n++; + } + } + + return n; +} + static void manager_clear_jobs_and_units(Manager *m) { Unit *u; @@ -3274,6 +3300,9 @@ int manager_loop(Manager *m) { if (manager_dispatch_release_resources_queue(m) > 0) continue; + if (manager_dispatch_stop_notify_queue(m) > 0) + continue; + if (manager_dispatch_dbus_queue(m) > 0) continue; diff --git a/src/core/manager.h b/src/core/manager.h index 39f822f4e2..c267ebe7ee 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -211,6 +211,9 @@ typedef struct Manager { /* Units that have resources open, and where it might be good to check if they can be released now */ LIST_HEAD(Unit, release_resources_queue); + /* Units that perform certain actions after some other unit deactivates */ + LIST_HEAD(Unit, stop_notify_queue); + sd_event *event; /* This maps PIDs we care about to units that are interested in them. We allow multiple units to be @@ -358,6 +361,7 @@ typedef struct Manager { /* Flags */ bool dispatching_load_queue; + int may_dispatch_stop_notify_queue; /* tristate */ /* Have we already sent out the READY=1 notification? */ bool ready_sent; diff --git a/src/core/socket.c b/src/core/socket.c index 1765e6f9c6..727840e678 100644 --- a/src/core/socket.c +++ b/src/core/socket.c @@ -12,6 +12,7 @@ #include "alloc-util.h" #include "bpf-program.h" +#include "bus-common-errors.h" #include "bus-error.h" #include "copy.h" #include "dbus-socket.h" @@ -69,6 +70,7 @@ static const UnitActiveState state_translation_table[_SOCKET_STATE_MAX] = { [SOCKET_START_CHOWN] = UNIT_ACTIVATING, [SOCKET_START_POST] = UNIT_ACTIVATING, [SOCKET_LISTENING] = UNIT_ACTIVE, + [SOCKET_DEFERRED] = UNIT_ACTIVE, [SOCKET_RUNNING] = UNIT_ACTIVE, [SOCKET_STOP_PRE] = UNIT_DEACTIVATING, [SOCKET_STOP_PRE_SIGTERM] = UNIT_DEACTIVATING, @@ -144,6 +146,8 @@ static void socket_init(Unit *u) { s->trigger_limit = RATELIMIT_OFF; s->poll_limit = RATELIMIT_OFF; + + s->defer_trigger_max_usec = USEC_INFINITY; } static void socket_unwatch_control_pid(Socket *s) { @@ -427,6 +431,9 @@ static int socket_verify(Socket *s) { if (s->accept && UNIT_ISSET(s->service)) return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Explicit service configuration for accepting socket units not supported. Refusing."); + if (s->accept && s->defer_trigger != SOCKET_DEFER_NO) + return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Socket unit is configured to be accepting with DeferTrigger= enabled. Refusing."); + if (!strv_isempty(s->symlinks) && !socket_find_symlink_target(s)) return log_unit_error_errno(UNIT(s), SYNTHETIC_ERRNO(ENOEXEC), "Unit has symlinks set but none or more than one node in the file system. Refusing."); @@ -672,8 +679,12 @@ static void socket_dump(Unit *u, FILE *f, const char *prefix) { prefix, s->max_connections_per_source); else fprintf(f, - "%sFlushPending: %s\n", - prefix, yes_no(s->flush_pending)); + "%sFlushPending: %s\n" + "%sDeferTrigger: %s\n" + "%sDeferTriggerMaxSec: %s\n", + prefix, yes_no(s->flush_pending), + prefix, socket_defer_trigger_to_string(s->defer_trigger), + prefix, FORMAT_TIMESPAN(s->defer_trigger_max_usec, USEC_PER_SEC)); if (s->priority >= 0) fprintf(f, @@ -1852,8 +1863,10 @@ static void socket_set_state(Socket *s, SocketState state) { old_state = s->state; s->state = state; - if (!SOCKET_STATE_WITH_PROCESS(state)) { + if (!SOCKET_STATE_WITH_PROCESS(state) && state != SOCKET_DEFERRED) s->timer_event_source = sd_event_source_disable_unref(s->timer_event_source); + + if (!SOCKET_STATE_WITH_PROCESS(state)) { socket_unwatch_control_pid(s); s->control_command = NULL; s->control_command_id = _SOCKET_EXEC_COMMAND_INVALID; @@ -1867,12 +1880,16 @@ static void socket_set_state(Socket *s, SocketState state) { SOCKET_START_CHOWN, SOCKET_START_POST, SOCKET_LISTENING, + SOCKET_DEFERRED, SOCKET_RUNNING, SOCKET_STOP_PRE, SOCKET_STOP_PRE_SIGTERM, SOCKET_STOP_PRE_SIGKILL)) socket_close_fds(s); + if (state != SOCKET_DEFERRED) + unit_remove_from_stop_notify_queue(UNIT(s)); + if (state != old_state) log_unit_debug(UNIT(s), "Changed %s -> %s", socket_state_to_string(old_state), socket_state_to_string(state)); @@ -1888,6 +1905,11 @@ static int socket_coldplug(Unit *u) { if (s->deserialized_state == s->state) return 0; + /* Patch "deferred" back to "listening" and let socket_enter_running() figure out what to do. + * This saves us the trouble of handling flipping of DeferTrigger= vs Accept= during reload. */ + if (s->deserialized_state == SOCKET_DEFERRED) + s->deserialized_state = SOCKET_LISTENING; + if (pidref_is_set(&s->control_pid) && pidref_is_unwaited(&s->control_pid) > 0 && SOCKET_STATE_WITH_PROCESS(s->deserialized_state)) { @@ -2347,6 +2369,76 @@ static void socket_enter_start_pre(Socket *s) { socket_enter_start_open(s); } +static bool socket_may_defer(Socket *s) { + assert(s); + + switch (s->defer_trigger) { + + case SOCKET_DEFER_NO: + return false; + + case SOCKET_DEFER_YES: + return !hashmap_isempty(UNIT(s)->manager->jobs); + + case SOCKET_DEFER_PATIENT: + assert(s->defer_trigger_max_usec > 0); + return true; + + default: + assert_not_reached(); + } +} + +static bool socket_stop_notify(Unit *u) { + Socket *s = ASSERT_PTR(SOCKET(u)); + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(s->state == SOCKET_DEFERRED); + + r = manager_add_job(u->manager, JOB_START, UNIT_DEREF(s->service), JOB_LENIENT, &error, /* ret = */ NULL); + if (r >= 0) { /* Yay! */ + socket_set_state(s, SOCKET_RUNNING); + return true; /* changed */ + } + if (sd_bus_error_has_name(&error, BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE)) { + if (s->defer_trigger == SOCKET_DEFER_PATIENT || !hashmap_isempty(u->manager->jobs)) + /* Wait for some more */ + return false; + + log_unit_warning_errno(u, r, "Service conflicts with active units even after all jobs have completed, giving up."); + } else + log_unit_warning_errno(u, r, "Failed to queue service startup job: %s", bus_error_message(&error, r)); + + socket_enter_stop_pre(s, SOCKET_FAILURE_RESOURCES); + return true; /* changed */ +} + +static void socket_enter_deferred(Socket *s) { + int r; + + assert(s); + assert(socket_may_defer(s)); + + /* So here's the thing: if there're currently units conflicting with the service we shall be + * triggering, and the previous transaction is still running (job pool is not empty), let's + * defer the activation a bit, and recheck upon any unit stop. IOW, the trigger in question + * becomes bound to the conflicting dependency, and not the socket IO because we never process them. + * Put a safety net around all this though, i.e. give up if the service still can't be started + * even after all existing jobs have completed, or DeferTriggerMaxSec= is reached. */ + + r = socket_arm_timer(s, /* relative = */ true, s->defer_trigger_max_usec); + if (r < 0) { + log_unit_warning_errno(UNIT(s), r, "Failed to install timer: %m"); + return socket_enter_stop_pre(s, SOCKET_FAILURE_RESOURCES); + } + + unit_add_to_stop_notify_queue(UNIT(s)); + + /* Disable IO event sources */ + socket_set_state(s, SOCKET_DEFERRED); +} + static void socket_enter_running(Socket *s, int cfd_in) { /* Note that this call takes possession of the connection fd passed. It either has to assign it * somewhere or close it. */ @@ -2368,6 +2460,11 @@ static void socket_enter_running(Socket *s, int cfd_in) { return; } + if (s->state == SOCKET_DEFERRED) { + assert(cfd < 0); + return; + } + if (!ratelimit_below(&s->trigger_limit)) { log_unit_warning(UNIT(s), "Trigger limit hit, refusing further activation."); socket_enter_stop_pre(s, SOCKET_FAILURE_TRIGGER_LIMIT_HIT); @@ -2392,9 +2489,17 @@ static void socket_enter_running(Socket *s, int cfd_in) { goto fail; } - r = manager_add_job(UNIT(s)->manager, JOB_START, UNIT_DEREF(s->service), JOB_REPLACE, &error, /* ret = */ NULL); - if (r == -EDEADLK) - return (void) log_unit_debug_errno(UNIT(s), r, "Failed to queue service startup job, ignoring: %s", bus_error_message(&error, r)); + if (s->defer_trigger != SOCKET_DEFER_NO) { + r = manager_add_job(UNIT(s)->manager, JOB_START, UNIT_DEREF(s->service), JOB_LENIENT, &error, /* ret = */ NULL); + if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE) && socket_may_defer(s)) + /* We only check BUS_ERROR_TRANSACTION_IS_DESTRUCTIVE here, not + * BUS_ERROR_TRANSACTION_JOBS_CONFLICTING or BUS_ERROR_TRANSACTION_ORDER_IS_CYCLIC, + * since those are errors in a single transaction, which are most likely + * caused by dependency issues in the unit configuration. + * Deferring activation probabaly won't help. */ + return socket_enter_deferred(s); + } else + r = manager_add_job(UNIT(s)->manager, JOB_START, UNIT_DEREF(s->service), JOB_REPLACE, &error, /* ret = */ NULL); if (r < 0) goto queue_error; } @@ -2598,7 +2703,7 @@ static int socket_stop(Unit *u) { return 0; } - assert(IN_SET(s->state, SOCKET_LISTENING, SOCKET_RUNNING)); + assert(IN_SET(s->state, SOCKET_LISTENING, SOCKET_DEFERRED, SOCKET_RUNNING)); socket_enter_stop_pre(s, SOCKET_SUCCESS); return 1; @@ -3285,6 +3390,11 @@ static int socket_dispatch_timer(sd_event_source *source, usec_t usec, void *use socket_enter_stop_pre(s, SOCKET_FAILURE_TIMEOUT); break; + case SOCKET_DEFERRED: + log_unit_warning(UNIT(s), "DeferTriggerMaxSec= elapsed. Stopping."); + socket_enter_stop_pre(s, SOCKET_FAILURE_TIMEOUT); + break; + case SOCKET_STOP_PRE: log_unit_warning(UNIT(s), "Stopping timed out. Terminating."); socket_enter_signal(s, SOCKET_STOP_PRE_SIGTERM, SOCKET_FAILURE_TIMEOUT); @@ -3414,7 +3524,7 @@ static void socket_trigger_notify(Unit *u, Unit *other) { Service *service = ASSERT_PTR(SERVICE(other)); /* Don't propagate state changes from the service if we are already down */ - if (!IN_SET(s->state, SOCKET_RUNNING, SOCKET_LISTENING)) + if (!IN_SET(s->state, SOCKET_RUNNING, SOCKET_LISTENING, SOCKET_DEFERRED)) return; /* We don't care for the service state if we are in Accept=yes mode */ @@ -3610,6 +3720,14 @@ SocketTimestamping socket_timestamping_from_string_harder(const char *p) { return r ? SOCKET_TIMESTAMPING_NS : SOCKET_TIMESTAMPING_OFF; /* If boolean yes, default to ns accuracy */ } +static const char* const socket_defer_trigger_table[_SOCKET_DEFER_MAX] = { + [SOCKET_DEFER_NO] = "no", + [SOCKET_DEFER_YES] = "yes", + [SOCKET_DEFER_PATIENT] = "patient", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(socket_defer_trigger, SocketDeferTrigger, SOCKET_DEFER_YES); + const UnitVTable socket_vtable = { .object_size = sizeof(Socket), .exec_context_offset = offsetof(Socket, exec_context), @@ -3659,6 +3777,8 @@ const UnitVTable socket_vtable = { .trigger_notify = socket_trigger_notify, + .stop_notify = socket_stop_notify, + .reset_failed = socket_reset_failed, .notify_handoff_timestamp = socket_handoff_timestamp, diff --git a/src/core/socket.h b/src/core/socket.h index 70291fb0d2..cec10dece9 100644 --- a/src/core/socket.h +++ b/src/core/socket.h @@ -66,6 +66,14 @@ typedef enum SocketTimestamping { _SOCKET_TIMESTAMPING_INVALID = -EINVAL, } SocketTimestamping; +typedef enum SocketDeferTrigger { + SOCKET_DEFER_NO, + SOCKET_DEFER_YES, + SOCKET_DEFER_PATIENT, + _SOCKET_DEFER_MAX, + _SOCKET_DEFER_INVALID = -EINVAL, +} SocketDeferTrigger; + typedef struct Socket { Unit meta; @@ -165,6 +173,9 @@ typedef struct Socket { RateLimit trigger_limit; RateLimit poll_limit; + + usec_t defer_trigger_max_usec; + SocketDeferTrigger defer_trigger; } Socket; SocketPeer *socket_peer_ref(SocketPeer *p); @@ -205,4 +216,7 @@ const char* socket_timestamping_to_string(SocketTimestamping p) _const_; SocketTimestamping socket_timestamping_from_string(const char *p) _pure_; SocketTimestamping socket_timestamping_from_string_harder(const char *p) _pure_; +const char* socket_defer_trigger_to_string(SocketDeferTrigger i) _const_; +SocketDeferTrigger socket_defer_trigger_from_string(const char *s) _pure_; + DEFINE_CAST(SOCKET, Socket); diff --git a/src/core/unit.c b/src/core/unit.c index 2906675202..9b95834263 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -617,6 +617,28 @@ void unit_submit_to_release_resources_queue(Unit *u) { u->in_release_resources_queue = true; } +void unit_add_to_stop_notify_queue(Unit *u) { + assert(u); + + if (u->in_stop_notify_queue) + return; + + assert(UNIT_VTABLE(u)->stop_notify); + + LIST_PREPEND(stop_notify_queue, u->manager->stop_notify_queue, u); + u->in_stop_notify_queue = true; +} + +void unit_remove_from_stop_notify_queue(Unit *u) { + assert(u); + + if (!u->in_stop_notify_queue) + return; + + LIST_REMOVE(stop_notify_queue, u->manager->stop_notify_queue, u); + u->in_stop_notify_queue = false; +} + static void unit_clear_dependencies(Unit *u) { assert(u); @@ -842,6 +864,8 @@ Unit* unit_free(Unit *u) { if (u->in_release_resources_queue) LIST_REMOVE(release_resources_queue, u->manager->release_resources_queue, u); + unit_remove_from_stop_notify_queue(u); + condition_free_list(u->conditions); condition_free_list(u->asserts); @@ -2804,6 +2828,9 @@ void unit_notify(Unit *u, UnitActiveState os, UnitActiveState ns, bool reload_su /* Maybe the concurrency limits now allow dispatching of another start job in this slice? */ unit_check_concurrency_limit(u); + /* Maybe someone else has been waiting for us to stop? */ + m->may_dispatch_stop_notify_queue = true; + } else if (UNIT_IS_ACTIVE_OR_RELOADING(ns)) { /* Start uphold units regardless if going up was expected or not */ check_uphold_dependencies(u); diff --git a/src/core/unit.h b/src/core/unit.h index d508865748..4c10514726 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -346,6 +346,9 @@ typedef struct Unit { /* Queue of units that should be checked if they can release resources now */ LIST_FIELDS(Unit, release_resources_queue); + /* Queue of units that should be informed when other units stop */ + LIST_FIELDS(Unit, stop_notify_queue); + /* PIDs we keep an eye on. Note that a unit might have many more, but these are the ones we care * enough about to process SIGCHLD for */ Set *pids; /* → PidRef* */ @@ -446,6 +449,7 @@ typedef struct Unit { bool in_start_when_upheld_queue:1; bool in_stop_when_bound_queue:1; bool in_release_resources_queue:1; + bool in_stop_notify_queue:1; bool sent_dbus_new_signal:1; @@ -670,6 +674,9 @@ typedef struct UnitVTable { * state or gains/loses a job */ void (*trigger_notify)(Unit *u, Unit *trigger); + /* Invoked when some other units stop */ + bool (*stop_notify)(Unit *u); + /* Called whenever CLOCK_REALTIME made a jump */ void (*time_change)(Unit *u); @@ -847,6 +854,8 @@ void unit_submit_to_stop_when_unneeded_queue(Unit *u); void unit_submit_to_start_when_upheld_queue(Unit *u); void unit_submit_to_stop_when_bound_queue(Unit *u); void unit_submit_to_release_resources_queue(Unit *u); +void unit_add_to_stop_notify_queue(Unit *u); +void unit_remove_from_stop_notify_queue(Unit *u); int unit_merge(Unit *u, Unit *other); int unit_merge_by_name(Unit *u, const char *other); diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index e51646a294..cc0449dd96 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -2623,7 +2623,8 @@ static int bus_append_socket_property(sd_bus_message *m, const char *field, cons "KeepAliveIntervalSec", "DeferAcceptSec", "TriggerLimitIntervalSec", - "PollLimitIntervalSec")) + "PollLimitIntervalSec", + "DeferTriggerMaxSec")) return bus_append_parse_sec_rename(m, field, eq); if (STR_IN_SET(field, "ReceiveBuffer", @@ -2646,7 +2647,8 @@ static int bus_append_socket_property(sd_bus_message *m, const char *field, cons "FileDescriptorName", "SocketUser", "SocketGroup", - "Timestamping")) + "Timestamping", + "DeferTrigger")) return bus_append_string(m, field, eq); if (streq(field, "Symlinks")) From a2b34106e7a6ae3b721257fb714a05e2f6cbd5dc Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sun, 18 May 2025 02:24:38 +0200 Subject: [PATCH 6/7] units/systemd-{udevd,journald}*.socket: enable DeferTrigger= Refer to d766c75acdd825abe1bcfd45406ae2e609a008b6 for the rationale behind the udevd change. systemd-journald.service conflicts with soft-reboot.target, so make sure anything surviving soft-reboot and trying to log to journal doesn't fail the socket units. --- units/systemd-journald-audit.socket | 1 + units/systemd-journald-dev-log.socket | 1 + units/systemd-journald.socket | 1 + units/systemd-udevd-control.socket | 1 + units/systemd-udevd-kernel.socket | 1 + units/systemd-udevd-varlink.socket | 1 + 6 files changed, 6 insertions(+) diff --git a/units/systemd-journald-audit.socket b/units/systemd-journald-audit.socket index cf9b6e8b84..d0b43c7f42 100644 --- a/units/systemd-journald-audit.socket +++ b/units/systemd-journald-audit.socket @@ -20,6 +20,7 @@ Service=systemd-journald.service ReceiveBuffer=128M ListenNetlink=audit 1 PassCredentials=yes +DeferTrigger=yes [Install] WantedBy=sockets.target diff --git a/units/systemd-journald-dev-log.socket b/units/systemd-journald-dev-log.socket index e2a9352719..aa2a406095 100644 --- a/units/systemd-journald-dev-log.socket +++ b/units/systemd-journald-dev-log.socket @@ -26,6 +26,7 @@ Service=systemd-journald.service SocketMode=0666 Symlinks=/dev/log Timestamping=us +DeferTrigger=yes # Increase both the send and receive buffer, so that things don't # block early. Note that journald internally uses the this socket both diff --git a/units/systemd-journald.socket b/units/systemd-journald.socket index e2ffb96bfc..7b49d43ee8 100644 --- a/units/systemd-journald.socket +++ b/units/systemd-journald.socket @@ -27,3 +27,4 @@ ReceiveBuffer=8M Service=systemd-journald.service SocketMode=0666 Timestamping=us +DeferTrigger=yes diff --git a/units/systemd-udevd-control.socket b/units/systemd-udevd-control.socket index 89304ab65a..942f6a5e8b 100644 --- a/units/systemd-udevd-control.socket +++ b/units/systemd-udevd-control.socket @@ -20,3 +20,4 @@ ListenSequentialPacket=/run/udev/control SocketMode=0600 PassCredentials=yes RemoveOnStop=yes +DeferTrigger=yes diff --git a/units/systemd-udevd-kernel.socket b/units/systemd-udevd-kernel.socket index d337b06ea0..b185716aae 100644 --- a/units/systemd-udevd-kernel.socket +++ b/units/systemd-udevd-kernel.socket @@ -24,3 +24,4 @@ Service=systemd-udevd.service ReceiveBuffer=128M ListenNetlink=kobject-uevent 1 PassCredentials=yes +DeferTrigger=yes diff --git a/units/systemd-udevd-varlink.socket b/units/systemd-udevd-varlink.socket index 94fee15b08..74353e7f1a 100644 --- a/units/systemd-udevd-varlink.socket +++ b/units/systemd-udevd-varlink.socket @@ -20,3 +20,4 @@ ListenStream=/run/udev/io.systemd.Udev FileDescriptorName=varlink SocketMode=0600 RemoveOnStop=yes +DeferTrigger=yes From c950bb3bd75f5e6eeba2c7bd6f87de8a7f515eb7 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sun, 18 May 2025 03:40:14 +0200 Subject: [PATCH 7/7] TEST-07-PID1: add test cases for DeferTrigger= --- test/units/TEST-07-PID1.socket-defer.sh | 151 ++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100755 test/units/TEST-07-PID1.socket-defer.sh diff --git a/test/units/TEST-07-PID1.socket-defer.sh b/test/units/TEST-07-PID1.socket-defer.sh new file mode 100755 index 0000000000..ec04f4f7e4 --- /dev/null +++ b/test/units/TEST-07-PID1.socket-defer.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +UNIT_NAME="TEST-07-PID1-socket-defer-$RANDOM" + +at_exit() { + systemctl stop "$UNIT_NAME*" + rm -f /run/systemd/system/"$UNIT_NAME".socket \ + /run/systemd/system/"$UNIT_NAME"{,-conflict1,-conflict2,-forever}.service +} + +trap at_exit EXIT + +mkdir -p "/tmp/$UNIT_NAME" + +cat >/run/systemd/system/"$UNIT_NAME-conflict1.service" </run/systemd/system/"$UNIT_NAME-conflict2.service" </run/systemd/system/"$UNIT_NAME-forever.service" </run/systemd/system/"$UNIT_NAME.socket" </run/systemd/system/"$UNIT_NAME.service" <>/run/systemd/system/"$UNIT_NAME.socket" +systemctl daemon-reload + +assert_eq "$(systemctl show "$UNIT_NAME.socket" -P SubState)" "running" +rm "/tmp/$UNIT_NAME/flag" +systemctl start --no-block "$UNIT_NAME-conflict1.service" +wait_for_stop +assert_eq "$(systemctl show "$UNIT_NAME-conflict1.service" -P SubState)" "start" + +# Wait in "deferred" state + +socat -u - UNIX-CONNECT:"/tmp/$UNIT_NAME/test" & +timeout 30 bash -c "until [[ \$(systemctl show '$UNIT_NAME.socket' -P SubState) == 'deferred' ]]; do sleep .5; done" +(! systemctl -q is-active "$UNIT_NAME.service") +kill %% +assert_eq "$(systemctl show "$UNIT_NAME-conflict1.service" -P SubState)" "start" + +systemctl daemon-reload +timeout 10 bash -c "until [[ \$(systemctl show '$UNIT_NAME.socket' -P SubState) == 'deferred' ]]; do sleep .2; done" +(! systemctl -q is-active "$UNIT_NAME.service") +assert_eq "$(systemctl show "$UNIT_NAME-conflict1.service" -P SubState)" "start" + +# Activate after conflicting unit exits + +touch "/tmp/$UNIT_NAME/flag" +timeout 30 bash -c "while systemctl -q is-active '$UNIT_NAME-conflict1.service'; do sleep .2; done" +wait_for_start + +# An irrelevant job running, during which one of the conflicting units exits + +systemctl start --no-block "$UNIT_NAME-forever.service" +systemctl start "$UNIT_NAME-conflict2.service" +wait_for_stop + +socat -u - UNIX-CONNECT:"/tmp/$UNIT_NAME/test" & +timeout 30 bash -c "until [[ \$(systemctl show '$UNIT_NAME.socket' -P SubState) == 'deferred' ]]; do sleep .5; done" +(! systemctl -q is-active "$UNIT_NAME.service") +kill %% + +rm "/tmp/$UNIT_NAME/flag" +timeout 30 bash -c "while systemctl -q is-active '$UNIT_NAME-conflict2.service'; do sleep .2; done" +wait_for_start +assert_eq "$(systemctl show "$UNIT_NAME-forever.service" -P SubState)" "start" + +# Fail if DeferTriggerMaxSec= is reached + +systemctl start --no-block "$UNIT_NAME-conflict1.service" +wait_for_stop +assert_eq "$(systemctl show "$UNIT_NAME-conflict1.service" -P SubState)" "start" + +socat -u - UNIX-CONNECT:"/tmp/$UNIT_NAME/test" & +timeout 30 bash -c "until [[ \$(systemctl show '$UNIT_NAME.socket' -P SubState) == 'deferred' ]]; do sleep .5; done" +(! systemctl -q is-active "$UNIT_NAME.service") +kill %% + +echo "DeferTriggerMaxSec=20s" >>/run/systemd/system/"$UNIT_NAME.socket" +systemctl daemon-reload + +timeout 10 bash -c "until [[ \$(systemctl show '$UNIT_NAME.socket' -P SubState) == 'deferred' ]]; do sleep .2; done" +(! systemctl -q is-active "$UNIT_NAME.service") + +sleep 10 +timeout 30 bash -c "until systemctl -q is-failed '$UNIT_NAME.socket'; do sleep .5; done" +assert_eq "$(systemctl show "$UNIT_NAME-conflict1.service" -P SubState)" "start"