mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-30 11:52:04 +00:00
fix(windows): hide remaining backend console-flash legs missed on main
main (cb982ad99) wired windows_hide_flags() into the auxiliary git/gh/wmic/
bash/powershell/taskkill legs but left two it didn't reach, plus the Electron
backend-launch leg it explicitly deferred. Cover them the same way:
- apps/desktop/electron/main.cjs: getNoConsoleVenvPython resolves the BASE
pythonw.exe instead of the venv Scripts\pythonw.exe shim, which re-execs a
console python.exe and flashes a conhost the desktop backend can't suppress.
Both backend creators put the venv site-packages on PYTHONPATH so imports
still resolve under the base interpreter. (main's commit said this Electron
leg "needs a Windows-tested change of its own".)
- tools/tts_tool.py, tools/transcription_tools.py, plugins/platforms/discord:
ffmpeg conversions (voice notes / TTS / STT) via windows_hide_flags().
- plugins/platforms/whatsapp: netstat + taskkill bridge-port cleanup via
windows_hide_flags().
All no-ops on POSIX. Tests assert the base-pythonw preference and the ffmpeg
legs pass CREATE_NO_WINDOW.
This commit is contained in:
parent
0c2e6c0049
commit
eeca59f489
7 changed files with 78 additions and 16 deletions
|
|
@ -1572,19 +1572,17 @@ function readVenvHome(venvRoot) {
|
|||
function getNoConsoleVenvPython(venvRoot) {
|
||||
if (!IS_WINDOWS) return getVenvPython(venvRoot)
|
||||
|
||||
// Prefer the venv's own pythonw shim — it carries pyvenv.cfg / site-packages
|
||||
// wiring. Falling back to the base uv/python.org pythonw.exe skips the venv
|
||||
// and breaks imports (yaml, hermes_cli, …) even when PYTHONPATH is patched.
|
||||
const venvPythonw = path.join(venvRoot, 'Scripts', 'pythonw.exe')
|
||||
if (fileExists(venvPythonw)) return venvPythonw
|
||||
|
||||
// The venv's ``Scripts\pythonw.exe`` is a uv launcher shim that re-execs the
|
||||
// base console ``python.exe``, allocating a conhost/Windows Terminal window
|
||||
// that CREATE_NO_WINDOW can't suppress. Use the base ``pythonw.exe`` directly;
|
||||
// callers put the venv site-packages on PYTHONPATH so imports still resolve.
|
||||
const baseHome = readVenvHome(venvRoot)
|
||||
if (baseHome) {
|
||||
const basePythonw = path.join(baseHome, 'pythonw.exe')
|
||||
if (fileExists(basePythonw)) return basePythonw
|
||||
}
|
||||
|
||||
return venvPythonw
|
||||
return path.join(venvRoot, 'Scripts', 'pythonw.exe')
|
||||
}
|
||||
|
||||
function toNoConsolePython(pythonPath) {
|
||||
|
|
@ -2847,7 +2845,7 @@ function createPythonBackend(root, label, dashboardArgs, options = {}) {
|
|||
args: ['-m', 'hermes_cli.main', ...dashboardArgs],
|
||||
env: buildDesktopBackendEnv({
|
||||
hermesHome: HERMES_HOME,
|
||||
pythonPathEntries: [root],
|
||||
pythonPathEntries: [root, ...getVenvSitePackagesEntries(venvRoot)],
|
||||
venvRoot
|
||||
}),
|
||||
root,
|
||||
|
|
@ -2871,7 +2869,7 @@ function createActiveBackend(dashboardArgs) {
|
|||
args: ['-m', 'hermes_cli.main', ...dashboardArgs],
|
||||
env: buildDesktopBackendEnv({
|
||||
hermesHome: HERMES_HOME,
|
||||
pythonPathEntries: [ACTIVE_HERMES_ROOT],
|
||||
pythonPathEntries: [ACTIVE_HERMES_ROOT, ...getVenvSitePackagesEntries(VENV_ROOT)],
|
||||
venvRoot: VENV_ROOT
|
||||
}),
|
||||
root: ACTIVE_HERMES_ROOT,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,22 @@ test('desktop background child processes opt into hidden Windows consoles', () =
|
|||
assert.match(source, /args: \['-m', 'hermes_cli\.main', \.\.\.dashboardArgs\]/)
|
||||
})
|
||||
|
||||
test('getNoConsoleVenvPython prefers base pythonw over the uv re-exec shim', () => {
|
||||
const source = readElectronFile('main.cjs')
|
||||
const body = source.slice(
|
||||
source.indexOf('function getNoConsoleVenvPython(venvRoot)'),
|
||||
source.indexOf('function getVenvSitePackagesEntries(venvRoot)')
|
||||
)
|
||||
|
||||
// The venv Scripts\pythonw.exe re-execs a console python.exe (flashes a
|
||||
// conhost); the base pythonw must be resolved first so it never runs.
|
||||
const baseIdx = body.indexOf('basePythonw')
|
||||
const shimIdx = body.indexOf("'Scripts', 'pythonw.exe'")
|
||||
assert.notEqual(baseIdx, -1, 'base pythonw resolution missing')
|
||||
assert.notEqual(shimIdx, -1, 'venv shim fallback missing')
|
||||
assert.ok(baseIdx < shimIdx, 'base pythonw must be preferred before the venv Scripts shim')
|
||||
})
|
||||
|
||||
test('intentional or interactive desktop child processes stay documented', () => {
|
||||
const source = readElectronFile('main.cjs')
|
||||
|
||||
|
|
|
|||
|
|
@ -665,6 +665,8 @@ class VoiceReceiver:
|
|||
f.write(pcm_data)
|
||||
pcm_path = f.name
|
||||
try:
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg", "-y", "-loglevel", "error",
|
||||
|
|
@ -679,6 +681,7 @@ class VoiceReceiver:
|
|||
check=True,
|
||||
timeout=10,
|
||||
stdin=subprocess.DEVNULL,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -78,10 +78,13 @@ def _kill_port_process(port: int) -> None:
|
|||
"""Kill any process *listening* on the given TCP port (a stale bridge)."""
|
||||
try:
|
||||
if _IS_WINDOWS:
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
|
||||
# Use netstat to find the PID bound to this port, then taskkill
|
||||
result = subprocess.run(
|
||||
["netstat", "-ano", "-p", "TCP"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split()
|
||||
|
|
@ -92,6 +95,7 @@ def _kill_port_process(port: int) -> None:
|
|||
subprocess.run(
|
||||
["taskkill", "/PID", parts[4], "/F"],
|
||||
capture_output=True, timeout=5,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
except subprocess.SubprocessError:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -261,3 +261,41 @@ def test_inline_skill_shell_hides_bash_window(monkeypatch):
|
|||
assert skill_preprocessing.run_inline_shell("echo ok", cwd=None, timeout=5) == "ok"
|
||||
assert captured[0][0] == ["bash", "-c", "echo ok"]
|
||||
assert captured[0][1]["creationflags"] == _CREATE_NO_WINDOW
|
||||
|
||||
|
||||
def test_tts_opus_conversion_hides_ffmpeg_window(monkeypatch, tmp_path):
|
||||
from tools import tts_tool
|
||||
|
||||
captured = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured.append((cmd, kwargs))
|
||||
return _Completed(returncode=0)
|
||||
|
||||
monkeypatch.setattr(tts_tool, "_has_ffmpeg", lambda: True)
|
||||
monkeypatch.setattr(tts_tool, "windows_hide_flags", lambda: _CREATE_NO_WINDOW)
|
||||
monkeypatch.setattr(tts_tool.subprocess, "run", fake_run)
|
||||
|
||||
tts_tool._convert_to_opus(str(tmp_path / "v.mp3"))
|
||||
|
||||
assert captured[0][0][0] == "ffmpeg"
|
||||
assert captured[0][1]["creationflags"] == _CREATE_NO_WINDOW
|
||||
|
||||
|
||||
def test_local_stt_audio_prep_hides_ffmpeg_window(monkeypatch, tmp_path):
|
||||
from tools import transcription_tools
|
||||
|
||||
captured = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured.append((cmd, kwargs))
|
||||
return _Completed(returncode=0)
|
||||
|
||||
monkeypatch.setattr(transcription_tools, "_find_ffmpeg_binary", lambda: "ffmpeg")
|
||||
monkeypatch.setattr(transcription_tools, "windows_hide_flags", lambda: _CREATE_NO_WINDOW)
|
||||
monkeypatch.setattr(transcription_tools.subprocess, "run", fake_run)
|
||||
|
||||
transcription_tools._prepare_local_audio(str(tmp_path / "in.m4a"), str(tmp_path))
|
||||
|
||||
assert captured[0][0][0] == "ffmpeg"
|
||||
assert captured[0][1]["creationflags"] == _CREATE_NO_WINDOW
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ from pathlib import Path
|
|||
from typing import Optional, Dict, Any
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
from utils import is_truthy_value
|
||||
from tools.managed_tool_gateway import resolve_managed_tool_gateway
|
||||
from tools.tool_backend_helpers import (
|
||||
|
|
@ -1187,7 +1188,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, stdin=subprocess.DEVNULL)
|
||||
subprocess.run(command, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())
|
||||
return converted_path, None
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("ffmpeg conversion timed out for %s", file_path)
|
||||
|
|
@ -1233,9 +1234,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, stdin=subprocess.DEVNULL)
|
||||
subprocess.run(command, shell=True, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())
|
||||
else:
|
||||
subprocess.run(shlex.split(command), check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)
|
||||
subprocess.run(shlex.split(command), check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())
|
||||
|
||||
|
||||
txt_files = sorted(Path(output_dir).glob("*.txt"))
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ from pathlib import Path
|
|||
from typing import Callable, Dict, Any, Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
from hermes_constants import display_hermes_home
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -910,6 +911,7 @@ def _convert_to_opus(mp3_path: str) -> Optional[str]:
|
|||
"-ac", "1", "-b:a", "64k", "-vbr", "off", ogg_path, "-y"],
|
||||
capture_output=True, timeout=30,
|
||||
stdin=subprocess.DEVNULL,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning("ffmpeg conversion failed with return code %d: %s",
|
||||
|
|
@ -1776,7 +1778,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, stdin=subprocess.DEVNULL)
|
||||
result = subprocess.run(cmd, capture_output=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())
|
||||
if result.returncode != 0:
|
||||
stderr = result.stderr.decode("utf-8", errors="ignore")[:300]
|
||||
raise RuntimeError(f"ffmpeg conversion failed: {stderr}")
|
||||
|
|
@ -1871,7 +1873,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, stdin=subprocess.DEVNULL)
|
||||
subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())
|
||||
os.remove(wav_path)
|
||||
else:
|
||||
# No ffmpeg — just rename the WAV to the expected path
|
||||
|
|
@ -2050,7 +2052,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, stdin=subprocess.DEVNULL)
|
||||
subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())
|
||||
try:
|
||||
os.remove(wav_path)
|
||||
except OSError:
|
||||
|
|
@ -2116,7 +2118,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, stdin=subprocess.DEVNULL)
|
||||
subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())
|
||||
os.remove(wav_path)
|
||||
else:
|
||||
# No ffmpeg — rename the WAV to the expected path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue