@threadedstack/wmd-web 0.1.17 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/RELEASE.json +39 -54
- package/guest/arm64/Image +2 -2
- package/guest/arm64/Image-4k +1 -1
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +4 -4
- package/relay/hosted.ts +10 -8
- package/relay/main.ts +2 -2
- package/relay/relay.ts +1 -0
- package/relay/token.ts +34 -5
- package/relay/wisp.ts +37 -5
- package/src/boot.js +25 -0
- package/src/build/builder.js +791 -0
- package/src/build/context.js +349 -0
- package/src/build/dockerfile.js +1055 -0
- package/src/build/dockerignore.js +270 -0
- package/src/build/fast.js +1187 -0
- package/src/build/inspect.js +78 -0
- package/src/build/pin.js +6 -0
- package/src/build/tar.js +304 -0
- package/src/host/ctl.js +17 -0
- package/src/host/disk-map.js +330 -0
- package/src/host/disk-worker.js +125 -0
- package/src/host/disk.js +219 -0
- package/src/host/wmd-imports.js +3 -3
- package/src/image-files.js +2 -1
- package/src/registry/hub.js +113 -44
- package/src/registry/load-tar.js +473 -0
- package/src/registry/pull.js +5 -3
- package/src/worker.js +2 -0
- package/wmdpack.wasm +0 -0
package/README.md
CHANGED
|
@@ -155,6 +155,50 @@ The interface this host implements is in [docs/host-interface.md](docs/host-inte
|
|
|
155
155
|
too) and shows the last 128 KB. P-B1's merged branch had the same rings at 4 MiB
|
|
156
156
|
(cherry-picked as `17e9ce50`); the merge keeps P-B2's 32 MiB rings and drop markers.
|
|
157
157
|
|
|
158
|
+
## Building images (`wmd-web/build`)
|
|
159
|
+
|
|
160
|
+
`createBuilder` runs BuildKit (moby/buildkit v0.33.0) in a privileged builder machine in the page and builds a
|
|
161
|
+
Dockerfile there. The result is `{index, packs}`, the shape `pullImage` returns, so `bootWmd` can run it at once.
|
|
162
|
+
Chromium, Firefox and WebKit all pass the same 18 build steps (plan `docs/plans/web-buildkit-impl.md`, section 10).
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
import { createBuilder } from 'wmd-web/build'
|
|
166
|
+
|
|
167
|
+
const builder = await createBuilder({
|
|
168
|
+
vmUrl, files, // as bootWmd: the engine and the builder arch's guest files
|
|
169
|
+
arch: 'arm64', // or 'amd64'
|
|
170
|
+
image: { tar: builderTarUrl, sha256 }, // the builder image; must match BUILDER_PINS in src/build/pin.js
|
|
171
|
+
relay: { url, protocol: 'wisp' }, // the guest's network: base images and RUN downloads
|
|
172
|
+
disk: { namespace: 'my-app' }, // BuildKit's cache on an ext4 disk in OPFS (default 10240 MiB)
|
|
173
|
+
converter, // wmdpack.wasm
|
|
174
|
+
pull: ref => pullImage(ref, { fetch, converter, platform: 'linux/arm64' }), // base images for the fast path (off without it)
|
|
175
|
+
})
|
|
176
|
+
const r = await builder.build({ context: { Dockerfile: 'FROM alpine:3.20\nRUN echo hi > /x\n' } })
|
|
177
|
+
// r: {index, packs, log, path: 'fast'|'buildkit', ms, cached}
|
|
178
|
+
await builder.close()
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- **The cache stays.** The disk outlives the builder and the visit. After a reload, or after a tab was killed
|
|
182
|
+
mid-build, the next build mounts the disk again and its steps come back `CACHED`. `close({keep: false})` deletes
|
|
183
|
+
the disk.
|
|
184
|
+
- **A second tab** that asks for a disk already open gets a temporary disk, deleted when it closes.
|
|
185
|
+
- **Features:** `RUN --mount=type=cache` and `type=secret`, `# syntax=` frontends, build args and target stages,
|
|
186
|
+
`.dockerignore`, file capabilities (`setcap`), push to a registry (TLS, or plain http in `httpRegistries`), and
|
|
187
|
+
`tar: true` for a Docker tar that `docker load` takes. `--mount=type=ssh` and another platform are refused before
|
|
188
|
+
any work.
|
|
189
|
+
- **The fast path.** With `pull` given, a Dockerfile with no `RUN` is built in the page without BuildKit, layer
|
|
190
|
+
for layer the same as BuildKit's (40 compared cases): under 0.4 s once its base image is pulled, where a warm
|
|
191
|
+
BuildKit takes 5 to 14 s.
|
|
192
|
+
- **Errors:** `BuildError {step, tail, reason}`: `reason` is `'no-space'`, `'quota'`, `'disk'`, `'timeout'`, `'pin'`
|
|
193
|
+
or `'secret'`. Cancelling with `signal` stops the step and leaves nothing behind.
|
|
194
|
+
- **The builder images** are built with `make web-builder` (wmd builds them; Docker Desktop is not used) into
|
|
195
|
+
`web/dist/builder/builder-<arch>.tar`, and their sha256 is pinned in `src/build/pin.js`. `make web-release` puts
|
|
196
|
+
them next to the release tgz.
|
|
197
|
+
- **Docker Hub's rate limit** applies to base images. `registryMirrors` gives BuildKit a mirror to try first.
|
|
198
|
+
- **Tests:** `bash ../scripts/bootlock.sh run --max 1800 -- bunx playwright test -c test/build --project=<browser>`
|
|
199
|
+
(`build.spec.js`: the 18 steps; `parity.spec.js`: the fast path against BuildKit). `test/build/seed-mirror.sh`
|
|
200
|
+
seeds a local mirror so the runs never touch Docker Hub.
|
|
201
|
+
|
|
158
202
|
## Running the tests
|
|
159
203
|
|
|
160
204
|
```sh
|
package/RELEASE.json
CHANGED
|
@@ -1,38 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wmd-web",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"commit": "
|
|
5
|
-
"previous":
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"commit": "cb2d3874a3d65236337c76c3c59147fe152a010c",
|
|
5
|
+
"previous": "wmd-web-v0.1.0",
|
|
6
6
|
"decisions": [
|
|
7
|
-
"D-388",
|
|
8
|
-
"D-395",
|
|
9
|
-
"D-408",
|
|
10
|
-
"D-415",
|
|
11
|
-
"D-416",
|
|
12
|
-
"D-417",
|
|
13
|
-
"D-419",
|
|
14
|
-
"D-423",
|
|
15
|
-
"D-428",
|
|
16
|
-
"D-430",
|
|
17
|
-
"D-432",
|
|
18
|
-
"D-433",
|
|
19
|
-
"D-438",
|
|
20
|
-
"D-442",
|
|
21
|
-
"D-443",
|
|
22
|
-
"D-444",
|
|
23
|
-
"D-445",
|
|
24
|
-
"D-446",
|
|
25
|
-
"D-447",
|
|
26
|
-
"D-448",
|
|
27
|
-
"D-449",
|
|
28
|
-
"D-451",
|
|
29
|
-
"D-453",
|
|
30
|
-
"D-454",
|
|
31
|
-
"D-468",
|
|
32
|
-
"D-476",
|
|
33
|
-
"D-477",
|
|
34
|
-
"D-481",
|
|
35
|
-
"D-482",
|
|
36
7
|
"D-486",
|
|
37
8
|
"D-488",
|
|
38
9
|
"D-494",
|
|
@@ -49,43 +20,53 @@
|
|
|
49
20
|
"D-539",
|
|
50
21
|
"D-542",
|
|
51
22
|
"D-544",
|
|
52
|
-
"D-545"
|
|
23
|
+
"D-545",
|
|
24
|
+
"D-547",
|
|
25
|
+
"D-548"
|
|
53
26
|
],
|
|
54
|
-
"vmSources": "
|
|
27
|
+
"vmSources": "7a707b82c43d3f1eb999476cad68e7ce79cc9cb5",
|
|
55
28
|
"guest": {
|
|
56
29
|
"arch": "arm64",
|
|
57
30
|
"files": {
|
|
58
31
|
"vm.wasm": {
|
|
59
|
-
"sha256": "
|
|
60
|
-
"bytes":
|
|
32
|
+
"sha256": "40299e37e788189e6d3bea9d9cdaefa241a5acf27931e6aae14117c100b036ec",
|
|
33
|
+
"bytes": 1561499
|
|
61
34
|
},
|
|
62
35
|
"Image": {
|
|
63
|
-
"sha256": "
|
|
64
|
-
"bytes":
|
|
36
|
+
"sha256": "1d296c86b1fee0a10d0c2571b7e19854fe85ca0250cc9fadfe75eb268e48be7b",
|
|
37
|
+
"bytes": 39049224
|
|
65
38
|
},
|
|
66
39
|
"Image-4k": {
|
|
67
|
-
"sha256": "
|
|
40
|
+
"sha256": "6296e3d9fc7bb06a26340bb4453b5f0970d6b55452903546ba2c762820e2df5b",
|
|
68
41
|
"bytes": 38828040
|
|
69
42
|
},
|
|
70
43
|
"initramfs.cpio.gz": {
|
|
71
|
-
"sha256": "
|
|
72
|
-
"bytes":
|
|
44
|
+
"sha256": "f008aa9e0f6b4e913242f9982d3b194dba7dc39042a24bc89be64630672125af",
|
|
45
|
+
"bytes": 4685204
|
|
73
46
|
}
|
|
74
47
|
}
|
|
75
48
|
},
|
|
76
49
|
"files": {
|
|
77
|
-
"README.md": "
|
|
50
|
+
"README.md": "3c12ca29aab5160b955ba3f50035f19be1dbf0278c4fb006e253a37ac9efbe6a",
|
|
78
51
|
"export-page/coi-sw.js": "0300e670b413cb2e0aae35843fc583546bbf6687df971ab6df41cfa3b0d494d7",
|
|
79
52
|
"export-page/index.html": "f5450c89e63601bbbbffef421f03491b231d1f143070012c1c5315d4b0059f43",
|
|
80
53
|
"export-page/run.css": "b57adf1e76553549c1f89ff157b7973409dedc21889cd3932948cd05b51adc6f",
|
|
81
54
|
"export-page/run.js": "717636a632a40469106ddf1383e198eaac5d83651b5866d0699ec04b82462b7f",
|
|
82
|
-
"guest/arm64/Image": "
|
|
83
|
-
"guest/arm64/Image-4k": "
|
|
84
|
-
"guest/arm64/initramfs.cpio.gz": "
|
|
85
|
-
"guest/arm64/vm.wasm": "
|
|
86
|
-
"package.json": "
|
|
55
|
+
"guest/arm64/Image": "1d296c86b1fee0a10d0c2571b7e19854fe85ca0250cc9fadfe75eb268e48be7b",
|
|
56
|
+
"guest/arm64/Image-4k": "6296e3d9fc7bb06a26340bb4453b5f0970d6b55452903546ba2c762820e2df5b",
|
|
57
|
+
"guest/arm64/initramfs.cpio.gz": "f008aa9e0f6b4e913242f9982d3b194dba7dc39042a24bc89be64630672125af",
|
|
58
|
+
"guest/arm64/vm.wasm": "40299e37e788189e6d3bea9d9cdaefa241a5acf27931e6aae14117c100b036ec",
|
|
59
|
+
"package.json": "778ef2457ab01f302387c811d796b7c61f97feb095725da285655629965f2b5e",
|
|
87
60
|
"src/base.js": "982f8e46dbb727ec542a494ebdb884ccd19a31ff4e091425b2d1fc01b0d4754a",
|
|
88
|
-
"src/boot.js": "
|
|
61
|
+
"src/boot.js": "62f5cf901d797d53bef584b3c6c0c7fb1a4cc8e2450e9570d6289c72036ed025",
|
|
62
|
+
"src/build/builder.js": "751ea022e1f6f0c6ab79bd4efd6cfe0d705268ff7f699af87dcc64d1871ccc52",
|
|
63
|
+
"src/build/context.js": "4baf1884d366455f3f76f397575d515671585ebf5deb27bc35bc2b1dd9c6bc11",
|
|
64
|
+
"src/build/dockerfile.js": "5957fe3d4ac8ff8b312f980ee04bd1d98ad4373a61f9a33e02f1ff1108b6ea53",
|
|
65
|
+
"src/build/dockerignore.js": "8fc03d09da020f43ea483979b1183891b71d76f012379ab7813838ea0e53c546",
|
|
66
|
+
"src/build/fast.js": "bf7373a43cce995b21751c317a58d18438f9fd2f670266b314b1151ef3cdb9e8",
|
|
67
|
+
"src/build/inspect.js": "52aa26d332c08406e8eeaf8f72661d8acc36bbd4f547a7d68e72b4f0fe265a22",
|
|
68
|
+
"src/build/pin.js": "8c67f017ae3ec560d2638fdfc0bd91ddd758150af16c188bbe3bf2cb4d7e28b2",
|
|
69
|
+
"src/build/tar.js": "25cf477488d9a60da9015be9ca5830bad3ad24428da1546f0621411cb7f44949",
|
|
89
70
|
"src/calib-worker.js": "3d610591a1b3b58f59c3a2ab457f43c90727d95973cd43055b3b73b76180611d",
|
|
90
71
|
"src/fs/9p/codec.js": "69711eb4ebc84eff39256138169fe18f5a5066d6cd9e4ff2daf61c65380d7ef7",
|
|
91
72
|
"src/fs/9p/server.js": "da0ba00ebb5522161bb0ef7cd94bfc9616e342bf865633dfef0ba5bc67627d7a",
|
|
@@ -110,7 +91,10 @@
|
|
|
110
91
|
"src/host/ckpt-save.js": "e222571033ae74a4439596c5e90e03b2aa9a5516665dd290f8dd243712294523",
|
|
111
92
|
"src/host/ckpt.js": "2155b5c8e6f609478805eb8a0b5d5210d47cdf35a80f79e0b9ae42b2c3170923",
|
|
112
93
|
"src/host/clock.js": "aba6af3a8b1a985ae340892222e4b482d33afe20f5c8b3a4c7403ce87d1e247c",
|
|
113
|
-
"src/host/ctl.js": "
|
|
94
|
+
"src/host/ctl.js": "ca2ebbb940c9668cb0573c959cd2ff8982f96d58dfdb8dce39d304b89e033dd7",
|
|
95
|
+
"src/host/disk-map.js": "3d7e0d7d488ce25fc6a0e4dc0a9b1c8fc2af96bac1c03491a1d30be392af268b",
|
|
96
|
+
"src/host/disk-worker.js": "9d470cdc72d9e1fbf955e466ec2562abe780f2c6d89174ca3e4fbb1828f8dd76",
|
|
97
|
+
"src/host/disk.js": "373e23539e3916674b7e8708d2e3ae22578214129e56936765db5c8f465cb343",
|
|
114
98
|
"src/host/errno.js": "26021d85962084be2c43c98fb3accafc2a8f11fbf3c8a5b8d484bd6a088ae4db",
|
|
115
99
|
"src/host/fs.js": "d6753f74db35b7b01baa8709fc7fdcdfc8d638dfe42f454b0a833b22111e8f4a",
|
|
116
100
|
"src/host/gcnudge.js": "4a6569cb73ae24a11fc4ac8c467380bd9efba0bb5fa9867b797bc4f153ab15ab",
|
|
@@ -127,9 +111,9 @@
|
|
|
127
111
|
"src/host/stdio.js": "43973675578eb1d324c2c6d7078e51a6a1f121190ada5ef14a5551364b8c7f5e",
|
|
128
112
|
"src/host/threads.js": "952381940e7f43114867e9a9714528f2f20c8064b6b0f2018ab075128dc601be",
|
|
129
113
|
"src/host/wasi.js": "fbafa239b8fb4083f7e8c910b1adab38901e8e7cffd247284a9abb2847eaf4bd",
|
|
130
|
-
"src/host/wmd-imports.js": "
|
|
114
|
+
"src/host/wmd-imports.js": "3a00562f7c9c7787ab3841b14dde5d903a78f43ed19c43a2f5e31fd8a14c09d2",
|
|
131
115
|
"src/host/yamux.js": "f4044f14f7e836224bdb4b1e53e59c565ee478b83b2e428fffb9f7a048768f79",
|
|
132
|
-
"src/image-files.js": "
|
|
116
|
+
"src/image-files.js": "7bac16bd278d17ba291dfe4771564627b5efbdda927b429018a75e8b906dd4cd",
|
|
133
117
|
"src/net/dhcp-dns.js": "fdfa7c7d5cbbb4a8774b7dee195bf2df22a0c3fe8f20ac9694b24485fa8f25d5",
|
|
134
118
|
"src/net/net-worker.js": "0a2ce998f69807e386ded7ff1bee53687a1d59ed76d7ef46e2eb6f958b14ad38",
|
|
135
119
|
"src/net/packet.js": "d658f26fa84324a88d7dbc26af9c4a9c10e977c3060f16f7db431496eacd1127",
|
|
@@ -143,12 +127,13 @@
|
|
|
143
127
|
"src/net/wisp.js": "06c50159f416d40a912fe2cce4806388b938f2ab0220b2ee6da890630ce3d113",
|
|
144
128
|
"src/registry/client.js": "fd38550e9afd82c5cb498513d245c33357a71697b3b2e6dd78538934bcc9d879",
|
|
145
129
|
"src/registry/converter.js": "1fb8294fd5275f6772c27130cc9ba70bd46fafaf440884d42448036618229f37",
|
|
146
|
-
"src/registry/hub.js": "
|
|
130
|
+
"src/registry/hub.js": "cd672323bd1f96bd1d7e9eed80d4f36891e919e456fc0deb378fa0c7e3b6f249",
|
|
131
|
+
"src/registry/load-tar.js": "9dd2b5d8884c64247339b141ab5a57b8ab1a2e007c32aa45b1103b37405b0ed7",
|
|
147
132
|
"src/registry/oci.js": "6a51ea16b34a65c0e732730f3e07d7ad16c50f6943a486ec05ca6aa3e98676a1",
|
|
148
133
|
"src/registry/pack-worker.js": "fd6798851cb7815dacfcb67fc78c41654209c5504d8fd343aae524e99de1c741",
|
|
149
|
-
"src/registry/pull.js": "
|
|
134
|
+
"src/registry/pull.js": "24f2e01a76c9f7f27896af69b989f8c97f0ca31712b3e8110aaa5b5e3148b04b",
|
|
150
135
|
"src/registry/ref.js": "24038e0fbcb6ac143cb8e8f7b9fac4dd0803d0891961786dc88dba01a4402e7b",
|
|
151
136
|
"src/registry/wasi-mem.js": "a7f684343010892460db27ee8c1e38e47e797c93dc4ebe4c30185dca6ca56ee2",
|
|
152
|
-
"src/worker.js": "
|
|
137
|
+
"src/worker.js": "c654be359e741d5b1fde1be05f8909968d4e00d1fca61be169e60ea691daa662"
|
|
153
138
|
}
|
|
154
139
|
}
|
package/guest/arm64/Image
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/guest/arm64/Image
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 39049224 bytes
|
|
4
|
+
sha256: 1d296c86b1fee0a10d0c2571b7e19854fe85ca0250cc9fadfe75eb268e48be7b
|
package/guest/arm64/Image-4k
CHANGED
|
Binary file
|
package/guest/arm64/vm.wasm
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threadedstack/wmd-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://github.com/threadedstack/wmd-releases",
|
|
6
6
|
"repository": {
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"description": "Browser host for wmd's guest vm.wasm (Web Workers + shared memory + WASI preview1)",
|
|
11
11
|
"wmd": {
|
|
12
|
-
"tag": "v0.1.
|
|
13
|
-
"commit": "
|
|
14
|
-
"web": "0.
|
|
12
|
+
"tag": "v0.1.18",
|
|
13
|
+
"commit": "cb2d3874a3d65236337c76c3c59147fe152a010c",
|
|
14
|
+
"web": "0.4.0"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
17
|
"main": "./src/boot.js",
|
package/relay/hosted.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type HostedOptions = {
|
|
|
26
26
|
now?: () => number // ms since the epoch (tests)
|
|
27
27
|
warn?: (line: string) => void // one-line operator notices (stderr)
|
|
28
28
|
sites?: string[] // A: page origin patterns served (required: none listed serves no page)
|
|
29
|
-
sitesPerAddrPerHour?: number // B: different sites per address group per hour (200: plan §16)
|
|
29
|
+
sitesPerAddrPerHour?: number // B: different sites per address group per hour (200: plan §16); a publish identity three times that
|
|
30
30
|
sitesPerNetPerHour?: number // B: per network (800)
|
|
31
31
|
monthBytes?: number // C: bytes relayed per UTC month (the operator's figure from the provider's free outbound data); 0 = no cap
|
|
32
32
|
usageFile?: string // C: the month's counter survives a restart here
|
|
@@ -57,6 +57,7 @@ export type Handle = { ip: string; grp: string; origin: string; end: (why: Why)
|
|
|
57
57
|
/** What a session holds from the governor until it ends. */
|
|
58
58
|
export type Admission = {
|
|
59
59
|
grp: string
|
|
60
|
+
publish: boolean
|
|
60
61
|
net: string
|
|
61
62
|
handle: Handle
|
|
62
63
|
fair: Bucket | null
|
|
@@ -403,7 +404,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
403
404
|
return m
|
|
404
405
|
}
|
|
405
406
|
const oldest = (m: Map<string, number>) => Math.min(...m.values()) + HOUR
|
|
406
|
-
function sites(grp: string, host: string): Why | null {
|
|
407
|
+
function sites(grp: string, host: string, publish: boolean): Why | null {
|
|
407
408
|
const t = now()
|
|
408
409
|
const site = host.toLowerCase().replace(/\.$/, '')
|
|
409
410
|
const net = coarseKey(grp)
|
|
@@ -414,8 +415,9 @@ export function createGovernor(o: HostedOptions) {
|
|
|
414
415
|
fresh(a, t)
|
|
415
416
|
fresh(n, t)
|
|
416
417
|
if (!a.has(site)) {
|
|
417
|
-
|
|
418
|
-
|
|
418
|
+
const max = publish ? 3 * c.sitesPerAddrPerHour : c.sitesPerAddrPerHour
|
|
419
|
+
if (a.size >= max) {
|
|
420
|
+
return { rule: 'too-many-sites', words: `${max} different sites in an hour from this address${publish ? ' while publishing' : ''}: a new site waits until one ages out; sites already reached keep working`, until: oldest(a) }
|
|
419
421
|
}
|
|
420
422
|
if (!n.has(site) && n.size >= c.sitesPerNetPerHour) {
|
|
421
423
|
return { rule: 'too-many-sites', words: `${c.sitesPerNetPerHour} different sites in an hour from this network: a new site waits until one ages out; sites already reached keep working`, until: oldest(n) }
|
|
@@ -452,7 +454,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
452
454
|
const grpOf = (s: Session): string => s.hx?.grp ?? s.p.grp
|
|
453
455
|
|
|
454
456
|
/** A session from `ip` (address group `grp`) with this page origin is about to open. */
|
|
455
|
-
function admitSession(i: { ip: string; grp: string; origin: string; end: (why: Why) => void; tell?: Tell; claimed?: boolean }): { ok: true; adm: Admission } | ({ ok: false } & Why) {
|
|
457
|
+
function admitSession(i: { ip: string; grp: string; origin: string; publish?: boolean; end: (why: Why) => void; tell?: Tell; claimed?: boolean }): { ok: true; adm: Admission } | ({ ok: false } & Why) {
|
|
456
458
|
const no = (w: Why) => ({ ok: false as const, ...w })
|
|
457
459
|
const denied = deniedClient(i.ip, i.origin)
|
|
458
460
|
if (denied) return no(denied)
|
|
@@ -467,7 +469,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
467
469
|
live.add(handle)
|
|
468
470
|
let done = false
|
|
469
471
|
const adm: Admission = {
|
|
470
|
-
grp: i.grp, net: coarseKey(i.grp), handle, fair: c.linkBytesPerSec ? new Bucket(Infinity) : null, sent: 0, waiting: false,
|
|
472
|
+
grp: i.grp, publish: !!i.publish, net: coarseKey(i.grp), handle, fair: c.linkBytesPerSec ? new Bucket(Infinity) : null, sent: 0, waiting: false,
|
|
471
473
|
release() {
|
|
472
474
|
if (done) return
|
|
473
475
|
done = true
|
|
@@ -488,7 +490,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
488
490
|
function admitConnect(s: Session, host: string, _port: number): Why | { waitMs: number } | null {
|
|
489
491
|
const closed = notAccepting()
|
|
490
492
|
if (closed) return closed
|
|
491
|
-
const crawl = sites(grpOf(s), host)
|
|
493
|
+
const crawl = sites(grpOf(s), host, !!s.hx?.publish)
|
|
492
494
|
if (crawl) return crawl
|
|
493
495
|
return dialGate(s)
|
|
494
496
|
}
|
|
@@ -691,7 +693,7 @@ export function createGovernor(o: HostedOptions) {
|
|
|
691
693
|
blockedAddr,
|
|
692
694
|
liveStreams: () => used,
|
|
693
695
|
limitsLine: () =>
|
|
694
|
-
`only listed
|
|
696
|
+
`only listed pages may use this relay; ${c.sitesPerAddrPerHour} different sites an hour per address, ${c.sitesPerNetPerHour} per network; failed dials slow the sender` +
|
|
695
697
|
(c.monthBytes ? `; ${Math.round(c.monthBytes / 1e9)} GB a month, heavy users slowed first when the month runs ahead` : ''),
|
|
696
698
|
stop() {
|
|
697
699
|
clearInterval(ticker)
|
package/relay/main.ts
CHANGED
|
@@ -53,7 +53,7 @@ Flags:
|
|
|
53
53
|
--allow-loopback-ports <list> guests may reach this computer's loopback on these ports
|
|
54
54
|
--block-ports <list> destination ports refused (none)
|
|
55
55
|
--allow-hosts <list> only these names (name[:port|port], a leading . for subdomains)
|
|
56
|
-
--hosted the public hosted profile (docs/relay-hosted.md): only listed
|
|
56
|
+
--hosted the public hosted profile (docs/relay-hosted.md): only listed pages may use this relay, no crawling,
|
|
57
57
|
failed dials slowed, the month's data cap, host limits, deny lists, a control
|
|
58
58
|
socket, /healthz, /readyz
|
|
59
59
|
--abuse-contact <text> required: an address or URL, shown in the MOTD, /healthz and every refusal
|
|
@@ -326,7 +326,7 @@ export function relayOptions(c: Conf, env: Record<string, string | undefined> =
|
|
|
326
326
|
if (c.behindTls && !isPrivateBind(host)) throw new UsageError(`--hosted --behind-tls binds only to loopback or a private address (the proxy\'s side), not ${host}: a public bind would let anyone skip the proxy and its TLS`)
|
|
327
327
|
if (!tls && !c.behindTls && !isPrivateBind(host)) throw new UsageError(`--hosted refuses plain ws:// on the public address ${host}: give --tls-cert and --tls-key, or put a TLS proxy in front with --behind-tls (bound to loopback)`)
|
|
328
328
|
if (c.localTls) throw new UsageError('--local-tls is for a person\'s own computer, not --hosted')
|
|
329
|
-
if (!c.sitesFile) throw new UsageError('--hosted needs --sites-file <file>: the page origins it serves (JSON {"sites": [...]});
|
|
329
|
+
if (!c.sitesFile) throw new UsageError('--hosted needs --sites-file <file>: the page origins it serves (JSON {"sites": [...]}); only listed pages may use this relay')
|
|
330
330
|
}
|
|
331
331
|
const w = c.wisp ?? {}
|
|
332
332
|
const secret = env.WMD_RELAY_SECRET || randomBytes(32).toString('base64url') // tokens (wmn's issuer) only when the secret is shared
|
package/relay/relay.ts
CHANGED
|
@@ -132,6 +132,7 @@ export function createRelay(o: RelayOptions) {
|
|
|
132
132
|
const r = health(url.pathname)
|
|
133
133
|
return req.method === 'HEAD' ? new Response(null, { status: r.status, headers: r.headers }) : r // same status and headers, no body
|
|
134
134
|
}
|
|
135
|
+
if (gov && wisp && url.pathname === '/publish-token') return wisp.publishToken(req, srv, url)
|
|
135
136
|
if (url.pathname === '/relay') {
|
|
136
137
|
const protos = req.headers.get('sec-websocket-protocol') || ''
|
|
137
138
|
if (wisp && protos.includes('wisp-v2') && !protos.includes(PROTOCOL)) return new Response(`this is the wmd-relay.v1 endpoint; Wisp is at ${wisp.path}\n`, { status: 400 })
|
package/relay/token.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// P-B10 relay: session tokens. A token is "v1.<payload>.<mac>", base64url without padding,
|
|
2
|
-
// where the payload is JSON {sid, exp (ms since the epoch), uid?, grp?} and the mac is
|
|
2
|
+
// where the payload is JSON {sid, exp (ms since the epoch), uid?, grp?, kind?} and the mac is
|
|
3
3
|
// HMAC-SHA256 of "v1.<payload>" with the relay's secret. Whoever serves the page
|
|
4
4
|
// (test/serve.ts, or wmn's server) mints them with the same secret (TokenIssuer below); the
|
|
5
5
|
// browser presents one as a WebSocket subprotocol "t.<token>" (not in the URL, so it stays
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// uid the guest identity the relay keeps caps for, across reconnects and fresh tokens
|
|
10
10
|
// (one guest machine of one page); defaults to sid
|
|
11
11
|
// grp the client the issuer saw (an address, a user): the relay caps sessions per grp
|
|
12
|
+
// kind "publish": a publish identity (TokenIssuer.issuePublish), which a hosted relay gives a higher sites-an-hour
|
|
13
|
+
// count (hosted.ts). Only the signature makes it one: the relay never takes a client's word for it
|
|
12
14
|
//
|
|
13
15
|
// Parsing is strict: a token is exactly one encoding of its payload and MAC, so the same
|
|
14
16
|
// token never verifies under two spellings.
|
|
@@ -19,19 +21,20 @@ const b64u = (b: Buffer) => b.toString('base64url')
|
|
|
19
21
|
const B64U = /^[A-Za-z0-9_-]+$/
|
|
20
22
|
const ID = /^[A-Za-z0-9_.:\/-]{1,128}$/
|
|
21
23
|
|
|
22
|
-
export type TokenFields = { sid?: string; uid?: string; grp?: string; ttlMs?: number; now?: number }
|
|
24
|
+
export type TokenFields = { sid?: string; uid?: string; grp?: string; kind?: string; ttlMs?: number; now?: number }
|
|
23
25
|
|
|
24
|
-
export function mintToken(secret: string, { sid = b64u(randomBytes(12)), uid, grp, ttlMs = 60 * 60_000, now = Date.now() }: TokenFields = {}): string {
|
|
26
|
+
export function mintToken(secret: string, { sid = b64u(randomBytes(12)), uid, grp, kind, ttlMs = 60 * 60_000, now = Date.now() }: TokenFields = {}): string {
|
|
25
27
|
if (!secret || secret.length < 16) throw new Error('relay secret: at least 16 characters')
|
|
26
28
|
const body: Record<string, unknown> = { sid, exp: now + ttlMs }
|
|
27
29
|
if (uid) body.uid = uid
|
|
28
30
|
if (grp) body.grp = grp
|
|
31
|
+
if (kind) body.kind = kind
|
|
29
32
|
const payload = b64u(Buffer.from(JSON.stringify(body)))
|
|
30
33
|
const mac = createHmac('sha256', secret).update(`v1.${payload}`).digest()
|
|
31
34
|
return `v1.${payload}.${b64u(mac)}`
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
export type TokenInfo = { sid: string; exp: number; uid?: string; grp?: string }
|
|
37
|
+
export type TokenInfo = { sid: string; exp: number; uid?: string; grp?: string; kind?: 'publish' }
|
|
35
38
|
|
|
36
39
|
/** The token's session, or an error string. */
|
|
37
40
|
export function verifyToken(secret: string, token: string, now = Date.now()): TokenInfo | string {
|
|
@@ -52,8 +55,9 @@ export function verifyToken(secret: string, token: string, now = Date.now()): To
|
|
|
52
55
|
}
|
|
53
56
|
if (typeof info !== 'object' || !info || typeof info.sid !== 'string' || !ID.test(info.sid) || typeof info.exp !== 'number') return 'bad token'
|
|
54
57
|
for (const k of ['uid', 'grp'] as const) if (info[k] !== undefined && (typeof info[k] !== 'string' || !ID.test(info[k]!))) return 'bad token'
|
|
58
|
+
if (info.kind !== undefined && info.kind !== 'publish') return 'bad token'
|
|
55
59
|
if (info.exp <= now) return 'token expired'
|
|
56
|
-
return { sid: info.sid, exp: info.exp, uid: info.uid, grp: info.grp }
|
|
60
|
+
return { sid: info.sid, exp: info.exp, uid: info.uid, grp: info.grp, ...(info.kind ? { kind: info.kind } : {}) }
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
/** The key a client's limits are kept under: IPv6 addresses by their /64 (one subscriber holds 2^64 of them). */
|
|
@@ -79,6 +83,7 @@ export function clientKey(client: string): string {
|
|
|
79
83
|
* an hour. At most `maxClients` clients are remembered (the least recently used go first).
|
|
80
84
|
*/
|
|
81
85
|
export const MIN_TTL_MS = 60_000
|
|
86
|
+
export const PUBLISH_TTL_MS = 60 * 60_000
|
|
82
87
|
export class TokenIssuer {
|
|
83
88
|
perMinute: number
|
|
84
89
|
instancesPerHour: number
|
|
@@ -86,6 +91,7 @@ export class TokenIssuer {
|
|
|
86
91
|
maxClients: number
|
|
87
92
|
minTtlMs: number
|
|
88
93
|
clients = new Map<string, { times: number[]; instances: Map<string, number> }>() // least recently used first
|
|
94
|
+
publishing = new Map<string, { instance: string; token: string; until: number }>() // per client: its live publish identity, in the order they end
|
|
89
95
|
constructor(public secret: string, { perMinute = 60, instancesPerHour = 20, maxTtlMs = 60 * 60_000, maxClients = 10_000, minTtlMs = MIN_TTL_MS } = {}) {
|
|
90
96
|
this.perMinute = perMinute
|
|
91
97
|
this.instancesPerHour = instancesPerHour
|
|
@@ -113,4 +119,27 @@ export class TokenIssuer {
|
|
|
113
119
|
const ttl = Math.min(Math.max(ttlMs, this.minTtlMs), this.maxTtlMs)
|
|
114
120
|
return { token: mintToken(this.secret, { uid: `${key}/${instance}`, grp: key, ttlMs: ttl, now }) }
|
|
115
121
|
}
|
|
122
|
+
/** A publish identity for this client, for one hour; at most one per client at a time. The page that holds it gets the
|
|
123
|
+
same token again (a reconnect); another page of the client is refused until its hour is over. When `maxClients`
|
|
124
|
+
identities are live, a new one is refused (503) until the first ends: a live one is never dropped. */
|
|
125
|
+
issuePublish(client: string, instance: string, now = Date.now()): { token: string; until: number } | { error: string; status: number; until?: number } {
|
|
126
|
+
if (!ID.test(client)) return { error: 'bad client', status: 400 }
|
|
127
|
+
if (!/^[A-Za-z0-9_-]{8,64}$/.test(instance || '')) return { error: 'instance: 8-64 characters [A-Za-z0-9_-]', status: 400 }
|
|
128
|
+
const key = clientKey(client)
|
|
129
|
+
for (const [k, p] of this.publishing) {
|
|
130
|
+
if (p.until > now) break
|
|
131
|
+
this.publishing.delete(k)
|
|
132
|
+
}
|
|
133
|
+
const live = this.publishing.get(key)
|
|
134
|
+
if (live && live.instance === instance) return { token: live.token, until: live.until }
|
|
135
|
+
if (live) return { error: `this address already has a publish identity; another can be had in ${Math.ceil((live.until - now) / 60_000)} min`, status: 429, until: live.until }
|
|
136
|
+
if (this.publishing.size >= this.maxClients) {
|
|
137
|
+
const first = this.publishing.values().next().value!
|
|
138
|
+
return { error: `the relay is giving out as many publish identities as it can hold; try again in ${Math.ceil((first.until - now) / 60_000)} min`, status: 503, until: first.until }
|
|
139
|
+
}
|
|
140
|
+
const until = now + PUBLISH_TTL_MS
|
|
141
|
+
const token = mintToken(this.secret, { uid: `${key}/publish.${instance}`, grp: key, kind: 'publish', ttlMs: PUBLISH_TTL_MS, now })
|
|
142
|
+
this.publishing.set(key, { instance, token, until })
|
|
143
|
+
return { token, until }
|
|
144
|
+
}
|
|
116
145
|
}
|
package/relay/wisp.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// W59: the Wisp v2 adapter (src/net/wisp.js, docs/wisp-wmd-extensions.md) over the relay engine.
|
|
2
2
|
//
|
|
3
3
|
// Who may use it (plan §5):
|
|
4
|
-
// - token: the HMAC `t.<token>` subprotocol, as wmd-relay.v1 (wmn's bundled relay)
|
|
4
|
+
// - token: the HMAC `t.<token>` subprotocol, as wmd-relay.v1 (wmn's bundled relay); on a hosted relay, the publish
|
|
5
|
+
// identity it issues itself (relay.ts /publish-token), used only from the address it was given to
|
|
5
6
|
// - open: no key (the standalone relay's default); identity = the client's instance id (0xE7)
|
|
6
7
|
// plus its address group, bandwidth shared per address
|
|
7
8
|
// - keys (optional): Wisp password auth, scrypt hashes; a relay with keys refuses keyless clients
|
|
@@ -23,7 +24,7 @@ import { type AllowEntry, type Engine, type Principal, type Session, type Shared
|
|
|
23
24
|
import { type Governor, type Why, resolveClient } from './hosted.ts'
|
|
24
25
|
import { type OwnerKey, verifySecret } from './keys.ts'
|
|
25
26
|
import type { Cidr } from './policy.ts'
|
|
26
|
-
import { clientKey, verifyToken } from './token.ts'
|
|
27
|
+
import { TokenIssuer, clientKey, verifyToken } from './token.ts'
|
|
27
28
|
|
|
28
29
|
export type WispOptions = {
|
|
29
30
|
enabled?: boolean
|
|
@@ -184,6 +185,32 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
184
185
|
return reply(body, status)
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
const issuer = w.hosted && o.secret ? new TokenIssuer(o.secret) : null
|
|
189
|
+
|
|
190
|
+
/** Hosted: GET /publish-token?instance=ID from a listed page gives {token, until}, a publish identity for this address
|
|
191
|
+
(token.ts issuePublish), or {error, until?} in words. The page's origin may read the answer. */
|
|
192
|
+
function publishToken(req: Request, srv: any, url: URL): Response {
|
|
193
|
+
const origin = req.headers.get('origin') || ''
|
|
194
|
+
if (!issuer || origin.length > 256 || !w.hosted!.siteAllowed(origin)) {
|
|
195
|
+
log({ ev: 'publish-token-refused', why: 'not-allowed-site' })
|
|
196
|
+
return reply('this page is not one of the sites this relay serves\n', 403)
|
|
197
|
+
}
|
|
198
|
+
const headers = { 'access-control-allow-origin': origin, vary: 'Origin', 'cache-control': 'no-store' }
|
|
199
|
+
if (req.method !== 'GET') return new Response(`GET only\n${w.hosted!.contactLine}`, { status: 405, headers })
|
|
200
|
+
const ip = clientIp(req, srv)
|
|
201
|
+
if (typeof ip !== 'string') {
|
|
202
|
+
for (const [k, v] of Object.entries(headers)) ip.headers.set(k, v)
|
|
203
|
+
return ip
|
|
204
|
+
}
|
|
205
|
+
const r = issuer.issuePublish(ip, url.searchParams.get('instance') || '')
|
|
206
|
+
if ('token' in r) {
|
|
207
|
+
log({ ev: 'publish-token', addr: clientKey(ip), origin })
|
|
208
|
+
return Response.json(r, { headers })
|
|
209
|
+
}
|
|
210
|
+
log({ ev: 'publish-token-refused', why: r.error, addr: clientKey(ip) })
|
|
211
|
+
return Response.json({ error: r.error, ...(r.until ? { until: r.until } : {}), abuseContact: w.hosted!.contact }, { status: r.status, headers })
|
|
212
|
+
}
|
|
213
|
+
|
|
187
214
|
async function fetch(req: Request, srv: any, url: URL): Promise<Response | undefined> {
|
|
188
215
|
const ip = clientIp(req, srv)
|
|
189
216
|
if (typeof ip !== 'string') return ip
|
|
@@ -210,11 +237,13 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
210
237
|
let uid = ''
|
|
211
238
|
let grp = ''
|
|
212
239
|
let exp = Infinity
|
|
240
|
+
let publish = false
|
|
213
241
|
let p: Principal | null = null
|
|
214
242
|
const tok = protos.find(x => x.startsWith('t.'))
|
|
215
243
|
if (tok && o.secret) {
|
|
216
244
|
const info = verifyToken(o.secret, tok.slice(2))
|
|
217
245
|
if (typeof info === 'string') return refuse(401, `${info}\n`, info, { origin })
|
|
246
|
+
if (w.hosted && info.grp !== addr) return refuse(401, 'this token was given to another address\n', 'token of another address', { addr })
|
|
218
247
|
if (engine.hasSession(info.sid)) {
|
|
219
248
|
stats.refusedSessions++
|
|
220
249
|
return reply('session already connected\n', 409)
|
|
@@ -224,7 +253,9 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
224
253
|
uid = info.uid || info.sid
|
|
225
254
|
grp = info.grp || uid
|
|
226
255
|
exp = info.exp
|
|
227
|
-
|
|
256
|
+
publish = info.kind === 'publish'
|
|
257
|
+
// a hosted publish session shares its address's bandwidth with the address's open sessions
|
|
258
|
+
const a = engine.admit(uid, grp, exp, { proto: 'wisp' }, w.hosted && publish ? sharedFor(grp) : undefined)
|
|
228
259
|
if (!a.ok) return reply(a.body, a.status)
|
|
229
260
|
p = a.p
|
|
230
261
|
} else if (rest) {
|
|
@@ -271,7 +302,7 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
271
302
|
const full = w.hosted?.claimHandshake() ?? null
|
|
272
303
|
const headers: Record<string, string> = {}
|
|
273
304
|
if (v2 && protos.length) headers['Sec-WebSocket-Protocol'] = protos.includes(SUBPROTOCOL) ? SUBPROTOCOL : protos[0]
|
|
274
|
-
const data = { kind: 'wisp', v2, mode, addr, ip, sid, uid, grp, exp, p, key, origin, hs: null as any, hsTimer: null as any, pre, claimed: !!w.hosted && !full, refuse: full as Why | null, at: performance.now() }
|
|
305
|
+
const data = { kind: 'wisp', v2, mode, addr, ip, sid, uid, grp, exp, publish, p, key, origin, hs: null as any, hsTimer: null as any, pre, claimed: !!w.hosted && !full, refuse: full as Why | null, at: performance.now() }
|
|
275
306
|
const ok = srv.upgrade(req, Object.keys(headers).length ? { data, headers } : { data }) // Bun refuses an empty headers object
|
|
276
307
|
if (!ok) {
|
|
277
308
|
release(data)
|
|
@@ -417,7 +448,7 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
417
448
|
tell(REASON_KIND.REFUSED, why.rule, why.until ?? 0, why.words)
|
|
418
449
|
ws.close(4011, why.words.slice(0, 120))
|
|
419
450
|
}
|
|
420
|
-
adm = w.hosted.admitSession({ ip: d.ip, grp: d.grp, origin: d.origin, end, tell, claimed: d.claimed })
|
|
451
|
+
adm = w.hosted.admitSession({ ip: d.ip, grp: d.grp, origin: d.origin, publish: d.publish, end, tell, claimed: d.claimed })
|
|
421
452
|
if (!adm.ok) return refuseHs(ws, client, adm)
|
|
422
453
|
d.claimed = false // the session holds the socket's count now
|
|
423
454
|
d.rel = adm.adm.release
|
|
@@ -664,6 +695,7 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
|
|
|
664
695
|
ws.close(1013, 'the relay is full')
|
|
665
696
|
},
|
|
666
697
|
fetch,
|
|
698
|
+
publishToken,
|
|
667
699
|
open: open_,
|
|
668
700
|
message,
|
|
669
701
|
close,
|
package/src/boot.js
CHANGED
|
@@ -51,9 +51,11 @@ import {
|
|
|
51
51
|
} from './host/ckpt.js'
|
|
52
52
|
import { captureJit, jitSnapStats, modulesOf, ownersNeedingHub, restoreStore } from './host/jit-snap.js'
|
|
53
53
|
export { CheckpointError, memoryStore, opfsStore } from './host/ckpt.js'
|
|
54
|
+
export { DiskError, deleteDisk, openDisk, sweepDisks } from './host/disk.js'
|
|
54
55
|
export { NetSwitch } from './net/switch.js'
|
|
55
56
|
export { ImageFiles, openImage, parseGroup, parsePasswd, resolveUser } from './image-files.js'
|
|
56
57
|
export { P9_CACHE_DEFAULT, p9InitCmdline, p9MountOptions } from './host/p9mount.js'
|
|
58
|
+
export { loadImageTar } from './registry/load-tar.js'
|
|
57
59
|
|
|
58
60
|
const PAGE = 65536
|
|
59
61
|
/** A trap with the memory this close to its maximum is reported as out of memory. */
|
|
@@ -414,12 +416,30 @@ function toShared(bytes) {
|
|
|
414
416
|
return sab
|
|
415
417
|
}
|
|
416
418
|
|
|
419
|
+
/** vm.wasm's arguments for a machine with `disk`: --swap <MiB> and wmd.disk=data appended to --append. */
|
|
420
|
+
export function diskArgs(args, disk) {
|
|
421
|
+
if (args.includes('--swap')) throw new TypeError('--swap and a disk: give one')
|
|
422
|
+
const mib = disk.bytes / 2 ** 20
|
|
423
|
+
if (!Number.isInteger(mib) || mib < 1) throw new TypeError(`disk size ${disk.bytes} is not whole MiB`)
|
|
424
|
+
const out = [...args, '--swap', String(mib)]
|
|
425
|
+
const i = out.indexOf('--append')
|
|
426
|
+
if (i < 0 || i + 1 >= out.length) throw new TypeError('a disk needs --append (the kernel line gets wmd.disk=data)')
|
|
427
|
+
// the disk holds data: a wmd.disk= of any other value would let the guest init write a swap header over it
|
|
428
|
+
const given = out[i + 1].match(/(?:^| )wmd\.disk(?:=(\S*))?(?= |$)/g)
|
|
429
|
+
if (given && given.some(g => g.trim() !== 'wmd.disk=data')) throw new TypeError(`a disk needs wmd.disk=data on the kernel line, not ${given.map(g => g.trim()).join(' ')}`)
|
|
430
|
+
if (!given) out[i + 1] = `${out[i + 1]} wmd.disk=data`
|
|
431
|
+
return out
|
|
432
|
+
}
|
|
433
|
+
|
|
417
434
|
/**
|
|
418
435
|
* @param {object} o
|
|
419
436
|
* @param {string|Uint8Array|ArrayBuffer} o.vmUrl URL of vm.wasm, or its bytes
|
|
420
437
|
* @param {Record<string,string|Uint8Array>} o.files preopen contents: path → URL or bytes
|
|
421
438
|
* @param {string[]} o.args vm.wasm arguments (after argv[0])
|
|
422
439
|
* @param {string[]} [o.env] "K=V" (the runner passes only WMD_*)
|
|
440
|
+
* @param {object} [o.disk] a disk from openDisk (host/disk.js): the guest gets it as virtio-blk
|
|
441
|
+
* /dev/vda (vm.wasm's --swap MiB sizes the device) with wmd.disk=data on the kernel line, so guest init leaves
|
|
442
|
+
* it alone. A machine with a disk takes no checkpoints: guest memory and the disk would not match
|
|
423
443
|
* @param {boolean} [o.heapReserve] W21 (default true, false in mobile WebKit: heapReserveDefault, D-516): vm.wasm grows its memory to the maximum
|
|
424
444
|
* before any thread starts (WMD_HEAP_RESERVE=1), so memory never grows under running
|
|
425
445
|
* vCPUs. Chromium checks memory.fill/copy against a per-Worker size that lags another
|
|
@@ -520,6 +540,10 @@ export async function bootWmd(o) {
|
|
|
520
540
|
o.onStderr?.(text)
|
|
521
541
|
}
|
|
522
542
|
const preopen = o.preopen || '/g'
|
|
543
|
+
if (o.disk) {
|
|
544
|
+
if (o.checkpoint || o.restore || o.restoreLazy) throw new TypeError('a machine with a disk takes no checkpoints')
|
|
545
|
+
o = { ...o, args: diskArgs(o.args, o.disk) }
|
|
546
|
+
}
|
|
523
547
|
const argv = ['vm.wasm', ...o.args]
|
|
524
548
|
const env = vmEnv(o.env, o.heapReserve ?? heapReserveDefault())
|
|
525
549
|
// the default Worker is started from a literal new URL(..., import.meta.url) so bundlers (Vite, webpack)
|
|
@@ -1550,6 +1574,7 @@ export async function bootWmd(o) {
|
|
|
1550
1574
|
tableMin,
|
|
1551
1575
|
memory,
|
|
1552
1576
|
ram: ramMem,
|
|
1577
|
+
blk: o.disk?.mailbox || null,
|
|
1553
1578
|
ctlSab,
|
|
1554
1579
|
files,
|
|
1555
1580
|
preopen,
|