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:
@@ -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 []:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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], [])
|
||||||
|
|||||||
Reference in New Issue
Block a user