Devices: fixes from review round 11

- A headset set up while a bare alias is in use doesn't take over by itself;
  a login change from ~/.ssh/config waits for running installs.
- Saving a headset writes only the login fields that changed, and only if the
  block still holds the old ones.
- SSH, SFTP, power and remote desktop open with the same headset and address
  as every other command, and refuse when there's no address.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-28 23:12:41 +10:00
1 parent c69ea6266f
commit 2e9bc8624b
4 files changed
+69 -13

No files matched your search

+30
View File
@@ -263,6 +263,36 @@ class Connecting(unittest.TestCase):
self.assertEqual(self.link.active_device()["alias"], "frame-bare")
self.assertEqual(self.routes[-1], ("frame-bare", []))
def test_setup_changing_the_login_waits_for_installs(self):
d = self.device("localhost")
self.hosts({"localhost": "ok"})
self.link.connect(["start"])
cfg = self.dir / "ssh" / "config"
cfg.write_text("# >>> steam-frame (frame-t) >>>\nHost frame-t\n HostName localhost\n User steamos\n"
f" Port {self.port}\nHost *\n# <<< steam-frame (frame-t) <<<\n")
self.link.watch_config() # the block's login is recorded
routes = len(self.routes)
cfg.write_text(cfg.read_text().replace("User steamos", "User deck"))
running = [1]
self.link.work = lambda: running[0]
self.link.config_mtime = None
self.link.watch_config()
self.assertEqual(len(self.routes), routes) # an install is running: not yet
running[0] = 0
self.link.watch_config()
self.assertIn("User=deck", self.routes[-1][1])
def test_a_rename_leaves_the_login_in_the_config_alone(self):
d = self.device("localhost")
cfg = self.dir / "ssh" / "config"
cfg.write_text("# >>> steam-frame (frame-t) >>>\nHost frame-t\n HostName localhost\n User deck\n"
"Host *\n# <<< steam-frame (frame-t) <<<\n") # setup wrote a new user, not yet imported
fl.devices_action(self.link, {"action": "update", "id": d["id"], "name": "Desk"}, None)
self.assertIn("User deck", cfg.read_text())
out = fl.devices_action(self.link, {"action": "update", "id": d["id"], "port": 2200}, None)
self.assertIn("User deck", cfg.read_text()) # changed meanwhile: left as it is
self.assertIn("left as it is", out["message"])
def test_probes_from_an_earlier_attempt_leave_the_new_rows_alone(self):
self.link.state.update(attempt=2, probes=[{"host": "b", "state": "waiting"}])
self.link.probe_update(0, 1, state="answered", ip="10.0.0.2")
+3 -3
View File
@@ -257,9 +257,9 @@ def open_steam_link():
return "Steam Link isn't installed; opened its download page"
def open_rdp(alias):
"""Remote desktop to the Frame's xrdp (user steamos)."""
host = ssh_hostname(alias)
def open_rdp(alias, host=None):
"""Remote desktop to the Frame's xrdp (user steamos), at `host` or where the alias points."""
host = host or ssh_hostname(alias)
if MAC:
if subprocess.run(["open", "-a", "Windows App"], capture_output=True).returncode == 0:
return "Opened Windows App"
+25 -6
View File
@@ -153,6 +153,9 @@ class Link:
def __init__(self, registry, *, env_alias, mux_base, control, apply, explain):
self.reg = registry
self.override = env_alias # FRAME_ALIAS, if set: the headset this server starts on
self.work_lock = threading.Lock() # the server's: held, no install starts (see server.working)
self.work = lambda: 0 # how many installs are running
self.deferred = False # a login change from ~/.ssh/config waiting for them
self.session_alias = env_alias # ...and stays selectable after switching away
self.mux_base = list(mux_base) # ["ssh", "-o", "BatchMode=yes", ControlPath...]
self.control = control # ControlPath, or None where ssh can't share connections
@@ -407,11 +410,19 @@ class Link:
if mtime != self.config_mtime:
self.config_mtime = mtime
before = self.active_device()
if before.get("transient") and not before.get("none") and self.routed is not None:
# A bare alias is in use: a headset set up now doesn't take over by itself.
self.override = self.override or before["alias"]
if self.reg.sync_from_config():
self.devices_changed()
after = self.active_device()
if (before.get("user"), before.get("port")) != (after.get("user"), after.get("port")):
self.invalidate() # Set Up Connection changed the active headset's login
self.deferred = True # Set Up Connection changed the active headset's login
if self.deferred:
with self.work_lock: # not while an install runs: it reads the route step by step
if not self.work():
self.deferred = False
self.invalidate()
def refresh_network(self):
net = frame_network.current_network(self.last_fp)
@@ -937,13 +948,21 @@ def devices_action(link, body, open_setup, busy=lambda: 0):
elif action == "update":
before = reg.get(did)
d = reg.update_device(did, name=body.get("name"), user=body.get("user"), port=body.get("port"))
if is_active and (d["user"], d["port"]) != (before["user"], before["port"]):
login_changed = (d["user"], d["port"]) != (before["user"], before["port"])
if is_active and login_changed:
link.invalidate() # before anything else can fail: the old login mustn't stay in use
try:
frame_devices.rewrite_block(d["alias"], user=d["user"], port=d["port"])
except OSError as e:
raise frame_devices.DeviceError(f"Saved, but couldn't update ~/.ssh/config: {e}")
msg = f"Saved {d['name']}"
if login_changed:
# Only what changed, and only if the block still says what it did: Set Up
# Connection may have written a new login meanwhile, which then stands.
try:
if not frame_devices.rewrite_block(d["alias"], user=d["user"], port=d["port"],
expect={"user": before["user"], "port": before["port"]}) \
and any(b["alias"] == d["alias"] and (b["user"], b["port"]) != (d["user"], d["port"])
for b in frame_devices.parse_blocks(frame_devices.read_config())):
msg += "; ~/.ssh/config changed meanwhile, so it was left as it is"
except OSError as e:
raise frame_devices.DeviceError(f"Saved, but couldn't update ~/.ssh/config: {e}")
elif action == "remove":
if not body.get("config") and len(reg.devices()) == 1 and reg.get(did)["alias"] in {
b["alias"] for b in frame_devices.parse_blocks(frame_devices.read_config())}:
+11 -4
View File
@@ -580,19 +580,25 @@ def open_thing(body):
if what in ("reboot", "poweroff", "suspend"):
return power(what, body.get("password"))
raise Failure("open that from the app", 400)
# The headset and address in use, as every other command gets them (one snapshot).
with _route_lock:
alias, opts = FRAME, list(HOST_OPTS)
host = next((o.split("=", 1)[1].replace("%%", "%") for o in opts if o.startswith("HostName=")), None)
if what in ("terminal", "reboot", "poweroff", "suspend", "rdp", "sftp") and host and host.endswith(".invalid"):
raise Failure("No headset address to use: add one on the Devices tab", 400)
try:
if what == "terminal":
return {"message": f"Opened an SSH session in {terminal(['ssh', FRAME])}"}
return {"message": f"Opened an SSH session in {terminal(['ssh', *opts, alias])}"}
if what in ("reboot", "poweroff", "suspend"):
# logind answers "challenge" over SSH, so sudo (and the password) is needed.
where = terminal(["ssh", "-t", FRAME, "sudo", "systemctl", what])
where = terminal(["ssh", "-t", *opts, alias, "sudo", "systemctl", what])
return {"message": f"Confirm with the Developer Mode password in {where} to {what}"}
if what == "steamlink":
return {"message": frame_host.open_steam_link()}
if what == "rdp":
return {"message": frame_host.open_rdp(FRAME)}
return {"message": frame_host.open_rdp(alias, host)}
if what == "sftp":
return {"message": f"Opened an SFTP session in {terminal(['sftp', FRAME])}"}
return {"message": f"Opened an SFTP session in {terminal(['sftp', *opts, alias])}"}
if what == "shots":
SHOTS_DIR.mkdir(parents=True, exist_ok=True)
frame_host.open_path(SHOTS_DIR)
@@ -1659,6 +1665,7 @@ def main():
if not LOCAL:
LINK = frame_link.Link(frame_devices.Registry(), env_alias=FRAME if FRAME_FROM_ENV else None,
mux_base=MUX_BASE, control=CONTROL, apply=route, explain=unreachable)
LINK.work_lock, LINK.work = _work_lock, lambda: _work[0]
LINK.start()
if not frame_host.WINDOWS:
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt))