From 896c459d444f78a167acdf565b56271eb5e5d240 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Fri, 21 Aug 2026 15:48:19 +1000 Subject: [PATCH] fix: run() lets a disconnect-during-step() crash out as a raw traceback A disconnect noticed *during* step() (SwiftRoute.py's expect_message()/ producer racing wait_closed(), jhavl/swift#130) raises TimeoutError directly out of step() -- run()'s own loop only ever expected a disconnect to surface via _check_disconnected()'s poll *between* steps, so this propagated straight out of run() as an uncaught exception instead of the same graceful "Swift browser tab closed." message/return every other disconnect path already gets. Not a hypothetical: closing the browser tab mid-session (e.g. an interactive robot.teach(backend="swift") built on run()) reliably hits this, every time, now that #130 made disconnect detection fast enough to land mid-step almost always rather than rarely. --- src/swift/Swift.py | 14 +++++++++++++- tests/test_protocol.py | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/swift/Swift.py b/src/swift/Swift.py index d259425..ff23a94 100644 --- a/src/swift/Swift.py +++ b/src/swift/Swift.py @@ -1108,7 +1108,19 @@ def run(self, duration: float | None = None, dt: float = 0.05, timeout: float | try: while duration is None or (self.sim_time - start_time) < duration: - self.step(dt) + try: + self.step(dt) + except TimeoutError: + # A disconnect noticed *during* step() (SwiftRoute.py's + # expect_message()/producer racing wait_closed()) raises + # here directly, rather than only ever surfacing via + # the _check_disconnected() poll below -- without this, + # it would propagate straight out of run() as a raw + # traceback instead of the same graceful message/return + # every other disconnect path already gets. + print("\nSwift browser tab closed.") + self.close() + return time.sleep(dt) disconnected_since, expired = self._check_disconnected(disconnected_since, timeout) if expired: diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 629ca06..1d5480d 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -725,6 +725,29 @@ def sleep_raises(s): assert closed == [True] +def test_run_exits_quietly_on_disconnect_during_step(capsys): + # A disconnect noticed *during* step() (SwiftRoute.py's + # expect_message()/producer racing wait_closed()) raises TimeoutError + # directly out of step(), rather than only ever surfacing via + # _check_disconnected()'s poll between steps -- run() must catch this + # the same way it already handles that poll-detected case (print, + # close(), return), not let it propagate as a raw traceback. + env = make_env() + env.headless = True + + def step_raises(dt=0.05, render=True): + raise TimeoutError("Swift browser tab stopped responding") + + env.step = step_raises + closed = [] + env.close = lambda *a, **kw: closed.append(True) + + env.run() # must return normally, not raise + + assert closed == [True] + assert "Swift browser tab closed." in capsys.readouterr().out + + def test_hold_exits_quietly_on_keyboard_interrupt(monkeypatch): env = make_env() env.headless = True