aboutsummaryrefslogtreecommitdiffstats

Windows packaging (W4)

A single per-user installer — MeshBay-Setup-<version>.exe — that lays down the Electron client and the Python node daemon. No hub (server-only, stays Linux). meshbay-common rides along inside the node runtime.

What the installer contains

%LOCALAPPDATA%\Programs\MeshBay\          (productName, not the npm package name)
├─ MeshBay.exe                     Electron client
├─ resources\
│  ├─ app.asar                     src/ + ui/ (the interface ships in the package)
│  ├─ firewall.ps1                 adds/removes the four inbound rules (see below)
│  ├─ service.ps1                  install/remove/status/run/end the boot-time task
│  ├─ service-mode.ps1             elevated helper: service.ps1 + firewall.ps1 in one UAC prompt
│  └─ node-runtime\
│     ├─ meshbay-node.exe          frozen daemon (PyInstaller onedir)
│     ├─ _internal\ …              its Python + deps (aiortc, av, aioquic, …)
│     ├─ ffmpeg.exe, ffprobe.exe   bundled by default, see Video (ffmpeg) below
│     ├─ av*.dll, swscale/swresample*.dll   what those two link against
│     └─ LICENSE-ffmpeg.txt        GPLv3 notice + where the source is
└─ Uninstall MeshBay.exe

Runtime data stays where the node already puts it: %LOCALAPPDATA%\meshbay\ (node.toml, keystore.enc, unlock.key, data\). The installer never writes there and the uninstaller never deletes it — installers place files, not secrets. That is also why service mode needed no code changes to platform.py: it runs as this same signed-in user (S4U, see below), so it is the same profile either way — not LocalSystem/NetworkService, which would have none of this.

build/installer.nsh also adds …\resources\node-runtime to the per-user Path (HKCU\Environment) so meshbay-node works in a terminal, and takes it back out on uninstall. New shells only — a WM_SETTINGCHANGE broadcast nudges open ones. It uses stock WordFunc.nsh (the EnVar plugin is not in electron-builder's NSIS bundle).

Autostart: two modes, one choice at install time

Per-user (default, no admin). A .vbs in the Startup folder (meshbay_node.platform._startup_vbs), toggled from the Node page or meshbay-node autostart install|remove. Starts when this user signs in.

Service mode (one admin confirmation, at install time only). A Scheduled Task, meshbay_node.platform.service_install / packaging/win/service.ps1, that starts at boot, before anyone signs in. A real Windows Service would run under LocalSystem/NetworkService — accounts with no normal user profile, so %LOCALAPPDATA%\meshbay\ (config, keystore, data) would not exist for it. Relocating storage to make that work is real surgery, deliberately not this.

The alternative used instead: schtasks /create ... /ru <user> /rp "" with no /it registers an S4U (Service For User) logon — no password stored anywhere, and unlike LocalSystem it loads this account's own profile, so %LOCALAPPDATA%\meshbay\ keeps working with zero code changes. The cost: S4U carries no network credential (cannot reach a domain share as this user), which the node never needed — everything it touches is local disk plus outbound internet.

Creating the task needs admin (a boot trigger touches system-wide scheduler state — the same reason /sc onlogon needed it too, back when Task Scheduler was tried for the per-user mode and abandoned for exactly that reason). Querying, starting and stopping an already-created task does not — Task Scheduler grants the owning user that much itself, which is what lets the Node page's Start/Stop/Restart drive it with no further UAC prompts (src/main.js's winServiceTaskStatus/Run/End, mirroring service.ps1).

One elevation, not two. Choosing service mode needs admin for both the Scheduled Task and the firewall rules; service-mode.ps1 runs both from a single ExecShellWait "runas" in installer.nsh, so the choice costs exactly one UAC prompt. Re-running setup (an upgrade, a repair install) asks nothing if the firewall rules are already there — checked first, unelevated, the same pattern the per-user-only firewall step already used.

Build

On a Windows machine with Node ≥ 22, Python ≥ 3.12 (py -3.12) and git:

cd packages\meshbay-client
npm run dist:win

That runs build-win.ps1:

Step
1 Node ≥ 22 check
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 — PyInstaller freeze → packages/meshbay-client/node-runtime/
6 electron-builder --win nsis → packages/meshbay-client/dist/MeshBay-Setup-<version>.exe

Video (ffmpeg)

Bundled by default — fetch-ffmpeg.ps1 downloads, checksum-verifies and stages it into node-runtime/ on every build, ~161 MB. Not optional in practice: MeshBay transcodes browser-incompatible video to H.264 (a real encode, not remux), and no LGPL-only ffmpeg build includes an H.264 encoder — libx264 itself is GPL. libx264 is also the floor rather than the only path: this build carries h264_qsv and h264_nvenc as well, which is what lets hwaccel.py move the encode onto an Intel or NVIDIA GPU on Windows without a second download. A re-pin that dropped those would cost every low-power Windows node its hardware encoding, silently — the node would simply go back to libx264. Asking an end user to separately run winget install ffmpeg was considered and rejected: it needs network access and winget/App Installer present at that exact moment, and its failure mode is silent — video just does not stream, with nothing pointing back at ffmpeg.

