A launchd-run python3 gets 'Operation not permitted' on ~/Library/CloudStorage; sshd does not. The page now lists and reads the folder over ssh to localhost with the panel's own key when the local read is blocked (the Ingest Mac's Setup card gains that authorise step). Pick the newest verified on NA Ingest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
159 lines
10 KiB
Python
159 lines
10 KiB
Python
"""The one-time set-up of each computer, generated from its record: the steps with a [how] each and the
|
|
scripts with the right names in them (the templates in remote-access/ hold no site-shaped value)."""
|
|
import os
|
|
import re
|
|
import shlex
|
|
|
|
from settings import PARENT
|
|
|
|
TEMPLATES = os.path.join(PARENT, "remote-access")
|
|
WIN_SAFE = re.compile(r"^[A-Za-z0-9 ._()\\:*-]+$")
|
|
|
|
|
|
def template(name):
|
|
with open(os.path.join(TEMPLATES, name), encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def fill(text, values):
|
|
for k, v in values.items():
|
|
text = text.replace("{{%s}}" % k, v)
|
|
left = re.findall(r"{{[A-Z_]+}}", text)
|
|
if left:
|
|
raise ValueError("unfilled placeholders: %s" % ", ".join(sorted(set(left))))
|
|
return text
|
|
|
|
|
|
def public_key(cfg):
|
|
"""(the key line, its comment) from this Mac's login key, or (None, None)."""
|
|
path = os.path.expanduser(cfg.get("ssh_key", "~/.ssh/id_status_panel")) + ".pub"
|
|
try:
|
|
line = open(path, encoding="utf-8").read().strip().splitlines()[0]
|
|
except (OSError, IndexError):
|
|
return None, None
|
|
parts = line.split()
|
|
if len(parts) < 2 or "'" in line:
|
|
return None, None
|
|
return line, (parts[2] if len(parts) > 2 else parts[1][-16:])
|
|
|
|
|
|
def ps_quote(value, what):
|
|
value = str(value)
|
|
if not WIN_SAFE.match(value):
|
|
raise ValueError("%s: on a PC only letters, digits, space and . _ ( ) \\ : * - are allowed, not %r" % (what, value))
|
|
return "'" + value + "'"
|
|
|
|
|
|
def site_values(cfg):
|
|
site = cfg.get("site") or {}
|
|
key, mark = public_key(cfg)
|
|
return {"INGEST_NAME": site.get("ingest_name") or "the Ingest Mac",
|
|
"INGEST_IPS": ", ".join("'%s'" % ip for ip in site.get("ingest_ips") or []) or "'127.0.0.1'",
|
|
"KEY": key or "<< this Mac has no login key yet: run install.sh, then come back >>",
|
|
"KEY_COMMENT": mark or "status-panel", "DVS_HELPER": site.get("dvs_helper") or "/usr/local/sbin/status-panel-dvs-start"}
|
|
|
|
|
|
def windows_setup(cfg, alias):
|
|
host = cfg["hosts"][alias]
|
|
tasks = [r for r in host.get("runs") or [] if r.get("kind") == "task"]
|
|
apps = []
|
|
for r in tasks:
|
|
name = re.sub(r"^Start\s+", "", str(r.get("name", "")), flags=re.I).strip()
|
|
exe = str(r.get("exe", "")).strip()
|
|
if not name or not exe:
|
|
continue
|
|
line = " @{ Name = %s; Exe = (Find-Exe %s); Args = %s" % (ps_quote(name, "task name"), ps_quote(exe, "program file"), ps_quote(r.get("args", ""), "arguments") if r.get("args") else "''")
|
|
if r.get("at_logon"):
|
|
line += "; AtLogon = $true"
|
|
apps.append(line + " }")
|
|
services = [r for r in host.get("runs") or [] if r.get("kind") == "service"]
|
|
values = dict(site_values(cfg), PC_NAME=host.get("name") or alias, APPS=",\n".join(apps) or " # no programs listed for this PC yet",
|
|
PROGRAMS=", ".join(re.sub(r"^Start\s+", "", r.get("name", ""), flags=re.I) for r in tasks) or "none yet",
|
|
SERVICES=", ".join(ps_quote(r.get("name", ""), "service") for r in services))
|
|
return fill(template("windows-setup.ps1.template"), values)
|
|
|
|
|
|
def mac_authorize(cfg):
|
|
return fill(template("mac-authorize.sh.template"), site_values(cfg))
|
|
|
|
|
|
def mac_dvs_helper(cfg):
|
|
macs = [h.get("name", a) for a, h in cfg.get("hosts", {}).items() if any(r.get("kind") == "helper_dvs" for r in h.get("runs") or [])]
|
|
return fill(template("mac-dvs-helper.sh.template"), dict(site_values(cfg), DVS_MACS=", ".join(macs) or "the Macs that run Dante Virtual Soundcard"))
|
|
|
|
|
|
def solo_install(cfg, alias):
|
|
host = cfg["hosts"][alias]
|
|
ssh = host.get("ssh", "user@address")
|
|
return ("# From %s, in Terminal (the swift source and the installer travel with the panel's code):\n"
|
|
"cd ~/status-panel-install && scp remote-access/solo-mode-agent.swift remote-access/install-solo-mode.sh %s:/tmp/ && ssh %s 'zsh /tmp/install-solo-mode.sh'\n"
|
|
"# Then, on %s itself: System Settings > Privacy & Security > Accessibility > allow solo-mode-agent.\n"
|
|
% ((cfg.get("site") or {}).get("ingest_name") or "the Ingest Mac", shlex.quote(ssh), shlex.quote(ssh), host.get("name", alias)))
|
|
|
|
|
|
def setup_for(cfg, alias):
|
|
"""The checklist for one computer: steps (text + how) and scripts (name + how + text)."""
|
|
host = cfg["hosts"][alias]
|
|
runs = host.get("runs") or []
|
|
kinds = {r.get("kind") for r in runs}
|
|
site = cfg.get("site") or {}
|
|
ingest = site.get("ingest_name") or "the Ingest Mac"
|
|
steps, scripts = [], []
|
|
if host.get("self"):
|
|
steps = [
|
|
("Command Line Tools / python 3.9-3.12", "Terminal: xcode-select --install. The watchdog runs on the Mac's own python3."),
|
|
("the watchdog and the settings page are loaded", "Copy the folder to ~/status-panel-install and run install.sh once; both LaunchAgents are written by it."),
|
|
("Companion's custom variables and blink clock exist", "In Companion: Variables > Custom: %s; a trigger every second that adds 1 to flash_tick (Companion only allows this in its own UI)."
|
|
% ", ".join((cfg.get("variables") or {}).values())),
|
|
("the login key pair exists", "install.sh makes ~/.ssh/id_status_panel; the other computers get its public half through the scripts below."),
|
|
("the Netgear login is in the keychain", "Terminal: security add-generic-password -s netgear-status-panel -a <user> -w (it asks for the password; only needed with Dante switches)."),
|
|
("the Spotify permission was allowed once", "Hold the SPOTIFY key once while sitting at this Mac and click Allow on the Automation prompt."),
|
|
("the DVS helper and its sudo rule", "Run the DVS helper script below on this Mac too (as root, through ARD or sudo)."),
|
|
("Google Drive is signed in (saved switcher files)", "Finder: Google Drive is mounted and the switcher folder on Site setup > ATEM exists."),
|
|
("the panel's key is accepted on this Mac too", "macOS keeps background programs out of cloud folders; the page reads the saved switcher files through this Mac's own Remote Login instead. Run the authorise line below here (Terminal, or ARD as root)."),
|
|
]
|
|
scripts.append({"name": "Authorise the panel's login on this Mac (run as root)", "how": "Terminal on this Mac with sudo, or ARD > Send UNIX Command as root.", "text": mac_authorize(cfg)})
|
|
if "helper_dvs" in kinds:
|
|
scripts.append({"name": "DVS helper (run as root)", "how": "Apple Remote Desktop > Send UNIX Command as root, or sudo zsh in Terminal here.", "text": mac_dvs_helper(cfg)})
|
|
elif host.get("os") == "windows":
|
|
steps = [
|
|
("the SSH service runs and starts with Windows", "The set-up script below installs and starts OpenSSH Server."),
|
|
("the firewall lets this Mac in", "The script opens port 22 for %s's addresses only." % ingest),
|
|
("the panel's login key is accepted", "The script writes %s's key into the administrators' and the user's authorized_keys." % ingest),
|
|
("a \\StatusPanel\\ task per program", "The script creates one scheduled task per program listed under 'What runs here' (Make a PC program startable adds one)."),
|
|
]
|
|
if "service" in kinds:
|
|
steps.append(("the services restart by themselves", "The script sets each listed service to restart when it stops."))
|
|
steps += [
|
|
("wake-on-LAN is on in Windows and in the BIOS", "The script turns it on for the wired adapter; the BIOS setting (Wake on LAN / Power On By PCI-E) is yours to set once."),
|
|
("the network profile is Public where rtpMIDI runs", "Settings > Network > the adapter > Public (rtpMIDI's Bonjour rules only allow Public)."),
|
|
("Wi-Fi power saving is off", "Device Manager > the Wi-Fi adapter > Power Management > untick 'allow the computer to turn off this device'; Ethernet is better."),
|
|
]
|
|
try:
|
|
text = windows_setup(cfg, alias)
|
|
except ValueError as e:
|
|
text = "# cannot generate: %s" % e
|
|
scripts.append({"name": "Set-up script (PowerShell, as administrator)", "how": "Save it as windows-setup.ps1, run PowerShell as administrator, Set-ExecutionPolicy RemoteSigned -Scope CurrentUser, then run the file. Paste the RESULT block below.", "text": text})
|
|
else:
|
|
steps = [
|
|
("Remote Login is on", "System Settings > General > Sharing > Remote Login on, for the user that is logged in."),
|
|
("the panel's login key is accepted", "Paste the authorise line below through Apple Remote Desktop (Send UNIX Command as root) or run it in Terminal on that Mac."),
|
|
("python3 is present (for MIDI reads)", "Terminal on that Mac: python3 --version (Command Line Tools install it)."),
|
|
]
|
|
if "helper_dvs" in kinds:
|
|
steps.append(("the DVS helper and its sudo rule", "Paste the DVS helper script below as root (ARD)."))
|
|
if "midi_session" in kinds:
|
|
steps.append(("Audio MIDI Setup sessions named as the keys expect", "Audio MIDI Setup > Window > Show MIDI Studio > Network: the session names on this computer's card."))
|
|
if "helper_solo" in kinds:
|
|
steps.append(("the Pro Tools helper runs and Accessibility is allowed", "Run the solo-mode install below from %s, then allow solo-mode-agent under Accessibility on that Mac." % ingest))
|
|
scripts.append({"name": "Authorise %s's login (run as root)" % ingest, "how": "ARD > Send UNIX Command > as root, or Terminal there with sudo.", "text": mac_authorize(cfg)})
|
|
if "helper_dvs" in kinds:
|
|
scripts.append({"name": "DVS helper (run as root)", "how": "ARD > Send UNIX Command > as root.", "text": mac_dvs_helper(cfg)})
|
|
if "helper_solo" in kinds:
|
|
scripts.append({"name": "Pro Tools solo-mode helper", "how": "Two lines from %s's Terminal; then one click under Accessibility on that Mac." % ingest, "text": solo_install(cfg, alias)})
|
|
return {"alias": alias, "name": host.get("name", alias), "steps": [{"text": t, "how": h} for t, h in steps], "scripts": scripts}
|
|
|
|
|
|
def setup_all(cfg):
|
|
return [setup_for(cfg, a) for a in cfg.get("hosts", {})]
|