fix: keep the server running after you log out
Nightly Build / build (push) Successful in 7m47s

A `systemctl --user` unit runs under the per-user manager, which systemd
starts at first login and stops when the user's last session ends — so
closing the SSH session that started Skald killed it, and it never came
up at boot. No crash and nothing in the journal: the whole cgroup is
simply torn down. Both installers now enable lingering after installing
the unit, and update.sh carries the same helper so an installation
predating this fix is healed by an ordinary update. A failure to enable
it only ever warns, with the manual command — it must not abort an
install.

Two things found on the way there:

update.sh matched `case "$OS" in Linux) ... Darwin)`, but $OS had already
been normalized to lowercase at the top of the file, so stop_service and
start_service were both silent no-ops. None of the ordering the file
documents at its head was executing: the tarball went over the running
binary (ETXTBSY, aborting the update mid-way) and the safety-net restart
in cleanup() was a no-op too, leaving the box down.

Neither workflow published install.sh / install-nightly.sh to the web
root, so the scripts served by builds.skaldagent.net were hand-copied and
drifting from the repo — an installer fix would reach every existing box
through update.sh but never a new one. Nightly publishes the nightly
installer, release publishes the release one, both with the same atomic
temp-and-rename the tarballs use.

Also on the unit: dropped `After=docker.service`, which a user manager
silently ignores rather than honouring advisorily, and moved
`Restart=on-failure` to `always` — run.sh exits 0 on any graceful
shutdown, including one nobody asked for, which on-failure reads as a
clean stop. That is also what absorbs the boot race against Docker now
that lingering makes us start at boot.
This commit is contained in:
2026-08-06 11:00:13 +01:00
parent 40663373d4
commit bb5226a9a9
6 changed files with 192 additions and 8 deletions
+46 -4
View File
@@ -43,6 +43,13 @@ fi
CHANNEL="$(tr -d '[:space:]' < "$CHANNEL_FILE")"
# ── Detect interactive stdin ──────────────────────────────────────────────────
if [ -t 0 ]; then
IS_INTERACTIVE=true
else
IS_INTERACTIVE=false
fi
# ── Colours (if terminal) ─────────────────────────────────────────────────────
if [ -t 1 ]; then
RED='\033[0;31m'
@@ -96,9 +103,15 @@ STOPPED=0 # set once the service has been stopped
STARTED=0 # set once it has been (re)started
# ── Stop service ──────────────────────────────────────────────────────────────
# NOTE: match the *normalized* OS values set above (linux/darwin), not uname's
# capitalized output. Getting this wrong turns both stop_service and
# start_service into silent no-ops, and then none of the ordering this file
# documents at the top actually happens: the tarball is extracted over the
# running binary (ETXTBSY on Linux, aborting the update mid-way), and the
# safety-net restart in cleanup() is a no-op too, so the box stays down.
stop_service() {
case "$OS" in
Linux)
linux)
if command -v systemctl >/dev/null 2>&1; then
if systemctl --user is-active skald-circle.service >/dev/null 2>&1; then
info "⏹️ Stopping service …"
@@ -106,7 +119,7 @@ stop_service() {
fi
fi
;;
Darwin)
darwin)
if command -v launchctl >/dev/null 2>&1; then
if launchctl list com.skald.circle >/dev/null 2>&1; then
info "⏹️ Stopping agent …"
@@ -145,13 +158,13 @@ wait_until_stopped() {
# ── Start service ─────────────────────────────────────────────────────────────
start_service() {
case "$OS" in
Linux)
linux)
if command -v systemctl >/dev/null 2>&1; then
info "▶ Starting service …"
systemctl --user start skald-circle.service
fi
;;
Darwin)
darwin)
if command -v launchctl >/dev/null 2>&1; then
info "▶ Starting agent …"
launchctl load "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true
@@ -160,6 +173,34 @@ start_service() {
esac
}
# ── systemd user lingering ────────────────────────────────────────────────────
# Same helper the installers run, repeated here so an install predating it gets
# healed by an ordinary update: a `systemctl --user` unit lives under the
# per-user manager, which systemd stops when the user's last session ends —
# so without lingering the server dies at logout and never starts at boot.
# Idempotent, and a failure is only ever a warning: the update itself is fine.
ensure_linger() {
[ "$OS" = "linux" ] || return 0
local target="${USER:-$(id -un)}"
command -v loginctl >/dev/null 2>&1 || return 0
case "$(loginctl show-user "$target" --property=Linger 2>/dev/null || true)" in
*=yes) return 0 ;;
esac
if loginctl enable-linger "$target" 2>/dev/null \
|| sudo -n loginctl enable-linger "$target" 2>/dev/null \
|| { [ "$IS_INTERACTIVE" = true ] && sudo loginctl enable-linger "$target"; }; then
info "✔ Lingering enabled — the server now survives logout and starts at boot"
else
warn "Lingering is not enabled for ${target}."
echo " The server stops when your last session ends. Run this once:"
echo " sudo loginctl enable-linger ${target}"
fi
}
# ── Cleanup + safety net ──────────────────────────────────────────────────────
# Runs on every exit. Removes temp files and, if the update died after the
# service was stopped but before it came back up, makes a best-effort restart so
@@ -279,6 +320,7 @@ main() {
fi
# ── Restart ────────────────────────────────────────────────────────────────
ensure_linger
start_service
STARTED=1