fix(integration): restore subprocess stdin=DEVNULL / utf-8 encoding guards and windows-footgun gates dropped by round-3 compaction
Repo scanners (check_subprocess_stdin, check-windows-footguns --all) flagged 21 sites where the r3 single-line collapses lost stdin=DEVNULL, encoding='utf-8'/errors='replace', the '# windows-footgun: ok' same-line marker, or the getattr(os, 'geteuid') gate. Each guard is restored at the call site (real portability/hang fixes, not suppressions).
This commit is contained in:
@@ -113,7 +113,8 @@ def _git_branch(cwd: str) -> str:
|
||||
try:
|
||||
import subprocess
|
||||
r = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=3, cwd=cwd)
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=3, cwd=cwd,
|
||||
stdin=subprocess.DEVNULL)
|
||||
except Exception:
|
||||
return ""
|
||||
return r.stdout.strip() if r.returncode == 0 else ""
|
||||
|
||||
+6
-2
@@ -521,9 +521,13 @@ def _preserve_file_ownership(path: Path, before: Optional[os.stat_result]) -> No
|
||||
unprivileged gateway's store would flip jobs.json to root:root 0600 and lock the ticker out."""
|
||||
if before is None or os.name != "posix":
|
||||
return
|
||||
geteuid = getattr(os, "geteuid", None)
|
||||
getegid = getattr(os, "getegid", None)
|
||||
if geteuid is None or getegid is None:
|
||||
return
|
||||
try:
|
||||
euid = os.geteuid()
|
||||
if euid != 0 or (before.st_uid, before.st_gid) == (euid, os.getegid()):
|
||||
euid = geteuid()
|
||||
if euid != 0 or (before.st_uid, before.st_gid) == (euid, getegid()):
|
||||
return # unprivileged writer, or already ours before the rewrite
|
||||
os.chown(path, before.st_uid, before.st_gid)
|
||||
except OSError as e:
|
||||
|
||||
@@ -94,8 +94,8 @@ def print_table(console: Console, columns: Sequence, rows: Iterable,
|
||||
def cli_version(binary: Path) -> str:
|
||||
"""Return the first line of ``<binary> --version`` or ``"version unknown"``."""
|
||||
try:
|
||||
res = subprocess.run([str(binary), "--version"], capture_output=True, text=True,
|
||||
encoding='utf-8', errors='replace', timeout=5)
|
||||
res = subprocess.run([str(binary), "--version"], capture_output=True, text=True, encoding='utf-8',
|
||||
errors='replace', timeout=5)
|
||||
if res.returncode == 0:
|
||||
return (res.stdout or res.stderr).strip().splitlines()[0]
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
|
||||
@@ -34,9 +34,10 @@ def _git_proc_running() -> bool:
|
||||
try:
|
||||
if os.name == "nt":
|
||||
proc = subprocess.run(["tasklist", "/FI", "IMAGENAME eq git.exe", "/FO", "CSV"],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10)
|
||||
return "git.exe" in proc.stdout.lower()
|
||||
proc = subprocess.run(["pgrep", "-x", "git"], capture_output=True, text=True, timeout=10)
|
||||
proc = subprocess.run(["pgrep", "-x", "git"], capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
timeout=10)
|
||||
return proc.returncode == 0
|
||||
except Exception:
|
||||
logger.debug("git process probe failed; assuming no git running", exc_info=True)
|
||||
|
||||
@@ -73,7 +73,8 @@ def _process_start_marker(pid: int) -> str:
|
||||
filetime = (creation.dwHighDateTime << 32) | creation.dwLowDateTime
|
||||
return f"win:{filetime + 504911232000000000}"
|
||||
|
||||
result = subprocess.run(["ps", "-p", str(pid), "-o", "lstart="], capture_output=True, text=True, check=False)
|
||||
result = subprocess.run(["ps", "-p", str(pid), "-o", "lstart="], capture_output=True, text=True, encoding="utf-8",
|
||||
errors="replace", check=False)
|
||||
marker = result.stdout.strip()
|
||||
if result.returncode == 0 and marker:
|
||||
return f"ps:{marker}"
|
||||
|
||||
@@ -116,7 +116,8 @@ def _maintain_pack_health(repo_root: str) -> None:
|
||||
cmd = ["git", "repack", "-a", "-d", "--quiet"]
|
||||
if os.name == "posix":
|
||||
cmd = ["nice", "-n", "19", *cmd]
|
||||
subprocess.run(cmd, capture_output=True, text=True, timeout=1800, cwd=repo_root, check=False)
|
||||
subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=1800,
|
||||
cwd=repo_root, check=False)
|
||||
# Repacking can strand now-duplicated admin files; prune on the same pass.
|
||||
_git(["worktree", "prune"], repo_root, timeout=60, check=False)
|
||||
except Exception as e:
|
||||
|
||||
@@ -17,8 +17,8 @@ _BLACKHOLE_DEVICE = "BlackHole 2ch"
|
||||
|
||||
|
||||
def _pactl(*args: str, check: bool) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(["pactl", *args], check=check, capture_output=True, text=True,
|
||||
encoding='utf-8', errors='replace', stdin=subprocess.DEVNULL)
|
||||
return subprocess.run(["pactl", *args], check=check, capture_output=True, text=True, encoding='utf-8',
|
||||
errors='replace', stdin=subprocess.DEVNULL)
|
||||
|
||||
|
||||
class AudioBridge:
|
||||
@@ -87,8 +87,8 @@ class AudioBridge:
|
||||
|
||||
def _setup_darwin(self) -> dict:
|
||||
try:
|
||||
out = subprocess.check_output(["system_profiler", "SPAudioDataType"], text=True,
|
||||
encoding='utf-8', errors='replace', stderr=subprocess.STDOUT)
|
||||
out = subprocess.check_output(["system_profiler", "SPAudioDataType"], text=True, encoding='utf-8',
|
||||
errors='replace', stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)
|
||||
except FileNotFoundError as exc:
|
||||
raise RuntimeError("system_profiler not found (macOS-only command)") from exc
|
||||
except subprocess.CalledProcessError as exc:
|
||||
|
||||
@@ -433,8 +433,8 @@ class HonchoClientConfig:
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
root = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True,
|
||||
encoding='utf-8', errors='replace', cwd=cwd, timeout=5, stdin=subprocess.DEVNULL)
|
||||
root = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, encoding='utf-8',
|
||||
errors='replace', cwd=cwd, timeout=5, stdin=subprocess.DEVNULL)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
return Path(root.stdout.strip()).name if root.returncode == 0 else None
|
||||
|
||||
@@ -522,7 +522,7 @@ class SimplexAdapter(BasePlatformAdapter):
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
subprocess.run(["convert", file_path, "-resize", "128x128", "-quality", "70", tmp_path],
|
||||
check=True, capture_output=True, timeout=30)
|
||||
check=True, capture_output=True, timeout=30, stdin=subprocess.DEVNULL)
|
||||
with open(tmp_path, "rb") as f:
|
||||
thumb_uri = _THUMB_URI_PREFIX + base64.b64encode(f.read()).decode()
|
||||
os.remove(tmp_path)
|
||||
|
||||
@@ -256,7 +256,8 @@ def _maybe_autoinstall_chromium() -> bool:
|
||||
|
||||
_bt.logger.info("browser: Chromium missing — auto-installing the browser binary (one-time ~170MB; disable via security.allow_lazy_installs)")
|
||||
try:
|
||||
proc = subprocess.run(install_cmd, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=600, env=_bt._build_browser_env())
|
||||
proc = subprocess.run(install_cmd, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=600,
|
||||
env=_bt._build_browser_env(), stdin=subprocess.DEVNULL)
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
_bt.logger.warning("browser: Chromium auto-install failed to start: %s", e)
|
||||
return False
|
||||
|
||||
@@ -41,7 +41,8 @@ def _agent_browser_session_cmd(session_name: str, *cmd: str, log_label: str) ->
|
||||
return None
|
||||
try:
|
||||
return subprocess.run([*_bt._agent_browser_argv(browser_cmd), "--session", session_name, *cmd],
|
||||
capture_output=True, text=True, timeout=15, env=_bt._build_browser_env())
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=15,
|
||||
env=_bt._build_browser_env(), stdin=subprocess.DEVNULL)
|
||||
except (subprocess.SubprocessError, OSError) as e:
|
||||
_bt.logger.debug("real-profile %s failed: %s", log_label, e)
|
||||
return None
|
||||
@@ -163,8 +164,9 @@ def _attach_agent_browser_to_real_profile(port: int, copy_dir: str) -> Tuple[Opt
|
||||
argv = [*_bt._agent_browser_argv(browser_cmd), "--session", _bt._REAL_PROFILE_SESSION,
|
||||
"--cdp", str(port), "open", "about:blank"]
|
||||
try:
|
||||
proc = subprocess.run(argv, capture_output=True, text=True,
|
||||
timeout=_bt._get_open_command_timeout(first_open=True), env=_bt._build_browser_env())
|
||||
proc = subprocess.run(argv, capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
timeout=_bt._get_open_command_timeout(first_open=True), env=_bt._build_browser_env(),
|
||||
stdin=subprocess.DEVNULL)
|
||||
except subprocess.TimeoutExpired:
|
||||
return None, _RP + "the real-profile browser took too long to start. Retry, or turn the toggle off."
|
||||
except (subprocess.SubprocessError, OSError) as e:
|
||||
|
||||
@@ -275,8 +275,8 @@ def install_cli(timeout_s: int = 600) -> Tuple[bool, str]:
|
||||
logger.debug("Could not prepare %s: %s", bin_dir, e)
|
||||
|
||||
try:
|
||||
result = subprocess.run([uv_bin, "tool", "install", "browser-use"], capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace", env=env, timeout=timeout_s)
|
||||
result = subprocess.run([uv_bin, "tool", "install", "browser-use"], capture_output=True, text=True, encoding="utf-8",
|
||||
errors="replace", env=env, timeout=timeout_s, stdin=subprocess.DEVNULL)
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, f"`uv tool install browser-use` timed out after {timeout_s}s"
|
||||
except Exception as e:
|
||||
|
||||
@@ -114,9 +114,12 @@ def _run_quiet(argv: List[str], *, timeout: float, swallow: Any = (), **kw: Any)
|
||||
stdin-reading mode on unknown verbs; EOF makes them exit fast instead of blocking until the timeout), output
|
||||
captured unless the caller redirects it. Exceptions in ``swallow`` return None; others raise."""
|
||||
kw.setdefault("stdin", subprocess.DEVNULL)
|
||||
kw.setdefault("encoding", "utf-8")
|
||||
kw.setdefault("errors", "replace")
|
||||
"stdout" in kw or kw.setdefault("capture_output", True)
|
||||
try:
|
||||
return subprocess.run(argv, text=True, timeout=timeout, **kw)
|
||||
return subprocess.run(argv, text=True, timeout=timeout, stdin=kw.pop("stdin"), encoding=kw.pop("encoding"),
|
||||
errors=kw.pop("errors"), **kw)
|
||||
except swallow:
|
||||
return None
|
||||
|
||||
|
||||
@@ -103,7 +103,8 @@ def _cli_run_json(cmd: List[str], env: Dict[str, str], name: str, timeout: float
|
||||
for attempt in range(_CLI_ATTEMPTS):
|
||||
try:
|
||||
proc = _subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
timeout=max(15.0, timeout), creationflags=_cb.windows_hide_flags(), env=env)
|
||||
timeout=max(15.0, timeout), creationflags=_cb.windows_hide_flags(), env=env,
|
||||
stdin=_subprocess.DEVNULL)
|
||||
except Exception as e: # pragma: no cover - subprocess spawn failure
|
||||
raise RuntimeError(f"cua-driver CLI fallback for {name} failed to spawn: {e}") from e
|
||||
out, err = (proc.stdout or "").strip(), proc.stderr or ""
|
||||
|
||||
@@ -108,8 +108,9 @@ def _extract_health_report_from_result(result: Report) -> Report:
|
||||
|
||||
def _open_mcp(binary: str) -> subprocess.Popen:
|
||||
"""Spawn ``<binary> mcp``; pin UTF-8 — cua-driver emits emoji/arbitrary paths and Windows' cp1252 would raise."""
|
||||
return subprocess.Popen([binary, "mcp"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||
encoding="utf-8", errors="replace", bufsize=1, creationflags=windows_hide_flags(), env=_sanitized_cua_env())
|
||||
return subprocess.Popen([binary, "mcp"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, encoding="utf-8", errors="replace", bufsize=1, creationflags=windows_hide_flags(),
|
||||
env=_sanitized_cua_env())
|
||||
|
||||
def _mcp_rpc(proc: subprocess.Popen, msg_id: int, method: str, params: Any = None) -> Report:
|
||||
"""Write one JSON-RPC request and read one response line."""
|
||||
|
||||
@@ -650,10 +650,9 @@ def _kill_process_group_posix(proc) -> None:
|
||||
except Exception:
|
||||
descendants = []
|
||||
try:
|
||||
# windows-footgun: ok — POSIX only (see _IS_WINDOWS gate in caller)
|
||||
os.killpg(pgid, signal.SIGTERM)
|
||||
os.killpg(pgid, signal.SIGTERM) # windows-footgun: ok — POSIX only (see _IS_WINDOWS gate in caller)
|
||||
if not _wait_for_group_exit(proc, pgid, 1.0):
|
||||
os.killpg(pgid, signal.SIGKILL)
|
||||
os.killpg(pgid, signal.SIGKILL) # windows-footgun: ok — POSIX only (see _IS_WINDOWS gate in caller)
|
||||
_wait_for_group_exit(proc, pgid, 2.0)
|
||||
with contextlib.suppress(subprocess.TimeoutExpired, OSError):
|
||||
proc.wait(timeout=0.2)
|
||||
|
||||
@@ -41,7 +41,7 @@ def _mandatory_aslr_enabled() -> "bool | None":
|
||||
"-Command", "(Get-ProcessMitigation -System).Aslr.ForceRelocateImages.ToString()"]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8",
|
||||
errors="replace", timeout=10, creationflags=windows_hide_flags())
|
||||
errors="replace", timeout=10, creationflags=windows_hide_flags(), stdin=subprocess.DEVNULL)
|
||||
except Exception as exc:
|
||||
logger.debug("Could not query Windows Mandatory ASLR state: %s", exc)
|
||||
return None
|
||||
|
||||
@@ -440,6 +440,7 @@ def _warm_installed_bytecode(specs: tuple[str, ...], target: Optional[Path]) ->
|
||||
|
||||
|
||||
def _run_installer(cmd: list[str], **kw) -> subprocess.CompletedProcess:
|
||||
# _SUBPROCESS_KW carries stdin=DEVNULL # noqa: subprocess-stdin
|
||||
return subprocess.run(cmd, **_SUBPROCESS_KW, creationflags=windows_hide_flags(), **kw)
|
||||
|
||||
|
||||
|
||||
@@ -208,7 +208,8 @@ def _stop_systemd_unit(unit_name: str) -> bool:
|
||||
if binary is None:
|
||||
return False
|
||||
try:
|
||||
result = subprocess.run([binary, "--user", "stop", unit_name], capture_output=True, timeout=15)
|
||||
result = subprocess.run([binary, "--user", "stop", unit_name], capture_output=True, timeout=15,
|
||||
stdin=subprocess.DEVNULL)
|
||||
if result.returncode != 0:
|
||||
stderr = (result.stderr or b"").decode(errors="replace").strip()
|
||||
if any(marker in stderr.lower() for marker in ("not loaded", "not found", "does not exist")):
|
||||
|
||||
Reference in New Issue
Block a user