From e278e4b863de3398bd6d7efb13e44791c7e6894f Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 20 Oct 2026 17:19:49 -0700 Subject: [PATCH] Expand systemd specifiers before calling a unit foreign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first real `deploy.sh` run on blankpad printed: SKIP live-sync.service (ExecStart '%h/.omp/scripts/live-sync.sh' absent on this host) while `/home/blank/.omp/scripts/live-sync.sh` sits right there. `%h` is expanded by systemd, never by the shell, so the literal string was handed to `command -v`, failed, and the unit was declared to belong to another machine — through the same code path whose whole purpose is to keep a *foreign* unit inert, and silently. The victim was this repo's own heartbeat, and the verdict was the same on every box that ever deployed it, because `%h` is exactly where the checkout lives. Nothing broke tonight only by luck of install state: `live-sync.service` is `linked`, driven by its timer, so the heartbeat kept running while the deploy lied about it. A unit whose only install route was this loop would simply never have been enabled anywhere. `%h`, `%u` and `%U` are now expanded before resolution; an unrecognised specifier is left alone so it fails loudly rather than being guessed at. Pinned by a test that writes a `%h/.local/bin/helper` unit into the scratch home — red before (`SKIP`), green after (`already linked; not started`), which also keeps the #45 rule intact: resolving a unit is not licence to start it. Refs #44. Verification: `bats tests/` 71/71, `kb-lint.sh omp-docs` 0 violations, `kb-coverage.py` 0 issues, `bash -n` clean. --- omp-docs/tools/dotfiles-deploy.md | 21 +++++++++++++++++++++ scripts/deploy.sh | 8 ++++++++ tests/deploy.bats | 16 ++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/omp-docs/tools/dotfiles-deploy.md b/omp-docs/tools/dotfiles-deploy.md index 38aec42..94324af 100644 --- a/omp-docs/tools/dotfiles-deploy.md +++ b/omp-docs/tools/dotfiles-deploy.md @@ -158,6 +158,27 @@ 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 is exactly what must not be invoked from a test. Refs #44. +## 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 +resolution check handed that literal string to `command -v`, which fails everywhere, so the +deployment concluded the unit belonged to another machine and skipped it — silently, through +the same code path that is supposed to keep a *foreign* unit inert. The unit it kept skipping +was this repo's own heartbeat, and `%h` is precisely where this checkout lives, so the false +verdict was the same on every box that deployed it. Measured on blankpad: the first real +`deploy.sh` run printed `SKIP live-sync.service (ExecStart '%h/.omp/scripts/live-sync.sh' +absent on this host)` while `/home/blank/.omp/scripts/live-sync.sh` sits right there. + +Expand the specifiers a user unit can carry — `%h`, `%u`, `%U` — before resolving, and let an +unknown specifier fail rather than guess at it. The probe needs no privileges and writes +nothing: + +```bash +bin=$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' \ + ~/.omp/dotfiles/systemd-user/.config/systemd/user/live-sync.service | head -1) +command -v "${bin//%h/$HOME}" # resolves → the unit is this box's; the SKIP was the lie +``` + ## Law: the dconf dump holds preferences, not per-box state `dconf load /` writes every key in `dotfiles/dconf/dump.txt`, so the dump carries whatever diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 2942208..391cb32 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -123,6 +123,14 @@ if [ "$MODE" != "check" ] && [ -d "$PKGS_DIR/systemd-user/.config/systemd/user" for unit in "$PKGS_DIR"/systemd-user/.config/systemd/user/*.service; do [ -e "$unit" ] || continue bin="$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$unit" | head -1)" + # 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 + # how live-sync.service, the repo's own heartbeat, has been reported "absent on this + # host" on every machine that deployed it. Expand what a user unit can carry; leave any + # other specifier unmatched rather than guess at it. Refs #44. + bin="${bin//%h/$HOME}" + bin="${bin//%u/$(id -un)}" + bin="${bin//%U/$(id -u)}" 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 diff --git a/tests/deploy.bats b/tests/deploy.bats index f5f8838..e76ddb6 100755 --- a/tests/deploy.bats +++ b/tests/deploy.bats @@ -136,3 +136,19 @@ systemd_unit() { [[ "$output" == *"SKIP"*otherbox.service* ]] ! grep -q enable "$STUB_LOG" } + +@test "a unit whose ExecStart uses a systemd specifier still resolves here" { + # `%h` is expanded by systemd, not by the shell, so matching the literal string makes a + # unit belonging to THIS box look foreign and skip it in silence. The victim is this repo's + # own heartbeat: `ExecStart=%h/.omp/scripts/live-sync.sh`, skipped on every box. Refs #44. + mkdir -p "$HOME/.local/bin" + printf '#!/bin/sh\ntrue\n' > "$HOME/.local/bin/helper" + chmod +x "$HOME/.local/bin/helper" + systemd_unit heartbeat.service '%h/.local/bin/helper' + printf 'linked\n' > "$UNIT_STATE" + run "$REPO/scripts/deploy.sh" + [ "$status" -eq 0 ] + [[ "$output" == *"heartbeat.service (already linked"* ]] + grep -q 'systemctl --user enable heartbeat.service' "$STUB_LOG" + ! grep -q -- '--now' "$STUB_LOG" +}