- the inserted administrator check split the header comment, leaving '} for ... run ONCE' as code: WS and SC
join.ps1 failed to parse (Windows' parser: Missing opening '(' after keyword 'for'); now after the whole line
- site.admin_ips / admin_keys (laptop 10.6.7.38, fixed in UniFi) go into every PC script's firewall rule and key
files, and into the Mac join line, so running a set-up again never shuts the laptop out
- scripts verified by Windows' own PowerShell parser on the NA Lighting PC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
187 lines
12 KiB
Python
187 lines
12 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 admin_keys(cfg):
|
|
"""The admin laptop's SSH keys (site.admin_keys), checked: one line each, no quotes."""
|
|
out = []
|
|
for k in (cfg.get("site") or {}).get("admin_keys") or []:
|
|
k = str(k).strip()
|
|
if not re.fullmatch(r"ssh-(ed25519|rsa) [A-Za-z0-9+/=]+( [A-Za-z0-9@._-]+)?", k):
|
|
raise ValueError("site.admin_keys: %r is not one public SSH key line" % k[:40])
|
|
out.append(k)
|
|
return out
|
|
|
|
|
|
def site_values(cfg):
|
|
site = cfg.get("site") or {}
|
|
key, mark = public_key(cfg)
|
|
# The admin laptop (site.admin_ips / admin_keys) gets in everywhere this Mac does, so running a set-up script
|
|
# again never shuts it out (2026-10-21, the user: "keep SSH on at all times for windows pc, just like the macs").
|
|
ips = list(dict.fromkeys(list(site.get("ingest_ips") or []) + list(site.get("admin_ips") or [])))
|
|
return {"INGEST_NAME": site.get("ingest_name") or "the Ingest Mac",
|
|
"INGEST_IPS": ", ".join("'%s'" % ip for ip in ips if re.fullmatch(r"[0-9a-fA-F.:]+", str(ip))) or "'127.0.0.1'",
|
|
"ADMIN_KEYS": ", ".join("'%s'" % k for k in admin_keys(cfg)),
|
|
"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"}
|
|
|
|
|
|
ADMIN_CHECK = """if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
throw 'Run this as administrator: close this window, right-click Windows PowerShell > Run as administrator, then paste the line again. Nothing was changed.'
|
|
}"""
|
|
|
|
|
|
def with_admin_check(text):
|
|
"""The script stops with plain words unless PowerShell runs as administrator: the check goes after the
|
|
whole header comment line (inserting it mid-line left "} for ... - run ONCE" as code, 2026-10-21)."""
|
|
lines = text.split("\n")
|
|
at = next((i for i, ln in enumerate(lines) if ln.startswith("# Status panel remote access")), 0)
|
|
return "\n".join(lines[:at + 1] + [ADMIN_CHECK] + lines[at + 1:])
|
|
|
|
|
|
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", {})]
|