diff --git a/.changeset/fix-async-command-handle-kill-blocking.md b/.changeset/fix-async-command-handle-kill-blocking.md new file mode 100644 index 0000000000..e5d4192f39 --- /dev/null +++ b/.changeset/fix-async-command-handle-kill-blocking.md @@ -0,0 +1,5 @@ +--- +"@e2b/python-sdk": patch +--- + +Fix `AsyncCommandHandle.wait()` blocking after `kill()`: `_iterate_events` now returns immediately after the "end" event, so `wait()` completes promptly even if the gRPC stream is slow to close. diff --git a/.gitignore b/.gitignore index 7ccb79cdee..2ffe367001 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ vale-styles .vercel .vscode .DS_Store +.worktrees # Logs logs diff --git a/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py b/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py index dfe34e8383..c829dddaec 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py @@ -128,6 +128,7 @@ async def _iterate_events( exit_code=event.event.end.exit_code, error=event.event.end.error, ) + return async def disconnect(self) -> None: """ @@ -159,6 +160,8 @@ async def _handle_events(self): pass except Exception as e: self._iteration_exception = handle_rpc_exception(e) + finally: + await self._events.aclose() async def wait(self) -> CommandResult: """ diff --git a/packages/python-sdk/tests/async/sandbox_async/commands/test_cmd_kill.py b/packages/python-sdk/tests/async/sandbox_async/commands/test_cmd_kill.py index a92f9b5a8a..abef113686 100644 --- a/packages/python-sdk/tests/async/sandbox_async/commands/test_cmd_kill.py +++ b/packages/python-sdk/tests/async/sandbox_async/commands/test_cmd_kill.py @@ -1,3 +1,5 @@ +import asyncio + import pytest from e2b import AsyncSandbox, CommandExitException @@ -17,3 +19,20 @@ async def test_kill_non_existing_process(async_sandbox: AsyncSandbox): non_existing_pid = 999999 assert not await async_sandbox.commands.kill(non_existing_pid) + + +async def test_kill_via_handle(async_sandbox: AsyncSandbox): + handle = await async_sandbox.commands.run("sleep 60", background=True) + killed = await handle.kill() + assert killed is True + with pytest.raises(CommandExitException): + await async_sandbox.commands.run(f"kill -0 {handle.pid}") + + +async def test_kill_handle_wait_raises(async_sandbox: AsyncSandbox): + handle = await async_sandbox.commands.run("sleep 60", background=True) + await handle.kill() + # Before the fix: this blocks forever (or until the 5s timeout fires as TimeoutError). + # After the fix: raises CommandExitException promptly because the process was killed. + with pytest.raises(CommandExitException): + await asyncio.wait_for(handle.wait(), timeout=5.0)