Fix process tree cancellation for tools
This commit is contained in:
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user