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