Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions openhack/agents/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,24 +375,25 @@ def kill_active_processes(self) -> None:
waiting on it, so the agent loop breaks at its next checkpoint instead of
after the command finishes on its own.
"""
from openhack.tools.process import kill_process_group

with self._proc_lock:
procs = list(self._active_procs)
survivors: list[tuple[Any, int]] = []
survivors: list[Any] = []
for p in procs:
try:
if p.poll() is not None:
continue # already exited & reaped — its PID may be recycled
pgid = os.getpgid(p.pid)
os.killpg(pgid, signal.SIGTERM)
survivors.append((p, pgid))
kill_process_group(p, signal.SIGTERM)
survivors.append(p)
except (ProcessLookupError, PermissionError, OSError):
pass # already gone / not ours
if survivors:
def _sigkill() -> None:
for p, pgid in survivors:
for p in survivors:
try:
if p.poll() is None: # still alive → force it
os.killpg(pgid, signal.SIGKILL)
kill_process_group(p)
except OSError:
pass
t = threading.Timer(0.4, _sigkill)
Expand Down
4 changes: 2 additions & 2 deletions openhack/shells.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def kill(self, sid: str) -> bool:
def _hard() -> None:
try:
if sh.proc.poll() is None:
kill_process_group(sh.proc, signal.SIGKILL)
kill_process_group(sh.proc) # hard kill (SIGKILL on Unix)
except OSError:
pass

Expand Down Expand Up @@ -171,6 +171,6 @@ def shutdown(self, grace: float = 0.4) -> None:
for sh in procs:
try:
if sh.proc.poll() is None:
kill_process_group(sh.proc, signal.SIGKILL)
kill_process_group(sh.proc) # hard kill (SIGKILL on Unix)
except OSError:
pass
34 changes: 32 additions & 2 deletions openhack/tools/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,43 @@
import os
import signal
import subprocess
import sys
from typing import Optional, Sequence, Union

__all__ = ["run_killable", "kill_process_group"]

# SIGKILL / process groups are Unix-only. On Windows, SIGTERM exists but
# SIGKILL does not — None means "hard kill" via TerminateProcess.
_SIGKILL = getattr(signal, "SIGKILL", None)

def kill_process_group(proc: "subprocess.Popen", sig: int = signal.SIGKILL) -> None:
"""Send *sig* to the child's whole process group; fall back to the pid."""

def kill_process_group(
proc: "subprocess.Popen",
sig: Optional[int] = None,
) -> None:
"""Send *sig* to the child's whole process group; fall back to the pid.

``sig=None`` (default) or ``SIGKILL`` is a hard kill. ``SIGTERM`` is soft.
On Windows there are no process groups / ``SIGKILL``; soft uses
``terminate()`` and hard uses ``kill()``.
"""
if proc.poll() is not None:
return

hard = sig is None or sig == _SIGKILL

if sys.platform == "win32":
try:
if hard:
proc.kill()
else:
proc.terminate()
except OSError:
pass
return

if sig is None:
sig = signal.SIGKILL
try:
os.killpg(os.getpgid(proc.pid), sig)
except (ProcessLookupError, PermissionError, OSError):
Expand Down
4 changes: 3 additions & 1 deletion openhack/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -7560,7 +7560,9 @@ def _on_fatal_signal(*_):
_restore_terminal()
os._exit(1)

signal.signal(signal.SIGHUP, _on_fatal_signal)
# SIGHUP is Unix-only; SIGTERM exists on Windows too.
if hasattr(signal, "SIGHUP"):
signal.signal(signal.SIGHUP, _on_fatal_signal)
signal.signal(signal.SIGTERM, _on_fatal_signal)
_configure_logging()

Expand Down