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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user