]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
manager: add 'needs-stop/needs-start' markers
authorLuca Boccassi <luca.boccassi@gmail.com>
Sat, 27 Dec 2025 11:02:30 +0000 (11:02 +0000)
committerLuca Boccassi <luca.boccassi@gmail.com>
Mon, 23 Feb 2026 13:05:23 +0000 (13:05 +0000)
Useful for packaging scripts, when units are removed.
When multiple markers are assigned without +/-, the last one wins.
When using +/-, the job merging logic is followed to the extent possible.

man/org.freedesktop.systemd1.xml
src/basic/unit-def.c
src/basic/unit-def.h
src/core/dbus-manager.c
src/core/dbus-unit.c
src/core/unit.c
src/core/unit.h
src/core/varlink-manager.c
src/core/varlink-unit.c
test/units/TEST-26-SYSTEMCTL.sh

index f0a7ccc6d6ef5fa77abf135b5f94f50c6e3ea9ce..f4a06901b036806f39a05d92e72d6c64f20f23d9 100644 (file)
@@ -2644,12 +2644,33 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
       </para>
 
       <para><varname>Markers</varname> is an array of string flags that can be set using
-      <function>SetUnitProperties()</function> to indicate that the service should be reloaded or
-      restarted. Currently known values are <literal>needs-restart</literal> and
-      <literal>needs-reload</literal>. Package scripts may use the first to mark units for later restart when
-      a new version of the package is installed. Configuration management scripts may use the second to mark
-      units for a later reload when the configuration is adjusted. Those flags are not set by the manager,
-      except to unset as appropriate when the unit is stopped, restarted, or reloaded.</para>
+      <function>SetUnitProperties()</function> to indicate that the service should be reloaded or restarted.
+      Currently known values are <literal>needs-restart</literal>, <literal>needs-stop</literal>,
+      <literal>needs-start</literal> and <literal>needs-reload</literal>. Package scripts may use the first
+      three to mark units for later restart or start or stop when a new version of the package is installed
+      or removed. Configuration management scripts may use the fourth to mark units for a later reload when
+      the configuration is adjusted. Those flags are not set by the manager, except to unset as appropriate
+      when the unit is stopped, restarted, or reloaded. When markers are set, they are normalized according
+      to the following precedence rules, modeled after the job type merging logic. When new markers are
+      applied incrementally (using the <literal>+</literal> prefix), conflicting existing markers are
+      cleared before the new markers are merged in:
+      <itemizedlist>
+        <listitem><para><literal>needs-reload</literal> loses against all other markers. If any of
+        <literal>needs-restart</literal>, <literal>needs-start</literal>, or <literal>needs-stop</literal>
+        is set, <literal>needs-reload</literal> is cleared.</para></listitem>
+        <listitem><para><literal>needs-stop</literal> wins against <literal>needs-restart</literal> and
+        <literal>needs-reload</literal>, clearing both.</para></listitem>
+        <listitem><para><literal>needs-start</literal> wins against <literal>needs-stop</literal>, clearing
+        it.</para></listitem>
+        <listitem><para><literal>needs-restart</literal> wins against <literal>needs-start</literal>. If
+        both are set, <literal>needs-start</literal> is cleared.</para></listitem>
+      </itemizedlist>
+      For example, if a unit currently has <literal>needs-stop</literal> set and a new
+      <literal>+needs-start</literal> marker is applied, the existing <literal>needs-stop</literal> is
+      cleared and only <literal>needs-start</literal> remains. Conversely, applying
+      <literal>+needs-stop</literal> to any existing marker will clear all other markers, as
+      <literal>needs-stop</literal> takes precedence over <literal>needs-restart</literal> and
+      <literal>needs-reload</literal>, and the new marker clears conflicting existing ones.</para>
 
       <para><varname>JobTimeoutUSec</varname> maps directly to the corresponding configuration setting in the
       unit file.</para>
index 6da61c510d8bf7b6e0dfa8e77681bc82e4de943a..ea4eebbf5d37f028bf8d7d457eb21642972d7831 100644 (file)
@@ -155,6 +155,8 @@ FreezerState freezer_state_objective(FreezerState state) {
 static const char* const unit_marker_table[_UNIT_MARKER_MAX] = {
         [UNIT_MARKER_NEEDS_RELOAD]  = "needs-reload",
         [UNIT_MARKER_NEEDS_RESTART] = "needs-restart",
+        [UNIT_MARKER_NEEDS_STOP]    = "needs-stop",
+        [UNIT_MARKER_NEEDS_START]   = "needs-start",
 };
 
 DEFINE_STRING_TABLE_LOOKUP(unit_marker, UnitMarker);
index 6ed4c0cbb7a9190884db5b6c28519c55b5d2d876..5fecd3ecec14e558a3dc013212fbccb8771efa28 100644 (file)
@@ -62,6 +62,8 @@ typedef enum FreezerState {
 typedef enum UnitMarker {
         UNIT_MARKER_NEEDS_RELOAD,
         UNIT_MARKER_NEEDS_RESTART,
+        UNIT_MARKER_NEEDS_STOP,
+        UNIT_MARKER_NEEDS_START,
         _UNIT_MARKER_MAX,
         _UNIT_MARKER_INVALID = -EINVAL,
 } UnitMarker;
index 232389e06cae4201ebf9beaab55d9b9fc5d569ab..ad79c6e96abf9b5b58fd56fdb4ecb92a2d7db1ee 100644 (file)
@@ -2132,17 +2132,26 @@ static int method_enqueue_marked_jobs(sd_bus_message *message, void *userdata, s
                         continue;
 
                 BusUnitQueueFlags flags;
-                if (BIT_SET(u->markers, UNIT_MARKER_NEEDS_RESTART))
+                JobType job;
+                if (BIT_SET(u->markers, UNIT_MARKER_NEEDS_RESTART)) {
                         flags = 0;
-                else if (BIT_SET(u->markers, UNIT_MARKER_NEEDS_RELOAD))
+                        job = JOB_TRY_RESTART;
+                } else if (BIT_SET(u->markers, UNIT_MARKER_NEEDS_RELOAD)) {
                         flags = BUS_UNIT_QUEUE_RELOAD_IF_POSSIBLE;
-                else
+                        job = JOB_TRY_RESTART;
+                } else if (BIT_SET(u->markers, UNIT_MARKER_NEEDS_STOP)) {
+                        flags = 0;
+                        job = JOB_STOP;
+                } else if (BIT_SET(u->markers, UNIT_MARKER_NEEDS_START)) {
+                        flags = 0;
+                        job = JOB_START;
+                } else
                         continue;
 
-                r = mac_selinux_unit_access_check(u, message, "start", &error);
+                r = mac_selinux_unit_access_check(u, message, job_type_to_access_method(job), &error);
                 if (r >= 0)
                         r = bus_unit_queue_job_one(message, u,
-                                                   JOB_TRY_RESTART, JOB_FAIL, flags,
+                                                   job, JOB_FAIL, flags,
                                                    reply, &error);
                 if (ERRNO_IS_NEG_RESOURCE(r))
                         return r;
index eff73591fde0279c831fcf99665a6a6ecd4e2a56..525ec76da260d3159076a960019505f7466fdf9a 100644 (file)
@@ -2151,9 +2151,8 @@ static int bus_unit_set_live_property(
 
                 if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
                         if (some_absolute)
-                                u->markers = settings;
-                        else
-                                u->markers = settings | (u->markers & ~mask);
+                                mask = UINT_MAX;
+                        u->markers = unit_normalize_markers((u->markers & ~mask), settings);
                 }
 
                 return 1;
index 78c67c9833ba8467e9981354c2eab5e1ddfa01f8..1995bb7f8eb05d8f82819dfa13ab85df46ed0221 100644 (file)
@@ -11,6 +11,7 @@
 #include "all-units.h"
 #include "alloc-util.h"
 #include "ansi-color.h"
+#include "bitfield.h"
 #include "bpf-restrict-fs.h"
 #include "bus-common-errors.h"
 #include "bus-internal.h"
@@ -479,6 +480,9 @@ bool unit_may_gc(Unit *u) {
         if (r <= 0 && !IN_SET(r, -ENXIO, -EOWNERDEAD))
                 return false; /* ENXIO/EOWNERDEAD means: currently not realized */
 
+        if (unit_can_start(u) && BIT_SET(u->markers, UNIT_MARKER_NEEDS_START))
+                return false;
+
         if (!UNIT_VTABLE(u)->may_gc)
                 return true;
 
@@ -2758,11 +2762,13 @@ void unit_notify(Unit *u, UnitActiveState os, UnitActiveState ns, bool reload_su
         /* Make sure the cgroup and state files are always removed when we become inactive */
         if (UNIT_IS_INACTIVE_OR_FAILED(ns)) {
                 SET_FLAG(u->markers,
-                         (1u << UNIT_MARKER_NEEDS_RELOAD)|(1u << UNIT_MARKER_NEEDS_RESTART),
+                         (1u << UNIT_MARKER_NEEDS_RELOAD)|(1u << UNIT_MARKER_NEEDS_RESTART)|(1u << UNIT_MARKER_NEEDS_STOP),
                          false);
                 unit_prune_cgroup(u);
                 unit_unlink_state_files(u);
-        } else if (ns != os && ns == UNIT_RELOADING)
+        } else if (UNIT_IS_ACTIVE_OR_ACTIVATING(ns))
+                SET_FLAG(u->markers, 1u << UNIT_MARKER_NEEDS_START, false);
+        else if (ns != os && ns == UNIT_RELOADING)
                 SET_FLAG(u->markers, 1u << UNIT_MARKER_NEEDS_RELOAD, false);
 
         unit_update_on_console(u);
@@ -7086,8 +7092,45 @@ int parse_unit_marker(const char *marker, unsigned *settings, unsigned *mask) {
         if (m < 0)
                 return -EINVAL;
 
+        /* When +- are not used, last one wins, so reset the bitmask before storing the new result */
+        if (!some_plus_minus)
+                *settings = 0;
+
         SET_FLAG(*settings, 1u << m, b);
         SET_FLAG(*mask, 1u << m, true);
 
         return some_plus_minus;
 }
+
+unsigned unit_normalize_markers(unsigned existing_markers, unsigned new_markers) {
+        /* Follow the job merging logic: when new markers conflict with existing ones, the new marker
+         * takes precedence and clears out conflicting existing markers. Then standard normalization
+         * resolves any remaining conflicts. */
+
+        /* New stop wins against all existing markers */
+        if (BIT_SET(new_markers, UNIT_MARKER_NEEDS_STOP))
+                CLEAR_BITS(existing_markers, UNIT_MARKER_NEEDS_RESTART, UNIT_MARKER_NEEDS_START, UNIT_MARKER_NEEDS_RELOAD);
+        /* New start wins against existing stop */
+        if (BIT_SET(new_markers, UNIT_MARKER_NEEDS_START))
+                CLEAR_BIT(existing_markers, UNIT_MARKER_NEEDS_STOP);
+        /* New restart wins against existing start and reload */
+        if (BIT_SET(new_markers, UNIT_MARKER_NEEDS_RESTART))
+                CLEAR_BITS(existing_markers, UNIT_MARKER_NEEDS_START, UNIT_MARKER_NEEDS_RELOAD);
+
+        unsigned markers = existing_markers | new_markers;
+
+        /* Standard normalization: reload loses against everything */
+        if (BIT_SET(markers, UNIT_MARKER_NEEDS_RESTART) || BIT_SET(markers, UNIT_MARKER_NEEDS_START) || BIT_SET(markers, UNIT_MARKER_NEEDS_STOP))
+                CLEAR_BIT(markers, UNIT_MARKER_NEEDS_RELOAD);
+        /* Stop wins against restart and reload */
+        if (BIT_SET(markers, UNIT_MARKER_NEEDS_STOP))
+                CLEAR_BITS(markers, UNIT_MARKER_NEEDS_RESTART, UNIT_MARKER_NEEDS_RELOAD);
+        /* Start wins against stop */
+        if (BIT_SET(markers, UNIT_MARKER_NEEDS_START))
+                CLEAR_BIT(markers, UNIT_MARKER_NEEDS_STOP);
+        /* Restart wins against start */
+        if (BITS_SET(markers, UNIT_MARKER_NEEDS_RESTART, UNIT_MARKER_NEEDS_START))
+                CLEAR_BIT(markers, UNIT_MARKER_NEEDS_START);
+
+        return markers;
+}
index 95959cfcea45cc45630aedd5c4b8d43811fec152..9c94113239ee0e2b67a821a2f1c5d8ec3bb55570 100644 (file)
@@ -1095,6 +1095,7 @@ DECLARE_STRING_TABLE_LOOKUP(oom_policy, OOMPolicy);
 int unit_queue_job_check_and_mangle_type(Unit *u, JobType *type, bool reload_if_possible, sd_bus_error *reterr_error);
 
 int parse_unit_marker(const char *marker, unsigned *settings, unsigned *mask);
+unsigned unit_normalize_markers(unsigned existing_markers, unsigned new_markers);
 
 /* Macros which append UNIT= or USER_UNIT= to the message */
 
index dbe97f20c9bcd73f6306e76954bc78fe2c76d016..d00f7e5a248a7de17fa22924b0cb5447acf445ea 100644 (file)
@@ -343,6 +343,7 @@ int vl_method_enqueue_marked_jobs_manager(sd_varlink *link, sd_json_variant *par
                 _cleanup_(sd_bus_error_free) sd_bus_error bus_error = SD_BUS_ERROR_NULL;
                 const char *error_id = NULL;
                 uint32_t job_id = 0; /* silence 'maybe-uninitialized' compiler warning */
+                JobType job;
 
                 /* ignore aliases */
                 if (u->id != k)
@@ -350,14 +351,20 @@ int vl_method_enqueue_marked_jobs_manager(sd_varlink *link, sd_json_variant *par
 
                 if (u->markers == 0)
                         continue;
+                if (BIT_SET(u->markers, UNIT_MARKER_NEEDS_STOP))
+                        job = JOB_STOP;
+                else if (BIT_SET(u->markers, UNIT_MARKER_NEEDS_START))
+                        job = JOB_START;
+                else
+                        job = JOB_TRY_RESTART;
 
-                r = mac_selinux_unit_access_check_varlink(u, link, job_type_to_access_method(JOB_TRY_RESTART));
+                r = mac_selinux_unit_access_check_varlink(u, link, job_type_to_access_method(job));
                 if (r < 0)
                         error_id = SD_VARLINK_ERROR_PERMISSION_DENIED;
                 else
                         r = varlink_unit_queue_job_one(
                                         u,
-                                        JOB_TRY_RESTART,
+                                        job,
                                         JOB_FAIL,
                                         /* reload_if_possible= */ !BIT_SET(u->markers, UNIT_MARKER_NEEDS_RESTART),
                                         &job_id,
index 790d0df2dfb3407fae75841b33edc024f6d4aaf4..b3375aebfe88f9670e1ccaa3e28dc4b6d94a21c9 100644 (file)
@@ -668,7 +668,7 @@ int vl_method_set_unit_properties(sd_varlink *link, sd_json_variant *parameters,
                 return r;
 
         if (p.markers_found)
-                unit->markers = p.markers | (unit->markers & ~p.markers_mask);
+                unit->markers = unit_normalize_markers((unit->markers & ~p.markers_mask), p.markers);
 
         return sd_varlink_reply(link, NULL);
 }
index 2b8c6fa2308ce836d0833f755361b7503ca41857..75feedd3b097e65e64d2fd01582ff5fadee93764 100755 (executable)
@@ -371,18 +371,153 @@ systemctl status 1
 
 # --marked
 systemctl restart "$UNIT_NAME"
-systemctl set-property "$UNIT_NAME" Markers=needs-restart
+systemctl set-property "$UNIT_NAME" "Markers=needs-reload needs-restart"
 systemctl show -P Markers "$UNIT_NAME" | grep needs-restart
+systemctl show -P Markers "$UNIT_NAME" | grep -v needs-reload
 systemctl reload-or-restart --marked
 (! systemctl show -P Markers "$UNIT_NAME" | grep needs-restart)
+systemctl is-active "$UNIT_NAME"
+systemctl set-property "$UNIT_NAME" "Markers=needs-reload needs-stop"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-stop
+systemctl show -P Markers "$UNIT_NAME" | grep -v needs-reload
+systemctl reload-or-restart --marked
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-stop)
+(! systemctl is-active "$UNIT_NAME")
+systemctl set-property "$UNIT_NAME" "Markers=needs-start"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-start
+systemctl show -P Markers "$UNIT_NAME" | grep -v needs-stop
+systemctl reload-or-restart --marked
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-start)
+systemctl is-active "$UNIT_NAME"
+systemctl set-property "$UNIT_NAME" "Markers=needs-start needs-stop"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-stop
+systemctl show -P Markers "$UNIT_NAME" | grep -v needs-start
+systemctl reload-or-restart --marked
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-stop)
+(! systemctl is-active "$UNIT_NAME")
+
+# Test marker normalization with incremental (+) syntax
+
+# needs-start + +needs-restart → needs-restart (restart wins against start)
+systemctl set-property "$UNIT_NAME" "Markers=needs-start"
+systemctl set-property "$UNIT_NAME" "Markers=+needs-restart"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-restart
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-start)
+systemctl set-property "$UNIT_NAME" "Markers="
+
+# needs-restart + +needs-start → needs-restart (restart wins against start)
+systemctl set-property "$UNIT_NAME" "Markers=needs-restart"
+systemctl set-property "$UNIT_NAME" "Markers=+needs-start"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-restart
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-start)
+systemctl set-property "$UNIT_NAME" "Markers="
+
+# needs-restart + +needs-reload → needs-restart (reload loses against restart)
+systemctl set-property "$UNIT_NAME" "Markers=needs-restart"
+systemctl set-property "$UNIT_NAME" "Markers=+needs-reload"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-restart
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-reload)
+systemctl set-property "$UNIT_NAME" "Markers="
+
+# needs-stop + +needs-start → needs-start (start overrides stop)
+systemctl set-property "$UNIT_NAME" "Markers=needs-stop"
+systemctl set-property "$UNIT_NAME" "Markers=+needs-start"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-start
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-stop)
+systemctl set-property "$UNIT_NAME" "Markers="
+
+# anything + +needs-stop → needs-stop (stop wins against everything)
+for marker in needs-start needs-restart needs-reload; do
+    systemctl set-property "$UNIT_NAME" "Markers=$marker"
+    systemctl set-property "$UNIT_NAME" "Markers=+needs-stop"
+    systemctl show -P Markers "$UNIT_NAME" | grep needs-stop
+    (! systemctl show -P Markers "$UNIT_NAME" | grep "$marker")
+    systemctl set-property "$UNIT_NAME" "Markers="
+done
+
+# needs-stop + +needs-reload → needs-stop (stop wins against reload)
+systemctl set-property "$UNIT_NAME" "Markers=needs-stop"
+systemctl set-property "$UNIT_NAME" "Markers=+needs-reload"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-stop
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-reload)
+systemctl set-property "$UNIT_NAME" "Markers="
 
 # again, but with varlinkctl instead
 systemctl restart "$UNIT_NAME"
-varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"needs-restart\"]}}"
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"needs-reload\", \"needs-restart\"]}}"
 systemctl show -P Markers "$UNIT_NAME" | grep needs-restart
+systemctl show -P Markers "$UNIT_NAME" | grep -v needs-reload
 varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Manager.EnqueueMarkedJobs '{}'
 timeout 30 bash -c "until systemctl list-jobs $UNIT_NAME | grep \"No jobs\" 2>/dev/null; do sleep 1; done"
 (! systemctl show -P Markers "$UNIT_NAME" | grep needs-restart)
+systemctl is-active "$UNIT_NAME"
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"needs-reload\", \"needs-stop\"]}}"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-stop
+systemctl show -P Markers "$UNIT_NAME" | grep -v needs-reload
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Manager.EnqueueMarkedJobs '{}'
+timeout 30 bash -c "until systemctl list-jobs $UNIT_NAME | grep \"No jobs\" 2>/dev/null; do sleep 1; done"
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-stop)
+(! systemctl is-active "$UNIT_NAME")
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"needs-start\"]}}"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-start
+systemctl show -P Markers "$UNIT_NAME" | grep -v needs-stop
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Manager.EnqueueMarkedJobs '{}'
+timeout 30 bash -c "until systemctl list-jobs $UNIT_NAME | grep \"No jobs\" 2>/dev/null; do sleep 1; done"
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-start)
+systemctl is-active "$UNIT_NAME"
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"needs-start\", \"needs-stop\"]}}"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-stop
+systemctl show -P Markers "$UNIT_NAME" | grep -v needs-start
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Manager.EnqueueMarkedJobs '{}'
+timeout 30 bash -c "until systemctl list-jobs $UNIT_NAME | grep \"No jobs\" 2>/dev/null; do sleep 1; done"
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-stop)
+(! systemctl is-active "$UNIT_NAME")
+
+# Test marker normalization with incremental (+) syntax via varlinkctl
+
+# needs-start + +needs-restart → needs-restart (restart wins against start)
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"needs-start\"]}}"
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"+needs-restart\"]}}"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-restart
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-start)
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": []}}"
+
+# needs-restart + +needs-start → needs-restart (restart wins against start)
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"needs-restart\"]}}"
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"+needs-start\"]}}"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-restart
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-start)
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": []}}"
+
+# needs-restart + +needs-reload → needs-restart (reload loses against restart)
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"needs-restart\"]}}"
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"+needs-reload\"]}}"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-restart
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-reload)
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": []}}"
+
+# needs-stop + +needs-start → needs-start (start overrides stop)
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"needs-stop\"]}}"
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"+needs-start\"]}}"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-start
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-stop)
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": []}}"
+
+# anything + +needs-stop → needs-stop (stop wins against everything)
+for marker in needs-start needs-restart needs-reload; do
+    varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"$marker\"]}}"
+    varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"+needs-stop\"]}}"
+    systemctl show -P Markers "$UNIT_NAME" | grep needs-stop
+    (! systemctl show -P Markers "$UNIT_NAME" | grep "$marker")
+    varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": []}}"
+done
+
+# needs-stop + +needs-reload → needs-stop (stop wins against reload)
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"needs-stop\"]}}"
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": [\"+needs-reload\"]}}"
+systemctl show -P Markers "$UNIT_NAME" | grep needs-stop
+(! systemctl show -P Markers "$UNIT_NAME" | grep needs-reload)
+varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": []}}"
 
 # --dry-run with destructive verbs
 # kexec is skipped intentionally, as it requires a bit more involved setup