fix(gateway/whatsapp): hide node.exe console on Windows via CREATE_NO_WINDOW (#29715) - #29807
Closed
xxxigm wants to merge 2 commits into
Closed
fix(gateway/whatsapp): hide node.exe console on Windows via CREATE_NO_WINDOW (#29715)#29807xxxigm wants to merge 2 commits into
xxxigm wants to merge 2 commits into
Conversation
…_WINDOW (NousResearch#29715) On Windows, ``WhatsAppAdapter._connect`` launched the Node bridge with: subprocess.Popen([...node, bridge.js, ...], stdout=bridge_log_fh, stderr=bridge_log_fh, preexec_fn=None if _IS_WINDOWS else os.setsid, env=bridge_env) — no ``creationflags``. ``preexec_fn`` is a no-op on Windows, so launching the console-subsystem ``node.exe`` from a parent that has no console (e.g. the gateway under ``pythonw.exe -m hermes_cli.main gateway run --replace``) caused Windows to allocate a fresh, visible console window just for the bridge. Closing that empty window killed the bridge with ``0xC000013A`` (``CTRL_LOGOFF_EVENT``-shaped exit), and the gateway reconnection watcher relaunched it, making the blank window pop back. Add a tiny ``_bridge_popen_extra_kwargs`` helper that returns ``{"creationflags": windows_hide_flags()}`` on Windows (i.e. ``CREATE_NO_WINDOW``) and ``{"preexec_fn": os.setsid}`` on POSIX, then plumb it into the existing ``subprocess.Popen`` call. The helper is exported as a stable seam so tests on a non-Windows host can simulate the Windows path via ``monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True)``. Deliberately uses ``windows_hide_flags()`` (``CREATE_NO_WINDOW`` only) rather than ``windows_detach_flags()`` (which also sets ``DETACHED_PROCESS``) — ``DETACHED_PROCESS`` severs stdio handles, which would break the ``stdout=bridge_log_fh`` redirect the adapter relies on for QR-code and connection diagnostics. POSIX behaviour is unchanged.
…ousResearch#29715) Adds tests/gateway/test_whatsapp_bridge_no_console_window.py — 13 cases across three classes: - TestBridgePopenExtraKwargsHelper (5): pure-helper contract of the new ``_bridge_popen_extra_kwargs`` — POSIX gets ``preexec_fn= os.setsid`` with no ``creationflags``; Windows (simulated via ``monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True)``) gets ``creationflags`` with ``CREATE_NO_WINDOW`` (0x08000000) set and ``DETACHED_PROCESS`` (0x00000008) explicitly NOT set; default no-arg call follows the module-level ``_IS_WINDOWS`` constant; the returned dict only contains ``creationflags``/``preexec_fn`` keys and never both at once. - TestConnectPlumbsExtraKwargsToPopen (3): end-to-end — drive ``WhatsAppAdapter.connect()`` through every preflight (Node requirements, ``creds.json``, npm install short-circuit, health probe miss, stale-pidfile / port-killer mocks) with ``subprocess.Popen`` patched to record kwargs, then assert the recorded call shape. Includes a third case verifying that the Windows branch still routes stdout/stderr to the same ``bridge.log`` handle (the whole reason we avoid ``DETACHED_PROCESS``). - TestWhatsAppSourceGuardrail (5): static asserts on ``gateway/platforms/whatsapp.py`` so a future refactor can't quietly drop the fix — helper is defined, ``Popen`` call site unpacks it via ``**_bridge_popen_extra_kwargs()``, the pre-fix ``preexec_fn=None if _IS_WINDOWS else os.setsid`` inline pattern isn't reintroduced, ``windows_hide_flags`` is imported from the shared compat module, and ``windows_detach_flags(`` is never called (it would re-introduce stdio severing).
Collaborator
This was referenced Jul 6, 2026
Contributor
|
Thanks for this PR — the underlying bug (WhatsApp bridge spawning Node with a visible console window on Windows) is now fixed on main via #60924, a salvage of #60647 which swaps the bare |
This was referenced Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes #29715 — on Windows, every time the gateway started the WhatsApp adapter under
pythonw.exe, Windows allocated a fresh visiblenode.execonsole window for the Node bridge. Closing the empty window killed the bridge with0xC000013A(CTRL_LOGOFF_EVENT-shaped exit); the gateway reconnection watcher then relaunched it, making the popup come back.Root cause.
gateway/platforms/whatsapp.py::WhatsAppAdapter.connect()launched the Node bridge with:No
creationflags.preexec_fnis a no-op on Windows, so when a console-subsystem child (node.exe) is launched from a parent that has no console (pythonw.exe -m hermes_cli.main gateway run --replace), Windows allocates a brand-new console window for the child.Fix.
Introduce a tiny
_bridge_popen_extra_kwargs()helper that returns:{"creationflags": windows_hide_flags()}on Windows — i.e.CREATE_NO_WINDOW(0x08000000) via the project's blessed cross-platform helper fromhermes_cli/_subprocess_compat.py.{"preexec_fn": os.setsid}on POSIX — unchanged behaviour, bridge gets its own session so Ctrl+C in the gateway terminal doesn't propagate.The
Popencall site unpacks**_bridge_popen_extra_kwargs()so the kwargs land cleanly without overriding stdout/stderr/env.Deliberately uses
windows_hide_flags()(CREATE_NO_WINDOWonly) rather than the siblingwindows_detach_flags()(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW) becauseDETACHED_PROCESSsevers stdio handles, which would break thestdout=bridge_log_fhredirect the adapter relies on for QR-code and connection diagnostics.The helper resolves
_IS_WINDOWSlazily inside the function body (the public arg acceptsNoneand re-reads the module-level constant at call time) so tests can simulate the Windows branch on a non-Windows host viamonkeypatch.setattr.Related Issue
Closes #29715 — WhatsApp bridge opens blank node.exe console window on Windows.
Type of Change
Changes Made
gateway/platforms/whatsapp.pywindows_hide_flagsfromhermes_cli._subprocess_compat(the project's blessed Windows-creationflags helper, already used incron/scheduler.py,tools/process_registry.py, etc.)._bridge_popen_extra_kwargs(is_windows=None)helper that returns the platform-correctPopenkwargs. Lazy_IS_WINDOWSresolution so the helper picks upmonkeypatch.setattr(_IS_WINDOWS, …)in tests.preexec_fn=None if _IS_WINDOWS else os.setsidargument at the bridgePopensite with**_bridge_popen_extra_kwargs().tests/gateway/test_whatsapp_bridge_no_console_window.py(new, +405 lines, 13 cases across three classes):TestBridgePopenExtraKwargsHelper(5) — pure-helper contract: POSIX →preexec_fn=os.setsidonly; simulated Windows (viamonkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True)) →creationflagswithCREATE_NO_WINDOWset andDETACHED_PROCESSexplicitly NOT set; default no-arg call follows module-level_IS_WINDOWS; returned dict only contains expected keys, never both at once.TestConnectPlumbsExtraKwargsToPopen(3) — end-to-end: driveWhatsAppAdapter.connect()through every preflight withsubprocess.Popenpatched to record kwargs, then assert the recorded call shape. Third case verifies the Windows branch still routes stdout/stderr to the samebridge.loghandle (the whole reason we avoidDETACHED_PROCESS).TestWhatsAppSourceGuardrail(5) — static asserts ongateway/platforms/whatsapp.pyso a future refactor can't quietly drop the fix: helper defined,Popencall site unpacks**_bridge_popen_extra_kwargs(), pre-fixpreexec_fn=None if _IS_WINDOWS else os.setsidinline pattern is not reintroduced,windows_hide_flagsis imported,windows_detach_flags(is never called.How to Test
pythonw.exe. Confirm:node.execonsole window appears.bridge.logstill receives QR codes /whatsapp-web.jsstartup logs.Checklist
Code
fix(gateway/whatsapp): …andtest(gateway/whatsapp): …)scripts/run_tests.sh tests/gateway/test_whatsapp_bridge_no_console_window.py tests/gateway/test_whatsapp_connect.py tests/gateway/test_whatsapp_group_gating.py tests/gateway/test_whatsapp_formatting.py tests/gateway/test_whatsapp_reply_prefix.py tests/hermes_cli/test_whatsapp_setup_ordering.pyand all 102 tests passconnect()wire-level, 5 source guardrail)monkeypatch-simulated tests on the helper and end-to-end pathsDocumentation & Housekeeping
_bridge_popen_extra_kwargs's docstring; no user-facing docs reference the bridge launch shapecli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A (the fix follows the existing Windows-creationflags pattern already used incron/scheduler.py,tools/code_execution_tool.py,tools/browser_tool.py, etc.)CREATE_NO_WINDOWScreenshots / Logs