From 8d8348212c7af14201285756197769bcabfc36b1 Mon Sep 17 00:00:00 2001 From: fischerxz Date: Tue, 21 Apr 2026 16:49:06 -0700 Subject: [PATCH 1/5] chore: add .worktrees to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 84589f961bbbd84f1c4375e6b1af607962be15be Mon Sep 17 00:00:00 2001 From: fischerxz Date: Mon, 27 Apr 2026 23:11:17 -0700 Subject: [PATCH 2/5] fix(python-sdk): return from _iterate_events after end event to unblock wait() Co-Authored-By: Claude Sonnet 4.6 --- packages/python-sdk/e2b/sandbox_async/commands/command_handle.py | 1 + 1 file changed, 1 insertion(+) 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..b4bf5d8591 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: """ From bf8422a190aef2e42986d72de1e27e2fbeca7504 Mon Sep 17 00:00:00 2001 From: fischerxz Date: Mon, 27 Apr 2026 23:11:22 -0700 Subject: [PATCH 3/5] test(python-sdk): add handle.kill() and non-blocking wait() tests for #1034 Co-Authored-By: Claude Sonnet 4.6 --- .../sandbox_async/commands/test_cmd_kill.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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) From dbbad45b2c40991d5e4cdb858fe485f025581bb3 Mon Sep 17 00:00:00 2001 From: fischerxz Date: Mon, 27 Apr 2026 23:11:26 -0700 Subject: [PATCH 4/5] chore: add changeset for AsyncCommandHandle kill blocking fix Co-Authored-By: Claude Sonnet 4.6 --- .changeset/fix-async-command-handle-kill-blocking.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-async-command-handle-kill-blocking.md 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. From a6bb6ad8c3735e948388dbc375a46631fb457a17 Mon Sep 17 00:00:00 2001 From: fischerxz Date: Tue, 28 Apr 2026 10:17:30 -0700 Subject: [PATCH 5/5] fix(python-sdk): aclose() event stream in _handle_events finally block to release HTTP connection Without this, returning early from _iterate_events after the "end" event leaves self._events (acall_server_stream) suspended with its async with http_resp block still open. The TCP connection is held until the handle is GC'd. Adding aclose() in finally releases it deterministically in all paths: normal completion, exception, and cancellation from disconnect(). Co-Authored-By: Claude Sonnet 4.6 --- .../python-sdk/e2b/sandbox_async/commands/command_handle.py | 2 ++ 1 file changed, 2 insertions(+) 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 b4bf5d8591..c829dddaec 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py @@ -160,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: """