Step 0 of the settings page: registry, safe restart, remembered state, test verb, page markers
- schema.py: every setting with type, default, help, owner screen and the rules a config must pass; tests/test_schema.py keeps it in step with the code (every key the code reads is declared, every default matches, check types and fix kinds agree, the glossary covers every word a key can show, today's config still builds today's pages apart from the key notes) - status_panel.py: VERSION + started.json; SIGTERM lets a running fix finish and starts nothing new (RESTARTING SOON); fixes-running.json and live-now.json for the settings page; state.json keeps the ALARMS override, acknowledgements, auto-fix tries and fix times across restarts, keyed by what a check is about rather than its position; 'test' and 'resurvey' commands; queued commands dropped at start; overnight windows; a HyperDeck switched off is always switched back on; the spotify fix kind (playlist set once, in config 'spotify'); Windows remote checks batched for cmd.exe with conditional echoes; every fix's kind and host validated; never_auto; wording and numbers from config (site block, timing, page_hold_ms, auto-fix keep_months); hyperdeck_probe removed; --check and --exit-timeout - make_page.py: numbers from config, markers on every key, nav templates per Companion version, --out; push_page.py: a page is overwritten only when every key carries a marker (foreign keys named by position), secret-free fetch, host from the site; nav_keys.py: host from the site; netgear.py: switches and the shared-port limit from config - install.sh: validates with the new code first, keeps prev/ for --rollback, --keep-config, derived ExitTimeOut, wider deploy list, waits for the start stamp - config.json: site, timing, spotify sections; Windows hosts marked; deployed to NA Ingest and both pages re-imported once (--force) so every key carries its marker Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+101
-27
@@ -1,9 +1,14 @@
|
||||
#!/bin/zsh
|
||||
# Install or update the status panel watchdog on this Mac (NA Ingest) as a LaunchAgent.
|
||||
# Re-run after changing status_panel.py, coremidi.py or config.json in this folder.
|
||||
# Install or update the status panel watchdog on this Mac (the Ingest Mac) as a LaunchAgent.
|
||||
# Re-run after changing any file in this folder.
|
||||
#
|
||||
# zsh install.sh install / update and (re)start
|
||||
# zsh install.sh --remove stop it and remove the LaunchAgent (files stay)
|
||||
# zsh install.sh install / update and (re)start
|
||||
# zsh install.sh --keep-config update the code but leave the live config.json as it is
|
||||
# zsh install.sh --rollback put the previous code back (kept in prev/) and restart
|
||||
# zsh install.sh --remove stop it and remove the LaunchAgent (files stay)
|
||||
#
|
||||
# The config is validated with the NEW code before anything is touched, so a stricter check can
|
||||
# never leave the watchdog crash-looping. The previous code is kept in prev/ for --rollback.
|
||||
set -e
|
||||
SRC=${0:A:h}
|
||||
LABEL=org.waterschurch.status-panel
|
||||
@@ -11,23 +16,29 @@ LABEL=org.waterschurch.status-panel
|
||||
DEST="$HOME/Library/Application Support/status-panel"
|
||||
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
||||
DOMAIN="gui/$(id -u)"
|
||||
CODE=(status_panel.py coremidi.py netgear.py make_page.py push_page.py nav_keys.py atem_watch.js atem_state.js nav_templates.json)
|
||||
KEEP_CONFIG=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--keep-config) KEEP_CONFIG=1 ;;
|
||||
--remove|--rollback) ;;
|
||||
*) echo "unknown option $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$1" == "--remove" ]]; then
|
||||
restart_agent() {
|
||||
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
|
||||
rm -f "$PLIST"
|
||||
echo "removed $LABEL (files left in $DEST)"
|
||||
exit 0
|
||||
fi
|
||||
# bootout returns before the old job is gone; bootstrapping too early fails with "5: Input/output error".
|
||||
for i in {1..50}; do launchctl print "$DOMAIN/$LABEL" >/dev/null 2>&1 || break; sleep 0.2; done
|
||||
for attempt in 1 2 3; do
|
||||
launchctl bootstrap "$DOMAIN" "$PLIST" && break
|
||||
[[ $attempt == 3 ]] && { echo "launchctl bootstrap failed" >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
/usr/bin/python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$SRC/config.json"
|
||||
mkdir -p "$DEST"
|
||||
if [[ -f "$DEST/config.json" ]] && ! cmp -s "$SRC/config.json" "$DEST/config.json"; then
|
||||
cp "$DEST/config.json" "$DEST/config.json.bak-$(date +%Y%m%d-%H%M%S)"
|
||||
fi
|
||||
cp "$SRC/status_panel.py" "$SRC/coremidi.py" "$SRC/netgear.py" "$SRC/config.json" "$SRC/atem_watch.js" "$DEST/"
|
||||
[[ -f "$SRC/atem-reference.xml" ]] && cp "$SRC/atem-reference.xml" "$DEST/"
|
||||
|
||||
cat > "$PLIST" <<EOF
|
||||
write_plist() { # $1 = exit timeout in seconds
|
||||
cat > "$PLIST" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
@@ -43,18 +54,81 @@ cat > "$PLIST" <<EOF
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>ThrottleInterval</key><integer>10</integer>
|
||||
<key>ExitTimeOut</key><integer>$1</integer>
|
||||
<key>StandardErrorPath</key><string>$HOME/Library/Logs/status-panel.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
}
|
||||
|
||||
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
|
||||
# bootout returns before the old job is gone; bootstrapping too early fails with "5: Input/output error".
|
||||
for i in {1..50}; do launchctl print "$DOMAIN/$LABEL" >/dev/null 2>&1 || break; sleep 0.2; done
|
||||
for attempt in 1 2 3; do
|
||||
launchctl bootstrap "$DOMAIN" "$PLIST" && break
|
||||
[[ $attempt == 3 ]] && { echo "launchctl bootstrap failed" >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
echo "installed $LABEL -> $DEST"
|
||||
wait_for_start() { # $1 = install time (unix): wait for a start stamp newer than that
|
||||
for i in {1..60}; do
|
||||
if [[ -f "$DEST/started.json" ]] && /usr/bin/python3 - "$DEST/started.json" "$1" <<'PY' 2>/dev/null
|
||||
import json, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
sys.exit(0 if d.get("started_at", 0) >= float(sys.argv[2]) else 1)
|
||||
PY
|
||||
then
|
||||
echo "running (code $(/usr/bin/python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("version"))' "$DEST/started.json"))"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
echo "WARNING: no start stamp after 30 s - check ~/Library/Logs/status-panel.log and status-panel.err.log" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ "$1" == "--remove" ]]; then
|
||||
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
|
||||
rm -f "$PLIST"
|
||||
echo "removed $LABEL (files left in $DEST)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--rollback" ]]; then
|
||||
[[ -d "$DEST/prev" ]] || { echo "nothing to roll back to ($DEST/prev is missing)" >&2; exit 1; }
|
||||
for f in "$DEST"/prev/*(N); do cp "$f" "$DEST/"; done
|
||||
/usr/bin/python3 "$DEST/status_panel.py" --check --config "$DEST/config.json"
|
||||
T=$(date +%s)
|
||||
write_plist "$(/usr/bin/python3 "$DEST/status_panel.py" --exit-timeout --config "$DEST/config.json")"
|
||||
restart_agent
|
||||
echo "rolled back to the previous code (version $(/usr/bin/python3 -c 'import sys; sys.path.insert(0, sys.argv[1]); import status_panel; print(status_panel.VERSION)' "$DEST"))"
|
||||
wait_for_start "$T" || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1. The config that will run, checked with the code that will run it.
|
||||
mkdir -p "$DEST"
|
||||
if [[ $KEEP_CONFIG == 1 && -f "$DEST/config.json" ]]; then
|
||||
CFG="$DEST/config.json"
|
||||
else
|
||||
CFG="$SRC/config.json"
|
||||
fi
|
||||
/usr/bin/python3 "$SRC/status_panel.py" --check --config "$CFG"
|
||||
EXIT_TIMEOUT=$(/usr/bin/python3 "$SRC/status_panel.py" --exit-timeout --config "$CFG")
|
||||
|
||||
# 2. Keep the code that runs now, for --rollback.
|
||||
mkdir -p "$DEST/prev"
|
||||
for f in $CODE; do [[ -f "$DEST/$f" ]] && cp "$DEST/$f" "$DEST/prev/"; done
|
||||
|
||||
# 3. Files. The live config is backed up when it changes; --keep-config leaves it alone.
|
||||
if [[ $KEEP_CONFIG == 0 ]]; then
|
||||
if [[ -f "$DEST/config.json" ]] && ! cmp -s "$SRC/config.json" "$DEST/config.json"; then
|
||||
cp "$DEST/config.json" "$DEST/config.json.bak-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "config.json replaced (the old one is kept as config.json.bak-...; use --keep-config to leave it)"
|
||||
fi
|
||||
cp "$SRC/config.json" "$DEST/"
|
||||
fi
|
||||
for f in $CODE; do [[ -f "$SRC/$f" ]] && cp "$SRC/$f" "$DEST/"; done
|
||||
[[ -f "$SRC/atem-reference.xml" ]] && cp "$SRC/atem-reference.xml" "$DEST/"
|
||||
if [[ -d "$SRC/companion-objects" ]]; then
|
||||
rm -rf "$DEST/companion-objects" && cp -R "$SRC/companion-objects" "$DEST/"
|
||||
fi
|
||||
|
||||
# 4. The LaunchAgent, with the exit timeout this config needs (a running fix finishes before a restart).
|
||||
T=$(date +%s)
|
||||
write_plist "$EXIT_TIMEOUT"
|
||||
restart_agent
|
||||
echo "installed $LABEL -> $DEST (exit timeout ${EXIT_TIMEOUT}s)"
|
||||
echo "log: ~/Library/Logs/status-panel.log"
|
||||
wait_for_start "$T" || true
|
||||
|
||||
Reference in New Issue
Block a user