From 484a50a189713726060c2622db4da4e910375d02 Mon Sep 17 00:00:00 2001 From: saphid <4596216+saphid@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:11:26 +1000 Subject: [PATCH] Mac in the headset: no viewer cleanup while a Show is launching; relay pump always ends Review round 10: a Show replacing its own stream could have its new viewer ended by the cleanup; a viewer reset left the delayed relay pending. Verified: the relay delivers what's queued, then closes the agent side. Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/macview-bench.py | 8 +++++--- tests/test_macview.py | 3 +++ ui/frame_macview.py | 12 +++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/scripts/macview-bench.py b/scripts/macview-bench.py index 4d9976b..25b0845 100755 --- a/scripts/macview-bench.py +++ b/scripts/macview-bench.py @@ -173,9 +173,11 @@ class Relay: writer.close() async def pump(): - while data := await reader.read(65536): - await queue.put((self.loop.time() + self.delay, data)) - await queue.put((0, None)) + try: + while data := await reader.read(65536): + await queue.put((self.loop.time() + self.delay, data)) + finally: # EOF or a reset: either way the sender finishes and closes + queue.put_nowait((0, None)) sender, pumper = asyncio.ensure_future(send()), asyncio.ensure_future(pump()) done, _ = await asyncio.wait({sender, pumper}, return_when=asyncio.FIRST_COMPLETED) diff --git a/tests/test_macview.py b/tests/test_macview.py index 950be75..54f8641 100644 --- a/tests/test_macview.py +++ b/tests/test_macview.py @@ -98,6 +98,9 @@ class Helpers(unittest.TestCase): mv.shows += 1 # Show pressed during the wait: leave the new viewer alone mv._end_viewer_browser(gen) self.assertEqual(len(calls), 1) + mv.launching = 1 # a Show replacing its own stream is still launching + mv._end_viewer_browser(mv.shows) + self.assertEqual(len(calls), 1) class WS: diff --git a/ui/frame_macview.py b/ui/frame_macview.py index 98f634b..d96dd29 100644 --- a/ui/frame_macview.py +++ b/ui/frame_macview.py @@ -129,6 +129,7 @@ class MacView: self.supervisor = None self.closing = False self.shows = 0 # counts Show presses, so a late cleanup can't close a new viewer + self.launching = 0 # Shows in progress (one may be replacing its own stream) self.shown = set() # sources with a viewer out there, connected or retrying self.browser_flags = list(BROWSER_FLAGS) @@ -280,6 +281,15 @@ class MacView: # ---- viewers on the Frame ---- def show(self, src, quality="balanced", width=None, height=None): + with self.lock: + self.launching += 1 + try: + return self._show(src, quality, width, height) + finally: + with self.lock: + self.launching -= 1 + + def _show(self, src, quality, width, height): if src != "test" and not src.startswith(("window:", "display:", "separate:")): raise MacViewError("Pick a window or display to show.") self.shows += 1 @@ -337,7 +347,7 @@ class MacView: 2026-09-28), so once nothing is shown, end it. It runs with a profile of its own, so nothing else is touched.""" time.sleep(2) # the viewers close their windows first - if self.shown or self.shows != shows: + if self.shown or self.shows != shows or self.launching: return try: self.run("pkill -f '[f]rame-control/mac-view|[d]ata/frame-mac-view' || true", timeout=10)