settings/builders.py renders a fix's command from a builder (template) and its fields; validate refuses a config whose stored command no longer equals what its fields build; the Fixes screen and the remote checks in the key editor rebuild through /api/build and /api/build-check. config.json adopts templates for all 27 fixes and 5 remote checks (ProPresenter's restart loop and Ableton's quoting normalised, same behaviour); tests/test_builders.py (174 tests pass). Deployed to NA Ingest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
431 lines
24 KiB
Python
431 lines
24 KiB
Python
"""Fix and check builders: a fix is built from fields, never typed as a command.
|
|
|
|
A fix with a `template` and `params` carries its generated `run` (and kind/host) beside them; the
|
|
watchdog runs only `run`. Every Mac value is shlex-quoted, every Windows value regex-checked.
|
|
`build(cfg, fix)` regenerates; `mismatches(cfg)` lists fixes and checks whose stored command no
|
|
longer equals what their fields build (validate refuses those). `adopt(cfg)` gives today's fixes
|
|
their templates (a one-time migration; it prints what it normalised)."""
|
|
import re
|
|
import shlex
|
|
|
|
WIN_SAFE = re.compile(r"^[A-Za-z0-9 ._()\\:*-]+$")
|
|
LABEL_SAFE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
|
|
|
|
def q(value):
|
|
return shlex.quote(str(value))
|
|
|
|
|
|
def win(value, what):
|
|
value = str(value)
|
|
if not WIN_SAFE.match(value):
|
|
raise ValueError("%s: on a PC only letters, digits, space and . _ ( ) \\ : * - are allowed, not %r" % (what, value))
|
|
return value
|
|
|
|
|
|
def label(value, what):
|
|
value = str(value)
|
|
if not LABEL_SAFE.match(value):
|
|
raise ValueError("%s: a launchd label holds letters, digits, dots, - and _ only, not %r" % (what, value))
|
|
return value
|
|
|
|
|
|
def home_dir(path):
|
|
"""~/x -> $HOME/x inside double quotes; refuses quotes and $ in the rest."""
|
|
path = str(path)
|
|
if re.search(r'["$`\\]', path.replace("$HOME", "", 1)):
|
|
raise ValueError("folder %r may not hold quotes, $ or backslashes" % path)
|
|
if path.startswith("~/"):
|
|
path = "$HOME/" + path[2:]
|
|
return path.rstrip("/")
|
|
|
|
|
|
def mac_quit(process, wait_seconds, force, stop_children=False):
|
|
"""Quit a Mac program gently, give it wait_seconds, then (force) kill it."""
|
|
p = q(process)
|
|
ticks = max(1, int(round(float(wait_seconds) * 2)))
|
|
s = ""
|
|
if stop_children:
|
|
s += "pg=$(ps -o pgid= -p \"$(pgrep -x %s | head -1)\" 2>/dev/null | tr -d ' '); " % p
|
|
s += "pkill -TERM -x %s; i=0; while pgrep -xq %s && [ $i -lt %d ]; do sleep 0.5; i=$((i+1)); done; " % (p, p, ticks)
|
|
if force:
|
|
s += "pgrep -xq %s && pkill -KILL -x %s; " % (p, p)
|
|
if stop_children:
|
|
s += "[ -n \"$pg\" ] && [ \"$pg\" != \"$(ps -o pgid= -p $$ | tr -d ' ')\" ] && kill -KILL -- -\"$pg\" 2>/dev/null; "
|
|
if force or stop_children:
|
|
s += "sleep 1; "
|
|
return s
|
|
|
|
|
|
def mac_open(app, background=False, arg=None):
|
|
"""open -a Name / open /path/App.app (+ -g to stay in the background)."""
|
|
app = str(app)
|
|
cmd = "open -g" if background else "open"
|
|
if app == "$APP":
|
|
cmd += " \"$APP\""
|
|
elif app.startswith("/"):
|
|
cmd += " " + q(app)
|
|
else:
|
|
cmd += " -a " + q(app)
|
|
if arg:
|
|
cmd += " " + arg
|
|
return cmd
|
|
|
|
|
|
ABLETON_SET = ("L=$(ls -td ~/Library/Preferences/Ableton/Live*/ | head -1); S=$(grep -a 'info: Loading document \"' \"${L}Log.txt\" "
|
|
"| grep -v '/Templates/' | tail -1 | sed -E 's/^.*Loading document \"(.*)\"[^\"]*$/\\1/'); ")
|
|
|
|
|
|
def ableton_open(app):
|
|
return "if [ -n \"$S\" ] && [ -f \"$S\" ]; then %s; else %s; fi" % (mac_open(app, arg="\"$S\""), mac_open(app))
|
|
|
|
|
|
def helper_restart(labels):
|
|
one = "launchctl kickstart -k gui/$(id -u)/%s || launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/%s.plist"
|
|
labels = [label(x, "helper") for x in labels]
|
|
if not labels:
|
|
raise ValueError("at least one helper (launchd label) is needed")
|
|
if len(labels) == 1:
|
|
return one % (labels[0], labels[0])
|
|
return "s=0; " + " ".join("{ %s; } || s=1;" % (one % (x, x)) for x in labels) + " exit $s"
|
|
|
|
|
|
def win_running(exe):
|
|
return "tasklist /NH /FI \"IMAGENAME eq %s\" | find /I \"%s\" >nul" % (exe, exe)
|
|
|
|
|
|
# --- the templates ---------------------------------------------------------------
|
|
# params: (name, words, help, kind, default) - kind: str, host, bool, int, list, app
|
|
P_HOST = ("host", "Computer", "Which computer it runs on.", "host", None)
|
|
TEMPLATES = {
|
|
"mac_start": {
|
|
"words": "Start a Mac program if it is not running",
|
|
"os": "mac", "start_only": True,
|
|
"params": [P_HOST, ("process", "Process name", "As pgrep sees it (Activity Monitor's process name).", "str", None),
|
|
("app", "App to open", "Its name, or the full /Applications/... path.", "str", None)],
|
|
"render": lambda p, cfg: "pgrep -xq %s || %s" % (q(p["process"]), mac_open(p["app"])),
|
|
"suggest": lambda p, cfg: "start %s if it is not running" % p["app"],
|
|
},
|
|
"mac_restart": {
|
|
"words": "Restart a Mac program (quit it, then open it again)",
|
|
"os": "mac", "closes": True,
|
|
"params": [P_HOST, ("process", "Process name", "As pgrep sees it.", "str", None),
|
|
("app", "App to open", "Its name, the full /Applications/... path, or $APP with 'newest of' below.", "str", None),
|
|
("wait_seconds", "Wait for it to quit", "Seconds before it is killed.", "int", 10),
|
|
("force", "Kill it if it will not quit", "", "bool", True),
|
|
("background", "Open it in the background", "No window comes to the front.", "bool", False),
|
|
("stop_children", "Also stop everything it started", "Its helper processes (a translation server, say).", "bool", False),
|
|
("newest_of", "Newest of", "A pattern like /Applications/ProPresenter*.app: the newest installed version opens.", "str", "")],
|
|
"render": lambda p, cfg: (("APP=$(ls -d %s | sort -V | tail -1); " % str(p["newest_of"]).strip()) if p.get("newest_of") else "")
|
|
+ mac_quit(p["process"], p.get("wait_seconds", 10), p.get("force", True), p.get("stop_children", False))
|
|
+ mac_open("$APP" if p.get("newest_of") else p["app"], p.get("background", False)),
|
|
"suggest": lambda p, cfg: "restart %s" % (p["app"] if not p.get("newest_of") else "%s (newest installed)" % p["process"]),
|
|
},
|
|
"ableton": {
|
|
"words": "Open Ableton Live with the set it last had open",
|
|
"os": "mac",
|
|
"params": [P_HOST, ("app", "Ableton app name", "As in /Applications.", "str", "Ableton Live 11 Suite"),
|
|
("restart", "Quit it first if it is running", "Otherwise it only starts when it is not running.", "bool", False),
|
|
("wait_seconds", "Wait for it to quit", "Seconds before it is killed (restart only).", "int", 15)],
|
|
"render": lambda p, cfg: (ABLETON_SET + mac_quit("Live", p.get("wait_seconds", 15), True) + ableton_open(p["app"])) if p.get("restart")
|
|
else "pgrep -xq Live || { " + ABLETON_SET + ableton_open(p["app"]) + "; }",
|
|
"suggest": lambda p, cfg: ("restart Ableton and reopen the set it had open (unsaved changes are lost)" if p.get("restart")
|
|
else "start Ableton if it is not running, with the set it last had open"),
|
|
},
|
|
"dvs": {
|
|
"words": "Restart Dante Virtual Soundcard (through its helper)",
|
|
"os": "mac", "closes": True,
|
|
"params": [P_HOST, ("then_process", "Then restart this program", "Process name of a program that keeps a dead DVS input (blank: none).", "str", ""),
|
|
("then_app", "...and open it as", "Its name or /Applications/... path.", "str", "")],
|
|
"render": lambda p, cfg: "sudo -n %s && open -g -a 'Dante Virtual Soundcard'" % q(dvs_helper(cfg))
|
|
+ ((" || exit 1; sleep 5; " + mac_quit(p["then_process"], 10, True) + mac_open(p["then_app"] or p["then_process"], True)) if p.get("then_process") else ""),
|
|
"suggest": lambda p, cfg: "restart Dante Virtual Soundcard (its engine restarts; audio to and from that Mac drops for 5-10 s)"
|
|
+ (", then %s" % p["then_process"] if p.get("then_process") else ""),
|
|
},
|
|
"helpers": {
|
|
"words": "Restart a helper on this Mac (a LaunchAgent)",
|
|
"os": "self", "closes": True,
|
|
"params": [("labels", "Helpers", "launchd labels, one or more (org.waterschurch.lobby-captions, say).", "list", [])],
|
|
"render": lambda p, cfg: helper_restart(p.get("labels") or []),
|
|
"suggest": lambda p, cfg: "restart %s" % ", ".join(p.get("labels") or []),
|
|
},
|
|
"win_start": {
|
|
"words": "Start a Windows program if it is not running (a StatusPanel task)",
|
|
"os": "windows", "start_only": True,
|
|
"params": [P_HOST, ("task", "Task name", "The scheduled task under \\StatusPanel\\ (Start Vista, say).", "str", None),
|
|
("exe", "Program file", "As Task Manager shows it (Vista.exe).", "str", None)],
|
|
"render": lambda p, cfg: "%s || (schtasks /Run /TN \"\\StatusPanel\\%s\" >nul && ping -n 9 127.0.0.1 >nul && %s)"
|
|
% (win_running(win(p["exe"], "program file")), win(p["task"], "task name"), win_running(p["exe"])),
|
|
"suggest": lambda p, cfg: "start %s if it is not running" % str(p["exe"]).replace(".exe", ""),
|
|
},
|
|
"win_restart": {
|
|
"words": "Restart a Windows program (close it, then its StatusPanel task)",
|
|
"os": "windows", "closes": True,
|
|
"params": [P_HOST, ("task", "Task name", "The scheduled task under \\StatusPanel\\.", "str", None),
|
|
("exe", "Program file", "As Task Manager shows it.", "str", None)],
|
|
"render": lambda p, cfg: "taskkill /F /T /IM \"%s\" >nul 2>&1 & ping -n 3 127.0.0.1 >nul & schtasks /Run /TN \"\\StatusPanel\\%s\" >nul && ping -n 9 127.0.0.1 >nul && %s"
|
|
% (win(p["exe"], "program file"), win(p["task"], "task name"), win_running(p["exe"])),
|
|
"suggest": lambda p, cfg: "restart %s" % str(p["exe"]).replace(".exe", ""),
|
|
},
|
|
"win_service": {
|
|
"words": "Start a Windows service (and its tray program) if they are not running",
|
|
"os": "windows", "start_only": True,
|
|
"params": [P_HOST, ("service", "Service name", "As services.msc lists it (rtpMIDIService).", "str", None),
|
|
("task", "Tray program's task", "The StatusPanel task that starts the tray program (blank: none).", "str", ""),
|
|
("exe", "Tray program file", "rtpMIDI.exe, say.", "str", "")],
|
|
"render": lambda p, cfg: "(sc query %s | find \"RUNNING\" >nul || net start %s)" % (win(p["service"], "service"), win(p["service"], "service"))
|
|
+ ((" && (%s || schtasks /Run /TN \"\\StatusPanel\\%s\")" % (win_running(win(p["exe"], "program file")), win(p["task"], "task name"))) if p.get("task") else ""),
|
|
"suggest": lambda p, cfg: "start %s if it is not running" % p["service"],
|
|
},
|
|
"solo_afl": {
|
|
"words": "Put Pro Tools back to Solo Mode AFL + Latch (through the solo helper)",
|
|
"os": "mac",
|
|
"params": [P_HOST],
|
|
"render": lambda p, cfg: "touch \"%s/solo-mode.fix\"; sleep 3; ! grep -q '\"bad\":true' \"%s/solo-mode.json\"" % (solo_dir(cfg), solo_dir(cfg)),
|
|
"suggest": lambda p, cfg: "set Pro Tools back to Solo Mode AFL + Latch (clicks Options > Solo Mode)",
|
|
},
|
|
"atem_midi_off": {
|
|
"words": "Turn off MIDI control in ATEM Software Control (quits and reopens it)",
|
|
"os": "self", "closes": True, "restart_ok": True,
|
|
"params": [],
|
|
"render": lambda p, cfg: ("was=$(pgrep -f \"ATEM Software Control.app/Contents/MacOS\"); pkill -TERM -f \"ATEM Software Control.app/Contents/MacOS\"; "
|
|
"i=0; while pgrep -fq \"ATEM Software Control.app/Contents/MacOS\" && [ $i -lt 20 ]; do sleep 0.5; i=$((i+1)); done; "
|
|
"defaults write \"com.blackmagic-design.ATEM Software Control\" midiEnabled -bool false; "
|
|
"[ -z \"$was\" ] || open -g -a \"ATEM Software Control\"; "
|
|
"[ \"$(defaults read \"com.blackmagic-design.ATEM Software Control\" midiEnabled)\" = 0 ]"),
|
|
"suggest": lambda p, cfg: "turn off MIDI control in ATEM Software Control (quits and reopens the app; the switcher is not affected)",
|
|
},
|
|
# kinds without a command: the template is the kind, the params are its fields
|
|
"reconnect": {"words": "Reconnect a Companion connection", "kind": "reconnect", "start_only": True, "params": [], "render": None,
|
|
"suggest": lambda p, cfg: "reconnect Companion to it"},
|
|
"hyperdeck": {"words": "Switch a HyperDeck's connection off and on", "kind": "hyperdeck", "start_only": True, "params": [], "render": None,
|
|
"suggest": lambda p, cfg: "turn Companion's connection to that HyperDeck off and on; never reboots the deck"},
|
|
"midi_connect": {"words": "Reconnect a network MIDI peer", "kind": "midi_connect", "start_only": True, "params": [], "render": None,
|
|
"suggest": lambda p, cfg: "reconnect the peer to the network MIDI session it dropped out of"},
|
|
"atem": {"words": "Put the switcher back to the saved file", "kind": "atem", "params": [], "render": None,
|
|
"suggest": lambda p, cfg: "set the switcher back to the saved file"},
|
|
"netgear_poe": {"words": "Power-cycle a PoE port on a Dante switch", "kind": "netgear_poe", "params": [], "render": None,
|
|
"suggest": lambda p, cfg: "switch the device's PoE port off and on"},
|
|
"spotify": {"words": "Play the service playlist in Spotify", "kind": "spotify", "start_only": True, "params": [], "render": None,
|
|
"suggest": lambda p, cfg: "play the %s playlist in Spotify on %s (opens Spotify if it is closed)"
|
|
% ((cfg.get("spotify") or {}).get("name") or "service", (cfg.get("site") or {}).get("ingest_name") or "this Mac")},
|
|
"custom": {"words": "Custom command (Advanced: typed by hand)", "params": [], "render": None, "suggest": lambda p, cfg: ""},
|
|
}
|
|
|
|
|
|
def dvs_helper(cfg):
|
|
return (cfg.get("site") or {}).get("dvs_helper") or "/usr/local/sbin/status-panel-dvs-start"
|
|
|
|
|
|
def solo_dir(cfg):
|
|
return home_dir((cfg.get("site") or {}).get("solo_dir") or "~/Library/Application Support/status-panel")
|
|
|
|
|
|
def template_list(cfg=None):
|
|
"""For the page: name, words, params with defaults, what it needs."""
|
|
out = []
|
|
for name, t in TEMPLATES.items():
|
|
out.append({"name": name, "words": t["words"], "os": t.get("os"), "kind": t.get("kind"), "start_only": bool(t.get("start_only")),
|
|
"closes": bool(t.get("closes")),
|
|
"params": [{"name": n, "words": w, "help": h, "type": k, "default": d} for n, w, h, k, d in t["params"]]})
|
|
return out
|
|
|
|
|
|
def build(cfg, fix):
|
|
"""A fix with template + params -> the same fix with kind/host/run regenerated. Raises ValueError with plain words."""
|
|
name = fix.get("template")
|
|
if not name or name == "custom":
|
|
return dict(fix)
|
|
t = TEMPLATES.get(name)
|
|
if t is None:
|
|
raise ValueError("unknown builder %s" % name)
|
|
params = dict(fix.get("params") or {})
|
|
for n, words, _, kind, default in t["params"]:
|
|
if params.get(n) in (None, "") and default is not None:
|
|
params[n] = default
|
|
if params.get(n) in (None, "") and default is None:
|
|
raise ValueError("%s: %s is needed" % (t["words"], words.lower()))
|
|
out = {k: v for k, v in fix.items() if k not in ("run", "host", "kind")}
|
|
out["template"], out["params"] = name, params
|
|
if t.get("kind"):
|
|
out["kind"] = t["kind"]
|
|
return out
|
|
hosts = cfg.get("hosts") or {}
|
|
host = params.get("host")
|
|
if t.get("os") == "self":
|
|
out["kind"] = "local"
|
|
else:
|
|
h = hosts.get(host)
|
|
if h is None:
|
|
raise ValueError("%s: unknown computer %s" % (t["words"], host))
|
|
want = t.get("os")
|
|
have = "mac" if h.get("self") else (h.get("os") or "mac")
|
|
if want and have != want:
|
|
raise ValueError("%s: %s is a %s computer, this builder is for %s" % (t["words"], h.get("name", host), have, want))
|
|
if h.get("self"):
|
|
out["kind"] = "local"
|
|
else:
|
|
out["kind"], out["host"] = "ssh", host
|
|
out["run"] = t["render"](params, cfg)
|
|
if t.get("restart_ok"):
|
|
out["restart_ok"] = True
|
|
return out
|
|
|
|
|
|
def suggest_label(cfg, fix):
|
|
t = TEMPLATES.get(fix.get("template") or "")
|
|
if not t:
|
|
return ""
|
|
try:
|
|
return t["suggest"](dict(fix.get("params") or {}), cfg)
|
|
except (KeyError, TypeError):
|
|
return ""
|
|
|
|
|
|
# --- check builders (the three remote checks that carry a command) ----------------
|
|
CHECK_TEMPLATES = {
|
|
"mac_program": {
|
|
"words": "A program is running on a Mac", "type": "remote",
|
|
"params": [P_HOST, ("process", "Process name", "As pgrep sees it.", "str", None)],
|
|
"render": lambda p, cfg: "pgrep -xq %s" % q(p["process"]),
|
|
},
|
|
"helper_flag": {
|
|
"words": "A helper file does not say something is wrong", "type": "remote",
|
|
"params": [P_HOST, ("file", "File", "In the helper's folder (solo-mode.json).", "str", "solo-mode.json"),
|
|
("bad", "The bad text", "The check fails when the file holds this.", "str", "\"bad\":true")],
|
|
"render": lambda p, cfg: "! grep -q %s \"%s/%s\"" % (q(p["bad"]), solo_dir(cfg), p["file"]),
|
|
},
|
|
"helper_fresh": {
|
|
"words": "A helper file is fresh and says it is trusted", "type": "remote",
|
|
"params": [P_HOST, ("file", "File", "In the helper's folder.", "str", "solo-mode.json"),
|
|
("max_age", "Fresh means younger than", "Seconds.", "int", 20),
|
|
("flag", "The good text", "The check fails unless the file holds this.", "str", "\"trusted\":true")],
|
|
"render": lambda p, cfg: "f=\"%s/%s\"; [ $(( $(date +%%s) - $(stat -f %%m \"$f\") )) -lt %d ] && grep -q %s \"$f\""
|
|
% (solo_dir(cfg), p["file"], int(p["max_age"]), q(p["flag"])),
|
|
},
|
|
}
|
|
|
|
|
|
def check_template_list():
|
|
return [{"name": n, "words": t["words"], "type": t["type"],
|
|
"params": [{"name": a, "words": w, "help": h, "type": k, "default": d} for a, w, h, k, d in t["params"]]} for n, t in CHECK_TEMPLATES.items()]
|
|
|
|
|
|
def build_check(cfg, chk):
|
|
name = chk.get("template")
|
|
if not name:
|
|
return dict(chk)
|
|
t = CHECK_TEMPLATES.get(name)
|
|
if t is None:
|
|
raise ValueError("unknown check builder %s" % name)
|
|
params = dict(chk.get("params") or {})
|
|
for n, words, _, kind, default in t["params"]:
|
|
if params.get(n) in (None, "") and default is not None:
|
|
params[n] = default
|
|
if params.get(n) in (None, "") and default is None:
|
|
raise ValueError("%s: %s is needed" % (t["words"], words.lower()))
|
|
h = (cfg.get("hosts") or {}).get(params.get("host"))
|
|
if h is None:
|
|
raise ValueError("%s: unknown computer %s" % (t["words"], params.get("host")))
|
|
if h.get("self"):
|
|
raise ValueError("%s: %s is this Mac - use 'A program is running on this Mac'" % (t["words"], h.get("name")))
|
|
out = dict(chk, type=t["type"], template=name, params=params, host=params["host"])
|
|
for k, v in chk.items():
|
|
if k in ("fix", "tag", "service_only", "debounce_seconds", "msg", "name"):
|
|
out[k] = v
|
|
for k in ("fix", "tag", "service_only", "debounce_seconds"):
|
|
if k in chk:
|
|
out[k] = chk[k]
|
|
out["cmd"] = t["render"](params, cfg)
|
|
return out
|
|
|
|
|
|
def mismatches(cfg):
|
|
"""Fixes and checks whose stored command differs from what their fields build (plain words)."""
|
|
out = []
|
|
for fid, fix in (cfg.get("fixes") or {}).items():
|
|
if fix.get("template") and fix["template"] != "custom":
|
|
try:
|
|
b = build(cfg, fix)
|
|
except ValueError as e:
|
|
out.append("fix %s: %s" % (fid, e))
|
|
continue
|
|
for k in ("run", "kind", "host"):
|
|
if (b.get(k) or None) != (fix.get(k) or None):
|
|
out.append("fix %s: its %s no longer matches its fields - open it on Fixes and press Save" % (fid, "command" if k == "run" else k))
|
|
break
|
|
for t in cfg.get("tiles") or []:
|
|
for i, c in enumerate(t.get("checks") or []):
|
|
if c.get("template"):
|
|
try:
|
|
b = build_check(cfg, c)
|
|
except ValueError as e:
|
|
out.append("key %s check %d: %s" % (t.get("label", t["id"]), i + 1, e))
|
|
continue
|
|
if b.get("cmd") != c.get("cmd") or b.get("host") != c.get("host"):
|
|
out.append("key %s check %d: its command no longer matches its fields" % (t.get("label", t["id"]), i + 1))
|
|
return out
|
|
|
|
|
|
# --- today's fixes get their templates (one-time) ------------------------------------
|
|
TODAY = {
|
|
"propresenter": ("mac_restart", {"host": "prop", "process": "ProPresenter", "app": "$APP", "newest_of": "/Applications/ProPresenter*.app", "wait_seconds": 10, "force": True}),
|
|
"ableton": ("ableton", {"host": "tracks", "app": "Ableton Live 11 Suite", "restart": False}),
|
|
"ableton_restart": ("ableton", {"host": "tracks", "app": "Ableton Live 11 Suite", "restart": True, "wait_seconds": 15}),
|
|
"translate": ("mac_start", {"host": "translation", "process": "Church Translate", "app": "Church Translate"}),
|
|
"translate_restart": ("mac_restart", {"host": "translation", "process": "Church Translate", "app": "Church Translate", "wait_seconds": 15, "force": True, "stop_children": True}),
|
|
"dvs_prop": ("dvs", {"host": "prop"}), "dvs_tracks": ("dvs", {"host": "tracks"}), "dvs_translation": ("dvs", {"host": "translation"}),
|
|
"dvs_ingest": ("dvs", {"host": "ingest", "then_process": "ProdCom", "then_app": "/Applications/ProdCom.app"}),
|
|
"bridge": ("helpers", {"labels": ["org.waterschurch.resi-propresenter"]}),
|
|
"captions": ("helpers", {"labels": ["org.waterschurch.lobby-captions", "org.waterschurch.lobby-captions-display"]}),
|
|
"caption_screen": ("helpers", {"labels": ["org.waterschurch.lobby-captions-display"]}),
|
|
"bibleapp": ("win_restart", {"host": "bibletv", "task": "Start BibleApp", "exe": "Bible App.exe"}),
|
|
"obs": ("win_start", {"host": "livestream", "task": "Start OBS", "exe": "obs64.exe"}),
|
|
"arena": ("win_start", {"host": "lighting", "task": "Start Arena", "exe": "Arena.exe"}),
|
|
"vista": ("win_start", {"host": "lighting", "task": "Start Vista", "exe": "Vista.exe"}),
|
|
"rtpmidi_start": ("win_service", {"host": "lighting", "service": "rtpMIDIService", "task": "Start rtpMIDI", "exe": "rtpMIDI.exe"}),
|
|
"solo_afl": ("solo_afl", {"host": "studiob"}),
|
|
"atem_midi": ("atem_midi_off", {}),
|
|
"proddash": ("mac_restart", {"host": "ingest", "process": "ProdDash", "app": "/Applications/ProdDash.app", "wait_seconds": 10, "force": False, "background": True}),
|
|
"prodcom": ("mac_restart", {"host": "ingest", "process": "ProdCom", "app": "/Applications/ProdCom.app", "wait_seconds": 10, "force": True, "background": True}),
|
|
"midi_reconnect": ("midi_connect", {}), "atem_audio": ("atem", {}), "atem_camera": ("atem", {}), "spotify_play": ("spotify", {}),
|
|
"tascam_reconnect": ("reconnect", {}), "hyperdeck": ("hyperdeck", {}),
|
|
}
|
|
TODAY_CHECKS = { # (key id, cmd) -> template + params
|
|
("apps", "pgrep -xq ProPresenter"): ("mac_program", {"host": "prop", "process": "ProPresenter"}),
|
|
("apps", "pgrep -xq Live"): ("mac_program", {"host": "tracks", "process": "Live"}),
|
|
("apps", "pgrep -xq 'Church Translate'"): ("mac_program", {"host": "translation", "process": "Church Translate"}),
|
|
("apps", "! grep -q '\"bad\":true' \"$HOME/Library/Application Support/status-panel/solo-mode.json\""): ("helper_flag", {"host": "studiob"}),
|
|
("apps", "f=\"$HOME/Library/Application Support/status-panel/solo-mode.json\"; [ $(( $(date +%s) - $(stat -f %m \"$f\") )) -lt 20 ] && grep -q '\"trusted\":true' \"$f\""):
|
|
("helper_fresh", {"host": "studiob"}),
|
|
}
|
|
|
|
|
|
def adopt(cfg):
|
|
"""Give today's fixes and checks their templates; returns [(what, before, after)] for every command that changed shape."""
|
|
changed = []
|
|
for fid, fix in cfg.get("fixes", {}).items():
|
|
if fix.get("template") or fid not in TODAY:
|
|
continue
|
|
name, params = TODAY[fid]
|
|
fix["template"], fix["params"] = name, params
|
|
built = build(cfg, fix)
|
|
if built.get("run") != fix.get("run"):
|
|
changed.append(("fix " + fid, fix.get("run"), built.get("run")))
|
|
fix.update({k: v for k, v in built.items() if k in ("run", "kind", "host", "restart_ok")})
|
|
for k in ("host",):
|
|
if k not in built and k in fix:
|
|
del fix[k]
|
|
for t in cfg.get("tiles", []):
|
|
for c in t.get("checks", []):
|
|
key = (t["id"], c.get("cmd"))
|
|
if c.get("template") or key not in TODAY_CHECKS:
|
|
continue
|
|
name, params = TODAY_CHECKS[key]
|
|
c["template"], c["params"] = name, params
|
|
built = build_check(cfg, c)
|
|
if built["cmd"] != c.get("cmd"):
|
|
changed.append(("key %s check" % t["id"], c.get("cmd"), built["cmd"]))
|
|
c["cmd"], c["host"] = built["cmd"], built["host"]
|
|
return changed
|