Merge PR #219: fix: guard POSIX-only process functions for Windows compatibility

Authored by Farukest. Fixes #218.
This commit is contained in:
teknium1 2026-03-02 17:07:49 -08:00
commit 2ba87a10b0
5 changed files with 129 additions and 16 deletions

View file

@ -20,6 +20,7 @@ Platform: Linux / macOS only (Unix domain sockets). Disabled on Windows.
import json
import logging
import os
import platform
import signal
import socket
import subprocess
@ -28,6 +29,8 @@ import tempfile
import threading
import time
import uuid
_IS_WINDOWS = platform.system() == "Windows"
from typing import Any, Dict, List, Optional
# Availability gate: UDS requires a POSIX OS
@ -405,7 +408,7 @@ def execute_code(
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.DEVNULL,
preexec_fn=os.setsid,
preexec_fn=None if _IS_WINDOWS else os.setsid,
)
# --- Poll loop: watch for exit, timeout, and interrupt ---
@ -514,7 +517,10 @@ def execute_code(
def _kill_process_group(proc, escalate: bool = False):
"""Kill the child and its entire process group."""
try:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
if _IS_WINDOWS:
proc.terminate()
else:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
except (ProcessLookupError, PermissionError):
try:
proc.kill()
@ -527,7 +533,10 @@ def _kill_process_group(proc, escalate: bool = False):
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
if _IS_WINDOWS:
proc.kill()
else:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except (ProcessLookupError, PermissionError):
try:
proc.kill()

View file

@ -1,12 +1,15 @@
"""Local execution environment with interrupt support and non-blocking I/O."""
import os
import platform
import shutil
import signal
import subprocess
import threading
import time
_IS_WINDOWS = platform.system() == "Windows"
from tools.environments.base import BaseEnvironment
# Noise lines emitted by interactive shells when stdin is not a terminal.
@ -74,7 +77,7 @@ class LocalEnvironment(BaseEnvironment):
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE if stdin_data is not None else subprocess.DEVNULL,
preexec_fn=os.setsid,
preexec_fn=None if _IS_WINDOWS else os.setsid,
)
if stdin_data is not None:
@ -107,12 +110,15 @@ class LocalEnvironment(BaseEnvironment):
while proc.poll() is None:
if _interrupt_event.is_set():
try:
pgid = os.getpgid(proc.pid)
os.killpg(pgid, signal.SIGTERM)
try:
proc.wait(timeout=1.0)
except subprocess.TimeoutExpired:
os.killpg(pgid, signal.SIGKILL)
if _IS_WINDOWS:
proc.terminate()
else:
pgid = os.getpgid(proc.pid)
os.killpg(pgid, signal.SIGTERM)
try:
proc.wait(timeout=1.0)
except subprocess.TimeoutExpired:
os.killpg(pgid, signal.SIGKILL)
except (ProcessLookupError, PermissionError):
proc.kill()
reader.join(timeout=2)
@ -122,7 +128,10 @@ class LocalEnvironment(BaseEnvironment):
}
if time.monotonic() > deadline:
try:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
if _IS_WINDOWS:
proc.terminate()
else:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
except (ProcessLookupError, PermissionError):
proc.kill()
reader.join(timeout=2)

View file

@ -32,6 +32,7 @@ Usage:
import json
import logging
import os
import platform
import shlex
import shutil
import signal
@ -39,6 +40,8 @@ import subprocess
import threading
import time
import uuid
_IS_WINDOWS = platform.system() == "Windows"
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
@ -199,7 +202,7 @@ class ProcessRegistry:
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
preexec_fn=os.setsid,
preexec_fn=None if _IS_WINDOWS else os.setsid,
)
session.process = proc
@ -551,7 +554,10 @@ class ProcessRegistry:
elif session.process:
# Local process -- kill the process group
try:
os.killpg(os.getpgid(session.process.pid), signal.SIGTERM)
if _IS_WINDOWS:
session.process.terminate()
else:
os.killpg(os.getpgid(session.process.pid), signal.SIGTERM)
except (ProcessLookupError, PermissionError):
session.process.kill()
elif session.env_ref and session.pid: