diff --git a/agent/proxy_sources/iron_proxy.py b/agent/proxy_sources/iron_proxy.py index 4f137f20c8..941908d083 100644 --- a/agent/proxy_sources/iron_proxy.py +++ b/agent/proxy_sources/iron_proxy.py @@ -734,6 +734,7 @@ def _spawn_daemon(bin_path: Path, cfg: Path, env: dict[str, str], log_path: Path # start_new_session is POSIX-only (Windows isn't supported anyway — no upstream binary). return subprocess.Popen( [str(bin_path), "-config", str(cfg)], env=env, stdin=subprocess.DEVNULL, stdout=log_fd, stderr=subprocess.STDOUT, + # detached-spawn: ok — a daemon meant to outlive the CLI that started it; `hermes egress stop` ends it (pidfile + nonce) **({} if platform.system() == "Windows" else {"start_new_session": True}), ) except OSError as exc: diff --git a/agent/secret_sources/command.py b/agent/secret_sources/command.py index 755c6d6739..54c51f92a5 100644 --- a/agent/secret_sources/command.py +++ b/agent/secret_sources/command.py @@ -78,6 +78,7 @@ def _run_helper(command: str, secret_key: str, timeout_seconds: float, max_outpu proc = subprocess.Popen( ["/bin/sh", "-c", command], env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, # stderr captured and DISCARDED — never inherited + # detached-spawn: ok — awaited; a hard timeout (default 3 s) kills the whole group start_new_session=True, # so the hard timeout can kill the whole group ) except OSError as exc: diff --git a/plugins/google_meet/process_manager.py b/plugins/google_meet/process_manager.py index 60479e733e..c6dc08b7e1 100644 --- a/plugins/google_meet/process_manager.py +++ b/plugins/google_meet/process_manager.py @@ -102,6 +102,7 @@ def start(url: str, *, out_dir: Optional[Path] = None, headed: bool = False, # Detach: stdout/stderr → log file, new session so parent signals don't propagate. with open(log_path, "ab", buffering=0) as log_fh: proc = subprocess.Popen([sys.executable, "-P", "-m", "plugins.google_meet.meet_bot"], stdin=subprocess.DEVNULL, + # detached-spawn: ok — reached via files across turns, stopped by meet_leave (pid in .active.json) stdout=log_fh, stderr=subprocess.STDOUT, env=env, start_new_session=True, close_fds=True) record = {"pid": proc.pid, "meeting_id": meeting_id, "out_dir": str(out), "url": url, diff --git a/plugins/platforms/photon/adapter.py b/plugins/platforms/photon/adapter.py index 5771aa45ea..4e156fe943 100644 --- a/plugins/platforms/photon/adapter.py +++ b/plugins/platforms/photon/adapter.py @@ -1054,6 +1054,7 @@ class PhotonAdapter(BasePlatformAdapter): self._sidecar_proc = subprocess.Popen( # noqa: ASYNC220 -- persistent sidecar; callers use the Popen pipes and poll() [self._node_bin, str(_sidecar_dir() / "index.mjs")], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, + # detached-spawn: ok — exits on stdin EOF (PHOTON_SIDECAR_WATCH_STDIN), so it dies with this process start_new_session=(sys.platform != "win32"), # Windows: run the persistent sidecar headless so it does not open # (or leave) a visible console window. CREATE_NO_WINDOW only (no diff --git a/plugins/web/ddgs/provider.py b/plugins/web/ddgs/provider.py index a5c502a60e..7ed8eb32d7 100644 --- a/plugins/web/ddgs/provider.py +++ b/plugins/web/ddgs/provider.py @@ -110,6 +110,7 @@ def _spawn_worker(env: dict[str, str]) -> subprocess.Popen: env["PYTHONPATH"] = path_entry + os.pathsep + child_pythonpath if child_pythonpath else path_entry worker_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_search_worker.py") # Own process group/session so terminate/kill also reach a hung primp grandchild. + # detached-spawn: ok — awaited under a 30 s cap that kills its group; blocked on its stdin pipe, it exits on owner death extra_kwargs: dict[str, Any] = {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP} if sys.platform == "win32" else {"start_new_session": True} # stdin/stdout/stderr stay explicit keyword args so scripts/check_subprocess_stdin.py sees them # (TUI gateway inherits stdin). stderr=DEVNULL: a chatty child would deadlock a stdout-only drain.