From a3793673e7e7ef64450ddd315fe096040298cd86 Mon Sep 17 00:00:00 2001 From: Seregon Date: Mon, 10 Aug 2026 19:13:28 +0200 Subject: [PATCH] Fix Windows crash from missing signal.SIGKILL Importing the login/setup path failed on Windows because SIGKILL and process-group APIs are Unix-only; use terminate/kill fallbacks instead. --- openhack/agents/session.py | 13 +++++++------ openhack/shells.py | 4 ++-- openhack/tools/process.py | 34 ++++++++++++++++++++++++++++++++-- openhack/tui.py | 4 +++- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/openhack/agents/session.py b/openhack/agents/session.py index b2a69de..26747aa 100644 --- a/openhack/agents/session.py +++ b/openhack/agents/session.py @@ -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) diff --git a/openhack/shells.py b/openhack/shells.py index 77405b9..a0603e6 100644 --- a/openhack/shells.py +++ b/openhack/shells.py @@ -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 @@ -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 diff --git a/openhack/tools/process.py b/openhack/tools/process.py index 37e13ad..9f324a5 100644 --- a/openhack/tools/process.py +++ b/openhack/tools/process.py @@ -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): diff --git a/openhack/tui.py b/openhack/tui.py index 47bcce7..9ff59a1 100644 --- a/openhack/tui.py +++ b/openhack/tui.py @@ -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()