Skip to content

Commit 6be5e37

Browse files
authored
Merge pull request #89 from DataKitchen/fix/TG-1084-installer-orphan-cleanup-and-log-encoding
fix(installer): cleanup orphan testgen+postgres and log-file encoding on Windows
2 parents c4b1e70 + 6b47d76 commit 6be5e37

1 file changed

Lines changed: 80 additions & 0 deletions

File tree

dk-installer.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,10 @@ def configure_logging(self, debug=False):
684684
"class": "logging.FileHandler",
685685
"filename": str(file_path),
686686
"formatter": "file",
687+
# Default is locale.getpreferredencoding(), which is
688+
# cp1252 on US Windows — chokes on non-ASCII chars like
689+
# ✓ that the installer prints in prereq status lines.
690+
"encoding": "utf-8",
687691
},
688692
"console": {
689693
"level": "DEBUG",
@@ -2508,6 +2512,71 @@ def stop_app_tree(proc: subprocess.Popen, timeout: int = 10) -> None:
25082512
proc.wait(timeout=5)
25092513

25102514

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

26282701
def execute(self, action, args):
26292702
# standalone-setup persists these env vars to ~/.testgen/config.env so
@@ -3102,6 +3175,13 @@ def _delete_docker(self, args):
31023175
def _delete_pip(self, args):
31033176
CONSOLE.title("Delete TestGen instance")
31043177

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

0 commit comments

Comments
 (0)