@threadedstack/wmd-web 0.1.19 → 0.1.20
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 +24 -3
- package/RELEASE.json +28 -44
- package/guest/arm64/Image +1 -1
- 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/src/blob.js +12 -0
- package/src/boot.js +9 -1
- package/src/build/builder.js +197 -12
- package/src/build/context.js +3 -2
- package/src/host/disk-worker.js +8 -1
- package/src/host/disk.js +1 -1
- package/src/host/jit.js +36 -0
- package/src/image-files.js +2 -1
- package/src/net/stack.js +153 -1
- package/src/registry/load-tar.js +2 -1
- package/wmdpack.wasm +0 -0
package/README.md
CHANGED
|
@@ -159,10 +159,11 @@ The interface this host implements is in [docs/host-interface.md](docs/host-inte
|
|
|
159
159
|
|
|
160
160
|
`createBuilder` runs BuildKit (moby/buildkit v0.33.0) in a privileged builder machine in the page and builds a
|
|
161
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
|
|
162
|
+
Chromium, Firefox and WebKit all pass the same 18 build steps, and Node and Go projects for arm64 and amd64 (plan
|
|
163
|
+
`docs/plans/web-buildkit-impl.md`, section 10).
|
|
163
164
|
|
|
164
165
|
```js
|
|
165
|
-
import { createBuilder } from 'wmd-web/build'
|
|
166
|
+
import { buildImage, createBuilder } from 'wmd-web/build'
|
|
166
167
|
|
|
167
168
|
const builder = await createBuilder({
|
|
168
169
|
vmUrl, files, // as bootWmd: the engine and the builder arch's guest files
|
|
@@ -176,6 +177,9 @@ const builder = await createBuilder({
|
|
|
176
177
|
const r = await builder.build({ context: { Dockerfile: 'FROM alpine:3.20\nRUN echo hi > /x\n' } })
|
|
177
178
|
// r: {index, packs, log, path: 'fast'|'buildkit', ms, cached}
|
|
178
179
|
await builder.close()
|
|
180
|
+
|
|
181
|
+
// or, for one build: createBuilder + build + close in one call
|
|
182
|
+
const r2 = await buildImage({ ...builderOptions, context, timeoutMs: 30 * 60_000 })
|
|
179
183
|
```
|
|
180
184
|
|
|
181
185
|
- **The cache stays.** The disk outlives the builder and the visit. After a reload, or after a tab was killed
|
|
@@ -190,7 +194,24 @@ await builder.close()
|
|
|
190
194
|
for layer the same as BuildKit's (40 compared cases): under 0.4 s once its base image is pulled, where a warm
|
|
191
195
|
BuildKit takes 5 to 14 s.
|
|
192
196
|
- **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.
|
|
197
|
+
or `'secret'`. Cancelling with `signal` stops the step and leaves nothing behind. A failed build also carries `jit`
|
|
198
|
+
(the engine's last JIT messages), `net` (the guest network's counters and events), and, when the machine itself
|
|
199
|
+
stopped, `machine` (why, and its last output).
|
|
200
|
+
- **RAM and vCPUs.** The default is 1536 MiB and 2 vCPUs (`ram`, `cpus`). Small builds and the proxy project in the
|
|
201
|
+
plan fit. A build that compiles a native addon or bundles a large app needed 3072 MiB and 4 vCPUs (plan 10.5); a
|
|
202
|
+
single `RUN` step must finish within `timeoutMs`, since BuildKit resumes from its cache only between steps.
|
|
203
|
+
- **WebKit** builds Go and Node images and the real projects of the plan, but slower: its code space fills and new
|
|
204
|
+
code runs in the interpreter for a while (a `{t:'slow', reason:'webkit-code-space'}` event says so).
|
|
205
|
+
- In WebKit, node in `RUN` steps runs its JavaScript in the interpreter (D-553, `nodeInterpreted`, an
|
|
206
|
+
`{t:'node-interpreted'}` event): node's own JIT writes code faster than WebKit's code pool takes it, and a
|
|
207
|
+
1,600-package `pnpm` install did not end in 27 minutes. A preload bound at `/dev/.wmd-node.js` and named in
|
|
208
|
+
`NODE_OPTIONS` (`--require`) turns off V8's compilers; WebAssembly still works. The image and the cache keys do
|
|
209
|
+
not change. `nodeInterpreted: false` turns it off; `true` turns it on elsewhere.
|
|
210
|
+
- npm closes a TLS handshake that is not done 15 s after the connect. The guest network lets 4 handshakes go at
|
|
211
|
+
once, sends a ClientHello that waited on a fresh connection, and sends an unanswered one again once (D-552); it
|
|
212
|
+
dials a stream again when the remote closes it before any byte crossed (D-551).
|
|
213
|
+
- WebKit cannot read a Blob of 128 MiB or more whole (`NotReadableError`). Read the `tar` Blob a build returns
|
|
214
|
+
with `blobBytes(blob)` from `wmd-web/build`, which reads it in slices (D-554).
|
|
194
215
|
- **The builder images** are built with `make web-builder` (wmd builds them; Docker Desktop is not used) into
|
|
195
216
|
`web/dist/builder/builder-<arch>.tar`, and their sha256 is pinned in `src/build/pin.js`. `make web-release` puts
|
|
196
217
|
them next to the release tgz.
|
package/RELEASE.json
CHANGED
|
@@ -1,66 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wmd-web",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"commit": "
|
|
5
|
-
"previous": "wmd-web-v0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"commit": "f0089e9f12e73d951b5a3a47f962764edc1c39a3",
|
|
5
|
+
"previous": "wmd-web-v0.5.0",
|
|
6
6
|
"decisions": [
|
|
7
|
-
"D-
|
|
8
|
-
"D-
|
|
9
|
-
"D-494",
|
|
10
|
-
"D-498",
|
|
11
|
-
"D-505",
|
|
12
|
-
"D-506",
|
|
13
|
-
"D-507",
|
|
14
|
-
"D-509",
|
|
15
|
-
"D-511",
|
|
16
|
-
"D-516",
|
|
17
|
-
"D-518",
|
|
18
|
-
"D-531",
|
|
19
|
-
"D-533",
|
|
20
|
-
"D-539",
|
|
21
|
-
"D-542",
|
|
22
|
-
"D-544",
|
|
23
|
-
"D-545",
|
|
24
|
-
"D-547",
|
|
25
|
-
"D-548"
|
|
7
|
+
"D-552",
|
|
8
|
+
"D-554"
|
|
26
9
|
],
|
|
27
|
-
"vmSources": "
|
|
10
|
+
"vmSources": "543f2ee1a363719c2a973fff350d54a54d325286",
|
|
28
11
|
"guest": {
|
|
29
12
|
"arch": "arm64",
|
|
30
13
|
"files": {
|
|
31
14
|
"vm.wasm": {
|
|
32
|
-
"sha256": "
|
|
33
|
-
"bytes":
|
|
15
|
+
"sha256": "73bee71d7453a130f7991c7cead3bc12787050116217f635f9f523f584d2577d",
|
|
16
|
+
"bytes": 1562868
|
|
34
17
|
},
|
|
35
18
|
"Image": {
|
|
36
|
-
"sha256": "
|
|
19
|
+
"sha256": "6952a8d4f1bafec9c3e3b17e2c4af20ab5de8a7086956d087979804be2a01353",
|
|
37
20
|
"bytes": 39049224
|
|
38
21
|
},
|
|
39
22
|
"Image-4k": {
|
|
40
|
-
"sha256": "
|
|
23
|
+
"sha256": "47a1dcf06bcd1cc31fb4f02adbd1e035bc9e9a243fe57f3efd71b4a287581de2",
|
|
41
24
|
"bytes": 38828040
|
|
42
25
|
},
|
|
43
26
|
"initramfs.cpio.gz": {
|
|
44
|
-
"sha256": "
|
|
45
|
-
"bytes":
|
|
27
|
+
"sha256": "efa6eb2338cb01d87eeebce8035ed7a26b00edcfc3b9db963b38272715d2ee9d",
|
|
28
|
+
"bytes": 4683846
|
|
46
29
|
}
|
|
47
30
|
}
|
|
48
31
|
},
|
|
49
32
|
"files": {
|
|
50
|
-
"README.md": "
|
|
33
|
+
"README.md": "6962e410444bcf44ba6dd8498673e2de17ee4f8b96e1d29044a55b8f0dbcd8de",
|
|
51
34
|
"export-page/coi-sw.js": "0300e670b413cb2e0aae35843fc583546bbf6687df971ab6df41cfa3b0d494d7",
|
|
52
35
|
"export-page/index.html": "f5450c89e63601bbbbffef421f03491b231d1f143070012c1c5315d4b0059f43",
|
|
53
36
|
"export-page/run.css": "b57adf1e76553549c1f89ff157b7973409dedc21889cd3932948cd05b51adc6f",
|
|
54
37
|
"export-page/run.js": "717636a632a40469106ddf1383e198eaac5d83651b5866d0699ec04b82462b7f",
|
|
55
|
-
"guest/arm64/Image": "
|
|
56
|
-
"guest/arm64/Image-4k": "
|
|
57
|
-
"guest/arm64/initramfs.cpio.gz": "
|
|
58
|
-
"guest/arm64/vm.wasm": "
|
|
59
|
-
"package.json": "
|
|
38
|
+
"guest/arm64/Image": "6952a8d4f1bafec9c3e3b17e2c4af20ab5de8a7086956d087979804be2a01353",
|
|
39
|
+
"guest/arm64/Image-4k": "47a1dcf06bcd1cc31fb4f02adbd1e035bc9e9a243fe57f3efd71b4a287581de2",
|
|
40
|
+
"guest/arm64/initramfs.cpio.gz": "efa6eb2338cb01d87eeebce8035ed7a26b00edcfc3b9db963b38272715d2ee9d",
|
|
41
|
+
"guest/arm64/vm.wasm": "73bee71d7453a130f7991c7cead3bc12787050116217f635f9f523f584d2577d",
|
|
42
|
+
"package.json": "7d43c05047d0a3ea56b4a7709f48a60de32e15c3bdf328d55435c46637981b40",
|
|
60
43
|
"src/base.js": "982f8e46dbb727ec542a494ebdb884ccd19a31ff4e091425b2d1fc01b0d4754a",
|
|
61
|
-
"src/
|
|
62
|
-
"src/
|
|
63
|
-
"src/build/
|
|
44
|
+
"src/blob.js": "bb2a2f0d87ce236280a7be2f246c926010dfe07f649c27da5e70d8eb5eaf7061",
|
|
45
|
+
"src/boot.js": "b1819d2b8abd46fd42f10a986719a311bedca705fc368a4cd19cc121b04b57d2",
|
|
46
|
+
"src/build/builder.js": "0d5355c8570b0ecedce25602ff7f60c49b7daaed669426328f05f52e26048213",
|
|
47
|
+
"src/build/context.js": "d38097c61dbdadc4d41290c010425a4422f499aba473fa2b83f2a5c41e543571",
|
|
64
48
|
"src/build/dockerfile.js": "5957fe3d4ac8ff8b312f980ee04bd1d98ad4373a61f9a33e02f1ff1108b6ea53",
|
|
65
49
|
"src/build/dockerignore.js": "8fc03d09da020f43ea483979b1183891b71d76f012379ab7813838ea0e53c546",
|
|
66
50
|
"src/build/fast.js": "bf7373a43cce995b21751c317a58d18438f9fd2f670266b314b1151ef3cdb9e8",
|
|
@@ -93,8 +77,8 @@
|
|
|
93
77
|
"src/host/clock.js": "aba6af3a8b1a985ae340892222e4b482d33afe20f5c8b3a4c7403ce87d1e247c",
|
|
94
78
|
"src/host/ctl.js": "ca2ebbb940c9668cb0573c959cd2ff8982f96d58dfdb8dce39d304b89e033dd7",
|
|
95
79
|
"src/host/disk-map.js": "3d7e0d7d488ce25fc6a0e4dc0a9b1c8fc2af96bac1c03491a1d30be392af268b",
|
|
96
|
-
"src/host/disk-worker.js": "
|
|
97
|
-
"src/host/disk.js": "
|
|
80
|
+
"src/host/disk-worker.js": "6e82bfeb4aa3f8b02bd7cb2de7427def26114e016f6b05196c8485d7fc8315ee",
|
|
81
|
+
"src/host/disk.js": "e941e62cc2d9a83582d53c80c09c5f0f682508f77390237bedff4051c489b2cd",
|
|
98
82
|
"src/host/errno.js": "26021d85962084be2c43c98fb3accafc2a8f11fbf3c8a5b8d484bd6a088ae4db",
|
|
99
83
|
"src/host/fs.js": "d6753f74db35b7b01baa8709fc7fdcdfc8d638dfe42f454b0a833b22111e8f4a",
|
|
100
84
|
"src/host/gcnudge.js": "4a6569cb73ae24a11fc4ac8c467380bd9efba0bb5fa9867b797bc4f153ab15ab",
|
|
@@ -103,7 +87,7 @@
|
|
|
103
87
|
"src/host/jit-hub-core.js": "f8d39dd0e4cbd42568bf56a8a7105f321d33bcf2d5faf8756d45a1ed692bd344",
|
|
104
88
|
"src/host/jit-hub.js": "cf3fc9f9d4e3fb50c41d6de5e2523f380d3c6c1feaf61c15d90cde89aec2f4c3",
|
|
105
89
|
"src/host/jit-snap.js": "3932f36047c9edeb7f59282d1c37fef19c8f5a9fe4bd0192fb93aa64030fe51d",
|
|
106
|
-
"src/host/jit.js": "
|
|
90
|
+
"src/host/jit.js": "72ef7334506fde2b3724cee93b52a987fbd6d45473b8de93d914db8a80ae9e17",
|
|
107
91
|
"src/host/layout.js": "d5bc5aa9292630b204438df5d6278fda6daa2a65acc061e1c57e0272b32fecc8",
|
|
108
92
|
"src/host/monitor.js": "ae1741ba35668bcc6192642246494433b175668933921634c699af5a0a69acc0",
|
|
109
93
|
"src/host/p9mount.js": "43246601fc1e27d5294742e4482ed166a50c665cb5f526c3646a47dd27e06150",
|
|
@@ -113,14 +97,14 @@
|
|
|
113
97
|
"src/host/wasi.js": "fbafa239b8fb4083f7e8c910b1adab38901e8e7cffd247284a9abb2847eaf4bd",
|
|
114
98
|
"src/host/wmd-imports.js": "3a00562f7c9c7787ab3841b14dde5d903a78f43ed19c43a2f5e31fd8a14c09d2",
|
|
115
99
|
"src/host/yamux.js": "f4044f14f7e836224bdb4b1e53e59c565ee478b83b2e428fffb9f7a048768f79",
|
|
116
|
-
"src/image-files.js": "
|
|
100
|
+
"src/image-files.js": "19f90a7d3c8accae5093e53bb85fe7e158ac1dd18866283722171eccb1d7e37a",
|
|
117
101
|
"src/net/dhcp-dns.js": "fdfa7c7d5cbbb4a8774b7dee195bf2df22a0c3fe8f20ac9694b24485fa8f25d5",
|
|
118
102
|
"src/net/net-worker.js": "0a2ce998f69807e386ded7ff1bee53687a1d59ed76d7ef46e2eb6f958b14ad38",
|
|
119
103
|
"src/net/packet.js": "d658f26fa84324a88d7dbc26af9c4a9c10e977c3060f16f7db431496eacd1127",
|
|
120
104
|
"src/net/peer.js": "d83cf85639f6197fe26c877eb79f8c9afc017bf2362dc2b22a8b6bd0f45f3f19",
|
|
121
105
|
"src/net/proto.js": "abfc8cd6f4331cbc98fcb1b0bba0795ff3042cc7db10476673ead1723249eee0",
|
|
122
106
|
"src/net/relay-client.js": "4101de76dc0251db46839dc786cce677e4020795adf022bdce14bcf7edf8f864",
|
|
123
|
-
"src/net/stack.js": "
|
|
107
|
+
"src/net/stack.js": "b721e4e116b0611abfe8ec1c6f2e212efd89a11f745fef2222ca5de474d8c0a9",
|
|
124
108
|
"src/net/switch.js": "bb047fb1ceeb10a84b5475916ead916362accf0e6f42a86561653dfad0c6c50d",
|
|
125
109
|
"src/net/wisp-client.js": "653961c4b735f2f74e5ea89e75c387f15ec8b8d9ed4c45d06c7d6b2a04968d42",
|
|
126
110
|
"src/net/wisp-transport.js": "2b71201f3c686078c867e8995bf5c1cd6d47a0d415f476f3a33fbd5729a55f74",
|
|
@@ -128,7 +112,7 @@
|
|
|
128
112
|
"src/registry/client.js": "fd38550e9afd82c5cb498513d245c33357a71697b3b2e6dd78538934bcc9d879",
|
|
129
113
|
"src/registry/converter.js": "1fb8294fd5275f6772c27130cc9ba70bd46fafaf440884d42448036618229f37",
|
|
130
114
|
"src/registry/hub.js": "cd672323bd1f96bd1d7e9eed80d4f36891e919e456fc0deb378fa0c7e3b6f249",
|
|
131
|
-
"src/registry/load-tar.js": "
|
|
115
|
+
"src/registry/load-tar.js": "219d6701f5c938e27e3ca36ade8fcea8114a5f2b1511496f0dbd60137290867f",
|
|
132
116
|
"src/registry/oci.js": "6a51ea16b34a65c0e732730f3e07d7ad16c50f6943a486ec05ca6aa3e98676a1",
|
|
133
117
|
"src/registry/pack-worker.js": "fd6798851cb7815dacfcb67fc78c41654209c5504d8fd343aae524e99de1c741",
|
|
134
118
|
"src/registry/pull.js": "24f2e01a76c9f7f27896af69b989f8c97f0ca31712b3e8110aaa5b5e3148b04b",
|
package/guest/arm64/Image
CHANGED
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.20",
|
|
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.20",
|
|
13
|
+
"commit": "f0089e9f12e73d951b5a3a47f962764edc1c39a3",
|
|
14
|
+
"web": "0.5.1"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
17
|
"main": "./src/boot.js",
|
package/src/blob.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// D-554: WebKit (Playwright's WebKit 26 here) cannot read a Blob of 128 MiB or more whole: arrayBuffer() and stream()
|
|
2
|
+
// fail with NotReadableError ("The I/O read operation failed."). Slices of the same Blob read. Measured with 128, 256
|
|
3
|
+
// and 400 MiB Blobs; 64 MiB read whole. Readers of large slices (a tar entry, a layer) go through here too.
|
|
4
|
+
const SLICE = 16 << 20
|
|
5
|
+
|
|
6
|
+
/** A Blob's bytes, read in 16 MiB slices. */
|
|
7
|
+
export async function blobBytes(b) {
|
|
8
|
+
if (b.size <= SLICE) return new Uint8Array(await b.arrayBuffer())
|
|
9
|
+
const out = new Uint8Array(b.size)
|
|
10
|
+
for (let o = 0; o < b.size; o += SLICE) out.set(new Uint8Array(await b.slice(o, o + SLICE).arrayBuffer()), o)
|
|
11
|
+
return out
|
|
12
|
+
}
|
package/src/boot.js
CHANGED
|
@@ -192,6 +192,13 @@ export const WEBKIT_CODE_BUDGET_MIB = 144
|
|
|
192
192
|
*/
|
|
193
193
|
export const WEBKIT_JIT_THRESHOLD = 1000
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* D-551: how deep compiled guest calls may nest in WebKit (vm.wasm's WMD_MIRROR_DEPTH; 32, and 64 for region
|
|
197
|
+
* calls, elsewhere). An amd64 go build in WebKit trapped with "Maximum call stack size exceeded" at about 33
|
|
198
|
+
* frames of block code, which killed the machine; Chromium and Firefox ran the same build at 32.
|
|
199
|
+
*/
|
|
200
|
+
export const WEBKIT_MIRROR_DEPTH = 8
|
|
201
|
+
|
|
195
202
|
const MiB = 1 << 20
|
|
196
203
|
|
|
197
204
|
/**
|
|
@@ -342,7 +349,7 @@ export function fmaFuses() {
|
|
|
342
349
|
|
|
343
350
|
/**
|
|
344
351
|
* W32: the environment vm.wasm starts with in this browser: vmEnv's, plus
|
|
345
|
-
* WMD_JIT_THRESHOLD=WEBKIT_JIT_THRESHOLD in WebKit, W30's WMD_TRACE_MAX for the engine and
|
|
352
|
+
* WMD_JIT_THRESHOLD=WEBKIT_JIT_THRESHOLD and D-551's WMD_MIRROR_DEPTH=WEBKIT_MIRROR_DEPTH in WebKit, W30's WMD_TRACE_MAX for the engine and
|
|
346
353
|
* W33's WMD_RESCUE_MS, D-539's WMD_FMA_FUSED=1 where the engine fuses relaxed_madd (fmaFuses) and, in
|
|
347
354
|
* Chromium, D-542's WMD_RGN_DUP=1 (regions copy sampled-hot compiled blocks of their page; Firefox's go ran
|
|
348
355
|
* slower with it, WebKit did not move), each unless the caller sets it. Kept out of the boot key (bootKeyOf hashes vmEnv's list), so a checkpoint or base
|
|
@@ -355,6 +362,7 @@ export function browserEnv(env, nav = globalThis.navigator, fused = fmaFuses())
|
|
|
355
362
|
const has = k => env.some(e => e.startsWith(`${k}=`))
|
|
356
363
|
const add = []
|
|
357
364
|
if (isWebKit(nav) && !has('WMD_JIT_THRESHOLD')) add.push(`WMD_JIT_THRESHOLD=${WEBKIT_JIT_THRESHOLD}`)
|
|
365
|
+
if (isWebKit(nav) && !has('WMD_MIRROR_DEPTH')) add.push(`WMD_MIRROR_DEPTH=${WEBKIT_MIRROR_DEPTH}`) // D-551
|
|
358
366
|
if (!has('WMD_TRACE_MAX')) add.push(`WMD_TRACE_MAX=${BROWSER_TRACE_MAX[engineOf(nav)]}`)
|
|
359
367
|
if (!has('WMD_RESCUE_MS')) add.push(`WMD_RESCUE_MS=${BROWSER_RESCUE_MS}`)
|
|
360
368
|
if (!has('WMD_FMA_FUSED') && fused) add.push('WMD_FMA_FUSED=1') // D-539
|
package/src/build/builder.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// the image back as a Docker tar on buildctl's stdout and converts it with loadImageTar. A Dockerfile with no RUN
|
|
7
7
|
// is built in the page instead (fast.js), unless fast: false.
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { blobBytes } from '../blob.js'
|
|
10
|
+
import { bootWmd, isWebKit } from '../boot.js'
|
|
10
11
|
import { MAIN_PROC_ID, containerBundle, fillRunRequest } from '../host/ctl.js'
|
|
11
12
|
import { DiskError, deleteDisk, openDisk, sweepDisks } from '../host/disk.js'
|
|
12
13
|
import { p9MountOptions } from '../host/p9mount.js'
|
|
@@ -15,6 +16,8 @@ import { packContext, readTarFile } from './context.js'
|
|
|
15
16
|
import { BUILDER_PINS } from './pin.js'
|
|
16
17
|
import { FastRefused, fastBuild, fastEligible } from './fast.js'
|
|
17
18
|
|
|
19
|
+
export { blobBytes } // D-554: read the tar Blob a build returns with it (WebKit cannot read 128 MiB or more whole)
|
|
20
|
+
|
|
18
21
|
export const ARCHES = {
|
|
19
22
|
arm64: {
|
|
20
23
|
platform: 'linux/arm64',
|
|
@@ -41,13 +44,68 @@ const FULL_MIB = 64 // /bk with less free than this during a failed build: the b
|
|
|
41
44
|
const enc = new TextEncoder()
|
|
42
45
|
/** host/jit.js's line when the engine's code pool is full (D-515). */
|
|
43
46
|
export const POOL_FULL_RE = /wmd-web: jit: the engine's code pool is full/
|
|
47
|
+
const JIT_LINE_RE = /^(?:vm\.wasm: JIT|wmd-web: jit:)/
|
|
48
|
+
const JIT_LINES = 60 // guest/host JIT messages kept (a ring)
|
|
49
|
+
const JIT_EVENTS = 200 // 'jit' events sent in a builder's life: the vm repeats some of them at every release
|
|
50
|
+
const JIT_KINDS = [
|
|
51
|
+
[/the engine's code pool is full/, 'pool-full'],
|
|
52
|
+
[/the engine compiles code again/, 'pool-back'],
|
|
53
|
+
[/blocks run in the interpreter from now on/, 'interpreter-from-now-on'], // vm cache_release: release_dead
|
|
54
|
+
[/waiting before releasing again/, 'release-deferred'], // D-445 grace window
|
|
55
|
+
[/JIT release/, 'release'],
|
|
56
|
+
[/module budget reached|budget \d+ KiB from now on|refused more code/, 'budget'],
|
|
57
|
+
]
|
|
58
|
+
/** What a JIT message line says, or null for a line that is not one. */
|
|
59
|
+
export function jitKind(line) {
|
|
60
|
+
if (!JIT_LINE_RE.test(line)) return null
|
|
61
|
+
return JIT_KINDS.find(([re]) => re.test(line))?.[1] || 'other'
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The guest's stderr into whole lines: the JIT's messages are kept in a ring with the time (performance.now, ms)
|
|
65
|
+
* and sent as {t:'jit', what, line} events, so a build that crawls in the interpreter can be told from one that hangs.
|
|
66
|
+
*/
|
|
67
|
+
export function jitWatch({ onEvent = () => {}, max = JIT_LINES, maxEvents = JIT_EVENTS, now = () => Math.round(performance.now()) } = {}) {
|
|
68
|
+
const ring = []
|
|
69
|
+
let buf = ''
|
|
70
|
+
let events = 0
|
|
71
|
+
let skipped = 0
|
|
72
|
+
const line = l => {
|
|
73
|
+
const what = jitKind(l)
|
|
74
|
+
if (!what) return
|
|
75
|
+
const at = now()
|
|
76
|
+
ring.push({ at, what, line: l.slice(0, 400) })
|
|
77
|
+
if (ring.length > max) ring.shift()
|
|
78
|
+
if (what === 'other') return
|
|
79
|
+
if (events < maxEvents || what === 'interpreter-from-now-on') {
|
|
80
|
+
events++
|
|
81
|
+
onEvent({ t: 'jit', what, line: l.slice(0, 400) })
|
|
82
|
+
} else skipped++
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
push(text) {
|
|
86
|
+
buf += text
|
|
87
|
+
let i
|
|
88
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
89
|
+
line(buf.slice(0, i))
|
|
90
|
+
buf = buf.slice(i + 1)
|
|
91
|
+
}
|
|
92
|
+
if (buf.length > 4096) buf = buf.slice(-4096)
|
|
93
|
+
},
|
|
94
|
+
lines: () => ring.slice(),
|
|
95
|
+
skipped: () => skipped,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
44
98
|
|
|
45
99
|
export class BuildError extends Error {
|
|
46
|
-
constructor(message, { step = null, tail = '', reason = null } = {}) {
|
|
100
|
+
constructor(message, { step = null, tail = '', reason = null, jit = null, machine = null } = {}) {
|
|
47
101
|
super(message)
|
|
48
102
|
this.name = 'BuildError'
|
|
49
103
|
this.step = step
|
|
50
104
|
this.tail = tail
|
|
105
|
+
this.jit = jit // the builder's last JIT messages ([{at, what, line}]), when it kept them
|
|
106
|
+
// when buildctl's stream ended without an exit (code 125): why it closed, whether the machine ended, the
|
|
107
|
+
// machine's stderr tail and the start script's last lines
|
|
108
|
+
this.machine = machine
|
|
51
109
|
this.reason = reason // 'no-space' | 'quota' | 'disk' | 'timeout' | 'pin' | 'secret' | null
|
|
52
110
|
}
|
|
53
111
|
}
|
|
@@ -220,7 +278,7 @@ async function indexDigest(index) {
|
|
|
220
278
|
async function bytesOf(x, fetchFn) {
|
|
221
279
|
if (x instanceof Uint8Array) return x
|
|
222
280
|
if (x instanceof ArrayBuffer) return new Uint8Array(x)
|
|
223
|
-
if (typeof Blob !== 'undefined' && x instanceof Blob) return
|
|
281
|
+
if (typeof Blob !== 'undefined' && x instanceof Blob) return blobBytes(x)
|
|
224
282
|
const r = await fetchFn(x)
|
|
225
283
|
if (!r.ok) throw new Error(`${x}: ${r.status} ${r.statusText}`)
|
|
226
284
|
return new Uint8Array(await r.arrayBuffer())
|
|
@@ -348,10 +406,88 @@ function aborted(signal) {
|
|
|
348
406
|
* @param {string[]} [o.registryMirrors] Docker Hub mirrors (host[:port], in order) BuildKit tries before docker.io; list a plain-http one
|
|
349
407
|
* in httpRegistries too
|
|
350
408
|
* @param {Record<string,string>} [o.registryCAs] host -> PEM: CAs BuildKit checks that registry's certificate against
|
|
409
|
+
* @param {boolean} [o.nodeInterpreted] D-553: node in RUN steps runs its JavaScript in the interpreter (a preload turns
|
|
410
|
+
* off V8's compilers; WebAssembly stays). Default: on in WebKit, off elsewhere.
|
|
411
|
+
* Without it, WebKit cannot keep up with the code node's JIT writes, and an npm
|
|
412
|
+
* install of 1,600 packages does not end in 30 minutes.
|
|
351
413
|
* @param {Function} [o.onEvent]
|
|
352
414
|
* @param {AbortSignal} [o.signal]
|
|
353
415
|
* @param {object} [o.boot] extra bootWmd options (tests)
|
|
354
416
|
*/
|
|
417
|
+
/**
|
|
418
|
+
* D-553: the preload node runs in a RUN step (NODE_OPTIONS --require). It turns off V8's compilers, so node runs its
|
|
419
|
+
* JavaScript in the interpreter and writes no new machine code; WebAssembly stays (node-gyp's undici needs it, which
|
|
420
|
+
* --jitless removed). Each flag only where that V8 has it (V8 prints an error for an unknown one): measured on node
|
|
421
|
+
* 12, 14, 16, 18, 20, 22 and 24.
|
|
422
|
+
*/
|
|
423
|
+
export const NODE_PRELOAD = `try {
|
|
424
|
+
var v = process.versions.v8.split('.').map(Number), m = v[0] * 100 + v[1], f = ['--no-opt']
|
|
425
|
+
if (m >= 708) f.push('--regexp-interpret-all')
|
|
426
|
+
if (m >= 904) f.push('--no-sparkplug', '--liftoff-only')
|
|
427
|
+
if (m >= 1100) f.push('--no-maglev')
|
|
428
|
+
require('v8').setFlagsFromString(f.join(' '))
|
|
429
|
+
} catch (e) {}
|
|
430
|
+
`
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* D-553: buildkit-runc that makes node in a RUN step load NODE_PRELOAD (runc ... --bundle <dir> ...). BuildKit writes
|
|
434
|
+
* config.json on one line (json.Encoder). awk walks it as JSON, strings and escapes included (inside a string a quote
|
|
435
|
+
* is \", so the keys it looks for cannot occur there):
|
|
436
|
+
* - it appends a read-only bind of /usr/local/lib/wmd-node.js at /dev/.wmd-node.js to the end of "mounts", after
|
|
437
|
+
* BuildKit's /dev tmpfs, so the file is in that tmpfs and never in the step's layer;
|
|
438
|
+
* - then it adds --require=/dev/.wmd-node.js to the first "env":[ (process.env)'s NODE_OPTIONS, or a NODE_OPTIONS
|
|
439
|
+
* element at the front.
|
|
440
|
+
* Without a "mounts" array it changes nothing (a --require of a missing file would stop node).
|
|
441
|
+
*/
|
|
442
|
+
export const RUNC_NODE = String.raw`#!/bin/sh
|
|
443
|
+
b=
|
|
444
|
+
p=
|
|
445
|
+
for a in "$@"; do
|
|
446
|
+
case "$a" in --bundle=*) b=$(printf %s "$a" | cut -c10-) ;; esac
|
|
447
|
+
case "$p" in --bundle|-b) b=$a ;; esac
|
|
448
|
+
p=$a
|
|
449
|
+
done
|
|
450
|
+
c="$b/config.json"
|
|
451
|
+
if [ -n "$b" ] && [ -f "$c" ]; then
|
|
452
|
+
awk -v M='{"destination":"/dev/.wmd-node.js","type":"bind","source":"/usr/local/lib/wmd-node.js","options":["rbind","ro"]}' -v R='--require=/dev/.wmd-node.js' '
|
|
453
|
+
function shut(s, st, n, i, ch, ins, esc, d) {
|
|
454
|
+
n = length(s); ins = 0; esc = 0; d = 0
|
|
455
|
+
for (i = st; i <= n; i++) {
|
|
456
|
+
ch = substr(s, i, 1)
|
|
457
|
+
if (ins) { if (esc) esc = 0; else if (ch == "\\") esc = 1; else if (ch == "\"") ins = 0 }
|
|
458
|
+
else if (ch == "\"") ins = 1
|
|
459
|
+
else if (ch == "[" || ch == "{") d++
|
|
460
|
+
else if (ch == "]" || ch == "}") { if (!d) return i; d-- }
|
|
461
|
+
}
|
|
462
|
+
return 0
|
|
463
|
+
}
|
|
464
|
+
{
|
|
465
|
+
s = $0
|
|
466
|
+
k = index(s, ",\"mounts\":["); if (!k) k = index(s, "{\"mounts\":[")
|
|
467
|
+
if (!k || index(s, "\"destination\":\"/dev/.wmd-node.js\"")) { print s; next }
|
|
468
|
+
st = k + 11; e = shut(s, st); if (!e) { print s; next }
|
|
469
|
+
s = substr(s, 1, e - 1) (e == st ? "" : ",") M substr(s, e)
|
|
470
|
+
k = index(s, "\"env\":["); if (!k) { print s; next }
|
|
471
|
+
st = k + 7; n = length(s); ins = 0; esc = 0; found = 0; q = 0; L = length(R) + 1
|
|
472
|
+
for (i = st; i <= n; i++) {
|
|
473
|
+
ch = substr(s, i, 1)
|
|
474
|
+
if (ins) {
|
|
475
|
+
if (esc) esc = 0
|
|
476
|
+
else if (ch == "\\") esc = 1
|
|
477
|
+
else if (ch == "\"") {
|
|
478
|
+
ins = 0
|
|
479
|
+
if (substr(s, q, 14) == "\"NODE_OPTIONS=") { found = 1; s = substr(s, 1, i - 1) " " R substr(s, i); i += L; n += L }
|
|
480
|
+
}
|
|
481
|
+
} else if (ch == "\"") { ins = 1; q = i }
|
|
482
|
+
else if (ch == "]") break
|
|
483
|
+
}
|
|
484
|
+
if (!found) s = substr(s, 1, st - 1) "\"NODE_OPTIONS=" R "\"" (substr(s, st, 1) == "]" ? "" : ",") substr(s, st)
|
|
485
|
+
print s
|
|
486
|
+
}' "$c" > "$c.wmd" && mv "$c.wmd" "$c"
|
|
487
|
+
fi
|
|
488
|
+
exec /usr/bin/buildkit-runc "$@"
|
|
489
|
+
`
|
|
490
|
+
|
|
355
491
|
export async function createBuilder(o) {
|
|
356
492
|
const archName = o.arch || 'arm64'
|
|
357
493
|
const arch = ARCHES[archName]
|
|
@@ -359,11 +495,13 @@ export async function createBuilder(o) {
|
|
|
359
495
|
if (o.disk !== false) checkNamespace(o.disk?.namespace)
|
|
360
496
|
if (!o.relay?.url) throw new TypeError('relay: {url}: the builder pulls base images through it')
|
|
361
497
|
const fetchFn = o.fetch || globalThis.fetch.bind(globalThis)
|
|
498
|
+
// every event carries `at`: performance.now() in ms (`ms` is a duration in some events)
|
|
362
499
|
const emit = ev => {
|
|
363
500
|
try {
|
|
364
|
-
o.onEvent?.(ev)
|
|
501
|
+
o.onEvent?.({ at: Math.round(performance.now()), ...ev })
|
|
365
502
|
} catch {}
|
|
366
503
|
}
|
|
504
|
+
const jit = jitWatch({ onEvent: emit })
|
|
367
505
|
const ram = o.ram || 1536
|
|
368
506
|
const cpus = o.cpus || 2
|
|
369
507
|
const t0 = performance.now()
|
|
@@ -398,7 +536,16 @@ export async function createBuilder(o) {
|
|
|
398
536
|
...(o.registryMirrors?.length ? [`WMD_BK_MIRRORS=${o.registryMirrors.map(checkHost).join(',')}`] : []),
|
|
399
537
|
...(o.registryCAs && Object.keys(o.registryCAs).length ? [`WMD_BK_CA=${Object.entries(o.registryCAs).map(([h, pem]) => `${checkHost(h)}=${b64utf8(pem)}`).join(' ')}`] : []),
|
|
400
538
|
]
|
|
401
|
-
const
|
|
539
|
+
const interp = o.nodeInterpreted ?? isWebKit()
|
|
540
|
+
// D-553: BuildKit starts each RUN step with buildkit-runc from PATH; a wrapper earlier in PATH makes node in the step
|
|
541
|
+
// load a preload that turns off V8's compilers, then runs the real one. The image and the cache keys stay as they are.
|
|
542
|
+
const argv = interp
|
|
543
|
+
? ['/bin/sh', '-c', 'mkdir -p /usr/local/lib && printf %s "$WMD_NODE_PRELOAD" > /usr/local/lib/wmd-node.js && printf %s "$WMD_RUNC_WRAP" > /usr/local/bin/buildkit-runc && chmod 0755 /usr/local/bin/buildkit-runc && exec /usr/local/bin/wmd-buildkitd']
|
|
544
|
+
: ['/usr/local/bin/wmd-buildkitd']
|
|
545
|
+
if (interp) {
|
|
546
|
+
env.push(`WMD_RUNC_WRAP=${RUNC_NODE}`, `WMD_NODE_PRELOAD=${NODE_PRELOAD}`)
|
|
547
|
+
emit({ t: 'node-interpreted', message: "node in RUN steps runs its JavaScript in the interpreter: WebKit's code pool cannot keep up with node's JIT" })
|
|
548
|
+
}
|
|
402
549
|
const b = containerBundle({ args: argv, env, cwd: '/', hostname: 'buildkitsandbox', networkDisabled: false, ip: '192.168.127.2', gateway: '192.168.127.1', privileged: true })
|
|
403
550
|
const append = [...arch.console, 'rdinit=/sbin/init', 'loglevel=4', 'norandmaps', 'wmd.init', 'wmd.p9tag=wmdroot', 'wmd.control=vport', 'wmd.exit'].join(' ')
|
|
404
551
|
const args = [
|
|
@@ -425,6 +572,7 @@ export async function createBuilder(o) {
|
|
|
425
572
|
...(disk ? { disk } : {}),
|
|
426
573
|
onStderr: s => {
|
|
427
574
|
stderr = (stderr + s).slice(-16384)
|
|
575
|
+
jit.push(s)
|
|
428
576
|
// WebKit: the engine's code pool filled, so new code waits and the guest runs slower (D-515; plan 5.10)
|
|
429
577
|
if (!slowSaid && POOL_FULL_RE.test(s)) {
|
|
430
578
|
slowSaid = true
|
|
@@ -482,6 +630,8 @@ export async function createBuilder(o) {
|
|
|
482
630
|
throw e
|
|
483
631
|
}
|
|
484
632
|
emit({ t: 'ready', ms: Math.round(performance.now() - t0), platform: arch.platform })
|
|
633
|
+
let endedWith = null
|
|
634
|
+
ended.then(r => (endedWith = { code: r?.code ?? null, reason: r?.reason ?? null }), () => {})
|
|
485
635
|
|
|
486
636
|
let seq = 0
|
|
487
637
|
let busy = false
|
|
@@ -539,14 +689,14 @@ export async function createBuilder(o) {
|
|
|
539
689
|
clearTimeout(killTimer)
|
|
540
690
|
signal?.removeEventListener('abort', kill)
|
|
541
691
|
}
|
|
542
|
-
if (!binary) return { code: r.code ?? null, out, err }
|
|
692
|
+
if (!binary) return { code: r.code ?? null, out, err, error: r.error ?? null }
|
|
543
693
|
const bytes = new Uint8Array(size)
|
|
544
694
|
let at = 0
|
|
545
695
|
for (const c of chunks) {
|
|
546
696
|
bytes.set(c, at)
|
|
547
697
|
at += c.length
|
|
548
698
|
}
|
|
549
|
-
return { code: r.code ?? null, out: bytes, err }
|
|
699
|
+
return { code: r.code ?? null, out: bytes, err, error: r.error ?? null }
|
|
550
700
|
})()
|
|
551
701
|
}
|
|
552
702
|
const sh = (cmd, opts) => inGuest(['/bin/sh', '-c', cmd], opts)
|
|
@@ -563,6 +713,15 @@ export async function createBuilder(o) {
|
|
|
563
713
|
inGuest,
|
|
564
714
|
sh,
|
|
565
715
|
log: () => mainLog.lines,
|
|
716
|
+
jitLines: () => jit.lines(),
|
|
717
|
+
/** The machine's state for a failure report: {ended ({code, reason} or null), stderr tail, start script's tail}. */
|
|
718
|
+
machineState: () => ({ ended: endedWith, stderr: stderr.slice(-4000), main: mainLog.tail.slice(-3000) }),
|
|
719
|
+
/** The guest network's counters and its last 40 events (resets, refusals, D-551 redials), or null. */
|
|
720
|
+
netStats: async () => {
|
|
721
|
+
const n = await vm.net?.stats?.().catch(() => null)
|
|
722
|
+
if (!n?.stack) return n ?? null
|
|
723
|
+
return { stack: { ...n.stack, events: (n.stack.events || []).slice(-40) }, relay: n.relay ?? null }
|
|
724
|
+
},
|
|
566
725
|
/** Free MiB and free inodes on /bk. */
|
|
567
726
|
async room() {
|
|
568
727
|
const r = await sh("df -Pm /bk | awk 'NR==2{print $4}'; df -Pi /bk | awk 'NR==2{print $4, $2}'")
|
|
@@ -609,6 +768,7 @@ async function keepBuilt(store, index, packs) {
|
|
|
609
768
|
async function runBuild(builder, req, emit) {
|
|
610
769
|
const t0 = performance.now()
|
|
611
770
|
const onEvent = ev => {
|
|
771
|
+
ev = { at: Math.round(performance.now()), ...ev }
|
|
612
772
|
emit(ev)
|
|
613
773
|
try {
|
|
614
774
|
req.onEvent?.(ev)
|
|
@@ -650,7 +810,18 @@ async function runBuild(builder, req, emit) {
|
|
|
650
810
|
const dir = `/run/ctx/${id}`
|
|
651
811
|
// req.signal and timeoutMs cover everything from here: the context, the upload, buildctl and the conversion
|
|
652
812
|
const ac = new AbortController()
|
|
653
|
-
|
|
813
|
+
let lastStep = null
|
|
814
|
+
const timer = req.timeoutMs
|
|
815
|
+
? setTimeout(
|
|
816
|
+
() => {
|
|
817
|
+
const e = new BuildError(`the build ran past ${req.timeoutMs} ms${lastStep ? `, in the step ${lastStep}` : ''}`, { reason: 'timeout', step: lastStep, tail: log.tail.slice(-4000), jit: builder.jitLines?.() ?? null })
|
|
818
|
+
ac.abort(e)
|
|
819
|
+
// the network's counters join the error while buildctl is stopped (SIGINT, then 5 s), before it is thrown
|
|
820
|
+
builder.netStats?.().then(n => (e.net = n), () => {})
|
|
821
|
+
},
|
|
822
|
+
req.timeoutMs,
|
|
823
|
+
)
|
|
824
|
+
: null
|
|
654
825
|
const onAbort = () => ac.abort(new BuildCancelled())
|
|
655
826
|
req.signal?.addEventListener('abort', onAbort, { once: true })
|
|
656
827
|
const check = () => {
|
|
@@ -698,7 +869,6 @@ async function runBuild(builder, req, emit) {
|
|
|
698
869
|
if (req.push?.auth) xenv.push(`DOCKER_CONFIG=${dir}/auth`)
|
|
699
870
|
args.push('--output', `type=docker,${req.tag ? `name=${req.tag},` : ''}dest=-`)
|
|
700
871
|
if (req.push) args.push('--output', `type=image,name=${req.push.name},push=true${req.push.insecure ? ',registry.insecure=true' : ''}`)
|
|
701
|
-
let lastStep = null
|
|
702
872
|
const errLines = [] // buildctl's own error lines (no step), not the steps' output
|
|
703
873
|
const pr = progressReader({
|
|
704
874
|
onStep: ev => {
|
|
@@ -737,8 +907,22 @@ async function runBuild(builder, req, emit) {
|
|
|
737
907
|
if (builder.disk?.events?.slice?.(diskMark).some(e => e.reason === 'quota')) reason = 'quota'
|
|
738
908
|
if (!reason && /input\/output error|read-only file system/i.test(tail)) reason = 'disk'
|
|
739
909
|
const failed = [...pr.vertexes.values()].find(v => v.error)
|
|
740
|
-
const why = failed?.error || tail.split('\n').filter(Boolean).pop() || `buildctl exited ${r.code}`
|
|
741
|
-
|
|
910
|
+
const why = failed?.error || tail.split('\n').filter(Boolean).pop() || `buildctl exited ${r.code}${r.error ? ` (${r.error})` : ''}`
|
|
911
|
+
// a stream that ended with no exit frame: the machine's side says why
|
|
912
|
+
let machine = null
|
|
913
|
+
if (r.code === 125 && r.error) {
|
|
914
|
+
await new Promise(res => setTimeout(res, 2000)) // let a machine that is going down report it
|
|
915
|
+
machine = { stream: r.error, ...(builder.machineState?.() ?? {}) }
|
|
916
|
+
}
|
|
917
|
+
const err = new BuildError(`the build failed${failed ? ` at ${failed.name}` : ''}: ${why.slice(0, 500)}`, { step: failed?.name || lastStep, tail, reason, jit: builder.jitLines?.() ?? null, machine })
|
|
918
|
+
err.net = (await builder.netStats?.().catch(() => null)) ?? null
|
|
919
|
+
// buildkitd's side (a session it closed, a worker error): its last log lines, when the machine still answers
|
|
920
|
+
if (r.code !== 125) {
|
|
921
|
+
const d = await Promise.race([builder.sh('tail -n 40 /run/buildkitd.log').catch(() => null), new Promise(res => setTimeout(res, 10_000, null))])
|
|
922
|
+
err.daemonLog = d?.out ? d.out.slice(-6000) : null
|
|
923
|
+
}
|
|
924
|
+
check() // a cancel or timeout during these waits wins
|
|
925
|
+
throw err
|
|
742
926
|
}
|
|
743
927
|
onEvent({ t: 'export', bytes: r.out.length })
|
|
744
928
|
const t1 = performance.now()
|
|
@@ -784,7 +968,8 @@ async function ensureRoom(builder, onEvent, signal) {
|
|
|
784
968
|
export async function buildImage(o) {
|
|
785
969
|
const builder = await createBuilder(o)
|
|
786
970
|
try {
|
|
787
|
-
|
|
971
|
+
// o.onEvent already hears every build event through the builder: given to build() too, each would come twice
|
|
972
|
+
return await builder.build({ ...o, onEvent: undefined })
|
|
788
973
|
} finally {
|
|
789
974
|
await builder.close()
|
|
790
975
|
}
|
package/src/build/context.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The build context (docs/plans/web-buildkit-impl.md 5.6): turn a file source, a plain object or a tar into the
|
|
2
2
|
// one tar the page sends to the builder. .dockerignore is applied here with a port of moby's patternmatcher, so
|
|
3
3
|
// ignored files are never read; BuildKit applies it again and stays the source of truth.
|
|
4
|
+
import { blobBytes } from '../blob.js'
|
|
4
5
|
import { matcher, parseDockerignore } from './dockerignore.js'
|
|
5
6
|
import { decodeText, scanTar, tarWriter } from './tar.js'
|
|
6
7
|
|
|
@@ -108,7 +109,7 @@ function tarReader(input) {
|
|
|
108
109
|
const u8 = input instanceof ArrayBuffer ? new Uint8Array(input) : input
|
|
109
110
|
const total = blob ? input.size : u8.length
|
|
110
111
|
const read = blob
|
|
111
|
-
?
|
|
112
|
+
? (o, n) => blobBytes(input.slice(o, o + n))
|
|
112
113
|
: (o, n) => u8.subarray(o, o + n)
|
|
113
114
|
return { blob, u8, total, read }
|
|
114
115
|
}
|
|
@@ -175,7 +176,7 @@ async function packTar(input, dockerfile, lim) {
|
|
|
175
176
|
if (err instanceof ContextError) throw err
|
|
176
177
|
throw new ContextError('bad-source', '', `build context: ${err.message}`)
|
|
177
178
|
}
|
|
178
|
-
const tar = blob ?
|
|
179
|
+
const tar = blob ? await blobBytes(input) : u8
|
|
179
180
|
return { tar, files, bytes, ignored: 0, dockerfile }
|
|
180
181
|
}
|
|
181
182
|
|
package/src/host/disk-worker.js
CHANGED
|
@@ -71,7 +71,14 @@ function serve(mailbox, disk, closeFiles) {
|
|
|
71
71
|
let ops = 0
|
|
72
72
|
const post = () => self.postMessage({ t: 'stats', stats: { ...stats, ...disk.usage() } })
|
|
73
73
|
for (;;) {
|
|
74
|
-
|
|
74
|
+
const ctl = Atomics.load(i32, 5)
|
|
75
|
+
if (ctl === 1) break
|
|
76
|
+
if (ctl === 2) {
|
|
77
|
+
// tests (gate 9): the Worker ends with no word, as a crashed one does; a vCPU waiting on it gives up after WAIT_MS
|
|
78
|
+
closeFiles()
|
|
79
|
+
self.close()
|
|
80
|
+
return
|
|
81
|
+
}
|
|
75
82
|
const s = Atomics.load(i32, 1)
|
|
76
83
|
if (s === STATE.GONE) break
|
|
77
84
|
if (s !== STATE.POSTED) {
|
package/src/host/disk.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// i32[2] op 0 read, 1 write, 2 flush, 3 discard (blk.c's BLK_OP_*)
|
|
11
11
|
// i32[3] len bytes (u32)
|
|
12
12
|
// i32[4] result 0 or a negative errno
|
|
13
|
-
// i32[5] control 1: the page asks the Worker to flush and close
|
|
13
|
+
// i32[5] control 1: the page asks the Worker to flush and close; 2 (tests): it ends at once and answers nothing
|
|
14
14
|
// f64[3] off byte offset (bytes 24..31)
|
|
15
15
|
// data from byte 64, DATA bytes; longer reads and writes go in pieces
|
|
16
16
|
//
|
package/src/host/jit.js
CHANGED
|
@@ -69,6 +69,8 @@ const POOL_CHECK_MS = 2000 // D-515: at most one code pool check a thread in thi
|
|
|
69
69
|
const POOL_PROBE_N = 1500000 // D-515: loop passes of the check: about 2 ms compiled, 40 ms interpreted
|
|
70
70
|
const POOL_SLOW_X = 6 // D-515: a check this many times slower than this thread's fastest means the pool is full
|
|
71
71
|
const POOL_LEARN = 3 / 4 // D-515: the budget from then on, of the modules live when the pool filled
|
|
72
|
+
const POOL_RECOVER_N = 5 // a learned budget grows again after this many checks in a row that found room (10 s at POOL_CHECK_MS)
|
|
73
|
+
const POOL_RECOVER_X = 4 / 3 // ... by this factor each time (the inverse of POOL_LEARN), never past maxLiveBytes
|
|
72
74
|
|
|
73
75
|
// counters after the header: each a pair of Int32 cells (low word, unsigned; high word).
|
|
74
76
|
// D-419: not BigInt64 cells: Atomics.add on a BigInt64Array returns a new BigInt, garbage a
|
|
@@ -404,6 +406,38 @@ export function createJitDoor({ store, memory, ram = null, log = () => {}, compi
|
|
|
404
406
|
|
|
405
407
|
let poolNext = 0
|
|
406
408
|
let poolFast = Infinity
|
|
409
|
+
let poolRoom = 0 // checks in a row that found room, with the shared budget unchanged
|
|
410
|
+
let budgetSeen = -1 // the shared budget at the last check: a change (another Worker grew or lowered it) starts the count again
|
|
411
|
+
/**
|
|
412
|
+
* The engine has had room for POOL_RECOVER_N checks in a row: a budget learned while released code
|
|
413
|
+
* was not yet freed (WebKit frees it 30-40 s late, D-445) grows back by POOL_RECOVER_X, and is
|
|
414
|
+
* dropped once it reaches maxLiveBytes (the original cap). Without a cap there is nothing to
|
|
415
|
+
* return to.
|
|
416
|
+
*/
|
|
417
|
+
function recoverBudget() {
|
|
418
|
+
if (!maxLiveBytes) return
|
|
419
|
+
// the budget is shared and every Worker checks: without this, N Workers would grow it N times as fast
|
|
420
|
+
const seen = Atomics.load(hdr, H_BUDGET_KIB)
|
|
421
|
+
if (seen !== budgetSeen) {
|
|
422
|
+
budgetSeen = seen
|
|
423
|
+
poolRoom = 0
|
|
424
|
+
}
|
|
425
|
+
if (++poolRoom < POOL_RECOVER_N) return
|
|
426
|
+
poolRoom = 0
|
|
427
|
+
if (!seen) return
|
|
428
|
+
lock()
|
|
429
|
+
try {
|
|
430
|
+
const cur = Atomics.load(hdr, H_BUDGET_KIB)
|
|
431
|
+
if (!cur) return
|
|
432
|
+
const next = Math.ceil(cur * POOL_RECOVER_X)
|
|
433
|
+
const done = next * 1024 >= maxLiveBytes
|
|
434
|
+
Atomics.store(hdr, H_BUDGET_KIB, done ? 0 : next)
|
|
435
|
+
budgetSeen = done ? 0 : next
|
|
436
|
+
log(`wmd-web: jit: the engine has had room for ${POOL_RECOVER_N} checks; budget ${done ? `back to its cap of ${maxLiveBytes >> 10} KiB` : `${next} KiB`}`)
|
|
437
|
+
} finally {
|
|
438
|
+
unlock()
|
|
439
|
+
}
|
|
440
|
+
}
|
|
407
441
|
/**
|
|
408
442
|
* D-515: before a new module is compiled. While the engine's code pool is full (codePool, checked
|
|
409
443
|
* at most once each poolEvery) the answer is JIT_LATER: the guest interprets the blocks and asks
|
|
@@ -423,8 +457,10 @@ export function createJitDoor({ store, memory, ram = null, log = () => {}, compi
|
|
|
423
457
|
if (ms > poolFast * POOL_SLOW_X) ms = Math.min(ms, codePool()) // not a pause of this thread
|
|
424
458
|
poolFast = Math.min(poolFast, Math.max(ms, 0.05))
|
|
425
459
|
const full = ms > poolFast * POOL_SLOW_X
|
|
460
|
+
if (full) poolRoom = 0
|
|
426
461
|
const at = () => new Date().toISOString().slice(11, 19)
|
|
427
462
|
if (!full) {
|
|
463
|
+
recoverBudget()
|
|
428
464
|
if (was && Atomics.compareExchange(hdr, H_POOL_FULL, 1, 0) === 1) {
|
|
429
465
|
add('poolBack')
|
|
430
466
|
log(`wmd-web: jit: the engine compiles code again (${at()})`)
|
package/src/image-files.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// `USER name`, devcontainer `remoteUser` or `--chown=app:app` (resolveUser). W66: another image's files,
|
|
5
5
|
// for `COPY --from` and multi-stage builds (walk).
|
|
6
6
|
|
|
7
|
+
import { blobBytes } from './blob.js'
|
|
7
8
|
import { ENOENT, FsError } from './fs/errno.js'
|
|
8
9
|
import { Overlay } from './fs/overlay.js'
|
|
9
10
|
import { Pack, bytesSource } from './fs/pack.js'
|
|
@@ -19,7 +20,7 @@ async function layerBytes(l, i, fetchFn) {
|
|
|
19
20
|
}
|
|
20
21
|
if (l instanceof Uint8Array) return l
|
|
21
22
|
if (l instanceof ArrayBuffer || (typeof SharedArrayBuffer !== 'undefined' && l instanceof SharedArrayBuffer)) return new Uint8Array(l)
|
|
22
|
-
if (typeof Blob !== 'undefined' && l instanceof Blob) return
|
|
23
|
+
if (typeof Blob !== 'undefined' && l instanceof Blob) return blobBytes(l)
|
|
23
24
|
throw new TypeError(`layer ${i}: a URL, bytes or a Blob, not ${Object.prototype.toString.call(l)}`)
|
|
24
25
|
}
|
|
25
26
|
|
package/src/net/stack.js
CHANGED
|
@@ -57,6 +57,22 @@ const HALF_OPEN_IDLE_MS = 10 * 60_000
|
|
|
57
57
|
const SYNACK_GIVE_UP_MS = 60_000 // Linux: tcp_synack_retries (5), about 63 s
|
|
58
58
|
const GUEST_CLOSED_IDLE_MS = 120_000 // the guest's FIN is in, the remote says nothing: its kernel forgot it (FIN_WAIT2, 60 s)
|
|
59
59
|
const MAX_PENDING = 32 // handshakes not finished at once (more get a RST), so abandoned ones cannot take every relay slot
|
|
60
|
+
// D-551: npm's CDN ends (FIN) a connection whose TLS handshake is not done 15 s after the connect, and the relay
|
|
61
|
+
// connects when the guest's SYN comes. A slow guest (WebKit) can miss that before its ClientHello. A remote that
|
|
62
|
+
// ends a connection nothing has crossed, REDIAL_MIN_MS or more after it opened, is dialled once more instead.
|
|
63
|
+
const REDIAL_MIN_MS = 5_000
|
|
64
|
+
// D-552: a guest that runs many TLS handshakes at once (pnpm opens 16) splits its CPU among them, and in a slow guest
|
|
65
|
+
// (WebKit) none ends inside npm's 15 s. At most TLS_HANDSHAKES run at once: a ClientHello waits here for its turn, then
|
|
66
|
+
// goes out on a stream dialled again when the one it has is TLS_FRESH_MS old. A remote that ends a stream before it
|
|
67
|
+
// answers the ClientHello gets the hello once more on a new stream. A handshake holds its turn at most TLS_TURN_MS.
|
|
68
|
+
const TLS_HANDSHAKES = 4
|
|
69
|
+
const TLS_FRESH_MS = 3_000
|
|
70
|
+
const TLS_TURN_MS = 20_000
|
|
71
|
+
const TLS_HELLO_MAX = 16 << 10 // bytes kept for a replay
|
|
72
|
+
const T_NONE = 0 // no guest bytes yet
|
|
73
|
+
const T_WAIT = 1 // a ClientHello waits for a turn
|
|
74
|
+
const T_TURN = 2 // the handshake has a turn
|
|
75
|
+
const T_DONE = 3 // past the handshake, or not TLS
|
|
60
76
|
const INTERNAL_ZONE = 'wmd.internal'
|
|
61
77
|
|
|
62
78
|
const P_CONNECTING = 1 // waiting for the relay's CONNECTED
|
|
@@ -122,6 +138,14 @@ class Conn {
|
|
|
122
138
|
this.lastRemote = 0 // the last data or end from the relay
|
|
123
139
|
this.bytesIn = 0 // guest -> relay
|
|
124
140
|
this.bytesOut = 0 // relay -> guest
|
|
141
|
+
this.redialAt = 0 // D-551: when the stream was dialled again (0: not); its answer is pending while redialing
|
|
142
|
+
this.redialing = false
|
|
143
|
+
this.dialAt = 0 // when the relay stream the guest's bytes go to connected
|
|
144
|
+
this.tls = T_NONE // D-552
|
|
145
|
+
this.tlsAt = 0 // when the handshake got its turn
|
|
146
|
+
this.hello = null // the guest's bytes sent before the remote answered (a replay sends them again)
|
|
147
|
+
this.helloBytes = 0
|
|
148
|
+
this.helloReplayed = false
|
|
125
149
|
}
|
|
126
150
|
inflight() {
|
|
127
151
|
return seqDiff(this.sndNxt, this.sndUna)
|
|
@@ -154,6 +178,9 @@ export class NetStack {
|
|
|
154
178
|
this.peers = o.peers || null // W56: the switch's guests (peer.js)
|
|
155
179
|
this.now = o.now || (() => performance.now())
|
|
156
180
|
this.maxConns = o.maxConns ?? 256
|
|
181
|
+
this.tlsMax = o.tlsHandshakes ?? TLS_HANDSHAKES
|
|
182
|
+
this.tlsTurns = new Set() // D-552: handshakes with a turn
|
|
183
|
+
this.tlsWait = [] // and those waiting for one, oldest first
|
|
157
184
|
this.log = o.log || (() => {})
|
|
158
185
|
this.conns = new Map() // key -> Conn
|
|
159
186
|
this.byId = new Map() // relay stream id -> Conn
|
|
@@ -473,6 +500,7 @@ export class NetStack {
|
|
|
473
500
|
/** The relay's answer to a connect. */
|
|
474
501
|
onConnected(id, code, credit, detail) {
|
|
475
502
|
const c = this.byId.get(id)
|
|
503
|
+
if (c?.redialing) return this.#redialed(c, code, credit, detail)
|
|
476
504
|
if (!c || c.phase !== P_CONNECTING) {
|
|
477
505
|
if (!c && code === E.OK) this.relay.abort(id) // a connect that came back after we gave up on it
|
|
478
506
|
return
|
|
@@ -494,7 +522,7 @@ export class NetStack {
|
|
|
494
522
|
if (this.stats.connectMs.length > 100) this.stats.connectMs.shift()
|
|
495
523
|
c.relayCredit = credit
|
|
496
524
|
c.phase = P_SYNACK
|
|
497
|
-
c.synAckAt = this.now()
|
|
525
|
+
c.synAckAt = c.dialAt = this.now()
|
|
498
526
|
c.sndNxt = seqAdd(c.iss, 1)
|
|
499
527
|
this.#synAck(c)
|
|
500
528
|
}
|
|
@@ -615,6 +643,8 @@ export class NetStack {
|
|
|
615
643
|
|
|
616
644
|
/** Send held guest bytes to the relay as credit allows, in messages of up to MAX_DATA (one per segment cost a WebSocket message each). */
|
|
617
645
|
#flushToRelay(c) {
|
|
646
|
+
if (c.redialing) return // D-551: the new stream has no answer yet; #redialed flushes
|
|
647
|
+
if (c.tls !== T_DONE && c.via === this.relay && !this.#tlsGate(c)) return
|
|
618
648
|
while (c.toRelay.length && c.relayCredit > 0) {
|
|
619
649
|
const n = Math.min(c.toRelayBytes, c.relayCredit, MAX_DATA)
|
|
620
650
|
let part
|
|
@@ -637,6 +667,13 @@ export class NetStack {
|
|
|
637
667
|
}
|
|
638
668
|
c.toRelayBytes -= n
|
|
639
669
|
c.relayCredit -= n
|
|
670
|
+
if (c.tls === T_TURN && !c.bytesOut && c.hello) {
|
|
671
|
+
// past the cap the hello is not kept at all (a replay with a gap would be garbage)
|
|
672
|
+
if (c.helloBytes + n <= TLS_HELLO_MAX) {
|
|
673
|
+
c.hello.push(part.slice())
|
|
674
|
+
c.helloBytes += n
|
|
675
|
+
} else c.hello = null
|
|
676
|
+
}
|
|
640
677
|
c.via.data(c.id, part)
|
|
641
678
|
}
|
|
642
679
|
if (c.guestFin && !c.toRelay.length && !c.relayEndSent) {
|
|
@@ -676,16 +713,119 @@ export class NetStack {
|
|
|
676
713
|
onRelayEnd(id) {
|
|
677
714
|
const c = this.byId.get(id)
|
|
678
715
|
if (!c) return
|
|
716
|
+
if (this.#redial(c) || this.#tlsReplay(c)) return
|
|
679
717
|
c.remoteEnd = true
|
|
680
718
|
c.lastRemote = this.now()
|
|
681
719
|
this.wantsSend.add(c)
|
|
682
720
|
this.#trySend(c)
|
|
683
721
|
}
|
|
684
722
|
|
|
723
|
+
/** D-551: the remote ended a stream nothing has crossed, a while after it opened: dial it again, once, and say nothing to the guest. False when it does not qualify. */
|
|
724
|
+
#redial(c) {
|
|
725
|
+
if (c.redialAt || c.via !== this.relay || c.phase !== P_OPEN || c.bytesOut || c.bytesIn - c.toRelayBytes || c.guestFin) return false
|
|
726
|
+
if (this.now() - c.dialAt < REDIAL_MIN_MS) return false
|
|
727
|
+
this.stats.redials = (this.stats.redials || 0) + 1
|
|
728
|
+
this.#dialAgain(c, 'ended unused')
|
|
729
|
+
return true
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** A new relay stream for c, the old one aborted; the guest hears nothing. Its bytes wait in toRelay until #redialed. */
|
|
733
|
+
#dialAgain(c, why) {
|
|
734
|
+
this.relay.abort(c.id)
|
|
735
|
+
this.byId.delete(c.id)
|
|
736
|
+
c.id = this.nextId++
|
|
737
|
+
this.byId.set(c.id, c)
|
|
738
|
+
c.relayCredit = 0
|
|
739
|
+
c.redialing = true
|
|
740
|
+
c.redialAt = this.now()
|
|
741
|
+
this.#event({ ev: 'redial', why, to: `${u32ToIp(c.rip)}:${c.rport}`, afterMs: Math.round(c.redialAt - c.dialAt) })
|
|
742
|
+
this.relay.connect(c.id, u32ToIp(c.rip), c.rport)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** D-552: may the guest's bytes go to the relay now? Sorts a stream's first bytes into TLS or not, and holds a ClientHello until it has a turn. */
|
|
746
|
+
#tlsGate(c) {
|
|
747
|
+
if (c.tls === T_TURN) {
|
|
748
|
+
// the guest's bytes after the server's first flight end its part of the handshake
|
|
749
|
+
if (c.bytesOut && c.toRelayBytes) this.#tlsDone(c)
|
|
750
|
+
return true
|
|
751
|
+
}
|
|
752
|
+
if (c.tls === T_WAIT) return false
|
|
753
|
+
if (!c.toRelayBytes) return true
|
|
754
|
+
const h = c.toRelay[0]
|
|
755
|
+
if (c.bytesOut || h[0] !== 0x16 || (h.length > 1 && h[1] !== 0x03)) {
|
|
756
|
+
c.tls = T_DONE // the remote spoke first, or not a TLS handshake record
|
|
757
|
+
return true
|
|
758
|
+
}
|
|
759
|
+
if (this.tlsTurns.size >= this.tlsMax) {
|
|
760
|
+
c.tls = T_WAIT
|
|
761
|
+
this.tlsWait.push(c)
|
|
762
|
+
this.stats.tlsWaits = (this.stats.tlsWaits || 0) + 1
|
|
763
|
+
return false
|
|
764
|
+
}
|
|
765
|
+
return this.#tlsStart(c)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** The handshake gets a turn. False when its stream is dialled again first (#redialed sends the hello). */
|
|
769
|
+
#tlsStart(c) {
|
|
770
|
+
c.tls = T_TURN
|
|
771
|
+
c.tlsAt = this.now()
|
|
772
|
+
c.hello = []
|
|
773
|
+
c.helloBytes = 0
|
|
774
|
+
this.tlsTurns.add(c)
|
|
775
|
+
if (!c.redialing && c.phase === P_OPEN && c.bytesIn === c.toRelayBytes && this.now() - c.dialAt >= TLS_FRESH_MS) {
|
|
776
|
+
this.stats.tlsFresh = (this.stats.tlsFresh || 0) + 1
|
|
777
|
+
this.#dialAgain(c, 'fresh for a ClientHello')
|
|
778
|
+
return false
|
|
779
|
+
}
|
|
780
|
+
return true
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
#tlsDone(c) {
|
|
784
|
+
c.tls = T_DONE
|
|
785
|
+
c.hello = null
|
|
786
|
+
if (this.tlsTurns.delete(c)) this.#tlsNext()
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** Turns free: the oldest waiting hellos go. */
|
|
790
|
+
#tlsNext() {
|
|
791
|
+
while (this.tlsTurns.size < this.tlsMax && this.tlsWait.length) {
|
|
792
|
+
const c = this.tlsWait.shift()
|
|
793
|
+
if (c.tls !== T_WAIT || this.conns.get(c.key) !== c) continue // closed, or its 4-tuple reused by a new connection
|
|
794
|
+
if (this.#tlsStart(c)) this.#flushToRelay(c)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** D-552: the remote ended or reset a stream before it answered the guest's ClientHello: send the hello once more on a new stream. */
|
|
799
|
+
#tlsReplay(c) {
|
|
800
|
+
if (c.tls !== T_TURN || c.bytesOut || c.helloReplayed || !c.hello || !c.helloBytes || c.guestFin || c.via !== this.relay) return false
|
|
801
|
+
c.helloReplayed = true
|
|
802
|
+
c.toRelay.unshift(...c.hello)
|
|
803
|
+
c.toRelayBytes += c.helloBytes
|
|
804
|
+
c.hello = []
|
|
805
|
+
c.helloBytes = 0
|
|
806
|
+
this.stats.tlsReplays = (this.stats.tlsReplays || 0) + 1
|
|
807
|
+
this.#dialAgain(c, 'replay the ClientHello')
|
|
808
|
+
return true
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
#redialed(c, code, credit, detail) {
|
|
812
|
+
c.redialing = false
|
|
813
|
+
if (code !== E.OK) return this.#close(c, `redial: ${E_NAMES[code] || code}${detail ? ` (${detail})` : ''}`, false)
|
|
814
|
+
c.dialAt = this.now()
|
|
815
|
+
c.relayCredit = credit
|
|
816
|
+
this.#flushToRelay(c)
|
|
817
|
+
if (this.#rcvWnd(c) > c.lastWndSent) {
|
|
818
|
+
c.needAck = true
|
|
819
|
+
this.acks.add(c)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
685
823
|
onRelayAbort(id, code, reason) {
|
|
686
824
|
const c = this.byId.get(id)
|
|
687
825
|
if (!c) return
|
|
826
|
+
if (c.redialing) return this.#redialed(c, code || E.GONE, 0, reason)
|
|
688
827
|
if (c.phase === P_CONNECTING) return this.onConnected(id, code || E.GONE, 0, reason)
|
|
828
|
+
if (this.#tlsReplay(c)) return
|
|
689
829
|
this.#event({ ev: 'abort', to: `${u32ToIp(c.rip)}:${c.rport}`, why: E_NAMES[code] || code, reason })
|
|
690
830
|
this.#close(c, `relay: ${E_NAMES[code] || code} ${reason || ''}`.trim(), false)
|
|
691
831
|
}
|
|
@@ -765,6 +905,12 @@ export class NetStack {
|
|
|
765
905
|
this.byId.delete(c.id)
|
|
766
906
|
this.wantsSend.delete(c)
|
|
767
907
|
this.acks.delete(c)
|
|
908
|
+
if (c.tls === T_TURN) this.#tlsDone(c)
|
|
909
|
+
else if (c.tls === T_WAIT) {
|
|
910
|
+
c.tls = T_DONE
|
|
911
|
+
const i = this.tlsWait.indexOf(c)
|
|
912
|
+
if (i >= 0) this.tlsWait.splice(i, 1)
|
|
913
|
+
}
|
|
768
914
|
}
|
|
769
915
|
|
|
770
916
|
/** After a batch of input: one ACK per connection that needs one. */
|
|
@@ -782,6 +928,12 @@ export class NetStack {
|
|
|
782
928
|
tick() {
|
|
783
929
|
const t = this.now()
|
|
784
930
|
for (const c of [...this.conns.values()]) {
|
|
931
|
+
if (c.redialing && t - c.redialAt > CONNECT_TIMEOUT_MS) {
|
|
932
|
+
this.relay.abort(c.id)
|
|
933
|
+
this.#redialed(c, E.TIMEOUT, 0, 'no answer from the relay')
|
|
934
|
+
continue
|
|
935
|
+
}
|
|
936
|
+
if (c.tls === T_TURN && t - c.tlsAt > TLS_TURN_MS) this.#tlsDone(c) // a slow handshake goes on without its turn
|
|
785
937
|
if (c.phase === P_CONNECTING) {
|
|
786
938
|
if (t - c.openedAt > CONNECT_TIMEOUT_MS) {
|
|
787
939
|
c.via.abort(c.id)
|
package/src/registry/load-tar.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// copy is the layer handed to the converter Worker); a Blob is read one entry at a time, so the whole tar is
|
|
12
12
|
// never in memory at once.
|
|
13
13
|
|
|
14
|
+
import { blobBytes } from '../blob.js'
|
|
14
15
|
import { createConverter } from './converter.js'
|
|
15
16
|
import { RegistryError, sha256 } from './hub.js'
|
|
16
17
|
import { CONVERTER } from './pull.js'
|
|
@@ -126,7 +127,7 @@ function source(input) {
|
|
|
126
127
|
if (input instanceof Uint8Array) return { size: input.length, read: async (off, len) => input.subarray(off, off + len) }
|
|
127
128
|
if (input instanceof ArrayBuffer) return source(new Uint8Array(input))
|
|
128
129
|
if (typeof Blob !== 'undefined' && input instanceof Blob) {
|
|
129
|
-
return { size: input.size, read:
|
|
130
|
+
return { size: input.size, read: (off, len) => blobBytes(input.slice(off, off + len)) }
|
|
130
131
|
}
|
|
131
132
|
throw new TypeError('loadImageTar: the tar as a Uint8Array, ArrayBuffer or Blob')
|
|
132
133
|
}
|
package/wmdpack.wasm
CHANGED
|
Binary file
|