Devices: each headset its own SSH connection, and each server keeps its own headset (review round 27)

ssh's %C hashes only address, user and port, so two headsets reached at one
address shared a ControlMaster and one's commands could run on the other: the
ControlPath now names the headset. Another Frame Control server choosing a
different headset no longer moves this one's commands mid-install.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-29 07:52:28 +10:00
1 parent c5a614d989
commit 409e328880
6 files changed
+52 -7

No files matched your search

+6
View File
@@ -461,6 +461,12 @@ class Registry:
data.setdefault("networks", {})
data.setdefault("active", None)
data["devices"] = [d for d in data["devices"] if self._sane(d)]
# The headset in use is this server's own choice: another server picking a
# different one mustn't move commands (an install, say) under it. The file's
# choice is only where a server starts.
mine = self.data.get("active")
if mine and any(d["id"] == mine for d in data["devices"]):
data["active"] = mine
self.data = data
@staticmethod
+5 -2
View File
@@ -56,12 +56,15 @@ def cache_dir(*parts):
return base.joinpath(*parts)
def control_path():
def control_path(tag="x"):
"""ssh ControlPath for the shared connection, or None where it isn't supported.
`tag` names the headset: ssh's %C hashes only the address, user and port, so two
headsets reached at the same address (one of them moved) would otherwise share a
connection, and one's commands would run on the other.
/tmp, not $TMPDIR: macOS's per-user temp path overflows the unix socket path limit.
"""
return f"/tmp/frame-ui-{os.getuid()}-%C" if MUX else None
return f"/tmp/frame-ui-{os.getuid()}-{tag}-%C" if MUX else None
def which(name, *extra):
+13 -2
View File
@@ -26,6 +26,7 @@ when the page asks.
Python stdlib only. Runs on this computer, never on the Frame.
"""
import copy
import hashlib
import ipaddress
import queue
import re
@@ -364,17 +365,27 @@ class Link:
return {"id": f"alias-{alias}", "name": alias, "alias": alias, "user": None, "port": None,
"addresses": [], "transient": True, "identity_files": []}
@staticmethod
def control_tag(device):
"""A short, path-safe name for the headset's ControlPath: its id, or for a bare
alias a hash of it (an alias can be too long for a socket path)."""
if device.get("transient"):
return "a" + hashlib.sha1(device["alias"].encode()).hexdigest()[:8]
return device["id"]
def host_opts(self, device, host):
"""What every ssh command adds to reach DEVICE at HOST."""
if device.get("none"):
return ["-o", "HostName=no-headset.invalid"] # fails at once, with ssh's own "can't resolve"
# Each headset its own shared connection (see frame_host.control_path).
mux = ["-o", f"ControlPath={frame_host.control_path(self.control_tag(device))}"] if self.control else []
if device.get("transient"):
return list(device.get("frozen") or []) # what ~/.ssh/config said when it was routed
return [*mux, *(device.get("frozen") or [])] # what ~/.ssh/config said when it was routed
if not host: # a headset with no addresses: reach nothing, not whatever ~/.ssh/config says
return ["-o", "HostName=no-address.invalid"]
# StrictHostKeyChecking=yes: whatever ~/.ssh/config says for Host *, every command
# checks the headset's pinned key (only the connector's first handshake may save one).
return ["-o", "StrictHostKeyChecking=yes", "-o", f"HostName={frame_devices.ssh_host(host)}",
return [*mux, "-o", "StrictHostKeyChecking=yes", "-o", f"HostName={frame_devices.ssh_host(host)}",
"-o", f"HostKeyAlias={frame_devices.host_key_alias(device['id'])}",
"-o", f"UserKnownHostsFile={frame_devices.known_hosts_opt(device['id'])}", "-o", "HashKnownHosts=no",
"-o", f"User={device['user']}", "-o", f"Port={device['port']}"]
+2 -1
View File
@@ -66,7 +66,8 @@ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", FRAME):
# Reuse one SSH connection for the frequent status/screenshot calls, where ssh
# supports it (not on Windows: there every command connects on its own).
CONTROL = None if LOCAL else frame_host.control_path()
MUX = ["ssh", "-o", "BatchMode=yes", *(["-o", f"ControlPath={CONTROL}"] if CONTROL else [])]
# The ControlPath itself is per headset: the connector puts it in HOST_OPTS.
MUX = ["ssh", "-o", "BatchMode=yes"]
MUX_BASE = list(MUX)
# Commands use the master when it's up and connect directly when it isn't.
SSH_TAIL = [*(["-o", "ControlMaster=no"] if CONTROL else []), "-o", "ConnectTimeout=5"]