From 5098e6cb54173b27673f5761ce187d799ba36b30 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 4 Sep 2026 03:23:10 +0200 Subject: fix(node): run on the default Windows event loop (Proactor) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified end to end: a live browser peer on Windows connecting to a Windows node — handshake, index sync, file download and an ffmpeg-transcoded video stream all work on the ProactorEventLoop. aiortc only hangs on it in the same-process loopback the tests use, which the repo-root conftest already handles for the suite. So the daemon no longer forces the SelectorEventLoop: that fixed aiortc-in-one-process but broke ffmpeg (no subprocess support on a Windows SelectorEventLoop). `use_compatible_event_loop()` becomes `configure_event_loop()` — a no-op unless MESHBAY_NODE_EVENT_LOOP=selector is set explicitly, as an escape hatch that probably never needs pulling. This drops the planned "move ffmpeg off the asyncio loop" work. Co-Authored-By: Claude Sonnet 5 --- packages/meshbay-node/src/meshbay_node/daemon.py | 4 ++-- packages/meshbay-node/src/meshbay_node/platform.py | 28 ++++++++++------------ packages/meshbay-node/tests/test_platform.py | 18 ++++++++++---- 3 files changed, 29 insertions(+), 21 deletions(-) (limited to 'packages') diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index 6bc3bef..5aa6654 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -1634,9 +1634,9 @@ def _systemctl_user(verb: str, unit: str, *, not_running_hint: str, def main() -> None: import argparse - from meshbay_node.platform import force_utf8_stdio, use_compatible_event_loop + from meshbay_node.platform import configure_event_loop, force_utf8_stdio force_utf8_stdio() - use_compatible_event_loop() + configure_event_loop() parser = argparse.ArgumentParser(description="MeshBay Node daemon") parser.add_argument("command", nargs="?", diff --git a/packages/meshbay-node/src/meshbay_node/platform.py b/packages/meshbay-node/src/meshbay_node/platform.py index 2eab9e1..351289f 100644 --- a/packages/meshbay-node/src/meshbay_node/platform.py +++ b/packages/meshbay-node/src/meshbay_node/platform.py @@ -27,25 +27,23 @@ def force_utf8_stdio() -> None: # ── Event loop ─────────────────────────────────────────────────────────────── -def use_compatible_event_loop() -> None: +def configure_event_loop() -> None: """ - On Windows, select the SelectorEventLoop: aiortc's ICE stack does not - complete a loopback DataChannel handshake on the default ProactorEventLoop. - - Cost: SelectorEventLoop cannot spawn subprocesses on Windows, so ffmpeg - streaming (asyncio.create_subprocess_exec in webrtc_server.py) does not work - under it. The video path needs a thread-based runner on Windows — tracked - for the port. No effect off Windows. - - Whether the hang is real for a live peer (vs. the same-process loopback in - the tests) is still being established: set MESHBAY_NODE_EVENT_LOOP=proactor - to keep the default loop and check. + The daemon runs on Windows' default ProactorEventLoop: verified end to end + (a live browser peer connecting, an index sync, a file download and an + ffmpeg-transcoded video stream). aiortc only ever hangs on it in the + *same-process loopback* the tests use, which the test suite handles on its + own (repo-root conftest). + + Escape hatch, opt-in only: MESHBAY_NODE_EVENT_LOOP=selector switches to the + SelectorEventLoop. That fixes aiortc-in-one-process but breaks ffmpeg + (SelectorEventLoop cannot spawn subprocesses on Windows), so it is not the + default and probably never should be. """ if sys.platform != "win32": return - if os.environ.get("MESHBAY_NODE_EVENT_LOOP", "").lower() == "proactor": - return - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + if os.environ.get("MESHBAY_NODE_EVENT_LOOP", "").lower() == "selector": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # ── Directories ────────────────────────────────────────────────────────────── diff --git a/packages/meshbay-node/tests/test_platform.py b/packages/meshbay-node/tests/test_platform.py index 2725df6..9358894 100644 --- a/packages/meshbay-node/tests/test_platform.py +++ b/packages/meshbay-node/tests/test_platform.py @@ -79,16 +79,26 @@ def test_check_media_tools_stores_the_resolved_paths(monkeypatch): # ── Event loop ─────────────────────────────────────────────────────────────── -def test_use_compatible_event_loop_is_a_noop_off_windows(monkeypatch): +def test_configure_event_loop_is_a_noop_off_windows(monkeypatch): monkeypatch.setattr(sys, "platform", "linux") + monkeypatch.setenv("MESHBAY_NODE_EVENT_LOOP", "selector") before = asyncio.get_event_loop_policy() - plat.use_compatible_event_loop() + plat.configure_event_loop() + assert asyncio.get_event_loop_policy() is before + + +def test_configure_event_loop_leaves_the_default_loop_alone_without_the_opt_in(monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + monkeypatch.delenv("MESHBAY_NODE_EVENT_LOOP", raising=False) + before = asyncio.get_event_loop_policy() + plat.configure_event_loop() assert asyncio.get_event_loop_policy() is before @pytest.mark.skipif(sys.platform != "win32", reason="WindowsSelectorEventLoopPolicy exists only on win32") -def test_use_compatible_event_loop_selects_the_selector_loop_on_windows(): - plat.use_compatible_event_loop() +def test_configure_event_loop_selector_opt_in(monkeypatch): + monkeypatch.setenv("MESHBAY_NODE_EVENT_LOOP", "selector") + plat.configure_event_loop() assert isinstance(asyncio.get_event_loop_policy(), asyncio.WindowsSelectorEventLoopPolicy) -- cgit v1.2.3