fix(hooks): detach Popen children so the hook can exit on Windows
The Stop hook spawns mining subprocesses via subprocess.Popen and then returns. On Windows the parent stays blocked at session end because the child inherits stdout/stderr handles and the OS waits for them to release before the parent can exit — the user-visible symptom is the "running stop hooks... 3/3" spinner hanging for minutes (#1268). Add _detached_popen_kwargs() helper that returns the right detach knobs per platform: - POSIX: start_new_session=True, stdin=DEVNULL, close_fds=True - Windows: creationflags=DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP| CREATE_BREAKAWAY_FROM_JOB, stdin=DEVNULL, close_fds=True Apply to all three fire-and-forget Popen sites in hooks_cli: _spawn_mine, _ingest_transcript, _desktop_toast. Leave _mine_sync's subprocess.run alone — that path is intentionally synchronous (the precompact hook must wait for the mine to finish). Note: the issue body references mempalace-stop.js, which does not exist in this repo (the plugin ships shell wrappers calling Python). The mechanism described — child holds parent open via inherited handles — is universal, so this fix targets the equivalent symptom in our Python hook path. Will follow up on the upstream JS file with the reporter.
This commit is contained in:
+24
-1
@@ -19,6 +19,27 @@ STATE_DIR = Path.home() / ".mempalace" / "hook_state"
|
||||
PALACE_ROOT = Path.home() / ".mempalace"
|
||||
|
||||
|
||||
def _detached_popen_kwargs() -> dict:
|
||||
"""Kwargs that fully detach a Popen child so the hook process can exit.
|
||||
|
||||
Without these, Windows holds the parent open until the child closes the
|
||||
inherited stdout/stderr handles — manifesting as "Stop hook hangs" at
|
||||
session end (#1268). On POSIX the parent can already exit (orphan
|
||||
reparents to init), but ``start_new_session`` makes the boundary
|
||||
explicit so signals to the hook don't propagate to the background mine.
|
||||
"""
|
||||
kwargs: dict = {"stdin": subprocess.DEVNULL, "close_fds": True}
|
||||
if os.name == "nt":
|
||||
flags = 0
|
||||
for name in ("DETACHED_PROCESS", "CREATE_NEW_PROCESS_GROUP", "CREATE_BREAKAWAY_FROM_JOB"):
|
||||
flags |= getattr(subprocess, name, 0)
|
||||
if flags:
|
||||
kwargs["creationflags"] = flags
|
||||
else:
|
||||
kwargs["start_new_session"] = True
|
||||
return kwargs
|
||||
|
||||
|
||||
def _palace_root_exists() -> bool:
|
||||
"""User-removable kill-switch.
|
||||
|
||||
@@ -285,7 +306,7 @@ def _spawn_mine(cmd: list) -> None:
|
||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
log_path = STATE_DIR / "hook.log"
|
||||
with open(log_path, "a") as log_f:
|
||||
proc = subprocess.Popen(cmd, stdout=log_f, stderr=log_f)
|
||||
proc = subprocess.Popen(cmd, stdout=log_f, stderr=log_f, **_detached_popen_kwargs())
|
||||
_MINE_PID_FILE.write_text(str(proc.pid))
|
||||
|
||||
|
||||
@@ -350,6 +371,7 @@ def _desktop_toast(body: str, title: str = "MemPalace"):
|
||||
["notify-send", "--app-name=MemPalace", "--icon=brain", title, body],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
**_detached_popen_kwargs(),
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
@@ -513,6 +535,7 @@ def _ingest_transcript(transcript_path: str):
|
||||
],
|
||||
stdout=log_f,
|
||||
stderr=log_f,
|
||||
**_detached_popen_kwargs(),
|
||||
)
|
||||
_log(f"Transcript ingest started: {path.name}")
|
||||
except OSError:
|
||||
|
||||
Reference in New Issue
Block a user