Wiring view: every machine and what connects it (Companion, MIDI, Dante, watchdog, web, Art-Net), hide types or single links, scripts per computer
RESI LIVE NOT REC follows the Resi auto-record service (custom autorec_status, not during its REC hold; stale heartbeat = no verdict); INGEST watches AUTO REC (running, heartbeat, Resi reachable), hold restarts it. Probes list each computer's scripts by file name only. HyperDeck addresses read from bonjourHost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,40 @@ class TestServer(unittest.TestCase):
|
||||
self.assertEqual(web["light"], "ok") # from snapshot.json, not a probe
|
||||
self.assertIn("mac", o["setup_steps"])
|
||||
|
||||
def test_wiring_shows_machines_their_connections_and_scripts(self):
|
||||
# 2026-10-27: "like a wiring diagram ... how many and what scripts are running on each machine"
|
||||
live = {"tracks": {"alias": "tracks", "reachable": True, "time": time.time(), "items": [],
|
||||
"found": {"scripts": [{"name": "statuspanel.py", "pid": 5, "by": "", "running": True}]}}}
|
||||
json.dump(live, open(os.path.join(self.data, "machines-live.json"), "w"))
|
||||
snap = json.load(open(os.path.join(self.data, "snapshot.json")))
|
||||
snap.update({"conns": {"ATEM": {"category": "good", "enabled": True}, "x32": {"category": "error", "enabled": True},
|
||||
"HyperDeck_1": {"category": "good", "enabled": True}, "tcp-HyperDeck-1": {"category": "good", "enabled": True}},
|
||||
"conn_hosts": {"ATEM": "10.1.10.2", "x32": "10.1.1.112", "HyperDeck_1": "10.1.10.11", "tcp-HyperDeck-1": "10.1.10.11"},
|
||||
"remote_midi": {"tracks": {"NA Tracks": {"peers": [{"address": "10.1.13.140:5004", "name": "SuperRack-LiveBox", "state": "running"}]}}},
|
||||
"dante_map": {"NA-Tracks": {"switch": "10.66.1.100", "port": "0/6", "seen": time.time()},
|
||||
"FOH-SuperRack-LiveBox": {"switch": "10.66.1.102", "port": "0/9", "seen": time.time()},
|
||||
"STUDIO-B-MON": {"switch": "10.66.1.100", "port": "0/2", "seen": time.time()}}})
|
||||
path = os.path.join(self.data, "snapshot.json")
|
||||
old = open(path).read()
|
||||
try:
|
||||
json.dump(snap, open(path, "w"))
|
||||
w = json.loads(get(self.port, "/api/wiring")[1])
|
||||
finally:
|
||||
open(path, "w").write(old)
|
||||
os.remove(os.path.join(self.data, "machines-live.json"))
|
||||
nodes = {n["id"]: n for n in w["nodes"]}
|
||||
edges = [(e["type"], e["from"], e["to"], e["light"]) for e in w["edges"]]
|
||||
self.assertEqual(w["me"], "h:ingest")
|
||||
self.assertEqual(nodes["h:tracks"]["scripts"][0]["name"], "statuspanel.py")
|
||||
self.assertIsNone(nodes["h:prop"]["scripts"]) # not looked yet
|
||||
self.assertIn(("companion", "h:ingest", "d:10.1.1.112", "problem"), edges)
|
||||
self.assertEqual(sorted(nodes["d:10.1.10.11"]["names"]), ["HyperDeck_1", "tcp-HyperDeck-1"]) # one deck, one box
|
||||
self.assertIn(("midi", "h:tracks", "d:10.1.13.140", "ok"), edges)
|
||||
self.assertIn(("dante", "h:tracks", "sw:10.66.1.100", "ok"), edges) # NA-Tracks is the Tracks Mac
|
||||
self.assertIn("FOH-SuperRack-LiveBox", nodes["d:10.1.13.140"]["names"]) # same box as its MIDI peer
|
||||
self.assertIn("d:studiobmon", nodes) # a monitor, not the Studio B Mac
|
||||
self.assertEqual({t["id"] for t in w["types"]} >= {"companion", "midi", "watch", "dante"}, True)
|
||||
|
||||
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))
|
||||
@@ -362,6 +396,25 @@ class TestBindingAndProbes(unittest.TestCase):
|
||||
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"])
|
||||
scripts = {x["name"]: x["running"] for x in found["scripts"]}
|
||||
self.assertEqual((scripts["Start Vista"], scripts["Start rtpMIDI"]), (True, False)) # Vista.exe runs; rtpMIDI.exe does not
|
||||
|
||||
def test_scripts_are_listed_by_file_name_only(self):
|
||||
agents = {"org.waterschurch.prop-guard": 412, "org.waterschurch.status-panel": None, "com.apple.x": 9}
|
||||
lines = ["412 /usr/bin/python3 /Users/m/Library/Application Support/status-panel/prop_guard.py /Users/m/cfg --token=SECRET",
|
||||
"77 /bin/bash /Users/m/backup/backup-campuses.sh", "55 /Applications/Companion.app/Contents/Resources/node main.js",
|
||||
"60 node /Users/m/Library/Application Support/companion/modules/x/main.js", "9 zsh -c echo SP_SCRIPTS",
|
||||
"88 /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/Resources/"
|
||||
"Python.app/Contents/MacOS/Python /Users/m/Library/Application Support/resi-autorec/resi_autorec.py"]
|
||||
got = probes.parse_scripts(lines, agents)
|
||||
self.assertEqual([(x["name"], x["by"], x["running"]) for x in got],
|
||||
[("backup-campuses.sh", "", True), ("prop_guard.py", "org.waterschurch.prop-guard", True),
|
||||
("resi_autorec.py", "", True),
|
||||
("status-panel", "org.waterschurch.status-panel", False)])
|
||||
self.assertNotIn("SECRET", json.dumps(got)) # never the rest of the line
|
||||
win = probes.parse_scripts(['812 powershell.exe -File "C:\\ProgramData\\StatusPanel\\start-vista.ps1"'],
|
||||
tasks={"\\StatusPanel\\Start Vista": "Running", "\\StatusPanel\\Start rtpMIDI": "Ready"})
|
||||
self.assertEqual([(x["name"], x["running"]) for x in win], [("Start Vista", True), ("start-vista.ps1", True), ("Start rtpMIDI", False)])
|
||||
|
||||
def test_probe_commands_never_carry_names_from_the_settings(self):
|
||||
cfg = json.loads(json.dumps(CONFIG))
|
||||
|
||||
+36
-18
@@ -76,7 +76,8 @@ def healthy_snapshot(now):
|
||||
snap.ping = {"10.1.3.227": 0, "10.6.7.13": 0, "10.1.0.1": 0}
|
||||
snap.link = True
|
||||
snap.launchd = {"org.waterschurch.resi-propresenter": 884, "org.waterschurch.lobby-captions": 900,
|
||||
"org.waterschurch.lobby-captions-display": 901, "org.waterschurch.digico-relay": 902}
|
||||
"org.waterschurch.lobby-captions-display": 901, "org.waterschurch.digico-relay": 902,
|
||||
"org.waterschurch.resi-autorec": 903}
|
||||
snap.launchd_time = now
|
||||
snap.procs = {"/Applications/ProdDash.app/Contents/MacOS/ProdDash": True}
|
||||
snap.remote = {"studiob\0pgrep -xq 'Pro Tools'": True} # the failover's check has a verdict
|
||||
@@ -488,12 +489,14 @@ class TestChecks(unittest.TestCase):
|
||||
db.execute("CREATE TABLE instances (id STRING, value STRING)")
|
||||
rows = [("a", {"label": "Axient_1", "config": {"host": "10.1.1.101", "port": 2202}}),
|
||||
("b", {"label": "OBS-Youtube", "config": {"host": " 10.1.9.126 "}}),
|
||||
("c", {"label": "no-host", "config": {}}), ("d", "not json")]
|
||||
("c", {"label": "no-host", "config": {}}), ("d", "not json"),
|
||||
("e", {"label": "HyperDeck_1", "config": {"bonjourHost": "10.1.10.11:9993"}})]
|
||||
db.executemany("INSERT INTO instances VALUES (?, ?)",
|
||||
[(i, v if isinstance(v, str) else json.dumps(v)) for i, v in rows])
|
||||
db.commit()
|
||||
db.close()
|
||||
self.assertEqual(sp.companion_conn_hosts(path), {"Axient_1": "10.1.1.101", "OBS-Youtube": "10.1.9.126"})
|
||||
self.assertEqual(sp.companion_conn_hosts(path), {"Axient_1": "10.1.1.101", "OBS-Youtube": "10.1.9.126",
|
||||
"HyperDeck_1": "10.1.10.11"})
|
||||
self.assertEqual(sp.companion_conn_hosts(os.path.join(d, "missing.sqlite")), {})
|
||||
|
||||
def test_fx_lights_midi_on_the_tracks_mac(self):
|
||||
@@ -1517,43 +1520,58 @@ class TestFixes(unittest.TestCase):
|
||||
self.assertEqual(self.plan("rec"), [("enable", "HyperDeck_1")])
|
||||
self.assertEqual(self.plan("x32"), [("enable", "x32")])
|
||||
|
||||
def autorec(self, status, hb=None):
|
||||
"""What the Resi auto-record service publishes (custom variables)."""
|
||||
self.snap.vars[("custom", "autorec_status")] = status
|
||||
self.snap.vars[("custom", "autorec_hb")] = str(int(SUNDAY + 100000 if hb is None else hb))
|
||||
|
||||
def test_resi_live_and_no_deck_recording_is_red_and_a_hold_starts_them(self):
|
||||
# 2026-10-27: "notify me if we are not recording on the hyperdecks, but resi is playing"
|
||||
self.snap.vars[("resi-studio-cues", "playback_state")] = "PLAYING"
|
||||
self.autorec("AUTO REC\nLIVE, NOT\nRECORDING")
|
||||
self.snap.vars[("HyperDeck_2", "slotId")] = "-" # optional deck, no disk: not in use
|
||||
self.snap.vars[("HyperDeck_2", "recordingTime")] = "--:--:--"
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 30)
|
||||
self.assertEqual(level(tiles["hd1"]), "ok") # not yet: Resi just started
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 30, 40)
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 10)
|
||||
self.assertEqual(level(tiles["hd1"]), "ok") # not yet: give it a moment
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 10, 30)
|
||||
self.assertEqual(tiles["hd1"]["x"], "HD1\nRESI LIVE NOT REC")
|
||||
self.assertEqual(level(tiles["rec"]), "err")
|
||||
self.assertEqual(level(tiles["hd2"]), "off")
|
||||
self.snap.vars[("HyperDeck_3", "status")] = "Record" # once one records, the others are NOT REC
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 70, 4)
|
||||
self.autorec("AUTO REC\nRECORDING\n9:30 AM")
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 40, 4)
|
||||
self.assertEqual(tiles["hd1"]["x"], "HD1\nNOT REC")
|
||||
started = []
|
||||
self.engine.record_executor = lambda ip, name: started.append((ip, name)) or True
|
||||
self.engine.run_fixes("rec", self.snap, None, SUNDAY + 80)
|
||||
self.engine.run_fixes("rec", self.snap, None, SUNDAY + 50)
|
||||
time.sleep(0.2)
|
||||
self.assertEqual([ip for ip, _ in started], ["10.1.10.11", "10.1.10.14", "10.1.10.15", "10.1.10.16"])
|
||||
self.assertTrue(started[0][1].startswith("CAM3-") and started[0][1].endswith("-"))
|
||||
self.assertTrue(started[3][1].startswith("PGM4-"))
|
||||
self.assertEqual(self.engine.notes["rec"]["text"], "STARTING\nREC")
|
||||
|
||||
def test_resi_paused_or_its_module_down_is_not_live(self):
|
||||
self.snap.vars[("resi-studio-cues", "playback_state")] = "PAUSED"
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 70)
|
||||
def test_not_live_while_auto_rec_waits_or_is_stuck(self):
|
||||
self.autorec("AUTO REC\nREC AT\n9:20 AM") # live early: REC held until 10 min before
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 40)
|
||||
self.assertEqual(level(tiles["hd1"]), "ok")
|
||||
self.snap.vars[("resi-studio-cues", "playback_state")] = "PLAYING"
|
||||
self.snap.conns["resi-studio-cues"]["category"] = "error"
|
||||
run_steps(self.engine, self.snap, SUNDAY + 70, 70)
|
||||
self.autorec("AUTO REC\nLIVE, NOT\nRECORDING", hb=SUNDAY - 600) # a stale verdict counts for nothing...
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 40, 200)
|
||||
self.assertFalse(self.engine.resi_live)
|
||||
self.assertEqual(tiles["ingest"]["x"], "INGEST\nAUTO REC STUCK") # ...and says the service is stuck
|
||||
ran = []
|
||||
self.engine.executor = lambda cfg, fix_id, chk: ran.append(chk.get("conn")) or True
|
||||
self.engine.executor = lambda cfg, fix_id, chk: ran.append(fix_id) or True
|
||||
self.engine.record_executor = lambda ip, name: self.fail("must not record when Resi is not live")
|
||||
self.engine.run_fixes("hd1", self.snap, None, SUNDAY + 150) # the usual hold: reconnect
|
||||
self.engine.run_fixes("ingest", self.snap, None, SUNDAY + 250) # the hold restarts the service
|
||||
time.sleep(0.2)
|
||||
self.assertEqual(ran, ["HyperDeck_1"])
|
||||
self.assertEqual(ran, ["autorec"])
|
||||
|
||||
def test_auto_rec_not_running_or_without_resi(self):
|
||||
del self.snap.launchd["org.waterschurch.resi-autorec"]
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 6)
|
||||
self.assertEqual(tiles["ingest"]["x"], "INGEST\nAUTO REC NOT LOADED")
|
||||
self.snap.launchd["org.waterschurch.resi-autorec"] = 903
|
||||
self.autorec("AUTO REC\nRESI ERROR")
|
||||
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 6, 200) # amber only once it lasts 3 minutes
|
||||
self.assertEqual((tiles["ingest"]["x"], level(tiles["ingest"])), ("INGEST\nAUTO REC NO RESI", "warn"))
|
||||
|
||||
def test_not_recording_is_not_fixed_by_a_reboot(self):
|
||||
for i in (1, 3):
|
||||
|
||||
Reference in New Issue
Block a user