mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 05:02:50 +02:00
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:
1 parent
c69ea6266f
commit
2e9bc8624b
4 files changed
+69
-13
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user