#!/bin/zsh # 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 --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 SETTINGS_LABEL=org.waterschurch.status-settings # App Support, not ~/Documents: launchd-run python3 cannot read ~/Documents. 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 schema.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|--now) ;; *) echo "unknown option $arg" >&2; exit 2 ;; esac done restart_agent() { # $1 = label, $2 = plist local label=${1:-$LABEL} plist=${2:-$PLIST} 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 for $label" >&2; exit 1; } sleep 1 done } SETTINGS_PLIST="$HOME/Library/LaunchAgents/$SETTINGS_LABEL.plist" write_settings_plist() { cat > "$SETTINGS_PLIST" < Label$SETTINGS_LABEL ProgramArguments /usr/bin/python3 -m settings.server --data-dir $DEST WorkingDirectory$DEST RunAtLoad KeepAlive ThrottleInterval10 AbandonProcessGroup StandardErrorPath$HOME/Library/Logs/status-settings.err.log EOF } write_plist() { # $1 = exit timeout in seconds cat > "$PLIST" < Label$LABEL ProgramArguments /usr/bin/python3 $DEST/status_panel.py --data-dir $DEST RunAtLoad KeepAlive ThrottleInterval10 ExitTimeOut$1 StandardErrorPath$HOME/Library/Logs/status-panel.err.log EOF } 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 launchctl bootout "$DOMAIN/$SETTINGS_LABEL" 2>/dev/null || true rm -f "$PLIST" "$SETTINGS_PLIST" echo "removed $LABEL and $SETTINGS_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 # 0. Not while a HyperDeck is recording (the watchdog restarts for a few seconds); --now overrides. if [[ -f "$DEST/live-now.json" && " $* " != *" --now "* ]] && /usr/bin/python3 - "$DEST/live-now.json" <<'PY' import json, sys, time d = json.load(open(sys.argv[1])) sys.exit(0 if d.get("live") and time.time() - d.get("time", 0) < 60 else 1) PY then echo "a HyperDeck is recording right now ($(/usr/bin/python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("reason"))' "$DEST/live-now.json")) - not installing; use --now to override" >&2 exit 3 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/" # The settings page (its own LaunchAgent) and the record of coloured page-number keys (site-owned: never overwritten). if [[ -d "$SRC/settings" ]]; then rm -rf "$DEST/settings" && cp -R "$SRC/settings" "$DEST/" && find "$DEST/settings" -name "__pycache__" -prune -exec rm -rf {} + 2>/dev/null fi [[ -f "$SRC/nav_locations.json" && ! -f "$DEST/nav_locations.json" ]] && cp "$SRC/nav_locations.json" "$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 if [[ -d "$DEST/settings" ]]; then write_settings_plist restart_agent "$SETTINGS_LABEL" "$SETTINGS_PLIST" PORT=$(/usr/bin/python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("site",{}).get("settings_port",8020))' "$DEST/config.json") echo "settings page: http://$(hostname -s).local:$PORT/ (log: ~/Library/Logs/status-settings.log)" fi