From b4ceb47b2e3e50a3fa30b363d63fcceb7ddcdcc6 Mon Sep 17 00:00:00 2001 From: Nick Renieris Date: Thu, 8 Oct 2026 02:41:19 +0300 Subject: [PATCH 1/2] No more console windows flashing on Windows when rdc starts its daemon or probes Saves a visible python.exe window per rdc call for example. The daemon was DETACHED_PROCESS, so every console child it or the CLI spawned (renderdoc probe, taskkill, netstat, adb) got its own visible console. The daemon now gets a hidden console instead and those children run with CREATE_NO_WINDOW. --- src/rdc/_platform.py | 17 ++++++++++++++--- src/rdc/commands/android.py | 8 +++++++- src/rdc/commands/doctor.py | 11 ++++++++++- src/rdc/commands/session.py | 1 + src/rdc/discover.py | 1 + tests/unit/test_platform.py | 16 ++++++++++------ 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/rdc/_platform.py b/src/rdc/_platform.py index 1b28d16..ddb8cd5 100644 --- a/src/rdc/_platform.py +++ b/src/rdc/_platform.py @@ -72,7 +72,7 @@ def terminate_process_tree(pid: int) -> bool: """Kill a process and all its children (Windows: taskkill /T, Unix: SIGTERM). On Windows, .venv trampoline scripts spawn a child python process. - DETACHED_PROCESS prevents kill cascading, so we use taskkill /F /T + Detached daemons are not killed with their parent, so we use taskkill /F /T to terminate the entire process tree. """ if pid <= 0: @@ -83,6 +83,7 @@ def terminate_process_tree(pid: int) -> bool: ["taskkill", "/F", "/T", "/PID", str(pid)], capture_output=True, timeout=5, + **hidden_kwargs(), ) return result.returncode == 0 except Exception: # noqa: BLE001 @@ -179,10 +180,19 @@ def secure_dir_permissions(path: Path) -> None: def popen_flags() -> dict[str, Any]: """Return extra kwargs for subprocess.Popen on this platform.""" if _WIN: # pragma: no cover - # DETACHED_PROCESS (0x8): detach from parent console + # CREATE_NO_WINDOW (0x08000000): hidden console that grandchildren inherit. + # Not DETACHED_PROCESS: a console-less daemon makes every console child + # (probe, taskkill) allocate its own visible window. # CREATE_NEW_PROCESS_GROUP (0x200): isolate signal handling # CREATE_BREAKAWAY_FROM_JOB (0x01000000): escape sshd job object - return {"creationflags": 0x00000008 | 0x00000200 | 0x01000000} + return {"creationflags": 0x08000000 | 0x00000200 | 0x01000000} + return {} + + +def hidden_kwargs() -> dict[str, Any]: + """Return subprocess kwargs that stop console children opening a window on Windows.""" + if _WIN: # pragma: no cover + return {"creationflags": 0x08000000} # CREATE_NO_WINDOW return {} @@ -196,6 +206,7 @@ def find_pid_by_port(port: int) -> int: capture_output=True, text=True, timeout=5, + **hidden_kwargs(), ) for line in result.stdout.splitlines(): # Match: TCP 127.0.0.1:PORT 0.0.0.0:0 LISTENING PID diff --git a/src/rdc/commands/android.py b/src/rdc/commands/android.py index 99e6972..ee76a60 100644 --- a/src/rdc/commands/android.py +++ b/src/rdc/commands/android.py @@ -14,6 +14,7 @@ import click +from rdc import _platform from rdc.capture_core import run_target_control_loop from rdc.discover import find_renderdoc from rdc.remote_core import connect_remote_server @@ -62,6 +63,7 @@ def _get_forwarded_port(serial: str | None, url: str) -> int | None: timeout=3, capture_output=True, text=True, + **_platform.hidden_kwargs(), ) except (subprocess.TimeoutExpired, OSError): return None @@ -92,6 +94,7 @@ def _is_mali_device(ctrl: Any, url: str, serial: str | None) -> bool: timeout=3, capture_output=True, text=True, + **_platform.hidden_kwargs(), ) prop = proc.stdout.strip().lower() return any(prop.startswith(p) for p in _MALI_PLATFORMS) @@ -124,7 +127,9 @@ def _adb(serial: str, *args: str, timeout: int = 10) -> subprocess.CompletedProc cmd = ["adb", "-s", serial, *args] log.debug("adb: %s", " ".join(cmd)) try: - return subprocess.run(cmd, timeout=timeout, capture_output=True, text=True) + return subprocess.run( + cmd, timeout=timeout, capture_output=True, text=True, **_platform.hidden_kwargs() + ) except subprocess.TimeoutExpired as exc: msg = f"adb command timed out: {' '.join(cmd)}" raise RuntimeError(msg) from exc @@ -242,6 +247,7 @@ def _resolve_serial(serial: str | None) -> str: timeout=5, capture_output=True, text=True, + **_platform.hidden_kwargs(), ) serials = [] for line in proc.stdout.strip().splitlines()[1:]: diff --git a/src/rdc/commands/doctor.py b/src/rdc/commands/doctor.py index 8c6cce4..e4ecc25 100644 --- a/src/rdc/commands/doctor.py +++ b/src/rdc/commands/doctor.py @@ -89,12 +89,18 @@ def _check_replay_support(module: Any | None) -> CheckResult: def _check_renderdoccmd() -> CheckResult: + from rdc import _platform + cmd_path = find_renderdoccmd() if cmd_path is None: return CheckResult("renderdoccmd", False, "not found in PATH or known paths") try: out = subprocess.run( - [str(cmd_path), "--version"], capture_output=True, text=True, timeout=3 + [str(cmd_path), "--version"], + capture_output=True, + text=True, + timeout=3, + **_platform.hidden_kwargs(), ) version = out.stdout.strip() or out.stderr.strip() or "unknown" except Exception: # noqa: BLE001 @@ -170,6 +176,8 @@ def _check_win_vs_build_tools() -> CheckResult: if sys.platform != "win32": return CheckResult("win-vs-build-tools", True, "n/a") + from rdc import _platform + vswhere = Path(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe") if not vswhere.exists(): found = shutil.which("vswhere") @@ -196,6 +204,7 @@ def _check_win_vs_build_tools() -> CheckResult: capture_output=True, text=True, timeout=5, + **_platform.hidden_kwargs(), ) installs: list[dict[str, Any]] = json.loads(proc.stdout or "[]") except subprocess.TimeoutExpired: diff --git a/src/rdc/commands/session.py b/src/rdc/commands/session.py index 57adee4..acbf317 100644 --- a/src/rdc/commands/session.py +++ b/src/rdc/commands/session.py @@ -44,6 +44,7 @@ def _adb_forwarded_port(serial: str) -> int | None: capture_output=True, text=True, timeout=5, + **_platform.hidden_kwargs(), ) except (FileNotFoundError, subprocess.TimeoutExpired): return None diff --git a/src/rdc/discover.py b/src/rdc/discover.py index c7bd2ff..491744e 100644 --- a/src/rdc/discover.py +++ b/src/rdc/discover.py @@ -119,6 +119,7 @@ def _probe_candidate(directory: str, timeout: float = 5.0) -> ProbeOutcome: capture_output=True, text=True, timeout=timeout, + **_platform.hidden_kwargs(), ) except subprocess.TimeoutExpired: return ProbeOutcome(ProbeResult.TIMEOUT, directory) diff --git a/tests/unit/test_platform.py b/tests/unit/test_platform.py index 426b182..1d34e46 100644 --- a/tests/unit/test_platform.py +++ b/tests/unit/test_platform.py @@ -14,6 +14,7 @@ from rdc._platform import ( data_dir, find_pid_by_port, + hidden_kwargs, install_shutdown_signal, is_pid_alive, join_cmdline, @@ -338,16 +339,19 @@ def test_windows_flags_present(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr("rdc._platform._WIN", True) result = popen_flags() flags = result["creationflags"] - assert flags & 0x00000008 # DETACHED_PROCESS + assert flags & 0x08000000 # CREATE_NO_WINDOW (own hidden console) assert flags & 0x00000200 # CREATE_NEW_PROCESS_GROUP assert flags & 0x01000000 # CREATE_BREAKAWAY_FROM_JOB - def test_windows_no_create_no_window(self, monkeypatch: pytest.MonkeyPatch) -> None: - """DS-02: CREATE_NO_WINDOW must NOT be set (mutually exclusive with DETACHED_PROCESS).""" + def test_windows_not_detached(self, monkeypatch: pytest.MonkeyPatch) -> None: + """DS-02: DETACHED_PROCESS must NOT be set (console children would flash windows).""" monkeypatch.setattr("rdc._platform._WIN", True) - result = popen_flags() - flags = result["creationflags"] - assert not (flags & 0x08000000) # CREATE_NO_WINDOW must NOT be set + assert not (popen_flags()["creationflags"] & 0x00000008) + + def test_hidden_kwargs(self, monkeypatch: pytest.MonkeyPatch) -> None: + assert hidden_kwargs() == {} + monkeypatch.setattr("rdc._platform._WIN", True) + assert hidden_kwargs() == {"creationflags": 0x08000000} # ── Group H: renderdoc_search_paths() ──────────────────────────────── From 0f227099b104e0718469bfb2fc8bbcfe337a777a Mon Sep 17 00:00:00 2001 From: Nick Renieris Date: Thu, 8 Oct 2026 11:49:56 +0300 Subject: [PATCH 2/2] Type the subprocess kwargs and pin _WIN in the hidden_kwargs test --- src/rdc/_platform.py | 10 +++++++--- tests/unit/test_platform.py | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/rdc/_platform.py b/src/rdc/_platform.py index ddb8cd5..c36c7e4 100644 --- a/src/rdc/_platform.py +++ b/src/rdc/_platform.py @@ -13,7 +13,7 @@ import sys from collections.abc import Callable from pathlib import Path -from typing import Any +from typing import TypedDict _WIN: bool = sys.platform == "win32" _MAC: bool = sys.platform == "darwin" @@ -177,7 +177,11 @@ def secure_dir_permissions(path: Path) -> None: path.chmod(0o700) -def popen_flags() -> dict[str, Any]: +class _SubprocessKwargs(TypedDict, total=False): + creationflags: int + + +def popen_flags() -> _SubprocessKwargs: """Return extra kwargs for subprocess.Popen on this platform.""" if _WIN: # pragma: no cover # CREATE_NO_WINDOW (0x08000000): hidden console that grandchildren inherit. @@ -189,7 +193,7 @@ def popen_flags() -> dict[str, Any]: return {} -def hidden_kwargs() -> dict[str, Any]: +def hidden_kwargs() -> _SubprocessKwargs: """Return subprocess kwargs that stop console children opening a window on Windows.""" if _WIN: # pragma: no cover return {"creationflags": 0x08000000} # CREATE_NO_WINDOW diff --git a/tests/unit/test_platform.py b/tests/unit/test_platform.py index 1d34e46..8819a4a 100644 --- a/tests/unit/test_platform.py +++ b/tests/unit/test_platform.py @@ -349,6 +349,7 @@ def test_windows_not_detached(self, monkeypatch: pytest.MonkeyPatch) -> None: assert not (popen_flags()["creationflags"] & 0x00000008) def test_hidden_kwargs(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("rdc._platform._WIN", False) assert hidden_kwargs() == {} monkeypatch.setattr("rdc._platform._WIN", True) assert hidden_kwargs() == {"creationflags": 0x08000000}