From 93805cb723a1e5f2bbba7d34f8d8137770761035 Mon Sep 17 00:00:00 2001 From: christian Date: Tue, 20 Oct 2026 16:33:28 -0400 Subject: [PATCH] No developer-tools pop-ups on the computers; apps found where they really are; SuperRack and X32-Edit On a Mac without the Command Line Tools, /usr/bin/python3 is a stub that opens Apple's installer on that Mac's screen: Woonsocket's Tracks and FOH Macs showed it after joining (and would every ten minutes). Python now runs on another Mac only when xcode-select says it is there (MIDI reads, MIDI reconnects, the Computers probe); without it those Macs' own MIDI sessions are simply not read. The app list used a glob that zsh (the Macs' login shell) turns into an error when one pattern matches nothing, so no app was ever found there: find instead, and a running program's app path is taken from the program itself. New recipes: SuperRack SoundGrid and X32-Edit (AUDIO). Woonsocket now has ABLETON, SUPERRACK and X32 EDIT keys, all OK, and NA's page 99 shows them. Co-Authored-By: Claude Opus 5.5 --- settings/autoconf.py | 8 ++++++-- settings/discover.py | 9 +++++++-- settings/probes.py | 2 +- status_panel.py | 16 +++++++++++++--- tests/test_campus.py | 15 +++++++++++++++ 5 files changed, 42 insertions(+), 8 deletions(-) diff --git a/settings/autoconf.py b/settings/autoconf.py index 5fd112c..d109602 100644 --- a/settings/autoconf.py +++ b/settings/autoconf.py @@ -62,6 +62,8 @@ MAC_APPS = [ {"app": "obs", "process": "obs", "label": "OBS", "open": "/Applications/OBS.app", "restart": True}, {"app": "translate", "process": "Church Translate", "label": "TRANSLATE", "open": "/Applications/Church Translate.app", "restart": True}, {"app": "protools", "process": "Pro Tools", "label": "PRO TOOLS", "open": "/Applications/Pro Tools.app", "restart": True}, + {"app": "superrack", "process": "SuperRack SoundGrid", "label": "SUPERRACK", "open": "/Applications/*SuperRack*.app", "restart": True, "group": "AUDIO"}, + {"app": "x32edit", "process": "X32-Edit", "label": "X32 EDIT", "open": "/Applications/X32-Edit.app", "restart": False, "group": "AUDIO"}, ] WIN_APPS = [ {"app": "vista", "exe": "Vista.exe", "label": "VISTA", "task": "Start Vista", "restart": True}, @@ -172,7 +174,7 @@ def items(inv, nets): if c.get("os", "mac") == "mac": if a["process"] not in procs: continue - app_path = newest_app(c.get("apps") or [], a["open"]) + app_path = (c.get("paths") or {}).get(a["process"]) or newest_app(c.get("apps") or [], a["open"]) if not app_path: continue check = {"type": "remote", "template": "mac_program", "params": {"host": alias, "process": a["process"]}, @@ -195,7 +197,7 @@ def items(inv, nets): ident = "app:%s|%s" % (alias, a["exe"]) fid = slug("%s_%s" % (a["app"], alias)) pretty = {"propresenter": "ProPresenter", "ableton": "Ableton", "arena": "Resolume Arena", "obs": "OBS", "translate": "Church Translate", - "protools": "Pro Tools", "vista": "Vista", "rtpmidi": "rtpMIDI"}.get(a["app"], a["label"].title()) + "protools": "Pro Tools", "vista": "Vista", "rtpmidi": "rtpMIDI", "superrack": "SuperRack", "x32edit": "X32-Edit"}.get(a["app"], a["label"].title()) where = c.get("name") or alias start = dict(start, label="start %s on %s if it is not running" % (pretty, where), short=a["label"]) again = dict(again, label="restart %s on %s (closes it first)" % (pretty, where), short=a["label"]) @@ -207,6 +209,8 @@ def items(inv, nets): item["restart"].append({"label": a["label"], "fix": rid}) if a["app"] == "arena": item["group"] = "VIDEO" + if a.get("group"): + item["group"] = a["group"] out.append(item) apps_at[(a["app"], c.get("ip"))] = item for ip in c.get("ips") or []: diff --git a/settings/discover.py b/settings/discover.py index d4a3234..e06adf3 100644 --- a/settings/discover.py +++ b/settings/discover.py @@ -20,7 +20,8 @@ from settings import PARENT # noqa: F401 (puts the project folder on sys.path) import status_panel as sp TIMEOUT = 8 -MAC_LOOK = "echo SP_PROCS; ps -axo comm= 2>/dev/null; echo SP_APPS; ls -d /Applications/*.app /Applications/*/*.app 2>/dev/null; echo SP_END" +# find, not a glob: in zsh (the Macs' login shell) a pattern that matches nothing aborts the whole line. +MAC_LOOK = "echo SP_PROCS; ps -axo comm= 2>/dev/null; echo SP_APPS; find /Applications -maxdepth 3 -name '*.app' -prune 2>/dev/null; echo SP_END" WIN_LOOK = "echo SP_PROCS & tasklist /FO CSV /NH 2>nul & echo SP_TASKS & schtasks /Query /FO CSV /NH 2>nul & echo SP_END" HOST_FIELDS = ("host", "bonjourHost", "serverHost", "ip", "address") @@ -243,10 +244,14 @@ def look_at(cfg, alias, host): out = {"reachable": True} if os_name == "mac": out["procs"] = sorted({os.path.basename(p.strip()) for p in got.get("PROCS", []) if p.strip()}) + # Where each running program's app is, from its own path (".../SuperRack SoundGrid.app/Contents/MacOS/..."). + out["paths"] = {os.path.basename(p.strip()): p.strip().split(".app/")[0] + ".app" + for p in got.get("PROCS", []) if ".app/Contents/MacOS/" in p} out["apps"] = sorted(a.strip() for a in got.get("APPS", []) if a.strip().endswith(".app")) try: - m = subprocess.run(sp.ssh_cmd(cfg, alias) + ["/usr/bin/python3", "-"], input=sp.remote_midi_script(), capture_output=True, text=True, timeout=15) + m = subprocess.run(sp.ssh_cmd(cfg, alias) + sp.remote_python("-"), input=sp.remote_midi_script(), capture_output=True, text=True, timeout=15) out["midi"] = sp.parse_remote_midi(m.stdout) or {} + out["python"] = m.returncode != 97 except subprocess.TimeoutExpired: out["midi"] = {} else: diff --git a/settings/probes.py b/settings/probes.py index d6f0446..76f47fb 100644 --- a/settings/probes.py +++ b/settings/probes.py @@ -22,7 +22,7 @@ TIMEOUT = 8 MAC_PROBE = ( 'echo "SP_NAME=$(uname -n)"; echo "SP_OSV=$(sw_vers -productVersion 2>/dev/null)"; ' - 'echo "SP_PY=$(/usr/bin/python3 --version 2>&1)"; ' + 'echo "SP_PY=$(xcode-select -p >/dev/null 2>&1 && /usr/bin/python3 --version 2>&1 || echo none)"; ' 'echo "SP_KEY=$(grep -c status-panel ~/.ssh/authorized_keys 2>/dev/null)"; ' 'echo SP_AGENTS; launchctl list 2>/dev/null; echo SP_PROCS; ps -axo comm= 2>/dev/null; ' 'echo SP_APPS; ls /Applications 2>/dev/null; echo SP_HELPER; ls -l {helper} 2>&1; sudo -n -l {helper} 2>&1 | tail -1; ' diff --git a/status_panel.py b/status_panel.py index 51d939f..815a163 100644 --- a/status_panel.py +++ b/status_panel.py @@ -2445,7 +2445,7 @@ def stream_remote_midi(snap, cfg, host, stop): proc = None try: proc = subprocess.Popen(ssh_cmd(cfg, host)[:1] + ["-o", "ServerAliveInterval=2", "-o", "ServerAliveCountMax=3"] - + ssh_cmd(cfg, host)[1:] + ["/usr/bin/python3", "-u", "-"], + + ssh_cmd(cfg, host)[1:] + remote_python("-u", "-"), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) proc.stdin.write(script) proc.stdin.close() @@ -2488,13 +2488,23 @@ def poll_remote_midi(snap, cfg): script = remote_midi_script() for host in remote_midi_hosts(cfg): try: - r = subprocess.run(ssh_cmd(cfg, host) + ["/usr/bin/python3", "-"], input=script, + r = subprocess.run(ssh_cmd(cfg, host) + remote_python("-"), input=script, capture_output=True, text=True, timeout=20) snap.remote_midi[host] = parse_remote_midi(r.stdout) if r.returncode == 0 else None remember_peers(snap, host, snap.remote_midi[host]) except (OSError, subprocess.SubprocessError): snap.remote_midi[host] = None +# Python on another Mac, only where it is really there: on a Mac without the Command Line Tools, +# /usr/bin/python3 is a stub that pops up Apple's "install the developer tools" window on that Mac's +# screen (2026-10-20: Woonsocket's Tracks and FOH Macs, every ten minutes). Exit 97 = no python there. +REMOTE_PY = "xcode-select -p >/dev/null 2>&1 || exit 97; exec /usr/bin/python3" + + +def remote_python(*args): + return [" ".join([REMOTE_PY] + list(args))] + + def ssh_cmd(cfg, host): """ssh to a machine in config "hosts", with NA Ingest's status-panel key; one shared connection per host.""" return ["/usr/bin/ssh", "-i", os.path.expanduser(cfg.get("ssh_key", "~/.ssh/id_status_panel")), @@ -2767,7 +2777,7 @@ def execute_fix(cfg, fix_id, chk): args = (chk["session"], chk["peer"], chk["address"]) repoint = bool(chk.get("repoint")) if chk.get("host"): - r = subprocess.run(ssh_cmd(cfg, chk["host"]) + ["/usr/bin/python3", "-"], + r = subprocess.run(ssh_cmd(cfg, chk["host"]) + remote_python("-"), input=remote_midi_script("ns['connect_peer'](%r, %r, %r, repoint=%r)" % (args + (repoint,))), capture_output=True, text=True, timeout=30) result = parse_remote_midi(r.stdout) if r.returncode == 0 else None diff --git a/tests/test_campus.py b/tests/test_campus.py index 02ec0ba..b5b6f67 100644 --- a/tests/test_campus.py +++ b/tests/test_campus.py @@ -98,6 +98,21 @@ class TestBuild(unittest.TestCase): self.assertIn("conn:x32", seen) self.assertIn("dante:SC-X32", seen) + def test_a_programs_app_comes_from_its_running_path(self): + """Woonsocket's FOH Mac: SuperRack lives three folders down, and zsh had hidden the app list.""" + inv = small() + inv["computers"]["foh"] = {"name": "WS FOH", "os": "mac", "user": "watersmedia", "ip": "10.4.1.12", "ips": ["10.4.1.12"], "reachable": True, + "procs": ["SuperRack SoundGrid", "X32-Edit"], "apps": [], + "paths": {"SuperRack SoundGrid": "/Applications/Waves/SuperRack SoundGrid/SuperRack SoundGrid.app", + "X32-Edit": "/Applications/X32-Edit.app"}} + cfg, _ = ac.build(BASE, inv, {"id": "ws", "name": "Woonsocket"}, "1234") + self.assertEqual(schema.validate(cfg)[0], []) + sr = next(t for t in cfg["tiles"] if t["label"] == "SUPERRACK") + self.assertEqual(sr["checks"][0]["cmd"], "pgrep -xq 'SuperRack SoundGrid'") + self.assertIn("/Applications/Waves/SuperRack SoundGrid/SuperRack SoundGrid.app", cfg["fixes"][sr["checks"][0]["fix"]]["run"]) + self.assertIn("X32 EDIT", [t["label"] for t in cfg["tiles"]]) + self.assertIn("SUPERRACK", [k["label"] for k in cfg["restart_page"]["keys"]]) + def test_a_big_campus_gets_group_keys_and_the_receivers_fold_first(self): cfg, _ = ac.build(BASE, big(), {"id": "ab", "name": "Apollo Beach"}, "1111") self.assertEqual(schema.validate(cfg)[0], [])