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 -5
View File
@@ -6,6 +6,7 @@ import os
import re
import selectors
import shutil
import signal
import subprocess
import sys
import time
@@ -2087,9 +2088,9 @@ def _run_python_exec(arguments: dict[str, Any], context: ToolExecutionContext) -
stderr=subprocess.PIPE,
text=True,
env=_build_subprocess_env(context),
**_subprocess_isolation_kwargs(),
)
_register_child_process(context, process)
_register_child_process(context, process)
if stdin and process.stdin is not None:
process.stdin.write(stdin)
process.stdin.close()
@@ -2108,7 +2109,7 @@ def _run_python_exec(arguments: dict[str, Any], context: ToolExecutionContext) -
except subprocess.TimeoutExpired:
if process is not None:
_terminate_process(process)
stdout, stderr = process.communicate()
stdout, stderr = _communicate_after_stop(process)
else:
stdout, stderr = '', ''
payload = [
@@ -2200,19 +2201,67 @@ def _unregister_child_process(
registry.discard(process)
def _subprocess_isolation_kwargs() -> dict[str, Any]:
if os.name == 'posix':
return {'start_new_session': True}
creation_flags = getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
if creation_flags:
return {'creationflags': creation_flags}
return {}
def _terminate_process(process: subprocess.Popen[Any]) -> None:
if process.poll() is not None:
return
_signal_process_tree(process, signal.SIGTERM)
try:
process.wait(timeout=1)
return
except (OSError, subprocess.TimeoutExpired):
pass
_signal_process_tree(process, signal.SIGKILL)
try:
process.terminate()
process.wait(timeout=1)
except (OSError, subprocess.TimeoutExpired):
try:
pass
def _signal_process_tree(process: subprocess.Popen[Any], sig: signal.Signals) -> None:
try:
if os.name == 'posix':
pgid = os.getpgid(process.pid)
if pgid == process.pid:
os.killpg(pgid, sig)
return
if sig == signal.SIGTERM:
process.terminate()
else:
process.kill()
except ProcessLookupError:
return
except OSError:
try:
if sig == signal.SIGTERM:
process.terminate()
else:
process.kill()
except OSError:
pass
def _communicate_after_stop(process: subprocess.Popen[str]) -> tuple[str, str]:
try:
stdout, stderr = process.communicate(timeout=1)
return stdout or '', stderr or ''
except subprocess.TimeoutExpired:
_terminate_process(process)
try:
stdout, stderr = process.communicate(timeout=1)
return stdout or '', stderr or ''
except (OSError, subprocess.TimeoutExpired):
return '', ''
def _run_python_package(arguments: dict[str, Any], context: ToolExecutionContext) -> str:
_ensure_process_execution_allowed(context, 'Python package management')
action = _require_string(arguments, 'action')
@@ -3633,7 +3682,9 @@ def _stream_bash(
text=True,
bufsize=1,
env=_build_subprocess_env(context),
**_subprocess_isolation_kwargs(),
)
_register_child_process(context, process)
except (ToolPermissionError, ToolExecutionError, OSError, subprocess.SubprocessError) as exc:
yield ToolStreamUpdate(
kind='result',
@@ -3663,7 +3714,7 @@ def _stream_bash(
timeout_error = (
f'Command timed out after {context.command_timeout_seconds:.1f}s: {command}'
)
process.kill()
_terminate_process(process)
break
events = selector.select(timeout=min(remaining, 0.1))
if not events and process.poll() is not None: