fix(#60): checks that lied, and claims the machine stopped honouring #61
No files matched your search
@@ -25,10 +25,15 @@ is stowed last and its files replace symlinks coming from base packages.
|
|||||||
| `deploy.sh` (sync) | adopt identicals, link new, quarantine divergents into `.quarantine/` and replace them |
|
| `deploy.sh` (sync) | adopt identicals, link new, quarantine divergents into `.quarantine/` and replace them |
|
||||||
| `deploy.sh --adopt` | like sync, but divergents are reported and skipped rather than quarantined |
|
| `deploy.sh --adopt` | like sync, but divergents are reported and skipped rather than quarantined |
|
||||||
|
|
||||||
Two package shapes are not symlinks. `dconf/dump.txt` is loaded with `dconf load /`, and
|
Two package shapes are not symlinks. `dconf/dump.txt` is loaded with `dconf load /`. The
|
||||||
`systemd-user` units get `daemon-reload` plus `enable`; a unit is **started** only when
|
`systemd-user` tier gets `daemon-reload`, then **every `*.service` and `*.timer` whose start
|
||||||
systemd reports it as nothing-yet (`""`/`disabled`), and never when its `ExecStart` binary
|
command resolves on this box** is enabled — a timer is the schedule that makes a oneshot service
|
||||||
is absent here, so a unit belonging to another box stays inert instead of crash-looping.
|
run at all, and it carries no `ExecStart` of its own, so the belonging test is applied to the
|
||||||
|
service it activates. A unit is **started** (`--now`) only when systemd reports it as nothing-yet
|
||||||
|
(`""`/`disabled`), and never when its binary is absent here, so a unit belonging to another box
|
||||||
|
stays inert instead of crash-looping. Three outcomes are printed, and each says what actually
|
||||||
|
happened: `ENABLE` only when `systemctl enable` returned success, `INERT` when it could not or did
|
||||||
|
not, `SKIP` when the unit is foreign. Refs #44, #60.
|
||||||
Starting something already installed is what makes `deploy.sh` unsafe to run beside a live
|
Starting something already installed is what makes `deploy.sh` unsafe to run beside a live
|
||||||
session — see *Never start a unit a session may already be running*.
|
session — see *Never start a unit a session may already be running*.
|
||||||
|
|
||||||
@@ -167,6 +172,15 @@ fresh box — where everything reports `disabled` — still gets its units up. B
|
|||||||
are pinned in `tests/deploy.bats` over a stub `systemctl`, because the real one on this box
|
are pinned in `tests/deploy.bats` over a stub `systemctl`, because the real one on this box
|
||||||
is exactly what must not be invoked from a test. Refs #44.
|
is exactly what must not be invoked from a test. Refs #44.
|
||||||
|
|
||||||
|
**A third state exists, and it is not an error.** A unit file with no `[Install]` section cannot be
|
||||||
|
enabled at all — `systemctl --user enable` answers with a non-zero exit and a paragraph about
|
||||||
|
missing `WantedBy`. `live-sync.service` is one on purpose: `live-sync.timer` pulls it in, which is
|
||||||
|
also why systemd reports it as `linked`. Until 2026-10-21 the deploy swallowed that failure and
|
||||||
|
printed `ENABLE` anyway, so the line an operator reads to learn what a rebuild armed was false on
|
||||||
|
every box, on the repo's own heartbeat. The section is readable from the file being deployed, so
|
||||||
|
the tool checks it and prints `INERT …(no [Install] section — another unit arms it, enable cannot)`
|
||||||
|
without making the call. Refs #60.
|
||||||
|
|
||||||
## Law: expand systemd specifiers before calling a unit foreign
|
## Law: expand systemd specifiers before calling a unit foreign
|
||||||
|
|
||||||
`ExecStart=%h/.omp/scripts/live-sync.sh` is valid systemd and meaningless to a shell. The
|
`ExecStart=%h/.omp/scripts/live-sync.sh` is valid systemd and meaningless to a shell. The
|
||||||
|
|||||||
+32
-12
@@ -120,9 +120,20 @@ done
|
|||||||
|
|
||||||
if [ "$MODE" != "check" ] && [ -d "$PKGS_DIR/systemd-user/.config/systemd/user" ]; then
|
if [ "$MODE" != "check" ] && [ -d "$PKGS_DIR/systemd-user/.config/systemd/user" ]; then
|
||||||
systemctl --user daemon-reload || true
|
systemctl --user daemon-reload || true
|
||||||
for unit in "$PKGS_DIR"/systemd-user/.config/systemd/user/*.service; do
|
UNITS_DIR="$PKGS_DIR/systemd-user/.config/systemd/user"
|
||||||
|
# Services AND timers. A timer is the schedule that makes a oneshot service run at all, and it
|
||||||
|
# carries no ExecStart of its own — the binary that decides whether the unit belongs on this
|
||||||
|
# box lives in the service it activates. Walking *.service only deployed live-sync.service on
|
||||||
|
# every rebuild and never armed live-sync.timer: the heartbeat was present and never ticked.
|
||||||
|
for unit in "$UNITS_DIR"/*.service "$UNITS_DIR"/*.timer; do
|
||||||
[ -e "$unit" ] || continue
|
[ -e "$unit" ] || continue
|
||||||
bin="$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$unit" | head -1)"
|
name="$(basename "$unit")"
|
||||||
|
probe="$unit"
|
||||||
|
if [ "${name##*.}" = timer ]; then
|
||||||
|
act="$(sed -n 's/^Unit=\(.*\)$/\1/p' "$unit" | head -1)"
|
||||||
|
probe="$UNITS_DIR/${act:-"${name%.timer}.service"}"
|
||||||
|
fi
|
||||||
|
bin="$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$probe" 2>/dev/null | head -1)"
|
||||||
# systemd expands specifiers inside ExecStart; this shell does not. Matching the literal
|
# systemd expands specifiers inside ExecStart; this shell does not. Matching the literal
|
||||||
# `%h/…` makes a unit of THIS box look foreign and skips it in silence — which is exactly
|
# `%h/…` makes a unit of THIS box look foreign and skips it in silence — which is exactly
|
||||||
# how live-sync.service, the repo's own heartbeat, has been reported "absent on this
|
# how live-sync.service, the repo's own heartbeat, has been reported "absent on this
|
||||||
@@ -132,22 +143,31 @@ if [ "$MODE" != "check" ] && [ -d "$PKGS_DIR/systemd-user/.config/systemd/user"
|
|||||||
bin="${bin//%u/$(id -un)}"
|
bin="${bin//%u/$(id -un)}"
|
||||||
bin="${bin//%U/$(id -u)}"
|
bin="${bin//%U/$(id -u)}"
|
||||||
if [ -n "$bin" ] && command -v "$bin" >/dev/null 2>&1; then
|
if [ -n "$bin" ] && command -v "$bin" >/dev/null 2>&1; then
|
||||||
name="$(basename "$unit")"
|
|
||||||
# Read the state, not the exit status: `is-enabled` exits 1 for a `linked` unit exactly
|
# Read the state, not the exit status: `is-enabled` exits 1 for a `linked` unit exactly
|
||||||
# as for a `disabled` one. Starting something already installed is how a deploy locks a
|
# as for a `disabled` one. Starting something already installed is how a deploy locks a
|
||||||
# live session — swayidle-lock sits enabled/inactive on blankpad and `--now` arms it
|
# live session — swayidle-lock sits enabled/inactive on blankpad and `--now` arms it
|
||||||
# mid-conversation. Only a unit nothing has installed yet gets started. Refs #44.
|
# mid-conversation. Only a unit nothing has installed yet gets started. Refs #44.
|
||||||
state="$(systemctl --user is-enabled "$name" 2>/dev/null || true)"
|
state="$(systemctl --user is-enabled "$name" 2>/dev/null || true)"
|
||||||
case "$state" in
|
if ! grep -q '^\[Install\]' "$unit"; then
|
||||||
"" | disabled)
|
# systemd refuses to enable these: no [Install] section means nothing can want the unit
|
||||||
systemctl --user enable --now "$name" || true
|
# at login, and live-sync.service has none on purpose — its timer pulls it in. The old
|
||||||
echo "ENABLE $name (ExecStart resolves here; nothing running it yet)" ;;
|
# branch swallowed that failure and printed ENABLE anyway, so the one line a rebuild is
|
||||||
*)
|
# checked by was false on every box. The section is readable right here, so do not make
|
||||||
systemctl --user enable "$name" || true
|
# the call at all. Refs #60.
|
||||||
echo "ENABLE $name (already $state; not started — a live session decides what runs)" ;;
|
echo "INERT $name (state=${state:-none}; no [Install] section — another unit arms it, enable cannot)"
|
||||||
esac
|
elif [ -z "$state" ] || [ "$state" = disabled ]; then
|
||||||
|
if systemctl --user enable --now "$name"; then
|
||||||
|
echo "ENABLE $name (ExecStart resolves here; nothing running it yet)"
|
||||||
else
|
else
|
||||||
echo "SKIP $(basename "$unit") (ExecStart '$bin' absent on this host)"
|
echo "INERT $name (enable --now failed — systemd's message above says why)"
|
||||||
|
fi
|
||||||
|
elif systemctl --user enable "$name"; then
|
||||||
|
echo "ENABLE $name (already $state; not started — a live session decides what runs)"
|
||||||
|
else
|
||||||
|
echo "INERT $name (already $state; enable failed — systemd's message above says why)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "SKIP $name (ExecStart '$bin' absent on this host)"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
+59
-1
@@ -37,7 +37,22 @@ if [ "$2" = is-enabled ]; then
|
|||||||
printf '%s\n' "$state"
|
printf '%s\n' "$state"
|
||||||
case "$state" in disabled | "") exit 1 ;; *) exit 0 ;; esac
|
case "$state" in disabled | "") exit 1 ;; *) exit 0 ;; esac
|
||||||
fi
|
fi
|
||||||
exit 0
|
if [ "$2" = enable ]; then
|
||||||
|
# Modelled, not stubbed flat: systemd refuses to enable a unit whose file has no [Install]
|
||||||
|
# section, and the deployed file is what it reads. A stub that always says yes hides the
|
||||||
|
# one case this repo actually ships (live-sync.service is driven by its timer).
|
||||||
|
target=""
|
||||||
|
for a in "$@"; do
|
||||||
|
case "$a" in
|
||||||
|
*.service | *.timer | *.socket) target="$a" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
file="$HOME/.config/systemd/user/${target##*/}"
|
||||||
|
if [ -e "$file" ] && ! grep -q '^\[Install\]' "$file"; then
|
||||||
|
printf 'The unit files have no installation config (WantedBy=, RequiredBy=, Also= settings in the [Install] section). This means they are not meant to be enabled using this command.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
STUB
|
STUB
|
||||||
chmod +x "$TEST_TMP/bin/systemctl"
|
chmod +x "$TEST_TMP/bin/systemctl"
|
||||||
export PATH="$TEST_TMP/bin:$PATH"
|
export PATH="$TEST_TMP/bin:$PATH"
|
||||||
@@ -57,6 +72,21 @@ systemd_unit() {
|
|||||||
> "$REPO/dotfiles/systemd-user/.config/systemd/user/$1"
|
> "$REPO/dotfiles/systemd-user/.config/systemd/user/$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# systemd_unit_no_install <name> <ExecStart> — a unit nothing can enable (live-sync.service:
|
||||||
|
# it is pulled in by live-sync.timer, so it carries no [Install] section on purpose).
|
||||||
|
systemd_unit_no_install() {
|
||||||
|
mkdir -p "$REPO/dotfiles/systemd-user/.config/systemd/user"
|
||||||
|
printf '[Unit]\nDescription=%s\n[Service]\nType=oneshot\nExecStart=%s\n' "$1" "$2" \
|
||||||
|
> "$REPO/dotfiles/systemd-user/.config/systemd/user/$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# systemd_timer <name> <service-name> — a schedule; it has no ExecStart of its own.
|
||||||
|
systemd_timer() {
|
||||||
|
mkdir -p "$REPO/dotfiles/systemd-user/.config/systemd/user"
|
||||||
|
printf '[Unit]\nDescription=%s\n[Timer]\nOnCalendar=hourly\nPersistent=true\n[Install]\nWantedBy=timers.target\n' "$1" \
|
||||||
|
> "$REPO/dotfiles/systemd-user/.config/systemd/user/$1"
|
||||||
|
}
|
||||||
|
|
||||||
@test "generated theme output is never adopted into a symlink" {
|
@test "generated theme output is never adopted into a symlink" {
|
||||||
repo_file "ghostty/.config/ghostty/themes/noctalia" 'background = "#f43841"'
|
repo_file "ghostty/.config/ghostty/themes/noctalia" 'background = "#f43841"'
|
||||||
live_file "$HOME/.config/ghostty/themes/noctalia" 'background = "#f43841"'
|
live_file "$HOME/.config/ghostty/themes/noctalia" 'background = "#f43841"'
|
||||||
@@ -152,3 +182,31 @@ systemd_unit() {
|
|||||||
grep -q 'systemctl --user enable heartbeat.service' "$STUB_LOG"
|
grep -q 'systemctl --user enable heartbeat.service' "$STUB_LOG"
|
||||||
! grep -q -- '--now' "$STUB_LOG"
|
! grep -q -- '--now' "$STUB_LOG"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "a unit nothing can enable is reported as what it is, never as ENABLE" {
|
||||||
|
# `systemctl --user enable live-sync.service` fails on every deploy: the file has no
|
||||||
|
# [Install] section (its timer arms it), and systemd says so with a non-zero exit. The
|
||||||
|
# deploy swallowed that and printed ENABLE, so the one line the operator reads to learn
|
||||||
|
# what a rebuild armed was false on every box, on the repo's own heartbeat.
|
||||||
|
systemd_unit_no_install heartbeat.service /bin/sh
|
||||||
|
printf 'linked\n' > "$UNIT_STATE"
|
||||||
|
run "$REPO/scripts/deploy.sh"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"heartbeat.service"*"[Install]"* ]]
|
||||||
|
[[ "$output" != *"ENABLE heartbeat.service"* ]]
|
||||||
|
# No futile call: the section is readable from the file being deployed.
|
||||||
|
! grep -q 'systemctl --user enable heartbeat.service' "$STUB_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "a timer in the tier is armed, because a service on its own has no schedule" {
|
||||||
|
# The loop walked *.service only. On a rebuilt box live-sync.service is deployed and the
|
||||||
|
# timer is never enabled, so the heartbeat that commits machine state simply does not run
|
||||||
|
# — and the deploy report says nothing about it.
|
||||||
|
systemd_unit heartbeat.service /bin/sh
|
||||||
|
systemd_timer heartbeat.timer heartbeat.service
|
||||||
|
printf 'disabled\n' > "$UNIT_STATE"
|
||||||
|
run "$REPO/scripts/deploy.sh"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" != *"SKIP heartbeat.timer"* ]]
|
||||||
|
grep -q 'systemctl --user enable --now heartbeat.timer' "$STUB_LOG"
|
||||||
|
}
|
||||||
Reference in new issue
Block a user