diff --git a/tests/test_devices.py b/tests/test_devices.py index 748d605..baabfea 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -113,6 +113,17 @@ class Migration(Base): self.assertEqual(len(data["devices"]), 2) self.assertEqual(fd.Registry(self.dir / "devices.json").devices(), self.reg.devices()) + def test_first_import_keeps_using_frame(self): + # Set Up Connection puts each new block first; the app used `frame` before. + blocks = CONFIG.split("# >>> steam-frame (frame-2) >>>") + head, first = blocks[0].split("# >>> steam-frame (frame) >>>") + second, tail = blocks[1].split("# <<< steam-frame (frame-2) <<<") + (self.ssh / "config").write_text(head + "# >>> steam-frame (frame-2) >>>" + second + "# <<< steam-frame (frame-2) <<<\n" + + "# >>> steam-frame (frame) >>>" + first + tail) + self.reg.sync_from_config(seed=False) + self.assertEqual([d["alias"] for d in self.reg.devices()], ["frame-2", "frame"]) + self.assertEqual(self.reg.active(), self.reg.by_alias("frame")["id"]) + def test_setup_finding_a_new_address_adds_it(self): self.reg.sync_from_config(seed=False) (self.ssh / "config").write_text(CONFIG.replace("HostName frame.tail1234.ts.net", "HostName 192.168.1.237")) diff --git a/tests/test_link.py b/tests/test_link.py index 2605de8..b43a2eb 100644 --- a/tests/test_link.py +++ b/tests/test_link.py @@ -304,6 +304,14 @@ class Connecting(unittest.TestCase): self.assertIn("User deck", cfg.read_text()) # changed meanwhile: left as it is self.assertIn("left as it is", out["message"]) + def test_a_late_failure_from_the_last_headset_is_ignored(self): + self.link.state["phase"] = "connected" + self.link.gen = 3 + self.link.lost("ssh: connect to host a port 22: Operation timed out", 2) # sent before the switch + self.assertEqual(self.link.kicks, []) + self.link.lost("ssh: connect to host b port 22: Operation timed out", 3) + self.assertEqual(len(self.link.kicks), 1) + 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") diff --git a/ui/frame_devices.py b/ui/frame_devices.py index 7119030..c02076d 100644 --- a/ui/frame_devices.py +++ b/ui/frame_devices.py @@ -672,6 +672,7 @@ class Registry: blocks = parse_blocks(read_config(self.config)) changed = False with self.lock: + first = not self.data["devices"] and not self.data.get("active") for b in blocks: host = b["hostname"] if b["hostname"] and HOST_RE.fullmatch(b["hostname"]) else None user = b["user"] if b["user"] and NAME_RE.fullmatch(b["user"]) else DEFAULT_USER @@ -714,6 +715,13 @@ class Registry: aliases = {b["alias"] for b in blocks} for d in self.data["devices"]: d["managed"] = d["alias"] in aliases + if first: + # First import: the headset the app used before is `frame`, even if Set Up + # Connection put another block above it. + frame = next((d for d in self.data["devices"] if d["alias"] == "frame"), None) + if frame and self.data.get("active") != frame["id"]: + self.data["active"] = frame["id"] + changed = True if changed: self.save() return changed diff --git a/ui/frame_link.py b/ui/frame_link.py index 08994c2..664dd42 100644 --- a/ui/frame_link.py +++ b/ui/frame_link.py @@ -325,8 +325,12 @@ class Link: def route_key(device): return device["id"], device.get("user"), device.get("port"), tuple(device.get("frozen") or ()) - def lost(self, message): - """A command couldn't reach the headset (Windows has no master to watch).""" + def lost(self, message, gen=None): + """A command couldn't reach the headset (Windows has no master to watch). `gen`: + the route it was sent on; one to a headset since switched away from says nothing + about this one.""" + if gen is not None and gen != self.gen: + return if self.state["phase"] == "connected": self.kick(f"lost: {message}") diff --git a/ui/server.py b/ui/server.py index 10f257c..49e0bf6 100755 --- a/ui/server.py +++ b/ui/server.py @@ -254,6 +254,7 @@ def ensure_master(): def ssh(remote, *, stdin=None, timeout=30, text=True): + route_gen = LINK.gen if LINK else None # which headset this command is for try: ensure_master() # Never let ssh inherit our stdin: under the app it's the pipe held open for @@ -266,7 +267,7 @@ def ssh(remote, *, stdin=None, timeout=30, text=True): if r.returncode != 0: err = (r.stderr or r.stdout) if text else (r.stderr or r.stdout).decode(errors="replace") if r.returncode == 255 and LINK and unreachable(err): - LINK.lost(err) # ssh itself failed: the connector reconnects + LINK.lost(err, route_gen) # ssh itself failed: the connector reconnects failure = Failure(strip_ansi(err).strip() or f"ssh exited {r.returncode}") failure.stdout = r.stdout if text else r.stdout.decode(errors="replace") raise failure