Files
status-indicators/settings/scripts.py
T
christianandClaude Opus 5.5 afd2758d87 PC set-up: one line per PC (/setup/<name>.ps1), the admin laptop always let in, and the admin check no longer breaks the script
- 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>
2026-10-21 16:29:36 -04:00

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", {})]