Source: BtbN/FFmpeg-Builds, the Windows x86_64 gpl-shared preset — ffmpeg.exe/ffprobe.exe plus the DLLs they both link against, rather than two independent static binaries (the "full" static build many devs already have via winget install ffmpeg is ~220 MB per executable; the equivalent Linux install used a shared build for the same reason). ffplay.exe (an SDL2 player, ~17 MB) is dropped — the daemon never invokes it. Pinned to one dated release tag, not the latest alias BtbN repoints on every auto-build; both the tag and the sha256 are hardcoded in the script and re-pinning is a deliberate edit, not automatic. LICENSE-ffmpeg.txt (GPLv3 notice + where the source is) rides along in the same directory — required, since this redistributes a GPL binary even though it is unmodified and invoked only as a subprocess.

npm run dist:win -- -SkipFfmpeg   # smaller, streaming-less build for local iteration only

Iterating

# rebuild only the installer, reuse an existing node-runtime\
powershell -File ..\..\packaging\win\build-win.ps1 -SkipNodeRuntime

# build just the frozen daemon, into an existing venv, keep it for next time
powershell -File ..\..\packaging\win\build-node-runtime.ps1 -Python C:\path\python.exe -KeepBuildVenv

Why PyInstaller and not the python-embed zip

The frozen meshbay-node.exe is a genuine relocatable single binary. It is what the client spawns (findNodeBinary in src/main.js) and what the W3 autostart launcher points at (meshbay_node.platform._node_exe). The python.org embeddable zip would need pip to make a meshbay-node.exe wrapper, and that wrapper bakes in an absolute interpreter path — it stops working the moment the tree is installed somewhere other than where it was built.

Networking (running the node)

The node's admin API is loopback-only, but its transport is not. WebRTC binds an ephemeral UDP port per connection and — because browsers/Electron publish their host candidate as an unresolvable <uuid>.local mDNS name that aioice discards — the browser always dials the node, never the reverse. So the node has to accept unsolicited inbound UDP from its peers.

Windows Defender Firewall. The setup wizard offers to add four inbound rules in one step — it needs one admin confirmation (build/installer.nsh runs firewall.ps1 via NSIS ExecShellWait "runas"; the per-user install itself never elevates):

Rule Program Protocol / port For
MeshBay MeshBay.exe UDP, any port WebRTC (ICE checks touch the client too)
MeshBay Node meshbay-node.exe UDP, any port WebRTC (the node — unsolicited inbound, see above)
MeshBay Cast MeshBay.exe TCP 19550–19553 LAN cast HTTP relay (src/cast-relay.js)
MeshBay Cast Discovery MeshBay.exe UDP 5353 Chromecast/Smart TV mDNS discovery (bonjour-service)

The WebRTC rules have no port restriction because there is no fixed port to name — aiortc binds a fresh one per connection — so they are scoped by program instead, which is the Windows-native answer to the same problem the Linux packaging solves with a broad 1024-65535/udp range (packaging/firewall/). The cast rules are scoped by program and port, since those two are fixed and known — matching packaging/firewall/*/meshbay-cast.xml exactly.

Setup checks first, unelevated (firewall.ps1 check — reading rules needs no admin, only creating them does), so running the installer again — an upgrade, a repair install — asks nothing and never pops UAC a second time once the rules are in place. Say yes the first time and every prompt you'd otherwise hit mid-use — connecting, or the first cast — is gone. Say no, or the UAC prompt is dismissed, and Windows falls back to its own "Allow access" dialog the first time each program/port combination is used — tick both Private and Public then (a libvirt/VM adapter, and sometimes a plain Ethernet one, registers as Public; a Private-only rule silently drops every peer). Missed one? Add it by hand: Windows Defender Firewall → Advanced → Inbound Rules → New Rule → Program → the exe → Allow → all profiles, restricting to the port above if it is a cast rule. firewall.ps1 is idempotent and re-runnable (powershell -File resources\firewall.ps1 add, elevated); it logs to %TEMP%\meshbay-firewall.log.

A flat LAN needs nothing else. The node offers a routable 192.168.x.y host candidate and browsers on the same subnet connect straight to it — same as the residential-NAT cases that work without TURN.

A routed or multi-subnet LAN (peers on 192.168.1.x reaching a node on 192.168.2.x, or a VM) additionally needs:

  • routing between the subnets (the two ends must have a path to each other's host-candidate address);
  • inbound UDP on the ephemeral range allowed for meshbay-node.exe and for any Linux host in the path — see the packaged packaging/firewall/ profiles and the "inbound WebRTC" section of docs/PACKAGING-GUIDE.md;
  • MESHBAY_WEBRTC_EXPOSE_LOCAL_IPS is not relevant to the node — that switch is the client's, and it is already on by default (src/main.js).

libvirt NAT is the awkward case. A guest on the default NAT network is reachable from its own hypervisor but not from other LAN machines, and its only host candidate is the 192.168.122.x/192.168.200.x address no third machine can route to. Give the guest bridged (or macvtap) networking so it gets a real LAN address, or run the node on the host. The node log now prints the host addresses it offered — WebRTC answer ready for peer=… (… host: 192.168.200.173, 1 srflx) — so "did the node even offer something routable" is answerable from the journal.

Dependency surface that needs watching

meshbay-node.spec pulls the awkward packages in whole (collect_all) because they have C/Rust extensions or do dynamic imports: aiortc, av (bundles FFmpeg DLLs), aioquic, pydantic_core, uvicorn, watchdog, guessit, blake3, zstandard, msgpack. If a frozen run raises ModuleNotFoundError, add the package to COLLECT_ALL / HIDDEN / COPY_META in the spec — that is the expected way the list grows.

Not done

  • Authenticode signing — Phase 13.9. Unsigned installer triggers SmartScreen.
  • CI — no Windows runner builds this yet (Phase 18.3). Build is manual.
  • Delta updates / auto-update — electron-updater is not wired.