fix(installer): survive self-modification mid-run; sturdier unit detection

Root cause of the mixed-up second install on the A2000 host: when run
from the clone it manages, the script's own git checkout/merge REPLACES
scripts/install.sh while bash is still executing it. Bash reads scripts
lazily by byte offset, so it resumed parsing the NEW file at the OLD
offset and executed an arbitrary tail of it — observed as the fresh-
service path running on a host whose unit already existed: the port scan
saw the still-running old service on 3000 and silently wrote a new unit
on 3001, while enable --now on the already-active service changed
nothing until a manual daemon-reload.

Fix: the whole script now runs inside main(), invoked as
'main "$@"; exit $?' so bash parses everything up front and never
reads the file again after main returns (the exit lives in the same
parse unit — demonstrated necessary: with a bare 'main "$@"' ending,
bash still executed the swapped file's trailing content after main
returned).

Also: unit existence is now checked with 'systemctl cat' instead of
grepping the full list-unit-files output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jasonwitty
2026-08-21 11:42:51 -07:00
parent d8cceb1795
commit 43ce4f1aaa
+17 -1
View File
@@ -32,6 +32,16 @@ say() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
# The entire remainder runs inside main(), invoked on the LAST line. This
# makes the script safe against being MODIFIED WHILE RUNNING: when executed
# from the clone it manages, the git checkout below replaces this very file,
# and bash reads scripts lazily by byte offset — without this wrapper it
# resumes parsing the NEW file at the OLD offset and executes an arbitrary
# tail of it (observed: the fresh-service path ran on a host whose unit
# already existed). With main(), the whole script is parsed before any of
# it executes.
main() {
OS="$(uname -s)"
ARCH="$(uname -m)"
@@ -131,7 +141,7 @@ if [ "$SYS_SUDO" = "__none__" ] && [ "$NO_SERVICE" -eq 0 ]; then
NO_SERVICE=1
fi
if [ "$OS" = "Linux" ] && [ "$NO_SERVICE" -eq 0 ] && command -v systemctl >/dev/null; then
if systemctl list-unit-files 2>/dev/null | grep -q '^socktop-agent\.service'; then
if systemctl cat socktop-agent.service >/dev/null 2>&1; then
# UPGRADE: the unit file is the operator's (SSL, tokens, ports may be
# configured there) — never overwrite it. Only the binary it points at
# is replaced, then the service is restarted.
@@ -228,3 +238,9 @@ say "Installed:"
"$PREFIX/socktop_agent" --version
say "Active on PATH: $(type -p socktop || true) / $(type -p socktop_agent || true)"
socktop --version
}
# exit in the same parse unit as the call: after main returns, bash must not
# read another byte from this (possibly replaced) file.
main "$@"; exit $?