Step 1: the settings page, read-only, live on NA Ingest at port 8020
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>
This commit is contained in:
@@ -125,6 +125,12 @@ The **page number key** on every page follows the ALL button. It's green when ev
|
|||||||
|
|
||||||
A problem has to last 3 seconds before it shows, so one bad reading doesn't trigger an alarm. Recovery shows immediately.
|
A problem has to last 3 seconds before it shows, so one bad reading doesn't trigger an alarm. Recovery shows immediately.
|
||||||
|
|
||||||
|
## The settings page
|
||||||
|
|
||||||
|
`http://na-ingest.local:8020/` (the Ingest Mac, LaunchAgent `org.waterschurch.status-settings`, log `~/Library/Logs/status-settings.log`). Anyone on the church network can look: the live deck, every key and what holding it does, every computer and what runs on it (with a light each after "Check computers now"), the gear, the logs, the history, the help pages (What each key does, the cheat sheet, Where things are). Nothing changes without the editing code, which is `site.editing_code` in `config.json`. Step 1 is read-only; editing and Apply arrive with step 2 (see `docs/settings-plan.md`).
|
||||||
|
|
||||||
|
The page reads what the watchdog writes next to its config (`snapshot.json` every 5 s, `started.json`, `live-now.json`, `fixes-running.json`) instead of asking the gear again. Its own look at the computers (`settings/probes.py`) runs fixed read-only commands over the same login key, fetches whole lists and matches them here, never while a fix runs.
|
||||||
|
|
||||||
## One-time Companion setup
|
## One-time Companion setup
|
||||||
|
|
||||||
1. **Variables → Custom Variables:** add three variables. Leave the start values empty and "persist" off:
|
1. **Variables → Custom Variables:** add three variables. Leave the start values empty and "persist" off:
|
||||||
|
|||||||
@@ -311,6 +311,11 @@
|
|||||||
"kind": "service",
|
"kind": "service",
|
||||||
"name": "rtpMIDIService"
|
"name": "rtpMIDIService"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "midi_session",
|
||||||
|
"name": "Lighting PC",
|
||||||
|
"peer_of": "NA Ingest"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "artnet",
|
"kind": "artnet",
|
||||||
"name": "Vista Art-Net",
|
"name": "Vista Art-Net",
|
||||||
|
|||||||
+12
-1
@@ -22,7 +22,7 @@ KEEP_CONFIG=0
|
|||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--keep-config) KEEP_CONFIG=1 ;;
|
--keep-config) KEEP_CONFIG=1 ;;
|
||||||
--remove|--rollback) ;;
|
--remove|--rollback|--now) ;;
|
||||||
*) echo "unknown option $arg" >&2; exit 2 ;;
|
*) echo "unknown option $arg" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -127,6 +127,17 @@ if [[ "$1" == "--rollback" ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
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.
|
# 1. The config that will run, checked with the code that will run it.
|
||||||
mkdir -p "$DEST"
|
mkdir -p "$DEST"
|
||||||
if [[ $KEEP_CONFIG == 1 && -f "$DEST/config.json" ]]; then
|
if [[ $KEEP_CONFIG == 1 && -f "$DEST/config.json" ]]; then
|
||||||
|
|||||||
+34
-13
@@ -1,6 +1,7 @@
|
|||||||
"""The teaching pages, built from the config: "What each key does", the cheat sheet, "Where things are"."""
|
"""The teaching pages, built from the config: "What each key does", the cheat sheet, "Where things are"."""
|
||||||
import html
|
import html
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from settings import PARENT # noqa: F401
|
from settings import PARENT # noqa: F401
|
||||||
import schema
|
import schema
|
||||||
@@ -23,7 +24,7 @@ table { border-collapse: collapse; width: 100%; font-size: .95rem; } th, td { te
|
|||||||
|
|
||||||
FAULT_WORDS = {
|
FAULT_WORDS = {
|
||||||
"conn": ["NO ANSWER", "UNREACHABLE", "REFUSED", "LOGIN", "OFFLINE", "WARNING", "DISABLED", "NOT IN COMPANION", "NO DATA"],
|
"conn": ["NO ANSWER", "UNREACHABLE", "REFUSED", "LOGIN", "OFFLINE", "WARNING", "DISABLED", "NOT IN COMPANION", "NO DATA"],
|
||||||
"hyperdeck": ["NO DISK", "FULL <minutes>", "LOW <minutes>", "NOT REC", "OFFLINE", "NO ANSWER"],
|
"hyperdeck": ["NO DISK", "FULL (minutes left)", "LOW (minutes left)", "NOT REC", "OFFLINE", "NO ANSWER"],
|
||||||
"dante": ["<device> GONE"], "shure": ["<channel> BATT", "<channel> INTERF"], "psm": ["<channel> RF MUTE"],
|
"dante": ["<device> GONE"], "shure": ["<channel> BATT", "<channel> INTERF"], "psm": ["<channel> RF MUTE"],
|
||||||
"tascam": ["<the recorder's own error or caution>"], "ping": ["<name> NO PING"], "launchd": ["<name> NOT LOADED", "<name> STOPPED"],
|
"tascam": ["<the recorder's own error or caution>"], "ping": ["<name> NO PING"], "launchd": ["<name> NOT LOADED", "<name> STOPPED"],
|
||||||
"process": ["<name> STOPPED"], "http": ["<name> UNREACH"], "disk": ["DISK <free>GB"],
|
"process": ["<name> STOPPED"], "http": ["<name> UNREACH"], "disk": ["DISK <free>GB"],
|
||||||
@@ -94,26 +95,46 @@ def fix_words(fix_id, cfg):
|
|||||||
return e(fix.get("label", fix_id)) if fix else e(fix_id)
|
return e(fix.get("label", fix_id)) if fix else e(fix_id)
|
||||||
|
|
||||||
|
|
||||||
|
def plain(markup):
|
||||||
|
"""The same words without tags or entities, for the page's JSON."""
|
||||||
|
return html.unescape(re.sub(r"<[^>]+>", "", markup))
|
||||||
|
|
||||||
|
|
||||||
def hold_words(tile, cfg):
|
def hold_words(tile, cfg):
|
||||||
fixes = []
|
always = tile.get("hold_always")
|
||||||
|
fixes, per_sub = [], []
|
||||||
for chk in tile.get("checks", []):
|
for chk in tile.get("checks", []):
|
||||||
fx = chk.get("fix")
|
fx = chk.get("fix")
|
||||||
|
if isinstance(fx, dict) and len(set(fx.values())) == 1:
|
||||||
|
fx = next(iter(fx.values())) # one fix for every sub-check: a plain fix
|
||||||
if isinstance(fx, dict):
|
if isinstance(fx, dict):
|
||||||
for sub, f in fx.items():
|
for sub, f in fx.items():
|
||||||
fixes.append("%s: %s" % ("for anything else" if sub == "*" else "for " + e(sub), fix_words(f, cfg)))
|
if f == sp.DANTE_AUTO or sub == "*":
|
||||||
|
per_sub.append(fix_words(f, cfg) if sub == "*" else "%s: %s" % (e(sub), fix_words(f, cfg)))
|
||||||
|
else:
|
||||||
|
per_sub.append("%s: %s" % (e(sub), fix_words(f, cfg)))
|
||||||
elif fx:
|
elif fx:
|
||||||
fixes.append(fix_words(fx, cfg))
|
fixes.append(fx)
|
||||||
elif chk["type"] in sp.RESTARTABLE and chk.get("conn"):
|
elif chk["type"] in sp.RESTARTABLE and chk.get("conn"):
|
||||||
fixes.append("reconnects Companion's %s connection" % e(chk["conn"]))
|
fixes.append(sp.RESTART_CONNECTION + ":" + chk["conn"])
|
||||||
out = []
|
words, seen = [], set()
|
||||||
if tile.get("hold_always"):
|
|
||||||
out.append("<b>also when the key is green:</b> " + fix_words(tile["hold_always"], cfg))
|
|
||||||
seen = []
|
|
||||||
for f in fixes:
|
for f in fixes:
|
||||||
if f not in seen:
|
if f in seen:
|
||||||
seen.append(f)
|
continue
|
||||||
if seen:
|
seen.add(f)
|
||||||
out.append("fixes what is failing: " + "; ".join(seen))
|
if f.startswith(sp.RESTART_CONNECTION + ":"):
|
||||||
|
words.append("reconnects Companion's %s connection" % e(f.split(":", 1)[1]))
|
||||||
|
elif f == always:
|
||||||
|
words.append(fix_words(f, cfg) + " <b>(also when the key is green)</b>")
|
||||||
|
else:
|
||||||
|
words.append(fix_words(f, cfg))
|
||||||
|
if always and always not in seen:
|
||||||
|
words.insert(0, fix_words(always, cfg) + " <b>(also when the key is green)</b>")
|
||||||
|
out = []
|
||||||
|
if words:
|
||||||
|
out.append("; ".join(words))
|
||||||
|
if per_sub:
|
||||||
|
out.append("for a named device, " + "; ".join(dict.fromkeys(per_sub)))
|
||||||
if tile.get("kind") == "network":
|
if tile.get("kind") == "network":
|
||||||
out.append("clears a remembered network blip")
|
out.append("clears a remembered network blip")
|
||||||
if tile.get("members"):
|
if tile.get("members"):
|
||||||
|
|||||||
+18
-5
@@ -39,10 +39,17 @@ WIN_PROBE = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def shell_path(path):
|
||||||
|
"""A path for a shell line: quoted, except that a leading ~ becomes $HOME outside the quotes."""
|
||||||
|
if path.startswith("~/"):
|
||||||
|
return '"$HOME"' + shlex.quote(path[1:])
|
||||||
|
return shlex.quote(path)
|
||||||
|
|
||||||
|
|
||||||
def quoted_paths(cfg):
|
def quoted_paths(cfg):
|
||||||
site = cfg.get("site", {})
|
site = cfg.get("site", {})
|
||||||
return {"helper": shlex.quote(site.get("dvs_helper", "/usr/local/sbin/status-panel-dvs-start")),
|
return {"helper": shell_path(site.get("dvs_helper", "/usr/local/sbin/status-panel-dvs-start")),
|
||||||
"solo": shlex.quote(site.get("solo_dir", "~/Library/Application Support/status-panel").replace("~", "$HOME"))}
|
"solo": shell_path(site.get("solo_dir", "~/Library/Application Support/status-panel"))}
|
||||||
|
|
||||||
|
|
||||||
def run_ssh(cfg, alias, command, timeout=TIMEOUT):
|
def run_ssh(cfg, alias, command, timeout=TIMEOUT):
|
||||||
@@ -146,11 +153,17 @@ def mac_items(host, parsed, cfg):
|
|||||||
label = run.get("label", "")
|
label = run.get("label", "")
|
||||||
item.update(light(label not in agents, "loaded - it must not run here"))
|
item.update(light(label not in agents, "loaded - it must not run here"))
|
||||||
elif kind == "daemon":
|
elif kind == "daemon":
|
||||||
proc = run.get("process", "")
|
want = run.get("process", "").lower().replace(" ", "")
|
||||||
item.update(light(any(proc in p for p in procs), "not running"))
|
running = any(want in p.lower().replace(" ", "") for p in procs) or any(want in a.lower() for a in agents) \
|
||||||
|
or (want.startswith("dantevirtual") and any("dvsd" in p for p in procs))
|
||||||
|
item.update(light(running, "not running"))
|
||||||
elif kind == "helper_dvs":
|
elif kind == "helper_dvs":
|
||||||
|
# `sudo -n -l <helper>` answers with the command itself when it is allowed without a password.
|
||||||
|
lines = [l for l in helper.splitlines() if l.strip()]
|
||||||
installed = "No such file" not in helper and "-rwx" in helper
|
installed = "No such file" not in helper and "-rwx" in helper
|
||||||
allowed = "NOPASSWD" in helper or "(root)" in helper or "may run" in helper.lower()
|
last = lines[-1] if lines else ""
|
||||||
|
allowed = installed and "status-panel-dvs-start" in last and "not allowed" not in last.lower() and "may not" not in last.lower() \
|
||||||
|
and "password" not in last.lower()
|
||||||
item.update(light(installed and allowed, "not installed" if not installed else "sudo rule missing"))
|
item.update(light(installed and allowed, "not installed" if not installed else "sudo rule missing"))
|
||||||
elif kind == "helper_solo":
|
elif kind == "helper_solo":
|
||||||
label = run.get("label", "org.waterschurch.status-panel.solo-mode")
|
label = run.get("label", "org.waterschurch.status-panel.solo-mode")
|
||||||
|
|||||||
+70
-24
@@ -163,11 +163,27 @@ class Site:
|
|||||||
if f["level"] not in ("err", "warn"):
|
if f["level"] not in ("err", "warn"):
|
||||||
continue
|
continue
|
||||||
text = f["msg"] if f["msg"] and f["msg"] != tile["label"] else tile["label"]
|
text = f["msg"] if f["msg"] and f["msg"] != tile["label"] else tile["label"]
|
||||||
hold = keyguide.hold_words(tile, cfg)
|
hold = keyguide.plain(keyguide.hold_words(tile, cfg))
|
||||||
problems.append({"key": tid, "label": tile["label"], "text": "%s: %s" % (tile["label"], f["msg"]),
|
problems.append({"key": tid, "label": tile["label"], "text": "%s: %s" % (tile["label"], f["msg"]),
|
||||||
"level": f["level"], "acked": f.get("acked"), "since": f.get("since"),
|
"level": f["level"], "acked": f.get("acked"), "since": f.get("since"),
|
||||||
"action": "hold the %s key (%s)" % (tile["label"], hold) if hold and not hold.startswith("nothing")
|
"action": "hold the %s key (%s)" % (tile["label"], hold) if hold and not hold.startswith("nothing")
|
||||||
else "no automatic fix: check the device"})
|
else "no automatic fix: check the device"})
|
||||||
|
fresh = snap.get("time") and now - snap["time"] < 30
|
||||||
|
if not problems and deck and not fresh:
|
||||||
|
# The watchdog has not written a snapshot (older code, or just started): read the deck itself.
|
||||||
|
for tid, s in deck.items():
|
||||||
|
tile = tiles.get(tid)
|
||||||
|
if not tile or tile.get("members") or tile.get("kind") == "master":
|
||||||
|
continue # groups and ALL repeat their members
|
||||||
|
if s.get("b") not in (0xFF0000, 0x8B0000, 0xFFA000, 0x3A0000):
|
||||||
|
continue
|
||||||
|
lines = (s.get("x") or "").split("\n")
|
||||||
|
text = " ".join(lines[1:]) if len(lines) > 1 else lines[0]
|
||||||
|
hold = keyguide.plain(keyguide.hold_words(tile, cfg))
|
||||||
|
problems.append({"key": tid, "label": tile["label"], "text": "%s: %s" % (tile["label"], text),
|
||||||
|
"level": "warn" if s.get("b") == 0xFFA000 else "err", "acked": s.get("b") == 0x8B0000, "since": None,
|
||||||
|
"action": "hold the %s key (%s)" % (tile["label"], hold) if hold and not hold.startswith("nothing")
|
||||||
|
else "no automatic fix: check the device"})
|
||||||
if started and started.get("config_sha") and started["config_sha"] != self.cfg_sha:
|
if started and started.get("config_sha") and started["config_sha"] != self.cfg_sha:
|
||||||
needs.append("The watchdog is running an older settings file than the one on disk (restart it to pick up the change).")
|
needs.append("The watchdog is running an older settings file than the one on disk (restart it to pick up the change).")
|
||||||
pages = self.pages_state()
|
pages = self.pages_state()
|
||||||
@@ -176,7 +192,8 @@ class Site:
|
|||||||
return {
|
return {
|
||||||
"time": now, "site": cfg.get("site", {}), "deck": deck, "heartbeat_age": (now - hb) if hb else None,
|
"time": now, "site": cfg.get("site", {}), "deck": deck, "heartbeat_age": (now - hb) if hb else None,
|
||||||
"companion_ok": companion_ok, "watchdog": self.watchdog_state(started, hb, now),
|
"companion_ok": companion_ok, "watchdog": self.watchdog_state(started, hb, now),
|
||||||
"live": live, "running": running, "service_on": snap.get("service_on"), "alarms_override": snap.get("alarms_override"),
|
"live": live, "running": running, "service_on": snap.get("service_on") if fresh else None,
|
||||||
|
"alarms_override": snap.get("alarms_override") if fresh else None,
|
||||||
"auto_fix": {"phrase": schema.phrases(cfg)["auto_fix"], "state": snap.get("auto_fix")},
|
"auto_fix": {"phrase": schema.phrases(cfg)["auto_fix"], "state": snap.get("auto_fix")},
|
||||||
"problems": sorted(problems, key=lambda p: (p["level"] != "err", p["since"] or 0)), "needs": needs,
|
"problems": sorted(problems, key=lambda p: (p["level"] != "err", p["since"] or 0)), "needs": needs,
|
||||||
"computers": self.computer_lines(snap), "pages": pages, "snapshot_age": (now - snap["time"]) if snap.get("time") else None,
|
"computers": self.computer_lines(snap), "pages": pages, "snapshot_age": (now - snap["time"]) if snap.get("time") else None,
|
||||||
@@ -196,34 +213,63 @@ class Site:
|
|||||||
state["heartbeat_fresh"] = bool(hb and now - hb < cfg_stale(self.cfg))
|
state["heartbeat_fresh"] = bool(hb and now - hb < cfg_stale(self.cfg))
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
def check_hosts(self, cfg):
|
||||||
|
"""{(key id, check identity): {computer aliases}}: which computers a check is about - the Mac a
|
||||||
|
MIDI session or remote command runs on, the peer's computer when a computer is named like it,
|
||||||
|
the computer a check's fix runs on, this Mac for local things."""
|
||||||
|
hosts = cfg.get("hosts", {})
|
||||||
|
me = next((a for a, h in hosts.items() if h.get("self")), None)
|
||||||
|
by_session = {}
|
||||||
|
for alias, h in hosts.items():
|
||||||
|
for run in h.get("runs", []):
|
||||||
|
if run.get("kind") == "midi_session" and run.get("name"):
|
||||||
|
by_session[run["name"]] = alias
|
||||||
|
if h.get("name"):
|
||||||
|
by_session.setdefault(h["name"], alias)
|
||||||
|
out = {}
|
||||||
|
for t in cfg["tiles"]:
|
||||||
|
for c in t.get("checks", []):
|
||||||
|
who = set()
|
||||||
|
if c["type"] in ("remote", "midi_peer") and c.get("host"):
|
||||||
|
who.add(c["host"])
|
||||||
|
elif c["type"] == "midi_peer" and me:
|
||||||
|
who.add(me)
|
||||||
|
if c["type"] == "midi_peer" and c.get("peer") in by_session:
|
||||||
|
who.add(by_session[c["peer"]])
|
||||||
|
if c["type"] in ("launchd", "process", "disk", "pref", "spotify", "atem", "midi_rx") and me:
|
||||||
|
who.add(me)
|
||||||
|
fx = c.get("fix")
|
||||||
|
for f in (fx.values() if isinstance(fx, dict) else [fx]):
|
||||||
|
fix = cfg.get("fixes", {}).get(f) if f else None
|
||||||
|
if fix and fix.get("host"):
|
||||||
|
who.add(fix["host"])
|
||||||
|
elif fix and fix.get("kind") in ("local", "spotify") and me:
|
||||||
|
who.add(me)
|
||||||
|
if t.get("hold_always"):
|
||||||
|
fix = cfg["fixes"].get(t["hold_always"], {})
|
||||||
|
if fix.get("host"):
|
||||||
|
who.add(fix["host"])
|
||||||
|
out[(t["id"], sp.check_identity(c))] = who
|
||||||
|
return out
|
||||||
|
|
||||||
def computer_lines(self, snap):
|
def computer_lines(self, snap):
|
||||||
"""One line per computer: a status word and the reason, from what the watchdog sees."""
|
"""One line per computer: a status word and the reason, from what the watchdog sees."""
|
||||||
cfg = self.cfg
|
cfg = self.cfg
|
||||||
live = self.read_json("machines-live.json", {}) or {}
|
live = self.read_json("machines-live.json", {}) or {}
|
||||||
tiles_by_host = {}
|
owners = self.check_hosts(cfg)
|
||||||
for t in cfg["tiles"]:
|
tiles_by_host, labels = {}, {t["id"]: t["label"] for t in cfg["tiles"]}
|
||||||
for c in t.get("checks", []):
|
for (tid, _), who in owners.items():
|
||||||
h = c.get("host") if c["type"] in ("remote", "midi_peer") else None
|
for alias in who:
|
||||||
if h:
|
tiles_by_host.setdefault(alias, set()).add(tid)
|
||||||
tiles_by_host.setdefault(h, set()).add(t["id"])
|
|
||||||
for fid, fix in cfg.get("fixes", {}).items():
|
|
||||||
if fix.get("host"):
|
|
||||||
for t in cfg["tiles"]:
|
|
||||||
own = set()
|
|
||||||
for c in t.get("checks", []):
|
|
||||||
fx = c.get("fix")
|
|
||||||
own |= set(fx.values()) if isinstance(fx, dict) else ({fx} if fx else set())
|
|
||||||
if fid in own or t.get("hold_always") == fid:
|
|
||||||
tiles_by_host.setdefault(fix["host"], set()).add(t["id"])
|
|
||||||
lines = []
|
lines = []
|
||||||
for alias, h in cfg.get("hosts", {}).items():
|
for alias, h in cfg.get("hosts", {}).items():
|
||||||
probe = live.get(alias) or {}
|
probe = live.get(alias) or {}
|
||||||
word, reason = "OK", ""
|
word, reason = "OK", ""
|
||||||
faults = []
|
faults = []
|
||||||
for tid in sorted(tiles_by_host.get(alias, [])):
|
for tid, fs in (snap.get("faults") or {}).items():
|
||||||
for f in (snap.get("faults") or {}).get(tid, []):
|
for f in fs:
|
||||||
if f["level"] == "err" and (f["type"] in ("remote", "midi_peer") or True):
|
if f["level"] == "err" and alias in owners.get((tid, f.get("check")), set()):
|
||||||
faults.append("%s %s" % (tiles_by_host and next(t["label"] for t in cfg["tiles"] if t["id"] == tid), f["msg"]))
|
faults.append("%s %s" % (labels.get(tid, tid), f["msg"]))
|
||||||
if probe and not probe.get("reachable") and not h.get("self"):
|
if probe and not probe.get("reachable") and not h.get("self"):
|
||||||
word, reason = "Problem", "cannot be reached (%s)" % probe.get("error", "no answer")
|
word, reason = "Problem", "cannot be reached (%s)" % probe.get("error", "no answer")
|
||||||
elif faults:
|
elif faults:
|
||||||
@@ -453,14 +499,14 @@ class Site:
|
|||||||
if c.get("tag"):
|
if c.get("tag"):
|
||||||
ws = ["%s %s" % (c["tag"], w) for w in ws]
|
ws = ["%s %s" % (c["tag"], w) for w in ws]
|
||||||
words += ws
|
words += ws
|
||||||
tiles.append(dict(t, says=sorted(set(words)), holding=re.sub(r"<[^>]+>", "", keyguide.hold_words(t, cfg)),
|
tiles.append(dict(t, says=sorted(set(words)), holding=keyguide.plain(keyguide.hold_words(t, cfg)),
|
||||||
checks=[dict(c, sentence=re.sub(r"<[^>]+>", "", keyguide.check_sentence(c, cfg)),
|
checks=[dict(c, sentence=keyguide.plain(keyguide.check_sentence(c, cfg)),
|
||||||
identity=sp.check_identity(c)) for c in t.get("checks", [])]))
|
identity=sp.check_identity(c)) for c in t.get("checks", [])]))
|
||||||
rp = cfg.get("restart_page")
|
rp = cfg.get("restart_page")
|
||||||
keys = []
|
keys = []
|
||||||
if rp:
|
if rp:
|
||||||
for k in rp["keys"]:
|
for k in rp["keys"]:
|
||||||
keys.append(dict(k, holding=re.sub(r"<[^>]+>", "", keyguide.fix_words(k["fix"], cfg))))
|
keys.append(dict(k, holding=keyguide.plain(keyguide.fix_words(k["fix"], cfg))))
|
||||||
return {"page": cfg["page"], "page_name": cfg["page_name"], "tiles": tiles, "restart_page": dict(rp, keys=keys) if rp else None,
|
return {"page": cfg["page"], "page_name": cfg["page_name"], "tiles": tiles, "restart_page": dict(rp, keys=keys) if rp else None,
|
||||||
"hold": schema.phrases(cfg)["hold"], "grid": cfg.get("page_grid", schema.F["page_grid"]["default"])}
|
"hold": schema.phrases(cfg)["hold"], "grid": cfg.get("page_grid", schema.F["page_grid"]["default"])}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -95,7 +95,7 @@
|
|||||||
<dl class="kv">
|
<dl class="kv">
|
||||||
<dt>Watchdog</dt><dd>${w.running ? "OK, running" : "<b>not running</b>"}${w.since ? " since " + esc(when(w.since)) : ""}${w.version ? ` <span class="pill">code ${esc(w.version)}</span>` : ""}${o.heartbeat_age != null ? ` · heartbeat ${esc(ago(o.heartbeat_age))}` : ""}</dd>
|
<dt>Watchdog</dt><dd>${w.running ? "OK, running" : "<b>not running</b>"}${w.since ? " since " + esc(when(w.since)) : ""}${w.version ? ` <span class="pill">code ${esc(w.version)}</span>` : ""}${o.heartbeat_age != null ? ` · heartbeat ${esc(ago(o.heartbeat_age))}` : ""}</dd>
|
||||||
<dt>Companion</dt><dd>${o.companion_ok ? "OK" : "<b>not answering</b>"}${o.pages.status ? (o.pages.status.ok ? ", pages OK" : ", page " + o.pages.status.page + " is called " + esc(o.pages.status.live || "?")) : ""}</dd>
|
<dt>Companion</dt><dd>${o.companion_ok ? "OK" : "<b>not answering</b>"}${o.pages.status ? (o.pages.status.ok ? ", pages OK" : ", page " + o.pages.status.page + " is called " + esc(o.pages.status.live || "?")) : ""}</dd>
|
||||||
<dt>Alarms</dt><dd>${o.service_on ? "ON" : "silenced until midnight"}${o.alarms_override != null ? " (by hand)" : ""} <span class="muted small">· ${esc(o.phrases.alarms)}</span></dd>
|
<dt>Alarms</dt><dd>${o.service_on == null ? "<span class='muted'>not reported yet</span>" : o.service_on ? "ON" : "silenced until midnight"}${o.alarms_override != null ? " (by hand)" : ""} <span class="muted small">· ${esc(o.phrases.alarms)}</span></dd>
|
||||||
<dt>Auto-fix</dt><dd>${esc(o.auto_fix.phrase)}${o.auto_fix.state && o.auto_fix.state.on ? " · <b>may act now</b>" : ""}${o.auto_fix.state && o.auto_fix.state.paused ? " (waiting out a network blip)" : ""}</dd>
|
<dt>Auto-fix</dt><dd>${esc(o.auto_fix.phrase)}${o.auto_fix.state && o.auto_fix.state.on ? " · <b>may act now</b>" : ""}${o.auto_fix.state && o.auto_fix.state.paused ? " (waiting out a network blip)" : ""}</dd>
|
||||||
<dt>Running now</dt><dd>${Object.keys(o.running || {}).length ? Object.values(o.running).map((r) => esc(r.what + " on " + r.key)).join(", ") : "no fix is running"}</dd>
|
<dt>Running now</dt><dd>${Object.keys(o.running || {}).length ? Object.values(o.running).map((r) => esc(r.what + " on " + r.key)).join(", ") : "no fix is running"}</dd>
|
||||||
<dt>Last change</dt><dd>${o.last_change ? esc(o.last_change.when + " · " + o.last_change.note) : "—"} <a href="#history" class="small">History</a></dd>
|
<dt>Last change</dt><dd>${o.last_change ? esc(o.last_change.when + " · " + o.last_change.note) : "—"} <a href="#history" class="small">History</a></dd>
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function computerCard(c) {
|
function computerCard(c) {
|
||||||
const items = c.items.map((i) => `<tr><td>${lightDot(i.light)}${esc(i.light === "ok" ? "OK" : i.light === "problem" ? "Problem" : i.light === "not_needed" ? "Not needed" : "Can't see from here")}</td><td>${esc(i.name)}${i.watched === false ? ' <span class="muted small">(not watched)</span>' : ""}</td><td class="muted small">${esc(i.reason || i.detail || (i.detail && JSON.stringify(i.detail)) || "")}</td></tr>`).join("");
|
const items = c.items.map((i) => `<tr><td>${lightDot(i.light)}${esc(i.light === "ok" ? "OK" : i.light === "problem" ? "Problem" : i.light === "not_needed" ? "Not needed" : "Can't see from here")}</td><td>${esc(i.name)}${i.watched === false ? ' <span class="muted small">(not watched)</span>' : ""}</td><td class="muted small">${esc(i.reason || (typeof i.detail === "object" && i.detail ? Object.entries(i.detail).map(([k, v]) => k + " " + (v === true ? "yes" : v === false ? "no" : k === "age" ? ago(v) : v)).join(", ") : i.detail || ""))}</td></tr>`).join("");
|
||||||
const found = c.found || {};
|
const found = c.found || {};
|
||||||
const foundHtml = Object.entries(found).filter(([, v]) => Array.isArray(v) && v.length).map(([k, v]) => `<p class="small"><b>${esc(k)}:</b> <span class="mono">${esc(typeof v[0] === "object" ? v.map((x) => x.iface + " " + x.ip + (x.active ? "" : " (down)")).join(", ") : v.join(", "))}</span></p>`).join("") || `<p class="muted small">Press "Check computers now" to look.</p>`;
|
const foundHtml = Object.entries(found).filter(([, v]) => Array.isArray(v) && v.length).map(([k, v]) => `<p class="small"><b>${esc(k)}:</b> <span class="mono">${esc(typeof v[0] === "object" ? v.map((x) => x.iface + " " + x.ip + (x.active ? "" : " (down)")).join(", ") : v.join(", "))}</span></p>`).join("") || `<p class="muted small">Press "Check computers now" to look.</p>`;
|
||||||
const done = Object.entries(c.manual_done || {}).map(([k, v]) => `<li>${lightDot("hand")}${esc(k.replace(/_/g, " "))}: done by hand (${esc(v.by)}, ${esc(v.on)})</li>`).join("");
|
const done = Object.entries(c.manual_done || {}).map(([k, v]) => `<li>${lightDot("hand")}${esc(k.replace(/_/g, " "))}: done by hand (${esc(v.by)}, ${esc(v.on)})</li>`).join("");
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
--bg: #14181a; --surface: #1d2326; --fg: #e6ebe6; --muted: #9aa69e; --line: #333c3f; --accent: #5fcf8a; --accent-soft: #1f3a2b;
|
--bg: #14181a; --surface: #1d2326; --fg: #e6ebe6; --muted: #9aa69e; --line: #333c3f; --accent: #5fcf8a; --accent-soft: #1f3a2b;
|
||||||
--deck: #0c0e0d; --ok: #3fbf6f; --problem: #ff6a5e; --warn: #f0b84a; --unknown: #8b96a0; --none: #4c5553; --hand: #7fb3ef; color-scheme: dark }
|
--deck: #0c0e0d; --ok: #3fbf6f; --problem: #ff6a5e; --warn: #f0b84a; --unknown: #8b96a0; --none: #4c5553; --hand: #7fb3ef; color-scheme: dark }
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
[hidden] { display: none !important; }
|
||||||
body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.45 var(--font); }
|
body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.45 var(--font); }
|
||||||
.top { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5; display: flex; align-items: center; gap: 12px; padding: 8px 16px; background: var(--surface); border-bottom: 1px solid var(--line); }
|
.top { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5; display: flex; align-items: center; gap: 12px; padding: 8px 16px; background: var(--surface); border-bottom: 1px solid var(--line); }
|
||||||
.brand { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
|
.brand { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
"""The settings page's server and probes, offline: routes against the fake Companion, the LAN
|
||||||
|
binding, the editing-code gate, the probe parsers on captured output, and the teaching pages."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from http.server import ThreadingHTTPServer
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, ROOT)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import status_panel as sp # noqa: E402
|
||||||
|
from settings import keyguide, probes, server # noqa: E402
|
||||||
|
from test_status_panel import CONFIG, FakeCompanion, healthy_snapshot # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def get(port, path, json_body=None, origin=None):
|
||||||
|
data = json.dumps(json_body).encode() if json_body is not None else None
|
||||||
|
req = urllib.request.Request("http://127.0.0.1:%d%s" % (port, path), data=data,
|
||||||
|
headers={"Content-Type": "application/json", **({"Origin": origin} if origin else {})})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as r:
|
||||||
|
body = r.read()
|
||||||
|
return r.status, body
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, e.read()
|
||||||
|
|
||||||
|
|
||||||
|
class TestServer(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
snap = healthy_snapshot(time.time())
|
||||||
|
FakeCompanion.store = {"conns": snap.conns, "vars": dict(snap.vars),
|
||||||
|
"custom": {"sp_state": json.dumps({"all": {"x": "ALL OK", "b": 1, "f": 2, "b2": 1, "f2": 2, "s": 20}}),
|
||||||
|
"sp_hb": str(int(time.time())), "sp_cmd": "", "flash_tick": "0"}, "restarts": []}
|
||||||
|
cls.fake = ThreadingHTTPServer(("127.0.0.1", 0), FakeCompanion)
|
||||||
|
threading.Thread(target=cls.fake.serve_forever, daemon=True).start()
|
||||||
|
cls.data = tempfile.mkdtemp(prefix="sp-settings-")
|
||||||
|
cfg = json.loads(json.dumps(CONFIG))
|
||||||
|
cfg["companion"] = {"host": "127.0.0.1", "port": cls.fake.server_address[1]}
|
||||||
|
cfg["site"]["editing_code"] = "apple river stone"
|
||||||
|
json.dump(cfg, open(os.path.join(cls.data, "config.json"), "w"))
|
||||||
|
now = time.time()
|
||||||
|
json.dump({"pid": 1, "started_at": now - 100, "config_sha": "x", "version": "test"}, open(os.path.join(cls.data, "started.json"), "w"))
|
||||||
|
json.dump({"live": True, "reason": "a HyperDeck is recording", "time": now}, open(os.path.join(cls.data, "live-now.json"), "w"))
|
||||||
|
json.dump({"time": now, "conns": {}, "faults": {"atem": [{"check": "ATEM", "type": "conn", "sub": "conn", "level": "err",
|
||||||
|
"msg": "NO ANSWER", "since": now - 60, "acked": False}],
|
||||||
|
"midi": [{"check": "tracks/NA Tracks/Lighting PC", "type": "midi_peer", "sub": "peer",
|
||||||
|
"level": "err", "msg": "FX LIGHTS MIDI LOST", "since": now - 60, "acked": False}]},
|
||||||
|
"midi": {"NA Ingest": {"peers": [{"name": "Lighting PC", "state": "running"}]}}, "remote_midi": {},
|
||||||
|
"http": {"http://10.6.7.13:8080/": True}, "ping": {"10.1.3.227": 0}, "service_on": True, "dante_map": {}},
|
||||||
|
open(os.path.join(cls.data, "snapshot.json"), "w"))
|
||||||
|
sock = socket.socket(); sock.bind(("127.0.0.1", 0)); cls.port = sock.getsockname()[1]; sock.close()
|
||||||
|
cls.site, cls.servers = server.serve(cls.data, cls.port, ["127.0.0.1"])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
for s in cls.servers:
|
||||||
|
s.shutdown()
|
||||||
|
cls.fake.shutdown()
|
||||||
|
|
||||||
|
def test_reads_are_open_and_secrets_are_masked(self):
|
||||||
|
status, body = get(self.port, "/api/config")
|
||||||
|
self.assertEqual(status, 200)
|
||||||
|
cfg = json.loads(body)["config"]
|
||||||
|
self.assertEqual(cfg["site"]["editing_code"], "set (17 characters)")
|
||||||
|
self.assertNotIn("apple river stone", body.decode())
|
||||||
|
|
||||||
|
def test_overview_shows_the_deck_the_problems_and_the_live_state(self):
|
||||||
|
status, body = get(self.port, "/api/overview")
|
||||||
|
o = json.loads(body)
|
||||||
|
self.assertEqual(o["deck"]["all"]["x"], "ALL OK")
|
||||||
|
self.assertTrue(o["companion_ok"])
|
||||||
|
self.assertEqual([p["text"] for p in o["problems"]], ["ATEM: NO ANSWER", "MIDI: FX LIGHTS MIDI LOST"])
|
||||||
|
words = {c["alias"]: c["word"] for c in o["computers"]}
|
||||||
|
self.assertEqual((words["tracks"], words["lighting"], words["prop"], words["ingest"]), ("Problem", "Problem", "Not looked yet", "OK"))
|
||||||
|
self.assertIn("hold the ATEM key", o["problems"][0]["action"])
|
||||||
|
self.assertTrue(o["live"]["live"])
|
||||||
|
self.assertEqual(len(o["computers"]), len(CONFIG["hosts"]))
|
||||||
|
self.assertEqual(o["watchdog"]["version"], "test")
|
||||||
|
|
||||||
|
def test_companion_view_lists_connections_with_their_watchers(self):
|
||||||
|
o = json.loads(get(self.port, "/api/companion")[1])
|
||||||
|
by = {c["label"]: c for c in o["connections"]}
|
||||||
|
self.assertIn("hd2", by["HyperDeck_2"]["watched_by"])
|
||||||
|
self.assertTrue(o["lights"]["variable sp_state"])
|
||||||
|
self.assertEqual(o["missing"], [])
|
||||||
|
|
||||||
|
def test_machines_use_what_the_watchdog_sees(self):
|
||||||
|
o = json.loads(get(self.port, "/api/machines")[1])
|
||||||
|
ingest = next(c for c in o["computers"] if c["self"])
|
||||||
|
self.assertEqual(ingest["word"], "OK")
|
||||||
|
bible = next(c for c in o["computers"] if c["alias"] == "bibletv")
|
||||||
|
web = next(i for i in bible["items"] if i["kind"] == "web")
|
||||||
|
self.assertEqual(web["light"], "ok") # from snapshot.json, not a probe
|
||||||
|
self.assertIn("mac", o["setup_steps"])
|
||||||
|
|
||||||
|
def test_gear_and_keys_and_pages(self):
|
||||||
|
gear = json.loads(get(self.port, "/api/gear")[1])
|
||||||
|
self.assertTrue(any(r["name"] == "HyperDeck_2" and "hd2" in r["watched_by"] for r in gear))
|
||||||
|
keys = json.loads(get(self.port, "/api/keys")[1])
|
||||||
|
hd2 = next(t for t in keys["tiles"] if t["id"] == "hd2")
|
||||||
|
self.assertIn("NO DISK", hd2["says"])
|
||||||
|
self.assertIn("off and on", hd2["holding"])
|
||||||
|
self.assertEqual(keys["hold"], "HOLD 2s")
|
||||||
|
for page in ("/key-guide.html", "/cheat.html", "/where.html", "/", "/app.js", "/style.css"):
|
||||||
|
status, body = get(self.port, page)
|
||||||
|
self.assertEqual(status, 200, page)
|
||||||
|
guide = get(self.port, "/key-guide.html")[1].decode()
|
||||||
|
for t in CONFIG["tiles"]:
|
||||||
|
self.assertIn(t["label"], guide)
|
||||||
|
self.assertNotIn("?", keyguide.cheat_sheet(CONFIG, "http://x/").split("<h2>Where to look")[0].split("</h1>")[1][:200])
|
||||||
|
|
||||||
|
def test_logs_are_whitelisted(self):
|
||||||
|
self.assertEqual(get(self.port, "/api/logs/nope")[0], 404)
|
||||||
|
self.assertEqual(get(self.port, "/api/logs/watchdog")[0], 200)
|
||||||
|
self.assertEqual(json.loads(get(self.port, "/api/logs/autofix")[1])["file"], None)
|
||||||
|
|
||||||
|
def test_editing_code_gate_and_lockout(self):
|
||||||
|
self.site.attempts.clear()
|
||||||
|
self.assertFalse(json.loads(get(self.port, "/api/editing/check", {"code": "nope"})[1])["ok"])
|
||||||
|
self.assertTrue(json.loads(get(self.port, "/api/editing/check", {"code": "Apple River Stone"})[1])["ok"])
|
||||||
|
for _ in range(5):
|
||||||
|
get(self.port, "/api/editing/check", {"code": "wrong"})
|
||||||
|
r = json.loads(get(self.port, "/api/editing/check", {"code": "apple river stone"})[1])
|
||||||
|
self.assertFalse(r["ok"])
|
||||||
|
self.assertIn("locked_for", r)
|
||||||
|
self.site.attempts.clear()
|
||||||
|
self.assertEqual(get(self.port, "/api/editing/check", {"code": "x"}, origin="http://evil.example")[0], 403)
|
||||||
|
|
||||||
|
def test_probe_is_refused_while_a_fix_runs(self):
|
||||||
|
json.dump({"fix-x": {"key": "hd2"}}, open(os.path.join(self.data, "fixes-running.json"), "w"))
|
||||||
|
try:
|
||||||
|
self.assertEqual(get(self.port, "/api/machines/probe", {})[0], 409)
|
||||||
|
finally:
|
||||||
|
os.remove(os.path.join(self.data, "fixes-running.json"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestBindingAndProbes(unittest.TestCase):
|
||||||
|
def test_bind_addresses(self):
|
||||||
|
ifaces = [("en0", "10.1.1.1", True), ("en1", "10.6.3.222", True), ("en17", "169.254.1.2", True), ("en20", "10.66.1.2", True),
|
||||||
|
("en22", "10.104.40.6", True)]
|
||||||
|
self.assertEqual(server.bind_addresses(CONFIG, ifaces), ["127.0.0.1", "10.1.1.1", "10.6.3.222"]) # the LAN ports only
|
||||||
|
cfg = json.loads(json.dumps(CONFIG))
|
||||||
|
cfg["hosts"]["ingest"]["networks"] = []
|
||||||
|
self.assertEqual(server.bind_addresses(cfg, ifaces), ["127.0.0.1", "10.1.1.1", "10.6.3.222", "169.254.1.2", "10.66.1.2", "10.104.40.6"])
|
||||||
|
self.assertTrue(server.allowed_client("192.168.4.7"))
|
||||||
|
self.assertFalse(server.allowed_client("8.8.8.8"))
|
||||||
|
|
||||||
|
def test_mac_probe_parsing(self):
|
||||||
|
out = ("SP_NAME=NA-Tracks.localdomain\nSP_OSV=26.5.1\nSP_PY=Python 3.9.6\nSP_KEY=1\nSP_AGENTS\nPID\tStatus\tLabel\n"
|
||||||
|
"780\t0\tapplication.com.audinate.DanteVirtualSoundcard.1.2\n-\t0\tcom.google.keystone.agent\nSP_PROCS\n"
|
||||||
|
"/Applications/Ableton Live 11 Suite.app/Contents/MacOS/Live\n/usr/sbin/sshd\n/Library/Audio/dvsd\nSP_APPS\nAbleton Live 11 Suite.app\n"
|
||||||
|
"SP_HELPER\n-rwxr-xr-x 1 root wheel 375 Oct 8 12:05 /usr/local/sbin/status-panel-dvs-start\n/usr/local/sbin/status-panel-dvs-start\n"
|
||||||
|
"SP_SOLO\n\nSP_NOW\n1791900000\nSP_END\n")
|
||||||
|
parsed = probes.sections(out, ("SP_AGENTS", "SP_PROCS", "SP_APPS", "SP_HELPER", "SP_SOLO", "SP_NOW", "SP_END"))
|
||||||
|
items, found = probes.mac_items(CONFIG["hosts"]["tracks"], parsed, CONFIG)
|
||||||
|
by = {i["name"]: i for i in items}
|
||||||
|
self.assertEqual(by["Ableton Live 11 Suite"]["light"], "ok")
|
||||||
|
self.assertEqual(by["the DVS helper"]["light"], "ok")
|
||||||
|
self.assertEqual(by["Dante Virtual Soundcard"]["light"], "ok")
|
||||||
|
self.assertEqual(probes.values(out)["KEY"], "1")
|
||||||
|
stopped = out.replace("/Contents/MacOS/Live\n", "")
|
||||||
|
items, _ = probes.mac_items(CONFIG["hosts"]["tracks"], probes.sections(stopped, ("SP_AGENTS", "SP_PROCS", "SP_APPS", "SP_HELPER", "SP_SOLO", "SP_NOW", "SP_END")), CONFIG)
|
||||||
|
self.assertEqual({i["name"]: i for i in items}["Ableton Live 11 Suite"]["reason"], "not running")
|
||||||
|
|
||||||
|
def test_windows_probe_parsing(self):
|
||||||
|
out = ('SP_NAME=LIGHTING-PC\r\nSP_TASKS\r\n"\\StatusPanel\\Start Arena","N/A","Running"\r\n"\\StatusPanel\\Start rtpMIDI","N/A","Ready"\r\n'
|
||||||
|
'"\\StatusPanel\\Start Vista","N/A","Running"\r\nSP_PROCS\r\n"Vista.exe","1234","Console","1","100 K"\r\n"Arena.exe","1","Console","1","1 K"\r\n'
|
||||||
|
'SP_SVC\r\nSERVICE_NAME: sshd\r\n STATE : 4 RUNNING\r\nSERVICE_NAME: rtpMIDIService\r\n STATE : 4 RUNNING\r\n'
|
||||||
|
'SP_PS\r\nPublic\r\nEnabled\r\n10.1.1.1,10.6.3.222\r\nSP_END\r\n')
|
||||||
|
parsed = probes.sections(out, ("SP_TASKS", "SP_PROCS", "SP_SVC", "SP_PS", "SP_END"))
|
||||||
|
items, found, setup = probes.windows_items(CONFIG["hosts"]["lighting"], parsed)
|
||||||
|
by = {i["name"]: i for i in items}
|
||||||
|
self.assertEqual(by["Start Vista"]["light"], "ok")
|
||||||
|
self.assertEqual(by["Start rtpMIDI"]["reason"], "rtpMIDI.exe is not running")
|
||||||
|
self.assertEqual(by["rtpMIDIService"]["light"], "ok")
|
||||||
|
self.assertEqual(by["sshd"]["light"], "ok")
|
||||||
|
self.assertEqual((setup["profile"], setup["wake_on_lan"], setup["firewall_allow"]), ("Public", "Enabled", "10.1.1.1,10.6.3.222"))
|
||||||
|
self.assertIn("\\StatusPanel\\Start Arena", found["tasks"])
|
||||||
|
|
||||||
|
def test_probe_commands_never_carry_names_from_the_settings(self):
|
||||||
|
cfg = json.loads(json.dumps(CONFIG))
|
||||||
|
cfg["hosts"]["tracks"]["runs"][1]["process"] = "x; rm -rf ~" # a hostile name in the settings
|
||||||
|
cfg["site"]["dvs_helper"] = "/usr/local/sbin/x; rm -rf ~" # and a hostile path
|
||||||
|
line = probes.MAC_PROBE.format(**probes.quoted_paths(cfg))
|
||||||
|
self.assertNotIn("rm -rf ~", line.replace("'/usr/local/sbin/x; rm -rf ~'", "")) # only inside quotes
|
||||||
|
self.assertNotIn("x; rm -rf ~\n", line)
|
||||||
|
self.assertNotIn("Live", line) # names are matched here, never sent
|
||||||
|
self.assertIn('"$HOME"\'/Library/Application Support/status-panel\'', probes.quoted_paths(CONFIG)["solo"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user