From 5f9e827d9a11daf952227101661688bb6c9ea6df Mon Sep 17 00:00:00 2001 From: Aarthy Adityan Date: Fri, 15 May 2026 17:56:35 -0400 Subject: [PATCH 1/2] fix(installer): kill orphan testgen + postgres before install/delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- dk-installer.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/dk-installer.py b/dk-installer.py index 73ac16c..592cc4a 100755 --- a/dk-installer.py +++ b/dk-installer.py @@ -2508,6 +2508,71 @@ def stop_app_tree(proc: subprocess.Popen, timeout: int = 10) -> None: proc.wait(timeout=5) +def stop_standalone_orphans() -> None: + """Best-effort kill of orphan ``testgen`` + embedded ``postgres`` processes + left over from a previous dirty exit. + + Called before steps that need a clean slate (``tg delete`` and the + standalone-setup step of ``tg install``). Silent on the happy path — + only logs when something is actually killed. + + Postgres is targeted by PID via ``/postmaster.pid`` so a user's + other Postgres installs aren't touched. ``testgen.exe`` is targeted by + image name on Windows — the installer itself is ``dk-installer.exe``, + so there's no risk of self-kill. Killing ``testgen.exe`` before + ``uv tool uninstall`` also matters on Windows: a running .exe holds an + exclusive file lock, so ``uv`` would otherwise fail to delete the binary. + """ + # Outer guard so a transient filesystem/permission glitch in this best-effort + # cleanup can never crash the install or delete flow. + try: + tg_home_env = os.environ.get("TG_TESTGEN_HOME") + tg_home = pathlib.Path(tg_home_env) if tg_home_env else pathlib.Path.home() / ".testgen" + pid_file = tg_home / "pgdata" / "postmaster.pid" + is_windows = platform.system() == "Windows" + + if pid_file.exists(): + with contextlib.suppress(Exception): + postgres_pid = int(pid_file.read_text().splitlines()[0].strip()) + LOG.info("Stopping orphan postgres (PID %d) from previous session", postgres_pid) + if is_windows: + subprocess.run( + ["taskkill", "/F", "/T", "/PID", str(postgres_pid)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + check=False, + ) + else: + with contextlib.suppress(ProcessLookupError): + os.kill(postgres_pid, signal.SIGKILL) + + if is_windows: + # Image-name match — covers any leftover `testgen run-app` parents. + # `/T` propagates to their children (UI/scheduler/server subprocesses). + with contextlib.suppress(Exception): + subprocess.run( + ["taskkill", "/F", "/T", "/IM", "testgen.exe"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + check=False, + ) + else: + # `pkill -f` matches against the full command line. The installer's own + # argv is `python dk-installer.py …` — doesn't contain `run-app`, so + # no self-kill risk. + with contextlib.suppress(Exception): + subprocess.run( + ["pkill", "-9", "-f", r"testgen.*run-app"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + except Exception: + LOG.exception("Unexpected error during orphan cleanup; continuing") + + def start_testgen_app(action, args) -> None: """Start ``testgen run-app`` and block until the user interrupts. @@ -2624,6 +2689,10 @@ def __init__(self): def pre_execute(self, action, args): self.username = DEFAULT_USER_DATA["username"] self.password = generate_password() + # Reach here only after `_resolve_install_mode` confirmed no existing + # install marker — so any running testgen/postgres processes are + # orphans from a previous dirty exit, safe to force-kill. + stop_standalone_orphans() def execute(self, action, args): # standalone-setup persists these env vars to ~/.testgen/config.env so @@ -3102,6 +3171,13 @@ def _delete_docker(self, args): def _delete_pip(self, args): CONSOLE.title("Delete TestGen instance") + # Stop any running testgen + embedded postgres before touching the + # installation. On Windows, a live testgen.exe locks its own binary + # so `uv tool uninstall` would fail to remove it; on either platform, + # a live postgres holds file handles into ~/.testgen that block + # `shutil.rmtree` from completing cleanly. + stop_standalone_orphans() + uv_path = resolve_uv_path(self.data_folder) if uv_path: try: From 6b47d765550b57bbdb61f542dfed6e28c88140f1 Mon Sep 17 00:00:00 2001 From: Aarthy Adityan Date: Fri, 15 May 2026 17:56:54 -0400 Subject: [PATCH 2/2] fix(installer): write log file as UTF-8 so non-ASCII chars don't error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, `logging.FileHandler` opens the log file with `locale.getpreferredencoding()` when no encoding is passed — cp1252 on a US-English install. Lines containing non-ASCII glyphs like ✓ (used in prereq-status output) hit UnicodeEncodeError on emit; the logging module catches it and prints `--- Logging error ---` plus a traceback to stderr. The install still completes, but the noise is alarming. Add `"encoding": "utf-8"` to the file-handler dictConfig so the file is opened in UTF-8 regardless of locale. Co-Authored-By: Claude Opus 4.7 (1M context) --- dk-installer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dk-installer.py b/dk-installer.py index 592cc4a..08f5084 100755 --- a/dk-installer.py +++ b/dk-installer.py @@ -684,6 +684,10 @@ def configure_logging(self, debug=False): "class": "logging.FileHandler", "filename": str(file_path), "formatter": "file", + # Default is locale.getpreferredencoding(), which is + # cp1252 on US Windows — chokes on non-ASCII chars like + # ✓ that the installer prints in prereq status lines. + "encoding": "utf-8", }, "console": { "level": "DEBUG",