diff --git a/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py b/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py index 90cd51aa..e2422bf1 100755 --- a/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py +++ b/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py @@ -66,19 +66,43 @@ def _positive_int(value: str | None) -> int | None: return parsed if parsed > 0 else None -def _hidden_subprocess_kwargs() -> dict[str, int]: +def _hidden_subprocess_kwargs() -> dict[str, Any]: if os.name != "nt": return {} - return {"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0)} + if os.environ.get("HERMES_DESKTOP", "").strip().lower() != "true": + return {} + create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) or 0x08000000 + kwargs: dict[str, Any] = {"creationflags": create_no_window} + try: + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= getattr(subprocess, "STARTF_USESHOWWINDOW", 1) + startupinfo.wShowWindow = getattr(subprocess, "SW_HIDE", 0) + kwargs["startupinfo"] = startupinfo + except Exception: + pass + return kwargs -def _add_hidden_creationflags(kwargs: dict[str, Any], create_no_window: int) -> None: +def _add_hidden_process_options(kwargs: dict[str, Any], create_no_window: int) -> None: flags = kwargs.get("creationflags", 0) or 0 try: kwargs["creationflags"] = int(flags) | create_no_window except Exception: kwargs["creationflags"] = create_no_window + startupinfo = kwargs.get("startupinfo") + if startupinfo is None: + try: + startupinfo = subprocess.STARTUPINFO() + except Exception: + return + kwargs["startupinfo"] = startupinfo + try: + startupinfo.dwFlags |= getattr(subprocess, "STARTF_USESHOWWINDOW", 1) + startupinfo.wShowWindow = getattr(subprocess, "SW_HIDE", 0) + except Exception: + pass + def _install_windows_hidden_subprocess_defaults() -> None: """Hide console windows for subprocesses launched inside desktop bridge runs. @@ -102,15 +126,15 @@ def _install_windows_hidden_subprocess_defaults() -> None: class HiddenPopen(original_popen): # type: ignore[misc, valid-type] def __init__(self, *args: Any, **kwargs: Any) -> None: - _add_hidden_creationflags(kwargs, create_no_window) + _add_hidden_process_options(kwargs, create_no_window) super().__init__(*args, **kwargs) async def hidden_create_subprocess_exec(*args: Any, **kwargs: Any) -> Any: - _add_hidden_creationflags(kwargs, create_no_window) + _add_hidden_process_options(kwargs, create_no_window) return await original_create_subprocess_exec(*args, **kwargs) async def hidden_create_subprocess_shell(*args: Any, **kwargs: Any) -> Any: - _add_hidden_creationflags(kwargs, create_no_window) + _add_hidden_process_options(kwargs, create_no_window) return await original_create_subprocess_shell(*args, **kwargs) subprocess.Popen = HiddenPopen # type: ignore[assignment] diff --git a/tests/server/agent-bridge-profile-env.test.ts b/tests/server/agent-bridge-profile-env.test.ts index 44323853..e3fe4641 100644 --- a/tests/server/agent-bridge-profile-env.test.ts +++ b/tests/server/agent-bridge-profile-env.test.ts @@ -124,6 +124,9 @@ original_popen = bridge.subprocess.Popen original_async_exec = bridge.asyncio.create_subprocess_exec original_async_shell = bridge.asyncio.create_subprocess_shell original_create_no_window = getattr(bridge.subprocess, "CREATE_NO_WINDOW", None) +original_startupinfo = getattr(bridge.subprocess, "STARTUPINFO", None) +original_startf = getattr(bridge.subprocess, "STARTF_USESHOWWINDOW", None) +original_sw_hide = getattr(bridge.subprocess, "SW_HIDE", None) original_installed = getattr(bridge.subprocess, "_hermes_hidden_defaults_installed", None) class FakePopen: @@ -142,6 +145,11 @@ async def fake_create_subprocess_shell(*args, **kwargs): async_calls.append({"kind": "shell", "args": args, "kwargs": kwargs}) return {"kind": "shell"} +class FakeStartupInfo: + def __init__(self): + self.dwFlags = 0 + self.wShowWindow = None + try: bridge.os.name = "nt" bridge.os.environ["HERMES_DESKTOP"] = "true" @@ -149,16 +157,22 @@ try: bridge.asyncio.create_subprocess_exec = fake_create_subprocess_exec bridge.asyncio.create_subprocess_shell = fake_create_subprocess_shell bridge.subprocess.CREATE_NO_WINDOW = 0x08000000 + bridge.subprocess.STARTUPINFO = FakeStartupInfo + bridge.subprocess.STARTF_USESHOWWINDOW = 0x00000001 + bridge.subprocess.SW_HIDE = 0 if hasattr(bridge.subprocess, "_hermes_hidden_defaults_installed"): delattr(bridge.subprocess, "_hermes_hidden_defaults_installed") bridge._install_windows_hidden_subprocess_defaults() bridge.subprocess.Popen(["git", "status"], creationflags=0x00000200) flags = FakePopen.calls[0]["kwargs"]["creationflags"] + startupinfo = FakePopen.calls[0]["kwargs"]["startupinfo"] bridge.asyncio.run(bridge.asyncio.create_subprocess_exec("git", "status", creationflags=0x00000400)) bridge.asyncio.run(bridge.asyncio.create_subprocess_shell("git status")) async_exec_flags = async_calls[0]["kwargs"]["creationflags"] + async_exec_startupinfo = async_calls[0]["kwargs"]["startupinfo"] async_shell_flags = async_calls[1]["kwargs"]["creationflags"] + async_shell_startupinfo = async_calls[1]["kwargs"]["startupinfo"] finally: bridge.os.name = original_os_name bridge.subprocess.Popen = original_popen @@ -171,6 +185,18 @@ finally: pass else: bridge.subprocess.CREATE_NO_WINDOW = original_create_no_window + for name, original in [ + ("STARTUPINFO", original_startupinfo), + ("STARTF_USESHOWWINDOW", original_startf), + ("SW_HIDE", original_sw_hide), + ]: + if original is None: + try: + delattr(bridge.subprocess, name) + except AttributeError: + pass + else: + setattr(bridge.subprocess, name, original) if original_installed is None: try: delattr(bridge.subprocess, "_hermes_hidden_defaults_installed") @@ -183,11 +209,14 @@ print(json.dumps({ "flags": flags, "has_create_no_window": bool(flags & 0x08000000), "kept_existing_flag": bool(flags & 0x00000200), + "startupinfo_hidden": bool(startupinfo.dwFlags & 0x00000001) and startupinfo.wShowWindow == 0, "async_exec_flags": async_exec_flags, "async_exec_has_create_no_window": bool(async_exec_flags & 0x08000000), "async_exec_kept_existing_flag": bool(async_exec_flags & 0x00000400), + "async_exec_startupinfo_hidden": bool(async_exec_startupinfo.dwFlags & 0x00000001) and async_exec_startupinfo.wShowWindow == 0, "async_shell_flags": async_shell_flags, "async_shell_has_create_no_window": bool(async_shell_flags & 0x08000000), + "async_shell_startupinfo_hidden": bool(async_shell_startupinfo.dwFlags & 0x00000001) and async_shell_startupinfo.wShowWindow == 0, })) `) @@ -195,11 +224,14 @@ print(json.dumps({ flags: 0x08000200, has_create_no_window: true, kept_existing_flag: true, + startupinfo_hidden: true, async_exec_flags: 0x08000400, async_exec_has_create_no_window: true, async_exec_kept_existing_flag: true, + async_exec_startupinfo_hidden: true, async_shell_flags: 0x08000000, async_shell_has_create_no_window: true, + async_shell_startupinfo_hidden: true, }) }) })