@ddtcorex/dsh-maestro-supervisor 0.7.10 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/boot-guard.d.ts +33 -0
- package/lib/boot-guard.js +68 -0
- package/lib/boot-lock.d.ts +56 -0
- package/lib/boot-lock.js +155 -0
- package/lib/cli.d.ts +22 -5
- package/lib/cli.js +120 -54
- package/lib/health-poller.d.ts +68 -1
- package/lib/health-poller.js +191 -133
- package/lib/intents.d.ts +17 -0
- package/lib/intents.js +23 -1
- package/lib/plugin.js +15 -16
- package/lib/restart-exec.d.ts +35 -0
- package/lib/restart-exec.js +60 -0
- package/lib/restart-guards.d.ts +20 -0
- package/lib/restart-guards.js +34 -1
- package/lib/restart-tool.d.ts +30 -1
- package/lib/restart-tool.js +222 -8
- package/lib/restart-web.d.ts +26 -0
- package/lib/restart-web.js +58 -0
- package/lib/resume.d.ts +10 -0
- package/lib/resume.js +45 -28
- package/lib/snapshot.d.ts +52 -15
- package/lib/snapshot.js +168 -18
- package/lib/supervisor.d.ts +26 -4
- package/lib/supervisor.js +118 -36
- package/package.json +1 -1
- package/skills/dsh-safe-restart/SKILL.md +62 -2
- package/skills/dsh-safe-restart/scripts/restart-dsh-web.sh +162 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dsh-safe-restart
|
|
3
|
-
description: Use before updating any dsh-maestro-* client bundle or DSH Web asset,
|
|
3
|
+
description: Use before updating any dsh-maestro-* client bundle or DSH Web asset, before reloading the running dsh web host process, or when a rebuilt plugin's lib/ is newer than the running supervisor daemon; validate first and perform a user-approved host restart through the bundled guarded recipe. In-session agents use the `dsh_web_restart` tool; hand off to a human only if the tool is unavailable.
|
|
4
4
|
compatibility: dsh
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -37,7 +37,18 @@ validate first and get explicit user consent before a real swap.
|
|
|
37
37
|
or run a no-server composition check instead.
|
|
38
38
|
3. Verify HTTP 200 and the new marker on the candidate. Retain last-known-good
|
|
39
39
|
assets until the real swap has passed post-swap checks.
|
|
40
|
-
4.
|
|
40
|
+
4. Check the **second** long-lived process, not only `dsh web`: the standalone
|
|
41
|
+
`dsh-web-supervisor` daemon also caches this package's `lib/*.js` in RAM at
|
|
42
|
+
start, and it is the process that judges the new boot. A daemon older than
|
|
43
|
+
the newest build still applies the OLD rollback rules — on 2026-09-13 that
|
|
44
|
+
is exactly what rolled `dsh web` back in a loop every ~90s
|
|
45
|
+
(`rollback — degraded: This operation was aborted`) while `:3080` answered
|
|
46
|
+
in 1.4ms. Compare the daemon's start with the newest build:
|
|
47
|
+
`ps -o lstart= -p "$(pgrep -f 'lib/bin.js daemon')"` against
|
|
48
|
+
`stat -c '%y' <package>/lib/*.js`; a build newer than the daemon's start
|
|
49
|
+
means the daemon is **stale** and must be reloaded BEFORE the swap.
|
|
50
|
+
`restart-dsh-web.sh --check-supervisor` reports this and changes nothing.
|
|
51
|
+
5. Ask for explicit consent and timing. “restart đi” is consent; silence is
|
|
41
52
|
not.
|
|
42
53
|
|
|
43
54
|
## Run the bundled helper only after consent
|
|
@@ -62,6 +73,52 @@ the append-only log destination. The helper refuses a real swap without
|
|
|
62
73
|
and refuses to launch if ports are still occupied. `--dry-run` never runs
|
|
63
74
|
`kill` or `setsid`.
|
|
64
75
|
|
|
76
|
+
The helper reloads a stale supervisor daemon itself, before it stops `dsh web`,
|
|
77
|
+
and aborts the swap (exit 70) when that reload fails — keeping the old process
|
|
78
|
+
beats handing a fresh boot to old rollback rules. Both daemon modes are
|
|
79
|
+
standalone: no `--repo`, no listeners, no `--confirm`, and they never touch
|
|
80
|
+
`dsh web`.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bash <skill-resource-base>/scripts/restart-dsh-web.sh --check-supervisor # fresh|stale|absent
|
|
84
|
+
bash <skill-resource-base>/scripts/restart-dsh-web.sh --reload-supervisor # reload only when stale
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`DSH_SUPERVISOR_RELOAD_WAIT` (seconds, default 15) bounds the wait for systemd
|
|
88
|
+
to bring the daemon back. Only systemd owns that relaunch
|
|
89
|
+
(`dsh-web-supervisor.service` is `Restart=always`), so a hand-started daemon is
|
|
90
|
+
reported and left running, never killed.
|
|
91
|
+
|
|
92
|
+
## In-session validation without restart
|
|
93
|
+
|
|
94
|
+
In-session agents have three tools (same row as `dsh_web_restart`) that never
|
|
95
|
+
touch the live process:
|
|
96
|
+
|
|
97
|
+
- `dsh_web_dryboot` (`{ timeoutMs? }`) — boots a copy of the live profile on an
|
|
98
|
+
ephemeral port with an isolated `DSH_HOME` and returns `{ ok, detail }`.
|
|
99
|
+
Prefer this over hand-rolled `cp -r` + spawn: the copy repairs relative
|
|
100
|
+
`link:` symlinks and the child is always killed with the temp home removed.
|
|
101
|
+
- `dsh_web_gc` (`{ confirm? }`) — lists orphaned dry-boot processes
|
|
102
|
+
(`DSH_HOME=/tmp/dsh-dryboot-*` + ephemeral listener, never self / live ports
|
|
103
|
+
/ real-home processes). Default returns the preview list; `confirm:true`
|
|
104
|
+
SIGKILLs and verifies absence.
|
|
105
|
+
- `dsh_web_restart_status` (no params) — reads the calling session's restart
|
|
106
|
+
outcome: `pending` until the daemon swaps, then `ok`/`failed` with
|
|
107
|
+
`oldPid`/`newPid`/`httpStatus`. `dsh_web_restart` itself now returns
|
|
108
|
+
`{ ok, detail, oldPid, intentPath }` — quote all four when reporting.
|
|
109
|
+
|
|
110
|
+
Settings-staging rule: config-lib memoizes settings per process, so an
|
|
111
|
+
out-of-band settings edit is invisible to the host until restart. Stage ALL
|
|
112
|
+
such edits, then restart ONCE. Prefer in-host Settings UI saves — they are
|
|
113
|
+
visible immediately with no restart at all.
|
|
114
|
+
|
|
115
|
+
TLS note for plugin authors: Node/undici reads `NODE_EXTRA_CA_CERTS` from the
|
|
116
|
+
birth environment only; assigning it at runtime is silently ignored. The
|
|
117
|
+
durable path for a local CA (e.g. a Govard/Caddy dev CA) is a systemd user
|
|
118
|
+
drop-in (`~/.config/systemd/user/dsh-web.service.d/*.conf` with
|
|
119
|
+
`Environment=NODE_EXTRA_CA_CERTS=<path>`), followed by a daemon reload and a
|
|
120
|
+
host restart.
|
|
121
|
+
|
|
65
122
|
## Post-swap checks
|
|
66
123
|
|
|
67
124
|
Do not read the top of an old append-only log as liveness evidence. Instead:
|
|
@@ -84,6 +141,8 @@ Do not read the top of an old append-only log as liveness evidence. Instead:
|
|
|
84
141
|
- Restart for a client-only or static change.
|
|
85
142
|
- Hard-code a PID or a local developer's workspace path.
|
|
86
143
|
- Treat a 200 response alone as proof that a rebuilt plugin was loaded.
|
|
144
|
+
- Treat a rebuilt `lib/` as loaded while the supervisor daemon that judges the
|
|
145
|
+
boot still runs the previous one; the two processes load the code separately.
|
|
87
146
|
|
|
88
147
|
## Agent handoff (required when you are an agent)
|
|
89
148
|
|
|
@@ -92,3 +151,4 @@ If you are running inside `dsh web` (any `dsh-*` skill, `maestro-*` skill, or su
|
|
|
92
151
|
1. Run all preflight checks (`--dry-run`, `pnpm verify`, marker grep) and report the results.
|
|
93
152
|
2. Run the `dsh_web_restart` tool instead of executing the helper — the helper is for external agents and humans.
|
|
94
153
|
3. If the tool reports it is unavailable, print the exact detached `setsid nohup ... --confirm` command for the human to run; then wait for explicit human confirmation in a new terminal. The interrupted turn will rehydrate from the log when the browser reconnects — no data is lost.
|
|
154
|
+
4. A stale supervisor daemon blocks a correct restart just as much as a stale `lib/`, and in-session you cannot run the helper (`restart-dsh-web` in a command is refused by the self-kill guard, as is any `systemctl`). Compare the daemon start (`ps -o lstart= -p "$(pgrep -f 'lib/bin.js daemon')"` — quote the pid you actually resolved) with `stat -c '%y' <package>/lib/*.js`; when the build is newer, reload with a bare `kill -TERM <daemon pid>` after confirming `/proc/<pid>/cmdline` contains `bin.js daemon`. systemd restarts it in ~2s and `dsh web`, the browser and the session are untouched — never signal the `dsh web` pid for this. If that form is refused, or the daemon does not come back as a new pid, stop and hand the human the exact pid plus the reload command (same handoff as step 3); never fall back to `systemctl` or a broad `pkill`.
|
|
@@ -27,6 +27,8 @@ PLUGIN_DIR="${DSH_SUPERVISOR_PLUGIN_DIR:-$(cd "$(dirname "$SCRIPT_SRC")/../../..
|
|
|
27
27
|
confirmed=false
|
|
28
28
|
dry_run=false
|
|
29
29
|
auto_mode=false
|
|
30
|
+
check_supervisor=false
|
|
31
|
+
reload_supervisor=false
|
|
30
32
|
|
|
31
33
|
dry_boot_and_verify() {
|
|
32
34
|
local dsh_repo="$1"
|
|
@@ -57,16 +59,25 @@ dry_boot_and_verify() {
|
|
|
57
59
|
usage() {
|
|
58
60
|
cat <<'EOF'
|
|
59
61
|
Usage: restart-dsh-web.sh --repo <deepseek-harness> [--log <path>] [--confirm|--auto] [--dry-run]
|
|
62
|
+
restart-dsh-web.sh --check-supervisor
|
|
63
|
+
restart-dsh-web.sh --reload-supervisor
|
|
60
64
|
|
|
61
65
|
Safely hand over the DSH Web process that owns ports 3000 and 3080.
|
|
62
66
|
|
|
63
67
|
Options:
|
|
64
|
-
--repo <path>
|
|
65
|
-
--log <path>
|
|
66
|
-
--confirm
|
|
67
|
-
--auto
|
|
68
|
-
--dry-run
|
|
69
|
-
-
|
|
68
|
+
--repo <path> DeepSeek Harness checkout (or set DSH_REPO).
|
|
69
|
+
--log <path> Append-only launch log (or set DSH_RESTART_LOG).
|
|
70
|
+
--confirm Permit a real process handover (human-gated). Required unless --dry-run.
|
|
71
|
+
--auto Permit auto handover (supervisor, no consent prompt). Alias for --confirm with auto log prefix.
|
|
72
|
+
--dry-run Print the resolved process tree; never stop or launch anything.
|
|
73
|
+
--check-supervisor Report supervisor-daemon freshness (fresh|stale|absent); changes nothing.
|
|
74
|
+
--reload-supervisor Reload the daemon only when it predates the newest lib/*.js build.
|
|
75
|
+
-h, --help Show this help text.
|
|
76
|
+
|
|
77
|
+
The supervisor daemon caches this package's lib/*.js in RAM like `dsh web`
|
|
78
|
+
does, and a stale daemon rolls back a healthy boot with its old rules. Every
|
|
79
|
+
real restart therefore reloads it first when stale; DSH_SUPERVISOR_RELOAD_WAIT
|
|
80
|
+
(seconds, default 15) bounds the wait for systemd to bring it back.
|
|
70
81
|
EOF
|
|
71
82
|
}
|
|
72
83
|
|
|
@@ -75,6 +86,120 @@ fail() {
|
|
|
75
86
|
exit "${2:-1}"
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
# --- supervisor daemon freshness -------------------------------------------------
|
|
90
|
+
# `dsh web` is not the only process that caches this package's lib/*.js in RAM:
|
|
91
|
+
# the standalone dsh-web-supervisor daemon does too, and it is the process that
|
|
92
|
+
# decides whether a boot is healthy or must be rolled back. A daemon started
|
|
93
|
+
# BEFORE the newest build keeps running the old rollback rules — on 2026-09-13
|
|
94
|
+
# exactly that judged a slow boot as down and rolled `dsh web` back in a loop
|
|
95
|
+
# ("rollback - degraded: This operation was aborted") while the port answered in
|
|
96
|
+
# 1.4ms. So: never swap `dsh web` under a stale daemon.
|
|
97
|
+
#
|
|
98
|
+
# Only systemd owns a relaunch (dsh-web-supervisor.service has Restart=always);
|
|
99
|
+
# a daemon someone started by hand has no owner to bring it back, so this step
|
|
100
|
+
# reports and leaves it alone rather than killing it.
|
|
101
|
+
|
|
102
|
+
# MainPID of the supervisor unit, or empty unless that pid really is the daemon.
|
|
103
|
+
supervisor_main_pid() {
|
|
104
|
+
local pid cmdline
|
|
105
|
+
pid="$(systemctl --user show -p MainPID --value dsh-web-supervisor.service 2>/dev/null | tr -dc '0-9' || true)"
|
|
106
|
+
[[ -n "$pid" && "$pid" != 0 ]] || return 0
|
|
107
|
+
[[ -d "/proc/$pid" ]] || return 0
|
|
108
|
+
# Never signal a pid that is not this daemon, whatever systemd reports.
|
|
109
|
+
cmdline="$(tr '\0' ' ' <"/proc/$pid/cmdline" 2>/dev/null || true)"
|
|
110
|
+
[[ "$cmdline" == *"bin.js daemon"* ]] || return 0
|
|
111
|
+
printf '%s' "$pid"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Newest mtime among the package's built lib/*.js, or empty when unbuilt.
|
|
115
|
+
newest_lib_mtime() {
|
|
116
|
+
local newest="" file stamp
|
|
117
|
+
for file in "$PLUGIN_DIR"/lib/*.js; do
|
|
118
|
+
[[ -f "$file" ]] || continue
|
|
119
|
+
stamp="$(stat -c %Y "$file" 2>/dev/null || true)"
|
|
120
|
+
[[ -n "$stamp" ]] || continue
|
|
121
|
+
if [[ -z "$newest" || "$stamp" -gt "$newest" ]]; then newest="$stamp"; fi
|
|
122
|
+
done
|
|
123
|
+
printf '%s' "$newest"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Classify the daemon: fresh | stale | absent | unknown.
|
|
127
|
+
supervisor_daemon_state() {
|
|
128
|
+
SUPERVISOR_PID="$(supervisor_main_pid)"
|
|
129
|
+
SUPERVISOR_LIB="$(newest_lib_mtime)"
|
|
130
|
+
if [[ -z "$SUPERVISOR_PID" ]]; then
|
|
131
|
+
SUPERVISOR_VERDICT=absent
|
|
132
|
+
return 0
|
|
133
|
+
fi
|
|
134
|
+
SUPERVISOR_START="$(stat -c %Y "/proc/$SUPERVISOR_PID" 2>/dev/null || true)"
|
|
135
|
+
if [[ -z "$SUPERVISOR_START" || -z "$SUPERVISOR_LIB" ]]; then
|
|
136
|
+
SUPERVISOR_VERDICT=unknown
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
if (( SUPERVISOR_LIB > SUPERVISOR_START )); then
|
|
140
|
+
SUPERVISOR_VERDICT=stale
|
|
141
|
+
else
|
|
142
|
+
SUPERVISOR_VERDICT=fresh
|
|
143
|
+
fi
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
print_supervisor_state() {
|
|
147
|
+
case "$SUPERVISOR_VERDICT" in
|
|
148
|
+
fresh) printf '[restart] supervisor daemon: fresh pid=%s start=%s lib=%s\n' "$SUPERVISOR_PID" "$SUPERVISOR_START" "$SUPERVISOR_LIB" ;;
|
|
149
|
+
stale) printf '[restart] supervisor daemon: stale pid=%s start=%s lib=%s (built after this daemon started)\n' "$SUPERVISOR_PID" "$SUPERVISOR_START" "$SUPERVISOR_LIB" ;;
|
|
150
|
+
unknown) printf '[restart] supervisor daemon: unknown pid=%s (lib build time unavailable)\n' "$SUPERVISOR_PID" ;;
|
|
151
|
+
*) printf '[restart] supervisor daemon: absent\n' ;;
|
|
152
|
+
esac
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Reload the daemon when it predates the newest build; report the verdict either
|
|
156
|
+
# way. A failed reload is fatal: continuing would hand the swap to the very
|
|
157
|
+
# process that can roll it back in a loop.
|
|
158
|
+
ensure_supervisor_current() {
|
|
159
|
+
supervisor_daemon_state
|
|
160
|
+
if [[ "$SUPERVISOR_VERDICT" != stale ]]; then
|
|
161
|
+
print_supervisor_state
|
|
162
|
+
return 0
|
|
163
|
+
fi
|
|
164
|
+
local old_pid="$SUPERVISOR_PID"
|
|
165
|
+
local wait_s="${DSH_SUPERVISOR_RELOAD_WAIT:-15}"
|
|
166
|
+
local new_pid=""
|
|
167
|
+
print_supervisor_state
|
|
168
|
+
printf '[restart] reloading the supervisor daemon so it runs the built lib/*.js\n'
|
|
169
|
+
kill -TERM "$old_pid" 2>/dev/null || true
|
|
170
|
+
for _ in $(seq 1 "$wait_s"); do
|
|
171
|
+
sleep 1
|
|
172
|
+
new_pid="$(supervisor_main_pid)"
|
|
173
|
+
if [[ -n "$new_pid" && "$new_pid" != "$old_pid" ]]; then break; fi
|
|
174
|
+
new_pid=""
|
|
175
|
+
done
|
|
176
|
+
if [[ -z "$new_pid" ]]; then
|
|
177
|
+
printf '[restart] FAIL: supervisor daemon %s did not come back within %ss\n' "$old_pid" "$wait_s" >&2
|
|
178
|
+
return 1
|
|
179
|
+
fi
|
|
180
|
+
printf '[restart] supervisor daemon: reloaded %s -> %s\n' "$old_pid" "$new_pid"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Single-flight + log scoping (D6, spec 2026-09-13-supervisor-safety-net-design):
|
|
184
|
+
# take the SAME boot.lock and append the SAME boot-boundary sentinel as the
|
|
185
|
+
# supervisor's own performSingleBootRestart, by calling the package's helper —
|
|
186
|
+
# never by re-implementing either marker here. The lock is owned by this shell's
|
|
187
|
+
# PID ($$), so it stays valid for as long as the script runs, and it is released
|
|
188
|
+
# on every exit path via the trap below.
|
|
189
|
+
BOOT_GUARD_HELPER="${PLUGIN_DIR}/lib/bin.js"
|
|
190
|
+
boot_guard_acquire() {
|
|
191
|
+
if [[ ! -f "$BOOT_GUARD_HELPER" ]]; then
|
|
192
|
+
fail "boot.lock helper is missing: $BOOT_GUARD_HELPER (run 'pnpm build' in the supervisor package)" 69
|
|
193
|
+
fi
|
|
194
|
+
if ! node "$BOOT_GUARD_HELPER" boot-guard acquire --pid "$$" >>"$log" 2>&1; then
|
|
195
|
+
fail 'another boot holds boot.lock (a dsh web boot or another restart is in flight)' 75
|
|
196
|
+
fi
|
|
197
|
+
}
|
|
198
|
+
boot_guard_release() {
|
|
199
|
+
[[ -f "$BOOT_GUARD_HELPER" ]] || return 0
|
|
200
|
+
node "$BOOT_GUARD_HELPER" boot-guard release --pid "$$" >>"$log" 2>&1 || true
|
|
201
|
+
}
|
|
202
|
+
|
|
78
203
|
while (($#)); do
|
|
79
204
|
case "$1" in
|
|
80
205
|
--repo)
|
|
@@ -100,6 +225,14 @@ while (($#)); do
|
|
|
100
225
|
dry_run=true
|
|
101
226
|
shift
|
|
102
227
|
;;
|
|
228
|
+
--check-supervisor)
|
|
229
|
+
check_supervisor=true
|
|
230
|
+
shift
|
|
231
|
+
;;
|
|
232
|
+
--reload-supervisor)
|
|
233
|
+
reload_supervisor=true
|
|
234
|
+
shift
|
|
235
|
+
;;
|
|
103
236
|
-h|--help)
|
|
104
237
|
usage
|
|
105
238
|
exit 0
|
|
@@ -110,6 +243,18 @@ while (($#)); do
|
|
|
110
243
|
esac
|
|
111
244
|
done
|
|
112
245
|
|
|
246
|
+
# Standalone supervisor-daemon modes: they never touch dsh web, so they need no
|
|
247
|
+
# --repo, no listeners and no consent.
|
|
248
|
+
if [[ "$check_supervisor" == true ]]; then
|
|
249
|
+
supervisor_daemon_state
|
|
250
|
+
print_supervisor_state
|
|
251
|
+
exit 0
|
|
252
|
+
fi
|
|
253
|
+
if [[ "$reload_supervisor" == true ]]; then
|
|
254
|
+
ensure_supervisor_current || fail 'supervisor daemon could not be reloaded — fix it before swapping dsh web' 70
|
|
255
|
+
exit 0
|
|
256
|
+
fi
|
|
257
|
+
|
|
113
258
|
[[ -n "$repo" ]] || fail 'provide --repo or DSH_REPO' 64
|
|
114
259
|
[[ -f "$repo/package.json" ]] || fail "repo has no package.json: $repo" 64
|
|
115
260
|
if [[ "$dry_run" != true && "$confirmed" != true ]]; then
|
|
@@ -222,10 +367,19 @@ printf '[restart] stopping process tree: %s\n' "$(tr '\n' ' ' <<<"$tree_pids")"
|
|
|
222
367
|
|
|
223
368
|
# Mark this as an intentional restart before the port goes down, so
|
|
224
369
|
# dsh-web-supervisor's health poll does not race us with its own rollback.
|
|
225
|
-
#
|
|
370
|
+
# The boot.lock below is the single-flight half of the same contract; both are
|
|
371
|
+
# released on every exit path (success or failure) via the trap.
|
|
372
|
+
mkdir -p "$(dirname "$log")"
|
|
373
|
+
boot_guard_acquire
|
|
226
374
|
mkdir -p "$(dirname "$marker")"
|
|
227
375
|
date -Iseconds > "$marker"
|
|
228
|
-
trap 'rm -f "$marker"' EXIT
|
|
376
|
+
trap 'boot_guard_release; rm -f "$marker"' EXIT
|
|
377
|
+
|
|
378
|
+
# Reload a stale supervisor daemon BEFORE the swap: it is the process that
|
|
379
|
+
# decides whether the boot below is healthy, and a daemon running pre-build
|
|
380
|
+
# rules can roll it back in a loop. Done while dsh web is still up, so the
|
|
381
|
+
# fresh daemon observes the planned-restart marker written just above.
|
|
382
|
+
ensure_supervisor_current || fail 'stale supervisor daemon could not be reloaded — refusing to swap dsh web under old rollback rules' 70
|
|
229
383
|
|
|
230
384
|
if [[ "$systemd_managed" == true ]]; then
|
|
231
385
|
# systemctl stop is a clean, intentional stop -- Restart=always does not
|