mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix: prevent TUI gateway stdin EOF crash across all TUI-context subprocess calls
When Hermes runs in TUI mode, the gateway child process communicates with the Node.js parent over a JSON-RPC protocol on stdin. Subprocess calls that inherit this stdin fd can trigger a race condition where the child's stdin read returns EOF, causing the gateway to exit cleanly (exit code 0) mid-tool- execution. This is the same root cause as issue #14036 (byterover plugin) and PR #39257 (SSH environment backend). This commit applies the fix — stdin=subprocess.DEVNULL — to all 85 subprocess.run() and subprocess.Popen() calls that execute inside the TUI gateway child process. Scope: TUI-context code only (agent/, tools/, plugins/, tui_gateway/server.py). CLI code (cli.py, hermes_cli/), tests, scripts, and gateway process management are excluded — they don't run inside the TUI child and inherit the terminal's stdin, not the JSON-RPC pipe. 85 call sites across 28 files. All files pass syntax check.
This commit is contained in:
parent
54318c65b0
commit
d1f23bb2d5
27 changed files with 84 additions and 20 deletions
|
|
@ -821,6 +821,7 @@ def _read_claude_code_credentials_from_keychain() -> Optional[Dict[str, Any]]:
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
logger.debug("Keychain: security command not available or timed out")
|
||||
|
|
@ -1165,7 +1166,7 @@ def run_oauth_setup_token() -> Optional[str]:
|
|||
|
||||
# Run interactively — stdin/stdout/stderr inherited so user can interact
|
||||
try:
|
||||
subprocess.run([claude_path, "setup-token"])
|
||||
subprocess.run([claude_path, "setup-token"], stdin=subprocess.DEVNULL)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ def _expand_git_reference(
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"{ref.raw}: git command timed out (30s)", None
|
||||
|
|
@ -482,6 +483,7 @@ def _rg_files(path: Path, cwd: Path, limit: int) -> list[Path] | None:
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ def _install_npm(
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
logger.warning(
|
||||
|
|
@ -310,6 +311,7 @@ def _install_go(pkg: str, bin_name: str) -> Optional[str]:
|
|||
text=True,
|
||||
timeout=600,
|
||||
env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
logger.warning(
|
||||
|
|
@ -347,6 +349,7 @@ def _install_pip(pkg: str, bin_name: str) -> Optional[str]:
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
logger.warning(
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ def _platform_asset_name() -> str:
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if "musl" in (res.stdout + res.stderr).lower():
|
||||
libc = "musl"
|
||||
|
|
@ -525,6 +526,7 @@ def _run_bws_list(
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=_BWS_RUN_TIMEOUT,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise RuntimeError(
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ def run_inline_shell(command: str, cwd: Path | None, timeout: int) -> str:
|
|||
text=True,
|
||||
timeout=max(1, int(timeout)),
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"[inline-shell timeout after {timeout}s: {command}]"
|
||||
|
|
|
|||
|
|
@ -378,6 +378,7 @@ def check_codex_binary(
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return False, (
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ class AudioBridge:
|
|||
["pactl", "unload-module", str(mod_id)],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except Exception:
|
||||
# Best-effort teardown — never raise from here.
|
||||
|
|
@ -111,6 +112,7 @@ class AudioBridge:
|
|||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise RuntimeError(
|
||||
|
|
@ -135,6 +137,7 @@ class AudioBridge:
|
|||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
# Roll back the null-sink we just created so we don't leak it.
|
||||
|
|
@ -142,6 +145,7 @@ class AudioBridge:
|
|||
["pactl", "unload-module", str(sink_mod_id)],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"pactl load-module virtual-source failed: {exc.stderr or exc}"
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ def _run_brv(args: List[str], timeout: int = _QUERY_TIMEOUT,
|
|||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True,
|
||||
timeout=timeout, cwd=effective_cwd, env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
stdout = result.stdout.strip()
|
||||
stderr = result.stderr.strip()
|
||||
|
|
|
|||
|
|
@ -695,6 +695,7 @@ class HindsightMemoryProvider(MemoryProvider):
|
|||
subprocess.run(
|
||||
[uv_path, "pip", "install", "--python", sys.executable, "--quiet", "--upgrade"] + deps_to_install,
|
||||
check=True, timeout=120, capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
print(" ✓ Dependencies up to date")
|
||||
except Exception as e:
|
||||
|
|
@ -1101,6 +1102,7 @@ class HindsightMemoryProvider(MemoryProvider):
|
|||
[uv_path, "pip", "install", "--python", sys.executable,
|
||||
"--quiet", "--upgrade", f"hindsight-client>={_MIN_CLIENT_VERSION}"],
|
||||
check=True, timeout=120, capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
logger.info("hindsight-client upgraded to >=%s", _MIN_CLIENT_VERSION)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -416,6 +416,7 @@ def _ensure_sdk_installed() -> bool:
|
|||
[sys.executable, "-m", "pip", "install", "honcho-ai>=2.0.1"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(" Installed.\n")
|
||||
|
|
|
|||
|
|
@ -628,6 +628,7 @@ class HonchoClientConfig:
|
|||
root = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True, text=True, cwd=cwd, timeout=5,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if root.returncode == 0:
|
||||
return Path(root.stdout.strip()).name
|
||||
|
|
|
|||
|
|
@ -520,6 +520,7 @@ class VoiceReceiver:
|
|||
],
|
||||
check=True,
|
||||
timeout=10,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -320,6 +320,7 @@ def decode_to_pcm(path: str, *, timeout: float = 30.0) -> Optional[bytes]:
|
|||
],
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
||||
logger.warning("decode_to_pcm failed for %s: %s", path, e)
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ def _run_git(
|
|||
timeout=timeout,
|
||||
env=env,
|
||||
cwd=str(normalized_working_dir),
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
ok = result.returncode == 0
|
||||
stdout = result.stdout.strip()
|
||||
|
|
@ -426,6 +427,7 @@ def _init_store(store: Path, working_dir: str) -> Optional[str]:
|
|||
["git", "init", "--bare", str(store)],
|
||||
capture_output=True, text=True,
|
||||
env=init_env, timeout=_GIT_TIMEOUT,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return f"Shadow store init failed: {result.stderr.strip()}"
|
||||
|
|
|
|||
|
|
@ -1618,6 +1618,7 @@ def _is_usable_python(python_path: str) -> bool:
|
|||
timeout=5,
|
||||
capture_output=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if _IS_WINDOWS else 0,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (OSError, subprocess.TimeoutExpired, subprocess.SubprocessError):
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ def _run(cmd: list[str], timeout: float = 3.0) -> tuple[int, str, str]:
|
|||
text=True,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
return result.returncode, (result.stdout or "").strip(), (result.stderr or "").strip()
|
||||
except FileNotFoundError:
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ def reap_orphan_containers(
|
|||
listing = subprocess.run(
|
||||
[docker, "ps", "-a", *filters, "--format", "{{.ID}}"],
|
||||
capture_output=True, text=True, timeout=15, check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, OSError) as e:
|
||||
logger.debug("orphan reaper docker ps failed: %s", e)
|
||||
|
|
@ -210,6 +211,7 @@ def reap_orphan_containers(
|
|||
result = subprocess.run(
|
||||
[docker, "rm", "-f", cid],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
removed += 1
|
||||
|
|
@ -239,6 +241,7 @@ def _container_finished_at(docker_exe: str, container_id: str):
|
|||
result = subprocess.run(
|
||||
[docker_exe, "inspect", "--format", "{{.State.FinishedAt}}", container_id],
|
||||
capture_output=True, text=True, timeout=10, check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, OSError) as e:
|
||||
logger.debug("orphan reaper docker inspect %s failed: %s", container_id[:12], e)
|
||||
|
|
@ -381,6 +384,7 @@ def _image_uses_init_entrypoint(docker_exe: str, image: str) -> bool:
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.SubprocessError, OSError) as e:
|
||||
logger.debug("Docker: could not inspect entrypoint for %s: %s", image, e)
|
||||
|
|
@ -453,6 +457,7 @@ def _ensure_docker_available() -> None:
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
logger.error(
|
||||
|
|
@ -833,6 +838,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
text=True,
|
||||
timeout=30,
|
||||
check=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
||||
logger.warning(
|
||||
|
|
@ -871,6 +877,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
text=True,
|
||||
timeout=120, # image pull may take a while
|
||||
check=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
||||
# Docker may create the container object before `docker run`
|
||||
|
|
@ -887,6 +894,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
subprocess.run(
|
||||
[self._docker_exe, "rm", "-f", container_name],
|
||||
capture_output=True, timeout=10,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
raise
|
||||
self._container_id = result.stdout.strip()
|
||||
|
|
@ -997,6 +1005,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
subprocess.run(
|
||||
[self._docker_exe, "start", cid],
|
||||
capture_output=True, text=True, timeout=30, check=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
self._container_id = cid
|
||||
logger.info("Recovery: restarted container %s", cid[:12])
|
||||
|
|
@ -1027,6 +1036,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
]
|
||||
result = subprocess.run(
|
||||
run_cmd, capture_output=True, text=True, timeout=120, check=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
self._container_id = result.stdout.strip()
|
||||
self._container_name = new_name
|
||||
|
|
@ -1081,6 +1091,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
result = subprocess.run(
|
||||
[docker, "info", "--format", "{{.Driver}}"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
driver = result.stdout.strip().lower()
|
||||
if driver != "overlay2":
|
||||
|
|
@ -1091,13 +1102,15 @@ class DockerEnvironment(BaseEnvironment):
|
|||
probe = subprocess.run(
|
||||
[docker, "create", "--storage-opt", "size=1m", "hello-world"],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if probe.returncode == 0:
|
||||
# Clean up the created container
|
||||
container_id = probe.stdout.strip()
|
||||
if container_id:
|
||||
subprocess.run([docker, "rm", container_id],
|
||||
capture_output=True, timeout=5)
|
||||
capture_output=True, timeout=5,
|
||||
stdin=subprocess.DEVNULL)
|
||||
_storage_opt_ok = True
|
||||
else:
|
||||
_storage_opt_ok = False
|
||||
|
|
@ -1132,6 +1145,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
text=True,
|
||||
timeout=10,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, OSError) as e:
|
||||
logger.debug("docker ps probe failed: %s — will start a fresh container", e)
|
||||
|
|
@ -1248,6 +1262,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
subprocess.run(
|
||||
[docker_exe, "stop", "-t", "10", container_id],
|
||||
capture_output=True, timeout=30,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, OSError) as e:
|
||||
logger.warning("docker stop %s timed out / failed: %s", log_id, e)
|
||||
|
|
@ -1256,6 +1271,7 @@ class DockerEnvironment(BaseEnvironment):
|
|||
subprocess.run(
|
||||
[docker_exe, "rm", "-f", container_id],
|
||||
capture_output=True, timeout=30,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, OSError) as e:
|
||||
logger.warning("docker rm -f %s failed: %s", log_id, e)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ def _ensure_singularity_available() -> str:
|
|||
try:
|
||||
result = subprocess.run(
|
||||
[exe, "version"], capture_output=True, text=True, timeout=10,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError(
|
||||
|
|
@ -136,6 +137,7 @@ def _get_or_build_sif(image: str, executable: str = "apptainer") -> str:
|
|||
result = subprocess.run(
|
||||
[executable, "build", str(sif_path), image],
|
||||
capture_output=True, text=True, timeout=600, env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning("SIF build failed, falling back to docker:// URL")
|
||||
|
|
@ -218,7 +220,7 @@ class SingularityEnvironment(BaseEnvironment):
|
|||
cmd.extend([str(self.image), self.instance_id])
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, stdin=subprocess.DEVNULL)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Failed to start instance: {result.stderr}")
|
||||
self._instance_started = True
|
||||
|
|
@ -250,6 +252,7 @@ class SingularityEnvironment(BaseEnvironment):
|
|||
subprocess.run(
|
||||
[self.executable, "instance", "stop", self.instance_id],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
logger.info("Singularity instance %s stopped", self.instance_id)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -365,6 +365,7 @@ def _venv_pip_install(specs: tuple[str, ...], *, timeout: int = 300) -> _Install
|
|||
r = subprocess.run(
|
||||
[uv_bin, "pip", "install", *specs],
|
||||
capture_output=True, text=True, timeout=timeout, env=uv_env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if r.returncode == 0:
|
||||
return _InstallResult(True, r.stdout or "", r.stderr or "")
|
||||
|
|
@ -378,6 +379,7 @@ def _venv_pip_install(specs: tuple[str, ...], *, timeout: int = 300) -> _Install
|
|||
probe = subprocess.run(
|
||||
pip_cmd + ["--version"],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if probe.returncode != 0:
|
||||
raise FileNotFoundError("pip not in venv")
|
||||
|
|
@ -386,6 +388,7 @@ def _venv_pip_install(specs: tuple[str, ...], *, timeout: int = 300) -> _Install
|
|||
subprocess.run(
|
||||
[sys.executable, "-m", "ensurepip", "--upgrade", "--default-pip"],
|
||||
capture_output=True, text=True, timeout=120, check=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
||||
return _InstallResult(False, "",
|
||||
|
|
@ -395,6 +398,7 @@ def _venv_pip_install(specs: tuple[str, ...], *, timeout: int = 300) -> _Install
|
|||
r = subprocess.run(
|
||||
pip_cmd + ["install", *specs],
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
return _InstallResult(r.returncode == 0, r.stdout or "", r.stderr or "")
|
||||
except subprocess.TimeoutExpired as e:
|
||||
|
|
|
|||
|
|
@ -472,6 +472,7 @@ class ProcessRegistry:
|
|||
text=True,
|
||||
timeout=10,
|
||||
creationflags=windows_hide_flags(),
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -301,6 +301,7 @@ class GitHubAuth:
|
|||
result = subprocess.run(
|
||||
["gh", "auth", "token"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
|
|
|
|||
|
|
@ -2436,13 +2436,13 @@ def check_terminal_requirements() -> bool:
|
|||
if not docker:
|
||||
logger.error("Docker executable not found in PATH or common install locations")
|
||||
return False
|
||||
result = subprocess.run([docker, "version"], capture_output=True, timeout=5)
|
||||
result = subprocess.run([docker, "version"], capture_output=True, timeout=5, stdin=subprocess.DEVNULL)
|
||||
return result.returncode == 0
|
||||
|
||||
elif env_type == "singularity":
|
||||
executable = shutil.which("apptainer") or shutil.which("singularity")
|
||||
if executable:
|
||||
result = subprocess.run([executable, "--version"], capture_output=True, timeout=5)
|
||||
result = subprocess.run([executable, "--version"], capture_output=True, timeout=5, stdin=subprocess.DEVNULL)
|
||||
return result.returncode == 0
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -288,6 +288,7 @@ def _verify_cosign(checksums_path: str, sig_path: str, cert_path: str) -> bool |
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
logger.info("cosign provenance verification passed")
|
||||
|
|
@ -734,6 +735,7 @@ def check_command_security(command: str) -> dict:
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except OSError as exc:
|
||||
# Covers FileNotFoundError, PermissionError, exec format error.
|
||||
|
|
|
|||
|
|
@ -490,6 +490,7 @@ def _terminate_command_stt_process_tree(proc: subprocess.Popen) -> None:
|
|||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=5,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
|
|
@ -555,7 +556,7 @@ def _run_command_stt(command: str, timeout: float) -> subprocess.CompletedProces
|
|||
else:
|
||||
popen_kwargs["start_new_session"] = True
|
||||
|
||||
proc = subprocess.Popen(command, **popen_kwargs)
|
||||
proc = subprocess.Popen(command, **popen_kwargs, stdin=subprocess.DEVNULL)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=timeout)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
|
|
@ -1186,7 +1187,7 @@ def _prepare_local_audio(file_path: str, work_dir: str) -> tuple[Optional[str],
|
|||
command = [ffmpeg, "-y", "-i", file_path, converted_path]
|
||||
|
||||
try:
|
||||
subprocess.run(command, check=True, capture_output=True, text=True, timeout=300)
|
||||
subprocess.run(command, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)
|
||||
return converted_path, None
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("ffmpeg conversion timed out for %s", file_path)
|
||||
|
|
@ -1232,9 +1233,9 @@ def _transcribe_local_command(file_path: str, model_name: str) -> Dict[str, Any]
|
|||
# User-provided templates (env var) may contain shell syntax; auto-detected commands are safe for list mode.
|
||||
use_shell = bool(os.getenv(LOCAL_STT_COMMAND_ENV, "").strip())
|
||||
if use_shell:
|
||||
subprocess.run(command, shell=True, check=True, capture_output=True, text=True, timeout=300)
|
||||
subprocess.run(command, shell=True, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)
|
||||
else:
|
||||
subprocess.run(shlex.split(command), check=True, capture_output=True, text=True, timeout=300)
|
||||
subprocess.run(shlex.split(command), check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)
|
||||
|
||||
|
||||
txt_files = sorted(Path(output_dir).glob("*.txt"))
|
||||
|
|
|
|||
|
|
@ -693,6 +693,7 @@ def _terminate_command_tts_process_tree(proc: subprocess.Popen) -> None:
|
|||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=5,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
|
|
@ -745,7 +746,7 @@ def _run_command_tts(command: str, timeout: float) -> subprocess.CompletedProces
|
|||
else:
|
||||
popen_kwargs["start_new_session"] = True
|
||||
|
||||
proc = subprocess.Popen(command, **popen_kwargs)
|
||||
proc = subprocess.Popen(command, **popen_kwargs, stdin=subprocess.DEVNULL)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=timeout)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
|
|
@ -882,6 +883,7 @@ def _convert_to_opus(mp3_path: str) -> Optional[str]:
|
|||
["ffmpeg", "-i", mp3_path, "-acodec", "libopus",
|
||||
"-ac", "1", "-b:a", "64k", "-vbr", "off", ogg_path, "-y"],
|
||||
capture_output=True, timeout=30,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning("ffmpeg conversion failed with return code %d: %s",
|
||||
|
|
@ -1504,7 +1506,7 @@ def _generate_gemini_tts(text: str, output_path: str, tts_config: Dict[str, Any]
|
|||
]
|
||||
else:
|
||||
cmd = [ffmpeg, "-i", wav_path, "-y", "-loglevel", "error", output_path]
|
||||
result = subprocess.run(cmd, capture_output=True, timeout=30)
|
||||
result = subprocess.run(cmd, capture_output=True, timeout=30, stdin=subprocess.DEVNULL)
|
||||
if result.returncode != 0:
|
||||
stderr = result.stderr.decode("utf-8", errors="ignore")[:300]
|
||||
raise RuntimeError(f"ffmpeg conversion failed: {stderr}")
|
||||
|
|
@ -1587,7 +1589,7 @@ def _generate_neutts(text: str, output_path: str, tts_config: Dict[str, Any]) ->
|
|||
"--device", device,
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, stdin=subprocess.DEVNULL)
|
||||
if result.returncode != 0:
|
||||
stderr = result.stderr.strip()
|
||||
# Filter out the "OK:" line from stderr
|
||||
|
|
@ -1599,7 +1601,7 @@ def _generate_neutts(text: str, output_path: str, tts_config: Dict[str, Any]) ->
|
|||
ffmpeg = shutil.which("ffmpeg")
|
||||
if ffmpeg:
|
||||
conv_cmd = [ffmpeg, "-i", wav_path, "-y", "-loglevel", "error", output_path]
|
||||
subprocess.run(conv_cmd, check=True, timeout=30)
|
||||
subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL)
|
||||
os.remove(wav_path)
|
||||
else:
|
||||
# No ffmpeg — just rename the WAV to the expected path
|
||||
|
|
@ -1670,6 +1672,7 @@ def _resolve_piper_voice_path(voice: str, download_dir: Path) -> str:
|
|||
[_sys.executable, "-m", "piper.download_voices", voice,
|
||||
"--download-dir", str(download_dir)],
|
||||
capture_output=True, text=True, timeout=300,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise RuntimeError(
|
||||
|
|
@ -1757,7 +1760,7 @@ def _generate_piper_tts(text: str, output_path: str, tts_config: Dict[str, Any])
|
|||
ffmpeg = shutil.which("ffmpeg")
|
||||
if ffmpeg:
|
||||
conv_cmd = [ffmpeg, "-i", wav_path, "-y", "-loglevel", "error", output_path]
|
||||
subprocess.run(conv_cmd, check=True, timeout=30)
|
||||
subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL)
|
||||
try:
|
||||
os.remove(wav_path)
|
||||
except OSError:
|
||||
|
|
@ -1823,7 +1826,7 @@ def _generate_kittentts(text: str, output_path: str, tts_config: Dict[str, Any])
|
|||
ffmpeg = shutil.which("ffmpeg")
|
||||
if ffmpeg:
|
||||
conv_cmd = [ffmpeg, "-i", wav_path, "-y", "-loglevel", "error", output_path]
|
||||
subprocess.run(conv_cmd, check=True, timeout=30)
|
||||
subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL)
|
||||
os.remove(wav_path)
|
||||
else:
|
||||
# No ffmpeg — rename the WAV to the expected path
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ def _termux_api_app_installed() -> bool:
|
|||
text=True,
|
||||
timeout=5,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
return "package:com.termux.api" in (result.stdout or "")
|
||||
except Exception:
|
||||
|
|
@ -388,7 +389,7 @@ class TermuxAudioRecorder:
|
|||
"-c", str(CHANNELS),
|
||||
]
|
||||
try:
|
||||
subprocess.run(command, capture_output=True, text=True, timeout=15, check=True)
|
||||
subprocess.run(command, capture_output=True, text=True, timeout=15, check=True, stdin=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError as e:
|
||||
details = (e.stderr or e.stdout or str(e)).strip()
|
||||
raise RuntimeError(f"Termux microphone start failed: {details}") from e
|
||||
|
|
@ -405,7 +406,7 @@ class TermuxAudioRecorder:
|
|||
mic_cmd = _termux_microphone_command()
|
||||
if not mic_cmd:
|
||||
return
|
||||
subprocess.run([mic_cmd, "-q"], capture_output=True, text=True, timeout=15, check=False)
|
||||
subprocess.run([mic_cmd, "-q"], capture_output=True, text=True, timeout=15, check=False, stdin=subprocess.DEVNULL)
|
||||
|
||||
def stop(self) -> Optional[str]:
|
||||
with self._lock:
|
||||
|
|
@ -1095,7 +1096,7 @@ def play_audio_file(file_path: str) -> bool:
|
|||
exe = shutil.which(cmd[0])
|
||||
if exe:
|
||||
try:
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
|
||||
with _playback_lock:
|
||||
_active_playback = proc
|
||||
proc.wait(timeout=300)
|
||||
|
|
|
|||
|
|
@ -1041,6 +1041,7 @@ def _git_branch_for_cwd(cwd: str) -> str:
|
|||
text=True,
|
||||
timeout=1.5,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
branch = result.stdout.strip()
|
||||
|
|
@ -1052,6 +1053,7 @@ def _git_branch_for_cwd(cwd: str) -> str:
|
|||
text=True,
|
||||
timeout=1.5,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
return head.stdout.strip() if head.returncode == 0 else ""
|
||||
except Exception:
|
||||
|
|
@ -5571,7 +5573,7 @@ def _(rid, params: dict) -> dict:
|
|||
str(pdf_path), str(out_prefix),
|
||||
]
|
||||
try:
|
||||
res = subprocess.run(argv, capture_output=True, text=True, timeout=120)
|
||||
res = subprocess.run(argv, capture_output=True, text=True, timeout=120, stdin=subprocess.DEVNULL)
|
||||
except subprocess.TimeoutExpired:
|
||||
return _err(rid, 5028, "pdftoppm timed out (>120s)")
|
||||
if res.returncode != 0:
|
||||
|
|
@ -6845,6 +6847,7 @@ def _(rid, params: dict) -> dict:
|
|||
timeout=min(int(params.get("timeout", 240)), 600),
|
||||
cwd=os.getcwd(),
|
||||
env=os.environ.copy(),
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
parts = [r.stdout or "", r.stderr or ""]
|
||||
out = "\n".join(p for p in parts if p).strip() or "(no output)"
|
||||
|
|
@ -6905,6 +6908,7 @@ def _(rid, params: dict) -> dict:
|
|||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
output = (
|
||||
(r.stdout or "")
|
||||
|
|
@ -7295,6 +7299,7 @@ def _list_repo_files(root: str) -> list[str]:
|
|||
capture_output=True,
|
||||
timeout=2.0,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if top_result.returncode == 0:
|
||||
top = top_result.stdout.decode("utf-8", "replace").strip()
|
||||
|
|
@ -7312,6 +7317,7 @@ def _list_repo_files(root: str) -> list[str]:
|
|||
capture_output=True,
|
||||
timeout=2.0,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if list_result.returncode == 0:
|
||||
for p in list_result.stdout.decode("utf-8", "replace").split("\0"):
|
||||
|
|
@ -9045,7 +9051,8 @@ def _(rid, params: dict) -> dict:
|
|||
return _err(rid, 5001, "shell.exec unavailable: approval safety module not importable")
|
||||
try:
|
||||
r = subprocess.run(
|
||||
cmd, shell=True, capture_output=True, text=True, timeout=30, cwd=os.getcwd()
|
||||
cmd, shell=True, capture_output=True, text=True, timeout=30, cwd=os.getcwd(),
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
return _ok(
|
||||
rid,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue