Fix process tree cancellation for tools

This commit is contained in:
武阳
2026-05-09 11:36:59 +08:00
parent 323ee23cd6
commit eea7e2f1b3
3 changed files with 141 additions and 8 deletions
+56
View File
@@ -1,7 +1,12 @@
from __future__ import annotations
import os
import re
import signal
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from unittest import TestCase
@@ -179,3 +184,54 @@ class PythonExecToolTests(TestCase):
self.assertTrue(result.ok)
self.assertIn('a|b', result.content)
self.assertEqual(result.metadata.get('mode'), 'script')
def test_python_exec_timeout_kills_spawned_children(self) -> None:
if os.name != 'posix':
self.skipTest('process group cleanup test is POSIX-only')
with tempfile.TemporaryDirectory() as tmp_dir:
config = AgentRuntimeConfig(
cwd=Path(tmp_dir),
permissions=AgentPermissions(allow_shell_commands=True),
)
context = build_tool_context(config)
child_code = (
'import subprocess, sys, time\n'
'child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"])\n'
'print(child.pid, flush=True)\n'
'time.sleep(60)\n'
)
result = execute_tool(
default_tool_registry(),
'python_exec',
{
'code': child_code,
'timeout_seconds': 1,
'max_output_chars': 2000,
},
context,
)
self.assertTrue(result.ok)
self.assertTrue(result.metadata.get('timed_out'))
match = re.search(r'\[stdout\]\s*(\d+)', result.content)
self.assertIsNotNone(match, result.content)
child_pid = int(match.group(1))
time.sleep(0.3)
if self._pid_is_running(child_pid):
try:
os.kill(child_pid, signal.SIGKILL)
except ProcessLookupError:
pass
self.fail(f'spawned child process is still running: {child_pid}')
@staticmethod
def _pid_is_running(pid: int) -> bool:
status = subprocess.run(
['ps', '-p', str(pid), '-o', 'stat='],
capture_output=True,
text=True,
check=False,
)
state = status.stdout.strip()
return bool(state) and not state.startswith('Z')