refactor(deadline): consolidate site-local tree-kills onto agent.deadline.kill_process_tree (#85125 4d)
Per-site decisions:
1. hermes_cli/_subprocess_compat.py kill_process_tree(proc) -> None:
MIGRATED. Body now delegates to agent.deadline.kill_process_tree(proc.pid)
via a function-local import; keeps the swallow-everything fail-open
contract and the (proc) -> None signature (agent/shell_hooks.py imports
it by name; _kill_git_process_tree alias preserved). The old body is kept
verbatim as _legacy_kill_process_tree and used as fallback when the
delegation import/call fails. A final proc.kill() is retained on the
happy path so Popen bookkeeping sees the exit (matches old behavior).
2. tools/browser_tool.py _kill_process_tree(proc): MIGRATED, same pattern
(delegate + _legacy_kill_process_tree fallback). Behavior delta: the old
body sent SIGTERM then SIGKILL with zero grace between them; the shared
primitive sends SIGKILL only. With no grace period the observable effect
is identical, and the psutil descendant sweep now also reaches
agent-browser's setsid'd daemon grandchild, which killpg alone missed.
tests/tools/test_browser_npx_warmup.py's TestKillProcessTree repointed at
the legacy fallback (its assertions describe the fallback's internals).
3. tools/code_execution_tool.py _kill_process_group(proc, escalate):
MIGRATED. It was a plain parent+descendants terminate (then wait 5s +
kill when escalate=True) — expressed as two delegated calls:
kill_process_tree(pid, sig=SIGTERM), then on escalate-timeout
kill_process_tree(pid, sig=SIGKILL). Delegation failure degrades to
proc.kill(), mirroring the old psutil-failure fallback. Delta: the old
body terminated children before the parent; the shared primitive
signals the group atomically (child is a session leader via
start_new_session=True) plus an identity-aware descendant sweep —
strictly wider coverage, same signals.
4. gateway/status.py: KEPT BOTH SITES.
- terminate_pid (~l305) taskkill wrapper: NOT migrated. Its contract is
incompatible with the shared primitive — it must RAISE OSError with
taskkill's stderr on non-zero exit (callers branch on that), falls back
to os.kill on FileNotFoundError, and its POSIX branch is deliberately a
single-PID SIGTERM/SIGKILL, not a tree kill. Wrapping the bool-returning
fail-soft primitive would invert the error contract.
- reap_gateway_children (~l2029): NOT migrated. It operates on a
pre-snapshotted child list from a parent that is already dead
(psutil.Process(pid) on the parent would fail), and every signal is
wrapped in identity/ownership checks the primitive lacks: is_running()
identity, zombie skip, and the skip-if-ppid-still-equals-parent guard,
plus SIGTERM -> wait_procs -> SIGKILL staging and a reaped-count return.
The coupling is the feature; migrating would delete the safety logic.
5. scripts/run_tests_parallel.py _kill_process_tree (~l253): NOT migrated.
Dev tooling that intentionally kills by CAPTURED pgid because the direct
child is usually already reaped (psutil/pid-based primitive cannot find
it), and it avoids the psutil import on the test-runner hot path. Its
docstring already documents why psutil is the wrong tool there.
New tests: tests/agent/test_treekill_consolidation.py — delegation +
raise-swallowing tests per migrated wrapper, consumer-identity checks, and
a live end-to-end probe (setsid grandchild dies through the compat wrapper,
zero survivors).
This commit is contained in:
@@ -1773,53 +1773,40 @@ def execute_code(
|
||||
|
||||
|
||||
def _kill_process_group(proc, escalate: bool = False):
|
||||
"""Kill the child and its entire process tree (cross-platform via psutil)."""
|
||||
import psutil
|
||||
try:
|
||||
parent = psutil.Process(proc.pid)
|
||||
children = parent.children(recursive=True)
|
||||
for child in children:
|
||||
"""Kill the child and its entire process tree (cross-platform).
|
||||
|
||||
Delegates to :func:`agent.deadline.kill_process_tree` (#85125 4d):
|
||||
SIGTERM to the whole tree first (killpg when the child leads its own
|
||||
group — it does, ``start_new_session=True`` — plus a psutil descendant
|
||||
sweep for setsid'd grandchildren; ``taskkill /T /F`` on Windows).
|
||||
With ``escalate=True`` the child gets 5s to exit after SIGTERM, then the
|
||||
surviving tree is SIGKILLed — same escalation the old psutil-local body
|
||||
implemented. Never raises; a delegation failure degrades to a plain
|
||||
``proc.kill()`` like the old psutil-failure fallback.
|
||||
"""
|
||||
import signal as _signal
|
||||
|
||||
def _tree_signal(sig) -> None:
|
||||
try:
|
||||
from agent.deadline import kill_process_tree as _deadline_kill_tree
|
||||
|
||||
_deadline_kill_tree(proc.pid, sig=sig)
|
||||
except Exception as e:
|
||||
logger.debug("Could not terminate process tree: %s", e, exc_info=True)
|
||||
try:
|
||||
child.terminate()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
try:
|
||||
parent.terminate()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
except (PermissionError, OSError) as e:
|
||||
logger.debug("Could not terminate process tree: %s", e, exc_info=True)
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception as e2:
|
||||
logger.debug("Could not kill process: %s", e2, exc_info=True)
|
||||
proc.kill()
|
||||
except Exception as e2:
|
||||
logger.debug("Could not kill process: %s", e2, exc_info=True)
|
||||
|
||||
# sig is ignored on Windows (taskkill /F is already forceful).
|
||||
_tree_signal(getattr(_signal, "SIGTERM", None))
|
||||
|
||||
if escalate:
|
||||
# Give the process 5s to exit after SIGTERM, then SIGKILL
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
parent = psutil.Process(proc.pid)
|
||||
for child in parent.children(recursive=True):
|
||||
try:
|
||||
child.kill()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
try:
|
||||
parent.kill()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
except (PermissionError, OSError) as e:
|
||||
logger.debug("Could not kill process tree: %s", e, exc_info=True)
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception as e2:
|
||||
logger.debug("Could not kill process: %s", e2, exc_info=True)
|
||||
_tree_signal(getattr(_signal, "SIGKILL", None))
|
||||
|
||||
|
||||
def _load_config() -> dict:
|
||||
|
||||
Reference in New Issue
Block a user