Overview (problems in the room with the hold action, the live deck, one line per computer), Keys (both pages, what each key will say and what holding does), Computers (map with the keys that turn red, cards with a light per thing that runs there from the watchdog's snapshot and a read-only probe, Gear, Setup), Logs, History (the installer's copies for now), Help (four cards, the generated key guide, cheat sheet and Where things are, glossary, Companion facts), Site setup and Fixes read-only. The editing code is checked (with a lockout) but nothing can change yet. Also: faults are attributed to the computers their check is about; the installer refuses while a HyperDeck is recording (--now overrides); the Lighting PC's MIDI session is listed; the probes read sudo's answer correctly and expand $HOME outside the quotes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
187 lines
7.7 KiB
Bash
187 lines
7.7 KiB
Bash
#!/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" <<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">
|
|
<dict>
|
|
<key>Label</key><string>$SETTINGS_LABEL</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/usr/bin/python3</string>
|
|
<string>-m</string>
|
|
<string>settings.server</string>
|
|
<string>--data-dir</string>
|
|
<string>$DEST</string>
|
|
</array>
|
|
<key>WorkingDirectory</key><string>$DEST</string>
|
|
<key>RunAtLoad</key><true/>
|
|
<key>KeepAlive</key><true/>
|
|
<key>ThrottleInterval</key><integer>10</integer>
|
|
<key>AbandonProcessGroup</key><true/>
|
|
<key>StandardErrorPath</key><string>$HOME/Library/Logs/status-settings.err.log</string>
|
|
</dict>
|
|
</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">
|
|
<dict>
|
|
<key>Label</key><string>$LABEL</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/usr/bin/python3</string>
|
|
<string>$DEST/status_panel.py</string>
|
|
<string>--data-dir</string>
|
|
<string>$DEST</string>
|
|
</array>
|
|
<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
|
|
}
|
|
|
|
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
|