From cec48832be00fa802069df7a6cbf555060c0ac7e Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sun, 16 Aug 2026 18:08:58 +0200 Subject: [PATCH] CB-600: make it safe to install the launchd agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - redeploy-bridged.sh now refuses (not warns) a supervised restart when its computed log path disagrees with the loaded plist's StandardOutPath — otherwise every post-restart check reads the wrong file and can report a clean restart while the daemon crash-loops. The check is a pure, testable function; the script gained a source-for-test guard so it can be exercised without installing the agent or touching launchd. - a failed 'launchctl load' after a successful 'unload' now retries once and, on ultimate failure, tells the operator the agent is stopped AND disabled plus the exact recovery command, instead of leaving that silently worse than the pre-redeploy state. - the plist documents honestly that the crash loop launchd retries is unbounded (ThrottleInterval only paces it), and what actually stops it. - fixed the requiredSecretEnvVars javadoc: the auth.tokenEnv startup throw is ~370 lines below its call site, not a few lines above it, and only fires in auth.mode: token. --- .../main/java/dev/ltms/bridged/Bridged.java | 5 +- deploy/dev.ltms.bridged.plist | 21 +++++- scripts/redeploy-bridged.sh | 67 ++++++++++++++++++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/bridged/src/main/java/dev/ltms/bridged/Bridged.java b/bridged/src/main/java/dev/ltms/bridged/Bridged.java index fed7b66..c65eac1 100644 --- a/bridged/src/main/java/dev/ltms/bridged/Bridged.java +++ b/bridged/src/main/java/dev/ltms/bridged/Bridged.java @@ -555,7 +555,10 @@ public final class Bridged { * where set (opt-in). Derived from the config, not hard-coded, so a new profile is covered for * free. A var required by more than one profile is one entry naming every profile that needs * it. Deliberately excludes {@code auth.tokenEnv}: that one is already enforced loudly, by a - * startup throw, a few lines above this method's call site. + * startup throw in {@code main()} — about 370 lines below this method's call site + * ({@link #reportRequiredSecrets(BridgedConfig)}), not a few lines above it. That throw only + * fires when {@code auth.mode: token} is configured; under the default loopback-trust mode it + * never runs, and {@code auth.tokenEnv} is simply not required. * *

