Skip to content

Commit 5f9e827

Browse files
aarthy-dkclaude
andcommitted
fix(installer): kill orphan testgen + postgres before install/delete
A previous standalone session that exited dirty (force-killed via Task Manager, browser tab close, etc.) leaves the embedded postgres alive — it was spawned with CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW, so it survives its parent. `tg install` then fails at standalone-setup because the orphan still owns ~/.testgen/pgdata; `tg delete` half-finishes because Windows file-locks the running testgen.exe binary, blocking `uv tool uninstall` from removing it. Add a `stop_standalone_orphans()` helper that: - reads ~/.testgen/pgdata/postmaster.pid → kills that specific PID (so a user's unrelated postgres installs are untouched), - then force-kills testgen.exe by image name (safe — installer is dk-installer.exe; no self-kill risk). Called from `_delete_pip` before `uv tool uninstall`, and from `TestgenStandaloneSetupStep.pre_execute` — which only runs after `_resolve_install_mode` has confirmed no install marker, so the existing "you already have an install, use upgrade or delete" invariant is preserved. Best-effort: silent on a clean machine, never raises (outer try/except guards against transient filesystem/permission glitches). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c4b1e70 commit 5f9e827

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

dk-installer.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2508,6 +2508,71 @@ def stop_app_tree(proc: subprocess.Popen, timeout: int = 10) -> None:
25082508
proc.wait(timeout=5)
25092509

25102510

2511+
def stop_standalone_orphans() -> None:
2512+
"""Best-effort kill of orphan ``testgen`` + embedded ``postgres`` processes
2513+
left over from a previous dirty exit.
2514+
2515+
Called before steps that need a clean slate (``tg delete`` and the
2516+
standalone-setup step of ``tg install``). Silent on the happy path —
2517+
only logs when something is actually killed.
2518+
2519+
Postgres is targeted by PID via ``<pgdata>/postmaster.pid`` so a user's
2520+
other Postgres installs aren't touched. ``testgen.exe`` is targeted by
2521+
image name on Windows — the installer itself is ``dk-installer.exe``,
2522+
so there's no risk of self-kill. Killing ``testgen.exe`` before
2523+
``uv tool uninstall`` also matters on Windows: a running .exe holds an
2524+
exclusive file lock, so ``uv`` would otherwise fail to delete the binary.
2525+
"""
2526+
# Outer guard so a transient filesystem/permission glitch in this best-effort
2527+
# cleanup can never crash the install or delete flow.
2528+
try:
2529+
tg_home_env = os.environ.get("TG_TESTGEN_HOME")
2530+
tg_home = pathlib.Path(tg_home_env) if tg_home_env else pathlib.Path.home() / ".testgen"
2531+
pid_file = tg_home / "pgdata" / "postmaster.pid"
2532+
is_windows = platform.system() == "Windows"
2533+
2534+
if pid_file.exists():
2535+
with contextlib.suppress(Exception):
2536+
postgres_pid = int(pid_file.read_text().splitlines()[0].strip())
2537+
LOG.info("Stopping orphan postgres (PID %d) from previous session", postgres_pid)
2538+
if is_windows:
2539+
subprocess.run(
2540+
["taskkill", "/F", "/T", "/PID", str(postgres_pid)],
2541+
stdout=subprocess.DEVNULL,
2542+
stderr=subprocess.DEVNULL,
2543+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
2544+
check=False,
2545+
)
2546+
else:
2547+
with contextlib.suppress(ProcessLookupError):
2548+
os.kill(postgres_pid, signal.SIGKILL)
2549+
2550+
if is_windows:
2551+
# Image-name match — covers any leftover `testgen run-app` parents.
2552+
# `/T` propagates to their children (UI/scheduler/server subprocesses).
2553+
with contextlib.suppress(Exception):
2554+
subprocess.run(
2555+
["taskkill", "/F", "/T", "/IM", "testgen.exe"],
2556+
stdout=subprocess.DEVNULL,
2557+
stderr=subprocess.DEVNULL,
2558+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
2559+
check=False,
2560+
)
2561+
else:
2562+
# `pkill -f` matches against the full command line. The installer's own
2563+
# argv is `python dk-installer.py …` — doesn't contain `run-app`, so
2564+
# no self-kill risk.
2565+
with contextlib.suppress(Exception):
2566+
subprocess.run(
2567+
["pkill", "-9", "-f", r"testgen.*run-app"],
2568+
stdout=subprocess.DEVNULL,
2569+
stderr=subprocess.DEVNULL,
2570+
check=False,
2571+
)
2572+
except Exception:
2573+
LOG.exception("Unexpected error during orphan cleanup; continuing")
2574+
2575+
25112576
def start_testgen_app(action, args) -> None:
25122577
"""Start ``testgen run-app`` and block until the user interrupts.
25132578
@@ -2624,6 +2689,10 @@ def __init__(self):
26242689
def pre_execute(self, action, args):
26252690
self.username = DEFAULT_USER_DATA["username"]
26262691
self.password = generate_password()
2692+
# Reach here only after `_resolve_install_mode` confirmed no existing
2693+
# install marker — so any running testgen/postgres processes are
2694+
# orphans from a previous dirty exit, safe to force-kill.
2695+
stop_standalone_orphans()
26272696

26282697
def execute(self, action, args):
26292698
# standalone-setup persists these env vars to ~/.testgen/config.env so
@@ -3102,6 +3171,13 @@ def _delete_docker(self, args):
31023171
def _delete_pip(self, args):
31033172
CONSOLE.title("Delete TestGen instance")
31043173

3174+
# Stop any running testgen + embedded postgres before touching the
3175+
# installation. On Windows, a live testgen.exe locks its own binary
3176+
# so `uv tool uninstall` would fail to remove it; on either platform,
3177+
# a live postgres holds file handles into ~/.testgen that block
3178+
# `shutil.rmtree` from completing cleanly.
3179+
stop_standalone_orphans()
3180+
31053181
uv_path = resolve_uv_path(self.data_folder)
31063182
if uv_path:
31073183
try:

0 commit comments

Comments
 (0)