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 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 16:33:28 -04:00
co-authored by Claude Opus 5.5
parent 777afac0b1
commit 93805cb723
5 changed files with 42 additions and 8 deletions
+6 -2
View File
@@ -62,6 +62,8 @@ MAC_APPS = [
{"app": "obs", "process": "obs", "label": "OBS", "open": "/Applications/OBS.app", "restart": True}, {"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": "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": "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 = [ WIN_APPS = [
{"app": "vista", "exe": "Vista.exe", "label": "VISTA", "task": "Start Vista", "restart": True}, {"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 c.get("os", "mac") == "mac":
if a["process"] not in procs: if a["process"] not in procs:
continue 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: if not app_path:
continue continue
check = {"type": "remote", "template": "mac_program", "params": {"host": alias, "process": a["process"]}, 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"]) ident = "app:%s|%s" % (alias, a["exe"])
fid = slug("%s_%s" % (a["app"], alias)) fid = slug("%s_%s" % (a["app"], alias))
pretty = {"propresenter": "ProPresenter", "ableton": "Ableton", "arena": "Resolume Arena", "obs": "OBS", "translate": "Church Translate", 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 where = c.get("name") or alias
start = dict(start, label="start %s on %s if it is not running" % (pretty, where), short=a["label"]) 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"]) 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}) item["restart"].append({"label": a["label"], "fix": rid})
if a["app"] == "arena": if a["app"] == "arena":
item["group"] = "VIDEO" item["group"] = "VIDEO"
if a.get("group"):
item["group"] = a["group"]
out.append(item) out.append(item)
apps_at[(a["app"], c.get("ip"))] = item apps_at[(a["app"], c.get("ip"))] = item
for ip in c.get("ips") or []: for ip in c.get("ips") or []:
+7 -2
View File
@@ -20,7 +20,8 @@ from settings import PARENT # noqa: F401 (puts the project folder on sys.path)
import status_panel as sp import status_panel as sp
TIMEOUT = 8 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" 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") HOST_FIELDS = ("host", "bonjourHost", "serverHost", "ip", "address")
@@ -243,10 +244,14 @@ def look_at(cfg, alias, host):
out = {"reachable": True} out = {"reachable": True}
if os_name == "mac": if os_name == "mac":
out["procs"] = sorted({os.path.basename(p.strip()) for p in got.get("PROCS", []) if p.strip()}) 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")) out["apps"] = sorted(a.strip() for a in got.get("APPS", []) if a.strip().endswith(".app"))
try: 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["midi"] = sp.parse_remote_midi(m.stdout) or {}
out["python"] = m.returncode != 97
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
out["midi"] = {} out["midi"] = {}
else: else:
+1 -1
View File
@@ -22,7 +22,7 @@ TIMEOUT = 8
MAC_PROBE = ( MAC_PROBE = (
'echo "SP_NAME=$(uname -n)"; echo "SP_OSV=$(sw_vers -productVersion 2>/dev/null)"; ' '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_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_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; ' 'echo SP_APPS; ls /Applications 2>/dev/null; echo SP_HELPER; ls -l {helper} 2>&1; sudo -n -l {helper} 2>&1 | tail -1; '
+13 -3
View File
@@ -2445,7 +2445,7 @@ def stream_remote_midi(snap, cfg, host, stop):
proc = None proc = None
try: try:
proc = subprocess.Popen(ssh_cmd(cfg, host)[:1] + ["-o", "ServerAliveInterval=2", "-o", "ServerAliveCountMax=3"] 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) stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
proc.stdin.write(script) proc.stdin.write(script)
proc.stdin.close() proc.stdin.close()
@@ -2488,13 +2488,23 @@ def poll_remote_midi(snap, cfg):
script = remote_midi_script() script = remote_midi_script()
for host in remote_midi_hosts(cfg): for host in remote_midi_hosts(cfg):
try: 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) capture_output=True, text=True, timeout=20)
snap.remote_midi[host] = parse_remote_midi(r.stdout) if r.returncode == 0 else None snap.remote_midi[host] = parse_remote_midi(r.stdout) if r.returncode == 0 else None
remember_peers(snap, host, snap.remote_midi[host]) remember_peers(snap, host, snap.remote_midi[host])
except (OSError, subprocess.SubprocessError): except (OSError, subprocess.SubprocessError):
snap.remote_midi[host] = None 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): def ssh_cmd(cfg, host):
"""ssh to a machine in config "hosts", with NA Ingest's status-panel key; one shared connection per 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")), 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"]) args = (chk["session"], chk["peer"], chk["address"])
repoint = bool(chk.get("repoint")) repoint = bool(chk.get("repoint"))
if chk.get("host"): 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,))), input=remote_midi_script("ns['connect_peer'](%r, %r, %r, repoint=%r)" % (args + (repoint,))),
capture_output=True, text=True, timeout=30) capture_output=True, text=True, timeout=30)
result = parse_remote_midi(r.stdout) if r.returncode == 0 else None result = parse_remote_midi(r.stdout) if r.returncode == 0 else None
+15
View File
@@ -98,6 +98,21 @@ class TestBuild(unittest.TestCase):
self.assertIn("conn:x32", seen) self.assertIn("conn:x32", seen)
self.assertIn("dante:SC-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): 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") cfg, _ = ac.build(BASE, big(), {"id": "ab", "name": "Apollo Beach"}, "1111")
self.assertEqual(schema.validate(cfg)[0], []) self.assertEqual(schema.validate(cfg)[0], [])