Package-private and pure (no I/O, no logging) so the derivation is unit-testable without * capturing log output; {@link #reportRequiredSecrets(BridgedConfig)} is the logging caller. diff --git a/deploy/dev.ltms.bridged.plist b/deploy/dev.ltms.bridged.plist index 073e138..6666d8d 100644 --- a/deploy/dev.ltms.bridged.plist +++ b/deploy/dev.ltms.bridged.plist @@ -82,8 +82,25 @@ RunAtLoad - + KeepAlive SuccessfulExit diff --git a/scripts/redeploy-bridged.sh b/scripts/redeploy-bridged.sh index a5622e7..96ad365 100755 --- a/scripts/redeploy-bridged.sh +++ b/scripts/redeploy-bridged.sh @@ -79,6 +79,53 @@ running_pid() { pgrep -f "$PATTERN" || true; } launchd_installed() { [ -f "$LAUNCHD_PLIST" ]; } launchd_loaded() { launchctl list "$LAUNCHD_LABEL" >/dev/null 2>&1; } +# CB-600: the script computes its own log path from where it sits on disk (REPO, above); the +# plist hard-codes an absolute StandardOutPath. Nothing forced the two to agree — if this script +# were ever run from a checkout other than the one the loaded plist names, launchd would start and +# log the daemon correctly, while every check below (the fresh "bridged listening" line, the +# ERROR-count scan) would read a different, empty or stale file and the script would report a +# clean restart while the daemon crash-loops. Pure and side-effect-free besides `die`/`ok` — reads +# the two paths, resolves them, compares — so it never touches launchd or the daemon and can be +# exercised by sourcing this script (see the SOURCED guard below) without installing the agent. +check_log_path_matches_plist() { + local script_out="$1" plist_path="$2" + local plist_out resolved_out resolved_plist_out + # Checked by exit status, not by emptiness: on a missing file/key PlistBuddy exits nonzero but + # still writes a message ("File Doesn't Exist, Will Create: ...") that command substitution + # would happily capture as if it were the real value — testing only `-z` missed that case. + if ! plist_out="$(/usr/libexec/PlistBuddy -c 'Print :StandardOutPath' "$plist_path" 2>/dev/null)" \ + || [ -z "$plist_out" ]; then + die "launchd agent is loaded but PlistBuddy could not read StandardOutPath from + $plist_path + — cannot verify the daemon logs where this script is about to look. Fix the plist before + redeploying supervised." + fi + resolved_out="$(cd "$(dirname "$script_out")" 2>/dev/null && pwd -P)/$(basename "$script_out")" || true + resolved_plist_out="$(cd "$(dirname "$plist_out")" 2>/dev/null && pwd -P)/$(basename "$plist_out")" || true + if [ -z "$resolved_out" ] || [ -z "$resolved_plist_out" ] || [ "$resolved_out" != "$resolved_plist_out" ]; then + die "log path mismatch — this script reads + $script_out (resolved: ${resolved_out:-}) + but the loaded plist's StandardOutPath is + $plist_out (resolved: ${resolved_plist_out:-}) + Under supervision the daemon writes to the PLIST's path, not necessarily this script's — every + post-restart check below (the fresh 'bridged listening' line, the ERROR-count scan) would read + the wrong file and could report a clean restart while the daemon crash-loops. Fix the mismatch + (move this checkout to match the plist, or edit the plist's StandardOutPath/StandardErrorPath) + before redeploying supervised." + fi + ok "log path check: script and plist agree ($resolved_out)" +} + +# CB-600: sourceable for testing. When this file is SOURCED (not executed) it stops here — nothing +# below runs — so a test harness can `source` it to call check_log_path_matches_plist (or the +# other pure helpers above) against a throwaway plist fixture without ever reaching the mutating +# flow (build/stop/start) or touching the real daemon or launchd. On a normal `./redeploy-bridged.sh` +# invocation `(return 0 2>/dev/null)` fails (return is illegal at top level of an executed script), +# so this whole block is a no-op and every line below still runs exactly as before. +if (return 0 2>/dev/null); then + return 0 +fi + # ---------------------------------------------------------------- report state say "current state" @@ -103,6 +150,9 @@ SUPERVISED=0 if launchd_loaded; then SUPERVISED=1 ok "launchd agent loaded ($LAUNCHD_LABEL) — launchd supervises this daemon" + # CB-600: fail loudly here, before ANY other check runs, if this script and the loaded plist + # would read different log files — every check after this point is worthless otherwise. + check_log_path_matches_plist "$OUT" "$LAUNCHD_PLIST" else warn "launchd agent not loaded — this script is the only thing that will restart the daemon." fi @@ -222,7 +272,22 @@ say "start" if [ "$SUPERVISED" = 1 ]; then echo " supervision is ON: using 'launchctl load' so launchd starts and keeps supervising this" echo " process, instead of a manual nohup that launchd would know nothing about." - launchctl load -w "$LAUNCHD_PLIST" || die "launchctl load failed" + # CB-600: 'launchctl unload -w' above already persisted Disabled=true for this label. A load -w + # that succeeds clears it; a load -w that FAILS leaves the agent both stopped and disabled — worse + # than before this script ran, because a later reboot or login will not bring it back either. One + # retry covers a transient race (e.g. launchd not yet fully done deregistering); if it still fails, + # die with the exact recovery command rather than a bare "failed". + if ! launchctl load -w "$LAUNCHD_PLIST" 2>/dev/null; then + warn "launchctl load failed on the first attempt — retrying once after a short pause" + sleep 2 + launchctl load -w "$LAUNCHD_PLIST" || die "launchctl load failed twice. + The agent is now STOPPED and DISABLED — it will NOT come back on its own, not even after a + reboot or login, because 'launchctl unload -w' above persisted Disabled=true and load -w + never got the chance to clear it. Recover with: + launchctl load -w \"$LAUNCHD_PLIST\" + If that still fails, check 'launchctl list $LAUNCHD_LABEL', validate the plist with + 'plutil -lint \"$LAUNCHD_PLIST\"', and check $OUT before assuming a retry will succeed." + fi else # Absolute jar path so `ps` names which checkout is running. ( cd "$BRIDGED" && zsh -lc "nohup java -jar '$JAR' >> bridged.out 2>&1 &" )