aboutsummaryrefslogtreecommitdiffstats
path: root/packaging
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-13 00:30:39 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-13 00:30:39 +0200
commitefbcff05721e71f1fdfcc30b7ee1b44ca1670762 (patch)
tree23018295b653510900ac24b71a780d74e32a01a2 /packaging
parente9a93c21ff28daa6f4017849b42fe073a5ee2005 (diff)
downloadmeshbay-efbcff05721e71f1fdfcc30b7ee1b44ca1670762.tar.gz
test(node): run the connection processes on Windows, source and frozen
Connection processes and the larger UDP receive buffer were written and measured on Linux; nothing had run on Windows, and `meshbay-node.exe webrtc-worker` had never run from a frozen build on any system. - test_connection_processes.py failed on Windows (8 tests): the selector loop the suite uses there cannot start a process. A test marked `daemon_loop` now runs on the daemon's own loop, chosen through pytest-asyncio's loop factories. - The build starts both frozen executables as connection processes and carries a connection through each (smoke-connection-process.py). The daemon's own smoke test never signs in to a hub, which is when the processes start. - The warning for a receive buffer below what was asked named net.core.rmem_max on every system; it names it on Linux only. Windows grants the 4 MB. Measured on an installed build in service mode, docs/WINDOWS-PORT.md §5.12: two processes at startup and on a live change, a browser's handshake, chat, a 200 MB download over 8 connections across both processes, an upload, a video stream, and stop, restart and start leaving no process behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packaging')
-rw-r--r--packaging/win/README.md2
-rw-r--r--packaging/win/build-node-runtime.ps116
-rw-r--r--packaging/win/smoke-connection-process.py86
3 files changed, 100 insertions, 4 deletions
diff --git a/packaging/win/README.md b/packaging/win/README.md
index eda728e..4ce1459 100644
--- a/packaging/win/README.md
+++ b/packaging/win/README.md
@@ -151,7 +151,7 @@ That runs [`build-win.ps1`](build-win.ps1):
| 2 | `npm ci` + download Electron's Chromium |
| 3 | bump Electron to the latest release (Chromium CVE policy; `-NoElectronBump` to skip) |
| 4 | `npm run sync-ui` — copy the interface from `meshbay-hub/.../static` |
-| 5 | [`build-node-runtime.ps1`](build-node-runtime.ps1) — PyInstaller freeze → `packages/meshbay-client/node-runtime/`, then [`smoke-node-runtime.ps1`](smoke-node-runtime.ps1) starts the frozen daemon in a throwaway profile and checks it answers with the right version and writes its log |
+| 5 | [`build-node-runtime.ps1`](build-node-runtime.ps1) — PyInstaller freeze → `packages/meshbay-client/node-runtime/`, then [`smoke-node-runtime.ps1`](smoke-node-runtime.ps1) starts the frozen daemon in a throwaway profile and checks it answers with the right version and writes its log, and [`smoke-connection-process.py`](smoke-connection-process.py) starts both executables as connection processes (`connection_processes` above 1) and carries a connection through each |
| 6 | `electron-builder --win nsis` → `packages/meshbay-client/dist/MeshBay-Setup-<version>.exe` |
### Video (ffmpeg)
diff --git a/packaging/win/build-node-runtime.ps1 b/packaging/win/build-node-runtime.ps1
index aa5e759..c6a8af1 100644
--- a/packaging/win/build-node-runtime.ps1
+++ b/packaging/win/build-node-runtime.ps1
@@ -156,9 +156,6 @@ Step "default.env copied from QE\"
# --- 6. publish ----------------------------------------------------
Move-Item $frozen $OutDir
Remove-Item -Recurse -Force $pyiWork, $pyiDist -ErrorAction SilentlyContinue
-if ($createdVenv -and -not $KeepBuildVenv) {
- Remove-Item -Recurse -Force $BuildVenv -ErrorAction SilentlyContinue
-}
# --- 7. smoke test -----------------------------------------------
# Capture, do NOT pipe to Select-Object -First: that stops the native process
@@ -192,6 +189,19 @@ $expect = [regex]::Match((Get-Content -LiteralPath $initPy -Raw), '__version__\s
if (-not $expect) { throw "cannot read __version__ from $initPy" }
& (Join-Path $WinDir "smoke-node-runtime.ps1") -Exe $exe -ExpectVersion $expect
+# That daemon never signs in to a hub, so it never starts a connection process
+# (connection_processes above 1): a frozen node starts those as copies of
+# itself, a path no source checkout takes. Both executables, since the Store
+# package's node is the windowless one. Run by the build interpreter, which
+# has the node's modules for the other end -- hence the venv removed after.
+Step "smoke test: the frozen node as a connection process"
+& $Python (Join-Path $WinDir "smoke-connection-process.py") $exe (Join-Path $OutDir "meshbay-nodew.exe")
+if ($LASTEXITCODE -ne 0) { throw "a frozen connection process did not carry a connection" }
+
+if ($createdVenv -and -not $KeepBuildVenv) {
+ Remove-Item -Recurse -Force $BuildVenv -ErrorAction SilentlyContinue
+}
+
$mb = (Get-ChildItem $OutDir -Recurse | Measure-Object Length -Sum).Sum / 1MB
Write-Host ""
Write-Host ("OK node-runtime ready at {0} ({1:N0} MB)" -f $OutDir, $mb) -ForegroundColor Green
diff --git a/packaging/win/smoke-connection-process.py b/packaging/win/smoke-connection-process.py
new file mode 100644
index 0000000..a4a61b1
--- /dev/null
+++ b/packaging/win/smoke-connection-process.py
@@ -0,0 +1,86 @@
+"""Start a frozen meshbay-node as a connection process and carry bytes through it.
+
+With `connection_processes` above 1 the node starts copies of itself as
+`meshbay-node.exe webrtc-worker` (transport/webrtc/workers.py). The daemon's own
+smoke test never gets there: the processes start once the node has signed in to
+a hub, and that test has none. This is the node's side of it, unchanged, with a
+frozen executable as the process and an aiortc peer here as the browser.
+
+Run by build-node-runtime.ps1 with the build venv's interpreter, which has the
+node's own modules. It takes any meshbay-node.exe, an installed one included
+(`resources/node-runtime` under the application's directory).
+"""
+
+import asyncio
+import sys
+import time
+
+from aiortc import RTCPeerConnection, RTCSessionDescription
+from meshbay_node.transport.webrtc import workers
+
+CHUNK = bytes(48_000)
+CHUNKS = 200
+
+
+async def until(test, what: str, seconds: float = 30.0) -> None:
+ deadline = time.monotonic() + seconds
+ while not test():
+ if time.monotonic() > deadline:
+ raise SystemExit(f"FAILED {what}: not within {seconds:.0f}s")
+ await asyncio.sleep(0.02)
+
+
+async def carry(exe: str) -> str:
+ workers._argv = lambda: [exe, "webrtc-worker"]
+ pool = workers.WorkerPool(2)
+ browser = RTCPeerConnection()
+ try:
+ await pool.ensure()
+ if pool.processes() != 2:
+ raise SystemExit(f"FAILED {pool.processes()} of 2 connection processes started")
+
+ channel = browser.createDataChannel("mnp")
+ got: list[bytes] = []
+ channel.on("message", got.append)
+ await browser.setLocalDescription(await browser.createOffer())
+ peer = await pool.open(browser.localDescription.sdp)
+ ours: list = []
+ peer.on("datachannel", ours.append)
+ await browser.setRemoteDescription(
+ RTCSessionDescription(sdp=await peer.negotiate(), type="answer"))
+ await until(lambda: channel.readyState == "open" and ours, "the data channel opens")
+
+ heard: list[bytes] = []
+ ours[0].on("message", heard.append)
+ channel.send(b"from the browser")
+ await until(lambda: heard == [b"from the browser"], "a message reaches the node")
+
+ for _ in range(CHUNKS):
+ ours[0].send(CHUNK)
+ while ours[0].bufferedAmount > 2_000_000:
+ await asyncio.sleep(0.005)
+ await until(lambda: sum(map(len, got)) == CHUNKS * len(CHUNK),
+ "what the node sent arrives whole")
+ await until(lambda: ours[0].bufferedAmount == 0,
+ "the node's count of what is buffered comes back to zero")
+ finally:
+ started = [w.process for w in pool._workers]
+ await browser.close()
+ await pool.close()
+ # A process still there would be one an upgrade finds holding
+ # meshbay-node.exe. Its exit code says nothing: the node ends a process
+ # whose socket has closed, and on Windows that reads 1 however it left.
+ if any(p.returncode is None for p in started):
+ raise SystemExit("FAILED a connection process outlived the node's stop")
+ return f"{CHUNKS * len(CHUNK) // 1_000_000} MB through a connection process"
+
+
+def main() -> None:
+ if len(sys.argv) < 2:
+ raise SystemExit(__doc__)
+ for exe in sys.argv[1:]:
+ print(f"OK {exe}: {asyncio.run(carry(exe))}")
+
+
+if __name__ == "__main__":
+ main()