aboutsummaryrefslogtreecommitdiffstats
path: root/packaging/win/README.md
blob: 5130e604f5eaab3af2e141f0e7da1e02b9850e27 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# 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-client\
├─ MeshBay.exe                     Electron client
├─ resources\
│  ├─ app.asar                     src/ + ui/ (the interface ships in the package)
│  ├─ firewall.ps1                 adds/removes the two inbound rules (see below)
│  └─ node-runtime\
│     ├─ meshbay-node.exe          frozen daemon (PyInstaller onedir)
│     ├─ _internal\ …              its Python + deps (aiortc, av, aioquic, …)
│     └─ ffmpeg.exe, ffprobe.exe   only if built with -FfmpegDir
└─ 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.

`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).

## Build

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

```powershell
cd packages\meshbay-client
npm run dist:win
```

That runs [`build-win.ps1`](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`](build-node-runtime.ps1) — PyInstaller freeze → `packages/meshbay-client/node-runtime/` |
| 6 | `electron-builder --win nsis` → `packages/meshbay-client/dist/MeshBay-Setup-<version>.exe` |

### ffmpeg

Not bundled by default — the node resolves `ffmpeg`/`ffprobe` from `PATH` at
startup, and video streaming needs them. To make the installer self-contained:

```powershell
npm run dist:win -- -FfmpegDir "C:\path\to\ffmpeg\bin"
# or: $env:MESHBAY_FFMPEG_DIR = "C:\path\to\ffmpeg\bin"; npm run dist:win
```

They are copied beside `meshbay-node.exe`, which is on the daemon's search path.

### Iterating

```powershell
# 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.

Say yes 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.