@@ -74,9 +74,11 @@ class CommandRunner:
7474
7575 def __init__ (self , * , shell_bin : str = "/bin/sh" ) -> None :
7676 self ._shell_bin = shell_bin
77+ self ._active_processes : dict [str , subprocess .Popen [str ]] = {}
7778
7879 async def start (self , spec : CommandRunSpec ) -> CommandRunHandle :
7980 command = self ._normalize_command (spec .command )
81+ run_id = uuid .uuid4 ().hex
8082 async with span (
8183 "sandbox.command.start" ,
8284 cwd = spec .cwd ,
@@ -91,10 +93,11 @@ async def start(self, spec: CommandRunSpec) -> CommandRunHandle:
9193 start_new_session = True ,
9294 text = True ,
9395 )
96+ self ._active_processes [run_id ] = process
9497 pid = process .pid
9598 Path (spec .pid_path ).write_text (f"{ pid } \n " , encoding = "utf-8" )
9699 return CommandRunHandle (
97- run_id = uuid . uuid4 (). hex ,
100+ run_id = run_id ,
98101 pid = pid ,
99102 command = command ,
100103 cwd = spec .cwd ,
@@ -121,6 +124,7 @@ async def snapshot(
121124 effective_started_at = started_at or handle .started_at
122125
123126 if result_payload is not None :
127+ self ._reap_process (handle .run_id )
124128 exit_code = self ._as_int (result_payload .get ("exit_code" ))
125129 timed_out = bool (result_payload .get ("timed_out" , False ))
126130 interrupted = bool (result_payload .get ("interrupted" , False ))
@@ -177,6 +181,7 @@ async def cancel(self, handle: CommandRunHandle) -> None:
177181 '{"exit_code":null,"duration_ms":0,"timed_out":false,"interrupted":true}' ,
178182 encoding = "utf-8" ,
179183 )
184+ self ._reap_process (handle .run_id )
180185
181186 @staticmethod
182187 def _normalize_command (command : list [str ]) -> list [str ]:
@@ -258,6 +263,18 @@ def _build_script(self, spec: CommandRunSpec, command: list[str]) -> str:
258263EOF
259264"""
260265
266+ def _reap_process (self , run_id : str ) -> None :
267+ process = self ._active_processes .get (run_id )
268+ if process is None :
269+ return
270+ try :
271+ process .wait (timeout = 0 )
272+ except subprocess .TimeoutExpired :
273+ # The shell wrapper is still unwinding; keep the handle so a later
274+ # snapshot or cancel can reap it without leaking a ResourceWarning.
275+ return
276+ self ._active_processes .pop (run_id , None )
277+
261278 @staticmethod
262279 def _read_pid (path : Path ) -> int | None :
263280 if not path .exists ():
0 commit comments