Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions src/rdc/_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -176,13 +177,26 @@ 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
# 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() -> _SubprocessKwargs:
"""Return subprocess kwargs that stop console children opening a window on Windows."""
if _WIN: # pragma: no cover
return {"creationflags": 0x08000000} # CREATE_NO_WINDOW
return {}


Expand All @@ -196,6 +210,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
Expand Down
8 changes: 7 additions & 1 deletion src/rdc/commands/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:]:
Expand Down
11 changes: 10 additions & 1 deletion src/rdc/commands/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/rdc/commands/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/rdc/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 11 additions & 6 deletions tests/unit/test_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from rdc._platform import (
data_dir,
find_pid_by_port,
hidden_kwargs,
install_shutdown_signal,
is_pid_alive,
join_cmdline,
Expand Down Expand Up @@ -338,16 +339,20 @@ 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:
monkeypatch.setattr("rdc._platform._WIN", False)
assert hidden_kwargs() == {}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
monkeypatch.setattr("rdc._platform._WIN", True)
assert hidden_kwargs() == {"creationflags": 0x08000000}


# ── Group H: renderdoc_search_paths() ────────────────────────────────
Expand Down
Loading