mirror of
https://github.com/usestrix/strix.git
synced 2026-10-10 03:28:11 +00:00
fix(tui): tear the sandbox down when Ctrl+Q quits twice
The sidecar sends app.quit and exits in the same batch, so the command handler and run()'s finally both call quit(). The second cancel lands on the docker delete the first one's teardown is awaiting, and the sandbox container is left running. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QE1brKHrPWcD8SHYooGBA1
This commit is contained in:
parent
4c1f00d1ee
commit
cf1897281b
2 changed files with 60 additions and 0 deletions
|
|
@ -71,6 +71,7 @@ class GoTuiRuntime:
|
|||
self._error_noted_agents: set[str] = set()
|
||||
self.model_verified = False
|
||||
self._setup_preflight: asyncio.Task[None] | None = None
|
||||
self._quit_task: asyncio.Task[None] | None = None
|
||||
self.controller = TuiController(
|
||||
args,
|
||||
live_view=self.live_view,
|
||||
|
|
@ -378,6 +379,21 @@ class GoTuiRuntime:
|
|||
await asyncio.sleep(0.5)
|
||||
|
||||
async def quit(self) -> None:
|
||||
"""Stop the scan once, however many callers ask for it.
|
||||
|
||||
Ctrl+Q quits twice: the sidecar sends ``app.quit`` and exits in the
|
||||
same batch, so the command handler and ``run``'s finally both land
|
||||
here. A second cancel while the first is tearing the sandbox down
|
||||
interrupts the docker delete and leaves the container running, so
|
||||
callers after the first wait on the quit already in flight.
|
||||
"""
|
||||
if self._quit_task is None:
|
||||
self._quit_task = asyncio.create_task(self._quit_once())
|
||||
# Shielded: awaiting a task hands it the waiter's own cancellation,
|
||||
# which is what this guard exists to keep out of the teardown.
|
||||
await asyncio.shield(self._quit_task)
|
||||
|
||||
async def _quit_once(self) -> None:
|
||||
self.controller.close_viewer()
|
||||
self.coordinator.mark_shutting_down()
|
||||
scan_task = self.scan_task
|
||||
|
|
|
|||
|
|
@ -877,6 +877,50 @@ async def test_scan_passes_max_turns_and_budget(monkeypatch: pytest.MonkeyPatch)
|
|||
assert runtime.controller.scan_state == "stopped"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quitting_twice_lets_the_sandbox_teardown_finish(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Ctrl+Q quits twice: the app.quit command and run()'s finally."""
|
||||
runtime = GoTuiRuntime(args())
|
||||
runtime.scan_config = {"run_name": "test-run"}
|
||||
torn_down: list[str] = []
|
||||
scanning = asyncio.Event()
|
||||
tearing_down = asyncio.Event()
|
||||
docker_replied = asyncio.Event()
|
||||
|
||||
async def run_scan(**kwargs: Any) -> None:
|
||||
try:
|
||||
scanning.set()
|
||||
await asyncio.sleep(3600)
|
||||
finally:
|
||||
# session_manager.cleanup() waits on the docker daemon here
|
||||
tearing_down.set()
|
||||
await docker_replied.wait()
|
||||
torn_down.append(kwargs["scan_id"])
|
||||
|
||||
monkeypatch.setattr(
|
||||
go_tui,
|
||||
"load_settings",
|
||||
lambda: SimpleNamespace(runtime=SimpleNamespace(image="test-image")),
|
||||
)
|
||||
monkeypatch.setattr(go_tui, "run_strix_scan", run_scan)
|
||||
monkeypatch.setattr(go_tui, "read_workspace_files", lambda _value: [])
|
||||
|
||||
runtime.start_scan()
|
||||
await scanning.wait()
|
||||
|
||||
command_quit = asyncio.create_task(runtime.quit())
|
||||
await tearing_down.wait()
|
||||
final_quit = asyncio.create_task(runtime.quit())
|
||||
for _ in range(3):
|
||||
await asyncio.sleep(0)
|
||||
docker_replied.set()
|
||||
await asyncio.gather(command_quit, final_quit)
|
||||
|
||||
assert torn_down == ["test-run"], "the sandbox container was left running"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_preflight_failure_does_not_start_scan(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue