Skip to content
Closed
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
65 changes: 57 additions & 8 deletions gateway/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,39 @@
_UNSET = object()


def _process_exists(pid: int) -> bool:
"""Check if a process exists, cross-platform.

Uses os.kill(pid, 0) on POSIX, and ctypes/OpenProcess on Windows.
This avoids SystemError on Windows where signal 0 is not supported.
"""
if _IS_WINDOWS:
import ctypes
from ctypes import wintypes

kernel32 = ctypes.windll.kernel32
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010

handle = kernel32.OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
False,
wintypes.DWORD(pid)
)
if handle:
kernel32.CloseHandle(handle)
return True
return False
else:
try:
os.kill(pid, 0)
return True
except (ProcessLookupError, PermissionError):
return False




def _get_pid_path() -> Path:
"""Return the path to the gateway PID file, respecting HERMES_HOME."""
home = get_hermes_home()
Expand Down Expand Up @@ -225,8 +258,28 @@ def _cleanup_invalid_pid_path(pid_path: Path, *, cleanup_stale: bool) -> None:


def write_pid_file() -> None:
"""Write the current process PID and metadata to the gateway PID file."""
_write_json_file(_get_pid_path(), _build_pid_record())
"""Write the current process PID and metadata to the gateway PID file.

Uses atomic O_CREAT | O_EXCL creation so that concurrent --replace
invocations race: exactly one process wins and the rest get
FileExistsError.
"""
path = _get_pid_path()
path.parent.mkdir(parents=True, exist_ok=True)
record = json.dumps(_build_pid_record())
try:
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
except FileExistsError:
raise # Let caller decide: another gateway is racing us
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(record)
except Exception:
try:
path.unlink(missing_ok=True)
except OSError:
pass
raise


def write_runtime_status(
Expand Down Expand Up @@ -339,9 +392,7 @@ def acquire_scoped_lock(scope: str, identity: str, metadata: Optional[dict[str,

stale = existing_pid is None
if not stale:
try:
os.kill(existing_pid, 0)
except (ProcessLookupError, PermissionError):
if not _process_exists(existing_pid):
stale = True
else:
current_start = _get_process_start_time(existing_pid)
Expand Down Expand Up @@ -574,9 +625,7 @@ def get_running_pid(
_cleanup_invalid_pid_path(resolved_pid_path, cleanup_stale=cleanup_stale)
return None

try:
os.kill(pid, 0) # signal 0 = existence check, no actual signal sent
except (ProcessLookupError, PermissionError):
if not _process_exists(pid):
_cleanup_invalid_pid_path(resolved_pid_path, cleanup_stale=cleanup_stale)
return None

Expand Down
Loading