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 &" )