From de4571c6dfed3d9aea6ff44860badb99725af206 Mon Sep 17 00:00:00 2001 From: zhangguangtao <50561768+zhanggttry@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:33:36 +0800 Subject: [PATCH 1/3] test --- test_write.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 test_write.txt diff --git a/test_write.txt b/test_write.txt new file mode 100644 index 0000000000000..30d74d258442c --- /dev/null +++ b/test_write.txt @@ -0,0 +1 @@ +test \ No newline at end of file From 33e989eb220006415aba2e8be89d543718295976 Mon Sep 17 00:00:00 2001 From: zhangguangtao <50561768+zhanggttry@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:33:51 +0800 Subject: [PATCH 2/3] cleanup --- test_write.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test_write.txt diff --git a/test_write.txt b/test_write.txt deleted file mode 100644 index 30d74d258442c..0000000000000 --- a/test_write.txt +++ /dev/null @@ -1 +0,0 @@ -test \ No newline at end of file From 4a6e21b4e10549ce4823d02b9ea4dbcd2eae3204 Mon Sep 17 00:00:00 2001 From: zhangguangtao <50561768+zhanggttry@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:55:55 +0800 Subject: [PATCH 3/3] fix(gateway): use ctypes.OpenProcess for Windows process existence check Replace os.kill(pid, 0) with _process_exists() helper that uses: - Windows: ctypes.windll.kernel32.OpenProcess() (avoids SystemError WinError 87) - POSIX: os.kill(pid, 0) (unchanged behavior) Fixes #5760, #8978, #12359 --- gateway/status.py | 65 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/gateway/status.py b/gateway/status.py index e1598e1797c20..89d8f2ab7469e 100644 --- a/gateway/status.py +++ b/gateway/status.py @@ -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() @@ -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( @@ -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) @@ -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