Files

443 lines
25 KiB
Python
Raw Permalink Normal View History

"""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)",
},
"outlet_cycle": {
"words": "Power-cycle a smart outlet when its device does not answer",
"os": "self", "closes": True,
"params": [("outlet", "Outlet", "As named under Settings > Outlets.", "str", None),
("address", "Address to watch", "The device's address: no power cycle while it answers; afterwards wait for it to answer.", "str", ""),
("dante", "Dante name", "Its Dante device name, if it has a Dante card: the fix waits for it to be back too.", "str", ""),
("wait_seconds", "Off for", "Seconds the outlet stays off.", "int", 8)],
"render": lambda p, cfg: "/usr/bin/python3 \"$HOME/Library/Application Support/status-panel/settings/outlet.py\" cycle %s%s%s --wait %d"
% (q(p["outlet"]), (" --ping " + q(p["address"])) if p.get("address") else "", (" --dante " + q(p["dante"])) if p.get("dante") else "", int(p.get("wait_seconds", 8))),
"suggest": lambda p, cfg: "power-cycle the %s outlet when %s does not answer, then wait for it to come back (never while a HyperDeck is recording)"
% (p["outlet"], p.get("address") or "its device"),
},
# 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