beam.com 0.1.0-rc.2 → 0.1.0-rc.3
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 +5 -3
- package/js/beam.com.mjs +37 -6
- package/js/download.mjs +31 -4
- package/js/node.mjs +8 -3
- package/js/wrangler.mjs +47 -0
- package/package.json +1 -1
- package/runtime/beam.mjs +1 -1
- package/runtime/beam.wasm +0 -0
- package/runtime/browser.js +8 -7
- package/runtime/cloudflare/durable.js +32 -2
- package/runtime/cloudflare/index.js +15 -4
- package/runtime/cloudflare/worker.js +907 -83
- package/runtime/deno/worker.js +907 -83
- package/runtime/deno.js +2 -0
- package/runtime/durable.js +32 -2
- package/runtime/global.js +6 -3
- package/runtime/page/beam.mjs +1 -1
- package/runtime/page/browser.js +8 -7
- package/runtime/page/runtime-id.js +1 -1
- package/runtime/page/worker.js +907 -83
- package/runtime/release.json +1 -1
- package/runtime/runtime-id.js +1 -1
- package/runtime/worker.js +907 -83
|
@@ -144,7 +144,7 @@ const ofRelease = (release, buffer) => (release.buffers ? release.buffers.has(bu
|
|
|
144
144
|
// The memory and the open files of a VM whose threads all returned
|
|
145
145
|
// (erts_wasm_hibernate), as snapshot.bin. The files that the boot wrote
|
|
146
146
|
// are the ones that are not views of release.bin (unpack: canOwn).
|
|
147
|
-
function capture(m, release, listeners, bootPoint = false, skip = () => false) {
|
|
147
|
+
function capture(m, release, listeners, bootPoint = false, skip = () => false, flags = '') {
|
|
148
148
|
const PAGE = 65536;
|
|
149
149
|
const heap = m.HEAPU8;
|
|
150
150
|
const words = new BigUint64Array(heap.buffer, 0, heap.length / 8);
|
|
@@ -187,6 +187,9 @@ function capture(m, release, listeners, bootPoint = false, skip = () => false) {
|
|
|
187
187
|
// The release of this memory (snapshot_key of .release.json): a VM of
|
|
188
188
|
// another release does not restore it.
|
|
189
189
|
release: releaseMeta(release).snapshot_key ?? null,
|
|
190
|
+
// The flags of the emulator of this memory (BEAM_ERL_FLAGS): a VM with
|
|
191
|
+
// other flags does not restore it (-Mea min changes the allocators).
|
|
192
|
+
flags,
|
|
190
193
|
nifs: libs && { count: libs.count, libs: libs.libs.map(({ data, ...l }) => l) },
|
|
191
194
|
}));
|
|
192
195
|
const out = new Uint8Array(12 + head.length + (pages.length + libPages) * PAGE);
|
|
@@ -219,18 +222,69 @@ async function loadSnapshot(env) {
|
|
|
219
222
|
|
|
220
223
|
// The header of a snapshot, without its pages: boot_point, release, ...
|
|
221
224
|
export function snapshotHeader(bytes) {
|
|
222
|
-
const { pagesData, ...head } = parseSnapshot(bytes);
|
|
225
|
+
const { pagesData, packed, ...head } = parseSnapshot(bytes);
|
|
223
226
|
return head;
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
|
|
229
|
+
// A snapshot is "BEAMSNP1" (the pages as they are) or "BEAMSNZ1" (the
|
|
230
|
+
// pages in gzip, packSnapshot): the same header, which the host reads
|
|
231
|
+
// with no inflate. The pages of BEAMSNZ1 are in packed, and inflate()
|
|
232
|
+
// gives pagesData.
|
|
233
|
+
export function parseSnapshot(bytes) {
|
|
227
234
|
const b = new Uint8Array(bytes);
|
|
228
|
-
|
|
235
|
+
const magic = new TextDecoder().decode(b.subarray(0, 8));
|
|
236
|
+
if (magic !== 'BEAMSNP1' && magic !== 'BEAMSNZ1') throw new Error('snapshot.bin: not a snapshot');
|
|
229
237
|
const len = new DataView(b.buffer, b.byteOffset).getUint32(8);
|
|
230
238
|
const head = JSON.parse(new TextDecoder().decode(b.subarray(12, 12 + len)));
|
|
231
|
-
return { ...head, pagesData: b.subarray(12 + len) }
|
|
239
|
+
return magic === 'BEAMSNP1' ? { ...head, pagesData: b.subarray(12 + len) }
|
|
240
|
+
: { ...head, pagesData: null, packed: b.subarray(12 + len) };
|
|
232
241
|
}
|
|
233
242
|
|
|
243
|
+
// The bytes of the pages of a snapshot (the pages of the VM, then the
|
|
244
|
+
// pages of the NIF libraries).
|
|
245
|
+
const pagesSize = (head) => 65536 * (head.pages.length + (head.nifs?.libs ?? []).reduce((n, l) => n + l.pages.length, 0));
|
|
246
|
+
|
|
247
|
+
// A snapshot of BEAMSNP1 as BEAMSNZ1: the pages in gzip. A snapshot of
|
|
248
|
+
// the build is a module of the Worker, and the module stays in the memory
|
|
249
|
+
// of the isolate: the pages of a Phoenix app are about 21 MB, and 5 MB in
|
|
250
|
+
// gzip.
|
|
251
|
+
export async function packSnapshot(bytes) {
|
|
252
|
+
const b = new Uint8Array(bytes);
|
|
253
|
+
if (new TextDecoder().decode(b.subarray(0, 8)) !== 'BEAMSNP1') return b;
|
|
254
|
+
const len = new DataView(b.buffer, b.byteOffset).getUint32(8);
|
|
255
|
+
const gz = new Uint8Array(await new Response(new Blob([b.subarray(12 + len)]).stream()
|
|
256
|
+
.pipeThrough(new CompressionStream('gzip'))).arrayBuffer());
|
|
257
|
+
const out = new Uint8Array(12 + len + gz.length);
|
|
258
|
+
out.set(new TextEncoder().encode('BEAMSNZ1'));
|
|
259
|
+
out.set(b.subarray(8, 12 + len), 8);
|
|
260
|
+
out.set(gz, 12 + len);
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The pages of a parsed snapshot (pagesData), with no copy of the whole
|
|
265
|
+
// gzip output: the header gives their size.
|
|
266
|
+
export async function inflateSnapshot(snap) {
|
|
267
|
+
if (!snap.packed) return snap;
|
|
268
|
+
const size = pagesSize(snap);
|
|
269
|
+
const out = new Uint8Array(size);
|
|
270
|
+
const reader = new Blob([snap.packed]).stream().pipeThrough(new DecompressionStream('gzip')).getReader();
|
|
271
|
+
let at = 0;
|
|
272
|
+
for (;;) {
|
|
273
|
+
const { value, done } = await reader.read();
|
|
274
|
+
if (done) break;
|
|
275
|
+
if (at + value.length > size) throw new Error('snapshot: more pages than its header gives');
|
|
276
|
+
out.set(value, at);
|
|
277
|
+
at += value.length;
|
|
278
|
+
}
|
|
279
|
+
if (at !== size) throw new Error('snapshot: fewer pages than its header gives');
|
|
280
|
+
snap.pagesData = out;
|
|
281
|
+
snap.packed = null;
|
|
282
|
+
return snap;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// BEAM_ERL_FLAGS as one text, with one space between the flags.
|
|
286
|
+
export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
|
|
287
|
+
|
|
234
288
|
// BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
|
|
235
289
|
// "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
|
|
236
290
|
// hosts, as "*:443"). The host resolves a name, so the VM cannot reach
|
|
@@ -383,7 +437,7 @@ function seed(m) {
|
|
|
383
437
|
// most).
|
|
384
438
|
const CHUNK = 1 << 20;
|
|
385
439
|
|
|
386
|
-
class Persist {
|
|
440
|
+
export class Persist {
|
|
387
441
|
constructor(sql, dirs) {
|
|
388
442
|
this.sql = sql;
|
|
389
443
|
this.dirs = dirs.map((d) => d.replace(/\/+$/, '')).filter((d) => d.startsWith('/') && d.length > 1);
|
|
@@ -429,6 +483,14 @@ class Persist {
|
|
|
429
483
|
}
|
|
430
484
|
}
|
|
431
485
|
|
|
486
|
+
// Each file that the VM wrote after the last save. The timer of a write
|
|
487
|
+
// calls it, and also the stop of the VM, so a stop loses no write.
|
|
488
|
+
saveDirty() {
|
|
489
|
+
clearTimeout(this.flush);
|
|
490
|
+
this.flush = null;
|
|
491
|
+
for (const p of [...this.dirty]) this.save(this.fs, p);
|
|
492
|
+
}
|
|
493
|
+
|
|
432
494
|
// A file or a directory, and all under it.
|
|
433
495
|
remove(path) {
|
|
434
496
|
for (const t of ['beam_fs', 'beam_fs_chunk']) {
|
|
@@ -448,6 +510,7 @@ class Persist {
|
|
|
448
510
|
// Wrap the functions of FS that change files (the system calls of the
|
|
449
511
|
// VM call them).
|
|
450
512
|
attach(FS) {
|
|
513
|
+
this.fs = FS;
|
|
451
514
|
const abs = (p) => (typeof p === 'string' && !p.startsWith('/') ? `${FS.cwd()}/${p}` : p);
|
|
452
515
|
const wrap = (name, after) => {
|
|
453
516
|
const f = FS[name];
|
|
@@ -460,10 +523,7 @@ class Persist {
|
|
|
460
523
|
wrap('write', (stream) => {
|
|
461
524
|
if (!this.under(stream.path)) return;
|
|
462
525
|
this.dirty.add(stream.path);
|
|
463
|
-
this.flush ??= setTimeout(() =>
|
|
464
|
-
this.flush = null;
|
|
465
|
-
for (const p of [...this.dirty]) this.save(FS, p);
|
|
466
|
-
}, 1000);
|
|
526
|
+
this.flush ??= setTimeout(() => this.saveDirty(), 1000);
|
|
467
527
|
});
|
|
468
528
|
wrap('close', (stream) => {
|
|
469
529
|
if (this.under(stream.path) && ((stream.flags & 3) !== 0 || this.dirty.has(stream.path))) this.save(FS, stream.path);
|
|
@@ -538,6 +598,8 @@ export default {
|
|
|
538
598
|
if (!vm) {
|
|
539
599
|
const v = vm = new Vm(env, { host: new URL(request.url).hostname });
|
|
540
600
|
v.ready.catch(() => { if (vm === v) vm = undefined; });
|
|
601
|
+
// A VM that stopped: the next request starts a new one.
|
|
602
|
+
v.onDead = () => { if (vm === v) vm = undefined; };
|
|
541
603
|
}
|
|
542
604
|
return vm.fetch(request, ctx);
|
|
543
605
|
},
|
|
@@ -1085,6 +1147,22 @@ export class Vm {
|
|
|
1085
1147
|
this.nextTimer = 1;
|
|
1086
1148
|
this.env = env;
|
|
1087
1149
|
this.waitListen = new Map(); // port -> the resolve functions of listening()
|
|
1150
|
+
// BEAM_YIELD_REDS (1000000, about 50 ms of work; "0": none): the
|
|
1151
|
+
// reductions between the timers of the turns (see turn).
|
|
1152
|
+
this.turns = 0;
|
|
1153
|
+
this.turnEvery = Math.ceil(limit(env.BEAM_YIELD_REDS, 1000000) / 20000);
|
|
1154
|
+
this.conns = new Set(); // the open connections of bridge() (see die)
|
|
1155
|
+
this.sockets = 0; // the open WebSockets: bridge() and /.tcp/PORT
|
|
1156
|
+
this.peak = 0; // the memory (MB) of the last log of the peak
|
|
1157
|
+
// The VM stopped (erlang:halt, or a trap such as an allocation that
|
|
1158
|
+
// failed): dead is the reason, and died resolves. onDead: the owner of
|
|
1159
|
+
// the VM drops it, so that a new VM takes the next request.
|
|
1160
|
+
this.dead = null;
|
|
1161
|
+
this.died = new Promise((resolve) => { this.markDead = resolve; });
|
|
1162
|
+
// The waits that the stop of the VM ends (stopWait): a race with
|
|
1163
|
+
// this.died itself keeps one reaction for each race until the VM stops.
|
|
1164
|
+
this.stopWaits = new Set();
|
|
1165
|
+
this.onDead = null;
|
|
1088
1166
|
// The static files of the release (appFiles of app-com.js), when the
|
|
1089
1167
|
// release is loaded: a request for one of them needs no VM.
|
|
1090
1168
|
this.statics = new Promise((resolve) => { this.gotStatics = resolve; });
|
|
@@ -1139,6 +1217,13 @@ export class Vm {
|
|
|
1139
1217
|
console.log('beam: the snapshot of the build is for another release: a boot in its place');
|
|
1140
1218
|
built = null;
|
|
1141
1219
|
}
|
|
1220
|
+
// A snapshot of the build with other flags of the emulator: a boot in
|
|
1221
|
+
// its place (npx beam.com --snapshot --env BEAM_ERL_FLAGS=...). A
|
|
1222
|
+
// snapshot with no flags in its header was made with none.
|
|
1223
|
+
if (built && (built.flags ?? '') !== erlFlags(env)) {
|
|
1224
|
+
console.log(`beam: the snapshot of the build has the flags "${built.flags ?? ''}", and BEAM_ERL_FLAGS is "${erlFlags(env)}": a boot in its place`);
|
|
1225
|
+
built = null;
|
|
1226
|
+
}
|
|
1142
1227
|
// A Durable Object with Ecto SQLite (sql of .release.json): the
|
|
1143
1228
|
// snapshot is made at the boot point (wasm_host_server), before the
|
|
1144
1229
|
// program starts and runs its migrations. So all the objects (the
|
|
@@ -1159,6 +1244,7 @@ export class Vm {
|
|
|
1159
1244
|
}
|
|
1160
1245
|
const fromFallback = !snap && !!fallback;
|
|
1161
1246
|
if (fromFallback) snap = fallback;
|
|
1247
|
+
if (snap) await inflateSnapshot(snap);
|
|
1162
1248
|
let snapBytes = snap ? snap.pagesData : null;
|
|
1163
1249
|
const nifs = await nifModules();
|
|
1164
1250
|
// restored: this VM comes from a snapshot (the page shows it).
|
|
@@ -1171,7 +1257,7 @@ export class Vm {
|
|
|
1171
1257
|
const t1 = Date.now();
|
|
1172
1258
|
return new Promise((resolve, reject) => {
|
|
1173
1259
|
const snapKB = snapBytes ? snapBytes.byteLength >> 10 : 0;
|
|
1174
|
-
this.onready = () => { console.log(`beam: ready in ${Date.now() - t0} ms (release ${release.byteLength >> 10} KB${snap ? `, snapshot ${snapKB} KB` : ''} in ${t1 - t0} ms), ${this.memory()}`); resolve(); };
|
|
1260
|
+
this.onready = () => { this.peak = this.megabytes(); console.log(`beam: ready in ${Date.now() - t0} ms (release ${release.byteLength >> 10} KB${snap ? `, snapshot ${snapKB} KB` : ''} in ${t1 - t0} ms), ${this.memory()}`); resolve(); };
|
|
1175
1261
|
createBeam({
|
|
1176
1262
|
noInitialRun: !!snap,
|
|
1177
1263
|
onRuntimeInitialized: snap ? () => {
|
|
@@ -1199,6 +1285,7 @@ export class Vm {
|
|
|
1199
1285
|
} : undefined,
|
|
1200
1286
|
// The boot arguments are set in preRun, after the release is unpacked.
|
|
1201
1287
|
arguments: [],
|
|
1288
|
+
jspiTurn: (f) => this.turn(f),
|
|
1202
1289
|
// A plain Worker runs the jobs and timers of the threads in its
|
|
1203
1290
|
// open requests. After adopt() (a Durable Object), they run on a
|
|
1204
1291
|
// MessageChannel and on setTimeout.
|
|
@@ -1278,15 +1365,20 @@ export class Vm {
|
|
|
1278
1365
|
m.beamHost.onsend = (bytes) => this.onsend(bytes);
|
|
1279
1366
|
if (this.hostFiles) m.beamHost.files = this.hostFiles;
|
|
1280
1367
|
}],
|
|
1281
|
-
print: (s) =>
|
|
1282
|
-
printErr: (s) =>
|
|
1368
|
+
print: (s) => this.log(s),
|
|
1369
|
+
printErr: (s) => this.log(s),
|
|
1283
1370
|
nifModule: (file) => nifModule(nifs, file),
|
|
1284
1371
|
// Workers compile no WebAssembly at run time: use the imported module.
|
|
1285
1372
|
instantiateWasm: (imports, done) => {
|
|
1286
1373
|
WebAssembly.instantiate(wasm, imports).then((instance) => { this.exports = instance.exports; done(instance); });
|
|
1287
1374
|
return {};
|
|
1288
1375
|
},
|
|
1289
|
-
|
|
1376
|
+
// In a plain Worker, the thread that stopped can run in the context
|
|
1377
|
+
// of a request that ended: die runs in an open request (run).
|
|
1378
|
+
onExit: (code) => {
|
|
1379
|
+
reject(new Error(`beam exited with status ${code}`));
|
|
1380
|
+
this.run(() => this.die(`exit status ${code}`));
|
|
1381
|
+
},
|
|
1290
1382
|
}).catch(reject);
|
|
1291
1383
|
});
|
|
1292
1384
|
}
|
|
@@ -1368,7 +1460,7 @@ export class Vm {
|
|
|
1368
1460
|
for (let i = 0; i < 5; i++) await tick();
|
|
1369
1461
|
}
|
|
1370
1462
|
try {
|
|
1371
|
-
return capture(this.beam, this.release, this.listeners, bootPoint, (p) => !!this.persist?.under(p));
|
|
1463
|
+
return capture(this.beam, this.release, this.listeners, bootPoint, (p) => !!this.persist?.under(p), erlFlags(this.env));
|
|
1372
1464
|
} finally {
|
|
1373
1465
|
x.erts_wasm_resume();
|
|
1374
1466
|
}
|
|
@@ -1446,11 +1538,119 @@ export class Vm {
|
|
|
1446
1538
|
}
|
|
1447
1539
|
}
|
|
1448
1540
|
|
|
1541
|
+
// A turn for a scheduler that computes (jspi_host_turn of jspi_lib.js,
|
|
1542
|
+
// each 20000 reductions with no wait), so that the I/O of the host (new
|
|
1543
|
+
// requests, sockets) runs while a process computes:
|
|
1544
|
+
// - Deno: setImmediate, which runs after the poll of the I/O, and costs
|
|
1545
|
+
// little;
|
|
1546
|
+
// - a Durable Object (and a web page): one turn for each BEAM_YIELD_REDS
|
|
1547
|
+
// reductions waits for a timer (about 1 ms or more); the other turns
|
|
1548
|
+
// are tasks;
|
|
1549
|
+
// - a plain Worker: only tasks. A timer there resumes the thread in the
|
|
1550
|
+
// request that fired it, and that request can end before the work.
|
|
1551
|
+
turn(f) {
|
|
1552
|
+
if (this.plain) {
|
|
1553
|
+
this.jobs.push(f);
|
|
1554
|
+
return void this.handlers.at(-1)?.wake?.();
|
|
1555
|
+
}
|
|
1556
|
+
if (typeof Deno == 'object') return void setImmediate(f);
|
|
1557
|
+
if (this.turnEvery && ++this.turns % this.turnEvery === 0) return void setTimeout(f, 0);
|
|
1558
|
+
this.post(f);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// The linear memory of the VM in MB. It never shrinks, so it is also its
|
|
1562
|
+
// peak.
|
|
1563
|
+
megabytes() {
|
|
1564
|
+
return this.beam ? this.beam.HEAPU8.length >> 20 : 0;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1449
1567
|
memory() {
|
|
1450
|
-
return `memory ${this.
|
|
1568
|
+
return `memory ${this.megabytes()} MB`;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// A log line each time the memory of the VM grew by 8 MB or more since
|
|
1572
|
+
// the last one (a Worker has 128 MB for all of the isolate).
|
|
1573
|
+
notePeak() {
|
|
1574
|
+
const mb = this.megabytes();
|
|
1575
|
+
if (mb < this.peak + 8) return;
|
|
1576
|
+
this.peak = mb;
|
|
1577
|
+
this.log(`beam: memory ${mb} MB (a new peak of this VM)`);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// The VM stopped: each open request gets 503, each WebSocket closes with
|
|
1581
|
+
// 1011, and the owner drops the VM (onDead). The threads of the VM do not
|
|
1582
|
+
// run again, so without this the open requests and all later ones wait
|
|
1583
|
+
// for an answer that does not come.
|
|
1584
|
+
die(reason) {
|
|
1585
|
+
if (this.dead) return;
|
|
1586
|
+
this.dead = reason;
|
|
1587
|
+
try { this.persist?.saveDirty(); } catch (e) { this.log(`beam: persist: the save at the stop failed (${e.message})`); }
|
|
1588
|
+
const open = [...this.conns].filter((c) => !c.status).length;
|
|
1589
|
+
this.log(`beam: the VM stopped (${reason}), ${this.memory()}: ${open} open requests get 503`);
|
|
1590
|
+
// Each connection in its own request (run): in a plain Worker, its
|
|
1591
|
+
// response, its socket and its timer belong to that request.
|
|
1592
|
+
for (const c of this.conns) {
|
|
1593
|
+
this.bridgeRoom(c);
|
|
1594
|
+
const status = c.status;
|
|
1595
|
+
if (!status) c.status = 503;
|
|
1596
|
+
if (c.ws) this.socketGone(c);
|
|
1597
|
+
this.run(() => {
|
|
1598
|
+
clearTimeout(c.timer);
|
|
1599
|
+
if (!status) {
|
|
1600
|
+
c.resolve(this.stopped());
|
|
1601
|
+
c.finished();
|
|
1602
|
+
} else if (c.ws) {
|
|
1603
|
+
try { c.ws.close(1011, 'the app stopped'); } catch {}
|
|
1604
|
+
} else if (!c.done) {
|
|
1605
|
+
c.done = true;
|
|
1606
|
+
c.writer?.abort(new Error('the app stopped')).catch(() => {});
|
|
1607
|
+
}
|
|
1608
|
+
this.bridgeRelease(c);
|
|
1609
|
+
}, c.h);
|
|
1610
|
+
}
|
|
1611
|
+
this.conns.clear();
|
|
1612
|
+
// The other sockets: /.tcp/PORT, and the connections of the VM.
|
|
1613
|
+
for (const [id, t] of this.tcps) {
|
|
1614
|
+
if (!id.startsWith('b')) this.run(() => { try { t.close(); } catch {} }, t.h);
|
|
1615
|
+
}
|
|
1616
|
+
this.tcps.clear();
|
|
1617
|
+
// The fetch() calls of the VM: no one reads their bodies now.
|
|
1618
|
+
for (const ac of this.fetches?.values() ?? []) ac.abort();
|
|
1619
|
+
this.markDead();
|
|
1620
|
+
for (const resolve of this.stopWaits ?? []) resolve();
|
|
1621
|
+
this.stopWaits?.clear();
|
|
1622
|
+
this.onDead?.(reason);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// A log line. In a plain Worker, a thread of the VM can run in the
|
|
1626
|
+
// context of a request that ended, where console.log throws: then an open
|
|
1627
|
+
// request writes the line (run).
|
|
1628
|
+
log(text) {
|
|
1629
|
+
try { console.log(text); } catch { this.run(() => console.log(text)); }
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// The answer of a VM that stopped. The owner starts a new VM for the
|
|
1633
|
+
// next request.
|
|
1634
|
+
stopped() {
|
|
1635
|
+
return new Response('The app stopped. Try again.\n',
|
|
1636
|
+
{ status: 503, headers: { 'content-type': 'text/plain', 'retry-after': '1' } });
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// The answer above a limit of the host (BEAM_MAX_REQUESTS,
|
|
1640
|
+
// BEAM_MAX_WEBSOCKETS): before the VM gets the request or its body.
|
|
1641
|
+
busy(what) {
|
|
1642
|
+
console.log(`beam: too many ${what}: 503`);
|
|
1643
|
+
return new Response(`The app has too many ${what}. Try again.\n`,
|
|
1644
|
+
{ status: 503, headers: { 'content-type': 'text/plain', 'retry-after': '1' } });
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// A WebSocket of the VM closed (once for each one).
|
|
1648
|
+
socketGone(c) {
|
|
1649
|
+
if (c.counted) { c.counted = false; this.sockets--; }
|
|
1451
1650
|
}
|
|
1452
1651
|
|
|
1453
1652
|
event(header, body) {
|
|
1653
|
+
if (this.dead) return;
|
|
1454
1654
|
const h = new TextEncoder().encode(JSON.stringify(header) + '\n');
|
|
1455
1655
|
if (!body || !body.byteLength) return this.beam.beamHost.push(h);
|
|
1456
1656
|
const b = new Uint8Array(h.length + body.byteLength);
|
|
@@ -1479,6 +1679,10 @@ export class Vm {
|
|
|
1479
1679
|
finished();
|
|
1480
1680
|
return file;
|
|
1481
1681
|
}
|
|
1682
|
+
if (this.dead) {
|
|
1683
|
+
finished();
|
|
1684
|
+
return this.stopped();
|
|
1685
|
+
}
|
|
1482
1686
|
await this.ready;
|
|
1483
1687
|
if (this.resume) {
|
|
1484
1688
|
const f = this.resume;
|
|
@@ -1488,7 +1692,13 @@ export class Vm {
|
|
|
1488
1692
|
// The first request makes the snapshot, and the other requests wait for
|
|
1489
1693
|
// it, so that the snapshot has the state of no request.
|
|
1490
1694
|
if (this.makeKey) this.snapping = this.makeSnapshot().finally(() => { this.snapping = null; });
|
|
1491
|
-
|
|
1695
|
+
// A snapshot that takes too long does not hold the request: the VM
|
|
1696
|
+
// goes on, and the snapshot ends later or fails.
|
|
1697
|
+
if (this.snapping) await this.stopWait(this.snapping, SNAPSHOT_WAIT);
|
|
1698
|
+
if (this.dead) {
|
|
1699
|
+
finished();
|
|
1700
|
+
return this.stopped();
|
|
1701
|
+
}
|
|
1492
1702
|
const url = new URL(request.url);
|
|
1493
1703
|
const upgrade = request.headers.get('upgrade')?.toLowerCase() === 'websocket';
|
|
1494
1704
|
const tcp = upgrade && url.pathname.match(/^\/\.tcp\/(\d+)$/);
|
|
@@ -1608,6 +1818,37 @@ export class Vm {
|
|
|
1608
1818
|
return new Promise((r) => this.waitListen.set(port, [...(this.waitListen.get(port) ?? []), r]));
|
|
1609
1819
|
}
|
|
1610
1820
|
|
|
1821
|
+
// The first of promise, the stop of the VM, and ms milliseconds (LATE).
|
|
1822
|
+
// The wait leaves nothing behind when it ends.
|
|
1823
|
+
async stopWait(promise, ms = 0) {
|
|
1824
|
+
if (this.dead) return;
|
|
1825
|
+
let resolve, timer;
|
|
1826
|
+
const stop = new Promise((r) => { resolve = r; });
|
|
1827
|
+
(this.stopWaits ??= new Set()).add(resolve);
|
|
1828
|
+
if (ms) timer = setTimeout(() => resolve(LATE), ms);
|
|
1829
|
+
try {
|
|
1830
|
+
return await Promise.race([promise, stop]);
|
|
1831
|
+
} finally {
|
|
1832
|
+
clearTimeout(timer);
|
|
1833
|
+
this.stopWaits.delete(resolve);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// The wait of a request for the listener of port, or null when it
|
|
1838
|
+
// listens. cancel() removes the wait of a request that ends first.
|
|
1839
|
+
listenWait(port) {
|
|
1840
|
+
if (this.listeners.has(port)) return null;
|
|
1841
|
+
let resolve;
|
|
1842
|
+
const promise = new Promise((r) => { resolve = r; });
|
|
1843
|
+
this.waitListen.set(port, [...(this.waitListen.get(port) ?? []), resolve]);
|
|
1844
|
+
const cancel = () => {
|
|
1845
|
+
const rest = (this.waitListen.get(port) ?? []).filter((r) => r !== resolve);
|
|
1846
|
+
if (rest.length) this.waitListen.set(port, rest);
|
|
1847
|
+
else this.waitListen.delete(port);
|
|
1848
|
+
};
|
|
1849
|
+
return { promise, cancel };
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1611
1852
|
// The Origin that the app gets. Phoenix compares the Origin of a
|
|
1612
1853
|
// WebSocket with the host of its config (PHX_HOST), so a page of the app on
|
|
1613
1854
|
// another name of the Worker (wrangler dev, a custom domain, a preview URL,
|
|
@@ -1623,9 +1864,53 @@ export class Vm {
|
|
|
1623
1864
|
|
|
1624
1865
|
// The request as a TCP connection to the HTTP server of the
|
|
1625
1866
|
// app on PORT. Bandit does the HTTP; here only the bytes are framed.
|
|
1867
|
+
// The limits of the host (vars, 0 for no limit):
|
|
1868
|
+
// - BEAM_MAX_REQUESTS: the requests that wait for the app at one time
|
|
1869
|
+
// (no limit by default); one more gets 503 before the VM reads it;
|
|
1870
|
+
// - BEAM_MAX_WEBSOCKETS: the open WebSockets (no limit by default);
|
|
1871
|
+
// - BEAM_REQUEST_TIMEOUT: the seconds until the head of the response
|
|
1872
|
+
// (60 by default), after the end of the request body; then the request
|
|
1873
|
+
// gets 504, and the app gets the end of the connection;
|
|
1874
|
+
// - BEAM_MAX_BODY: the bytes of a request body (no limit by default);
|
|
1875
|
+
// a larger one gets 413.
|
|
1876
|
+
// The body goes to the app in the chunks of the client (bridgeUpload).
|
|
1626
1877
|
async bridge(request, url, upgrade, h, finished) {
|
|
1878
|
+
const maxSockets = limit(this.env.BEAM_MAX_WEBSOCKETS, 0);
|
|
1879
|
+
if (upgrade && maxSockets && this.sockets >= maxSockets) {
|
|
1880
|
+
finished();
|
|
1881
|
+
return this.busy('WebSockets');
|
|
1882
|
+
}
|
|
1883
|
+
const maxRequests = limit(this.env.BEAM_MAX_REQUESTS, 0);
|
|
1884
|
+
if (!upgrade && maxRequests && [...this.conns].filter((c) => !c.ws).length >= maxRequests) {
|
|
1885
|
+
finished();
|
|
1886
|
+
return this.busy('requests');
|
|
1887
|
+
}
|
|
1888
|
+
const body = request.method === 'GET' || request.method === 'HEAD' || upgrade ? null : request.body;
|
|
1889
|
+
const declared = request.headers.get('content-length');
|
|
1890
|
+
const maxBody = limit(this.env.BEAM_MAX_BODY, 0);
|
|
1891
|
+
if (body && maxBody && declared !== null && Number(declared) > maxBody) {
|
|
1892
|
+
finished();
|
|
1893
|
+
return this.tooLarge(maxBody);
|
|
1894
|
+
}
|
|
1627
1895
|
const port = Number(this.env.PORT ?? 4000);
|
|
1628
|
-
|
|
1896
|
+
// The time limit counts from here: the app can also fail to listen.
|
|
1897
|
+
const timeout = limit(this.env.BEAM_REQUEST_TIMEOUT, 60);
|
|
1898
|
+
const deadline = Date.now() + timeout * 1000;
|
|
1899
|
+
// Only a request that comes before the app listens waits here.
|
|
1900
|
+
const listen = this.listenWait(port);
|
|
1901
|
+
let waited;
|
|
1902
|
+
if (listen) {
|
|
1903
|
+
waited = await this.stopWait(listen.promise, timeout * 1000);
|
|
1904
|
+
listen.cancel();
|
|
1905
|
+
}
|
|
1906
|
+
if (this.dead) {
|
|
1907
|
+
finished();
|
|
1908
|
+
return this.stopped();
|
|
1909
|
+
}
|
|
1910
|
+
if (waited === LATE) {
|
|
1911
|
+
finished();
|
|
1912
|
+
return this.late(`the app did not listen on port ${port}`, timeout);
|
|
1913
|
+
}
|
|
1629
1914
|
const id = `b${this.nextId++}`;
|
|
1630
1915
|
const headers = new Headers(request.headers);
|
|
1631
1916
|
headers.set('host', url.host);
|
|
@@ -1638,6 +1923,8 @@ export class Vm {
|
|
|
1638
1923
|
if (origin) headers.set('origin', origin);
|
|
1639
1924
|
headers.delete('transfer-encoding');
|
|
1640
1925
|
headers.delete('sec-websocket-extensions'); // no compression: frames as they are
|
|
1926
|
+
// The host has the whole request: no "100 Continue" from the app.
|
|
1927
|
+
headers.delete('expect');
|
|
1641
1928
|
// The Workers runtime compresses the response for each client. The app
|
|
1642
1929
|
// gets only gzip of Accept-Encoding: Plug.Static serves the files that
|
|
1643
1930
|
// are gzipped already (Livebook has no other ones), and the response
|
|
@@ -1652,43 +1939,184 @@ export class Vm {
|
|
|
1652
1939
|
} else {
|
|
1653
1940
|
headers.set('connection', 'close');
|
|
1654
1941
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
if (body
|
|
1942
|
+
// A body of no declared length goes to the app as chunked.
|
|
1943
|
+
if (body && declared !== null) headers.set('content-length', declared);
|
|
1944
|
+
else if (body) {
|
|
1945
|
+
headers.delete('content-length');
|
|
1946
|
+
headers.set('transfer-encoding', 'chunked');
|
|
1947
|
+
} else if (!upgrade && !['GET', 'HEAD'].includes(request.method)) headers.set('content-length', '0');
|
|
1658
1948
|
let head = `${request.method} ${url.pathname}${url.search} HTTP/1.1\r\n`;
|
|
1659
1949
|
for (const [k, v] of headers) head += `${k}: ${v}\r\n`;
|
|
1660
|
-
|
|
1661
|
-
const bytes =
|
|
1662
|
-
|
|
1663
|
-
|
|
1950
|
+
// The values of Headers are bytes (one character for each byte).
|
|
1951
|
+
const bytes = latin1Bytes(head + '\r\n');
|
|
1952
|
+
if (this.dead) {
|
|
1953
|
+
finished();
|
|
1954
|
+
return this.stopped();
|
|
1955
|
+
}
|
|
1664
1956
|
return new Promise((resolve) => {
|
|
1665
|
-
const c = { id,
|
|
1957
|
+
const c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
|
|
1666
1958
|
if (h) h.sockets++;
|
|
1667
|
-
this.
|
|
1668
|
-
|
|
1669
|
-
|
|
1959
|
+
this.conns.add(c);
|
|
1960
|
+
c.timeout = timeout;
|
|
1961
|
+
// A body can take long to come: the time limit starts at its end.
|
|
1962
|
+
if (!body) this.bridgeClock(c, deadline - Date.now());
|
|
1963
|
+
// An error of one connection ends that connection, not the VM.
|
|
1964
|
+
// A send ends when the client read its bytes (the newest write of
|
|
1965
|
+
// the response, c.wrote): tcpSend then tells the VM (tcp_sent).
|
|
1966
|
+
this.tcps.set(id, {
|
|
1967
|
+
send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
|
|
1968
|
+
close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
|
|
1969
|
+
ack: (n) => this.bridgeRead(c, n), h,
|
|
1970
|
+
});
|
|
1971
|
+
// ack: wasm_tcp tells the host what the app read (tcp_read): the
|
|
1972
|
+
// body of the request, and the messages of a WebSocket. sent: the
|
|
1973
|
+
// host tells wasm_tcp what the client took (tcp_sent).
|
|
1974
|
+
this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
|
|
1975
|
+
this.bridgeSend(c, bytes);
|
|
1976
|
+
if (body) this.bridgeUpload(c, body, declared === null, maxBody);
|
|
1670
1977
|
});
|
|
1671
1978
|
}
|
|
1672
1979
|
|
|
1980
|
+
// Bytes to the app on the connection c.
|
|
1981
|
+
bridgeSend(c, bytes) {
|
|
1982
|
+
c.sent += bytes.length;
|
|
1983
|
+
this.event({ t: 'tcp_data', id: c.id }, bytes);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// The body of a request to the app, in parts of UPLOAD_PART bytes or more
|
|
1987
|
+
// (or the rest of the body): one event for each part, not for each small
|
|
1988
|
+
// chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
|
|
1989
|
+
// unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
|
|
1990
|
+
// the memory of the VM, and a slow app slows the client.
|
|
1991
|
+
async bridgeUpload(c, body, chunked, max) {
|
|
1992
|
+
// workerd has readAtLeast. The option min of the standard BYOB read is
|
|
1993
|
+
// not safe: when the stream closes with fewer bytes than min, the read
|
|
1994
|
+
// errors the stream (Node.js 26, Chromium). So the other hosts use the
|
|
1995
|
+
// default reader, and the parts come from the chunks here.
|
|
1996
|
+
let byob = null;
|
|
1997
|
+
try { byob = body.getReader({ mode: 'byob' }); } catch {}
|
|
1998
|
+
if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
|
|
1999
|
+
const reader = byob ?? body.getReader();
|
|
2000
|
+
const read = byob ? () => byob.readAtLeast(UPLOAD_PART, new Uint8Array(UPLOAD_READ)) : () => reader.read();
|
|
2001
|
+
let total = 0;
|
|
2002
|
+
let part = [];
|
|
2003
|
+
let size = 0;
|
|
2004
|
+
// The gathered part to the app, when less than UPLOAD_WINDOW bytes are
|
|
2005
|
+
// unread. False when the connection ended while it waited.
|
|
2006
|
+
const flush = async () => {
|
|
2007
|
+
while (c.sent - c.read >= UPLOAD_WINDOW && !c.done && !this.dead) {
|
|
2008
|
+
await new Promise((resolve) => { c.room = resolve; });
|
|
2009
|
+
}
|
|
2010
|
+
if (c.done || this.dead) return false;
|
|
2011
|
+
const bytes = part.length === 1 ? part[0] : join(part, size);
|
|
2012
|
+
part = [];
|
|
2013
|
+
size = 0;
|
|
2014
|
+
this.bridgeSend(c, chunked ? chunk(bytes) : bytes);
|
|
2015
|
+
return true;
|
|
2016
|
+
};
|
|
2017
|
+
try {
|
|
2018
|
+
for (;;) {
|
|
2019
|
+
// A read can end the stream with its last bytes (done and a value).
|
|
2020
|
+
const { value, done } = await read();
|
|
2021
|
+
if (c.done || this.dead) break;
|
|
2022
|
+
if (value?.length) {
|
|
2023
|
+
total += value.length;
|
|
2024
|
+
if (max && total > max) {
|
|
2025
|
+
this.bridgeRefuse(c, this.tooLarge(max));
|
|
2026
|
+
break;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
// A chunk of the client can be large (Deno): it goes in pieces of
|
|
2030
|
+
// UPLOAD_READ bytes or less, so the window stays small.
|
|
2031
|
+
let ok = true;
|
|
2032
|
+
for (let at = 0; ok && value && at < value.length; at += UPLOAD_READ) {
|
|
2033
|
+
const piece = value.subarray(at, at + UPLOAD_READ);
|
|
2034
|
+
part.push(piece);
|
|
2035
|
+
size += piece.length;
|
|
2036
|
+
if (size >= UPLOAD_PART) ok = await flush();
|
|
2037
|
+
}
|
|
2038
|
+
if (ok && done && size) ok = await flush();
|
|
2039
|
+
if (!ok) break;
|
|
2040
|
+
if (done) {
|
|
2041
|
+
if (chunked) this.bridgeSend(c, CHUNKS_END);
|
|
2042
|
+
this.bridgeClock(c, c.timeout * 1000);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
reader.cancel().catch(() => {});
|
|
2047
|
+
} catch {
|
|
2048
|
+
// The client stopped the upload.
|
|
2049
|
+
this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
|
|
2054
|
+
bridgeClock(c, ms) {
|
|
2055
|
+
if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// The app read n bytes of the connection c (tcp_read).
|
|
2059
|
+
bridgeRead(c, n) {
|
|
2060
|
+
c.read += n;
|
|
2061
|
+
this.bridgeRoom(c);
|
|
2062
|
+
c.inbound?.flush();
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// The upload of c waits no more (more room, or the end of c).
|
|
2066
|
+
bridgeRoom(c) {
|
|
2067
|
+
const room = c.room;
|
|
2068
|
+
c.room = null;
|
|
2069
|
+
room?.();
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// The answer of the host in place of the app (when the app has not
|
|
2073
|
+
// answered yet), and the end of the connection for the app.
|
|
2074
|
+
bridgeRefuse(c, response) {
|
|
2075
|
+
if (!c.status) {
|
|
2076
|
+
clearTimeout(c.timer);
|
|
2077
|
+
c.status = response.status;
|
|
2078
|
+
c.resolve(response);
|
|
2079
|
+
c.finished();
|
|
2080
|
+
}
|
|
2081
|
+
this.bridgeDone(c);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// The answer above BEAM_MAX_BODY.
|
|
2085
|
+
tooLarge(max) {
|
|
2086
|
+
this.log(`beam: a request body above ${max} bytes: 413`);
|
|
2087
|
+
return new Response(`The body is larger than ${max} bytes.\n`, { status: 413, headers: { 'content-type': 'text/plain' } });
|
|
2088
|
+
}
|
|
2089
|
+
|
|
1673
2090
|
// Bytes from the HTTP server of the app to the connection c of bridge().
|
|
1674
2091
|
bridgeData(c, data) {
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
2092
|
+
c.in.push(data);
|
|
2093
|
+
while (!c.status) {
|
|
2094
|
+
// The head of the response: it is small, so the bytes are joined.
|
|
2095
|
+
const head = c.in.peek(c.in.size);
|
|
2096
|
+
const end = indexOf(head, [13, 10, 13, 10]);
|
|
2097
|
+
if (end < 0) {
|
|
2098
|
+
if (head.length > HEAD_MAX) throw new Error(`a head of a response above ${HEAD_MAX} bytes`);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
// The bytes of a header value are characters of one byte (Headers
|
|
2102
|
+
// takes no character above U+00FF).
|
|
2103
|
+
const lines = latin1(head.subarray(0, end)).split('\r\n');
|
|
2104
|
+
c.in.drop(end + 4);
|
|
2105
|
+
const status = Number(lines[0].split(' ')[1]);
|
|
2106
|
+
// An informational head (100 Continue, 103 Early Hints): the head of
|
|
2107
|
+
// the response comes after it.
|
|
2108
|
+
if (status >= 100 && status < 200 && !(status === 101 && c.upgrade)) continue;
|
|
1684
2109
|
const headers = new Headers();
|
|
1685
2110
|
for (const l of lines.slice(1)) {
|
|
1686
2111
|
const i = l.indexOf(':');
|
|
1687
2112
|
headers.append(l.slice(0, i).trim(), l.slice(i + 1).trim());
|
|
1688
2113
|
}
|
|
1689
|
-
c.
|
|
1690
|
-
if (
|
|
1691
|
-
|
|
2114
|
+
clearTimeout(c.timer);
|
|
2115
|
+
if (status === 101) {
|
|
2116
|
+
c.status = 101;
|
|
2117
|
+
return this.bridgeUpgrade(c);
|
|
2118
|
+
}
|
|
2119
|
+
if (c.upgrade && status === 403) {
|
|
1692
2120
|
console.log(`beam: the app refused the WebSocket of ${c.path} (403) from the origin ${c.origin}. ` +
|
|
1693
2121
|
`A Phoenix app compares the Origin with the host of its config (PHX_HOST=${this.env.PHX_HOST ?? this.vars.PHX_HOST ?? ''}): see check_origin.`);
|
|
1694
2122
|
}
|
|
@@ -1696,13 +2124,21 @@ export class Vm {
|
|
|
1696
2124
|
c.chunked = /chunked/i.test(headers.get('transfer-encoding') ?? '');
|
|
1697
2125
|
headers.delete('transfer-encoding');
|
|
1698
2126
|
headers.delete('connection');
|
|
1699
|
-
if (c.head ||
|
|
2127
|
+
if (c.head || status === 204 || status === 304) { c.length = 0; c.chunked = false; }
|
|
2128
|
+
c.left = 0;
|
|
1700
2129
|
const { readable, writable } = new TransformStream();
|
|
1701
|
-
c.writer = writable.getWriter();
|
|
1702
2130
|
// The app compressed the body (Bandit: gzip, deflate): the runtime
|
|
1703
2131
|
// must send it as it is, not compress it again.
|
|
1704
2132
|
const encodeBody = headers.has('content-encoding') ? 'manual' : 'automatic';
|
|
1705
|
-
|
|
2133
|
+
// The Response first: a status that it refuses ends the connection
|
|
2134
|
+
// with 502 (bridgeGuard), and the client gets an answer.
|
|
2135
|
+
const response = new Response(c.length === 0 ? null : readable, { status, headers, encodeBody });
|
|
2136
|
+
c.status = status;
|
|
2137
|
+
c.writer = writable.getWriter();
|
|
2138
|
+
// The client went away (or the stream failed): the app gets the end
|
|
2139
|
+
// of the connection.
|
|
2140
|
+
c.writer.closed.catch(() => this.bridgeDone(c));
|
|
2141
|
+
c.resolve(response);
|
|
1706
2142
|
c.finished();
|
|
1707
2143
|
}
|
|
1708
2144
|
if (c.ws) return this.bridgeFrames(c);
|
|
@@ -1710,32 +2146,95 @@ export class Vm {
|
|
|
1710
2146
|
}
|
|
1711
2147
|
|
|
1712
2148
|
// The body of a response: by content-length, chunked, or until the end.
|
|
2149
|
+
// The pieces of the app go to the stream as they are (views, no copy):
|
|
2150
|
+
// also the data of a large chunk, before its end.
|
|
1713
2151
|
bridgeBody(c) {
|
|
1714
2152
|
if (c.done) return;
|
|
2153
|
+
const pieces = c.in;
|
|
1715
2154
|
if (c.chunked) {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
2155
|
+
while (pieces.size && !c.done) {
|
|
2156
|
+
if (c.left > 0) {
|
|
2157
|
+
const part = pieces.next(c.left);
|
|
2158
|
+
c.left -= part.length;
|
|
2159
|
+
this.bridgeWrite(c, part);
|
|
2160
|
+
if (c.left === 0) c.left = -2; // then the CRLF of the chunk
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2163
|
+
if (c.left < 0) {
|
|
2164
|
+
const n = Math.min(-c.left, pieces.size);
|
|
2165
|
+
pieces.drop(n);
|
|
2166
|
+
c.left += n;
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
// The size line of the next chunk (a chunk extension after ";").
|
|
2170
|
+
const line = pieces.peek(Math.min(pieces.size, CHUNK_LINE));
|
|
2171
|
+
const nl = line.indexOf(10);
|
|
2172
|
+
if (nl < 0) {
|
|
2173
|
+
if (line.length >= CHUNK_LINE) this.bridgeBad(c);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
const size = parseInt(new TextDecoder().decode(line.subarray(0, nl)), 16);
|
|
2177
|
+
pieces.drop(nl + 1);
|
|
1720
2178
|
if (size === 0) return this.bridgeDone(c);
|
|
1721
|
-
if (
|
|
1722
|
-
c.
|
|
1723
|
-
c.buf = c.buf.slice(nl + 2 + size + 2);
|
|
2179
|
+
if (!(size > 0)) return this.bridgeBad(c);
|
|
2180
|
+
c.left = size;
|
|
1724
2181
|
}
|
|
2182
|
+
return;
|
|
1725
2183
|
}
|
|
1726
|
-
|
|
1727
|
-
const part = c.length
|
|
1728
|
-
if (part.length) c.writer.write(part.slice());
|
|
2184
|
+
while (pieces.size && c.length !== 0) {
|
|
2185
|
+
const part = pieces.next(c.length ?? Infinity);
|
|
1729
2186
|
if (c.length !== null) c.length -= part.length;
|
|
1730
|
-
c
|
|
2187
|
+
this.bridgeWrite(c, part);
|
|
1731
2188
|
}
|
|
1732
2189
|
if (c.length === 0) this.bridgeDone(c);
|
|
1733
2190
|
}
|
|
1734
2191
|
|
|
2192
|
+
// A part of the body to the client. A write to a stream that the client
|
|
2193
|
+
// closed fails, and writer.closed then ends the connection.
|
|
2194
|
+
bridgeWrite(c, part) {
|
|
2195
|
+
if (c.writer) c.wrote = c.writer.write(part).catch(() => {});
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// fn for the connection c (an event of the app). An exception ends c:
|
|
2199
|
+
// 502 before the head of the response, else the end of the stream or of
|
|
2200
|
+
// the WebSocket. Before, it stopped the thread of the VM, and so the VM.
|
|
2201
|
+
bridgeGuard(c, fn) {
|
|
2202
|
+
try {
|
|
2203
|
+
fn();
|
|
2204
|
+
} catch (e) {
|
|
2205
|
+
this.log(`beam: the response of ${c.path} failed (${e.message}): the connection ends`);
|
|
2206
|
+
if (c.ws) {
|
|
2207
|
+
try { c.ws.close(1011); } catch {}
|
|
2208
|
+
this.socketGone(c);
|
|
2209
|
+
this.conns.delete(c);
|
|
2210
|
+
this.tcps.delete(c.id);
|
|
2211
|
+
this.event({ t: 'tcp_closed', id: c.id });
|
|
2212
|
+
this.bridgeRelease(c);
|
|
2213
|
+
} else if (!c.status) {
|
|
2214
|
+
this.bridgeRefuse(c, new Response('bad gateway\n', { status: 502 }));
|
|
2215
|
+
} else {
|
|
2216
|
+
c.writer?.abort(e).catch(() => {});
|
|
2217
|
+
c.writer = null;
|
|
2218
|
+
this.bridgeDone(c);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// A chunked body that is not HTTP/1.1: the stream of the response
|
|
2224
|
+
// fails, and the app gets the end of the connection.
|
|
2225
|
+
bridgeBad(c) {
|
|
2226
|
+
console.log(`beam: a bad chunked body from the app for ${c.path}: the response stops`);
|
|
2227
|
+
c.writer?.abort(new Error('a bad chunked body from the app')).catch(() => {});
|
|
2228
|
+
c.writer = null;
|
|
2229
|
+
this.bridgeDone(c);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
1735
2232
|
bridgeDone(c) {
|
|
1736
2233
|
if (c.done) return;
|
|
1737
2234
|
c.done = true;
|
|
2235
|
+
this.bridgeRoom(c);
|
|
1738
2236
|
c.writer?.close().catch(() => {});
|
|
2237
|
+
this.conns.delete(c);
|
|
1739
2238
|
this.tcps.delete(c.id);
|
|
1740
2239
|
this.event({ t: 'tcp_closed', id: c.id });
|
|
1741
2240
|
this.bridgeRelease(c);
|
|
@@ -1746,17 +2245,40 @@ export class Vm {
|
|
|
1746
2245
|
if (c.released) return;
|
|
1747
2246
|
c.released = true;
|
|
1748
2247
|
if (c.h) { c.h.sockets--; c.h.wake?.(); }
|
|
2248
|
+
this.notePeak();
|
|
1749
2249
|
}
|
|
1750
2250
|
|
|
1751
2251
|
// The app closed the connection.
|
|
1752
2252
|
bridgeEnd(c) {
|
|
2253
|
+
clearTimeout(c.timer);
|
|
1753
2254
|
this.tcps.delete(c.id);
|
|
1754
2255
|
if (!c.status) {
|
|
2256
|
+
c.status = 502;
|
|
1755
2257
|
c.resolve(new Response('bad gateway\n', { status: 502 }));
|
|
1756
2258
|
c.finished();
|
|
1757
2259
|
}
|
|
1758
|
-
if (c.ws) {
|
|
1759
|
-
|
|
2260
|
+
if (c.ws) {
|
|
2261
|
+
try { c.ws.close(); } catch {}
|
|
2262
|
+
this.socketGone(c);
|
|
2263
|
+
this.conns.delete(c);
|
|
2264
|
+
this.bridgeRelease(c);
|
|
2265
|
+
} else this.bridgeDone(c);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// No head of a response in BEAM_REQUEST_TIMEOUT seconds: 504 for the
|
|
2269
|
+
// client, and the end of the connection for the app (tcp_closed).
|
|
2270
|
+
bridgeTimeout(c, seconds) {
|
|
2271
|
+
if (c.status || c.done) return;
|
|
2272
|
+
c.status = 504;
|
|
2273
|
+
c.resolve(this.late(`no response for ${c.path}`, seconds));
|
|
2274
|
+
c.finished();
|
|
2275
|
+
this.bridgeDone(c);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// The answer of BEAM_REQUEST_TIMEOUT.
|
|
2279
|
+
late(what, seconds) {
|
|
2280
|
+
console.log(`beam: ${what} in ${seconds} s: 504`);
|
|
2281
|
+
return new Response('The app did not answer in time.\n', { status: 504, headers: { 'content-type': 'text/plain' } });
|
|
1760
2282
|
}
|
|
1761
2283
|
|
|
1762
2284
|
// A WebSocket: the client end to the browser, frames to the app.
|
|
@@ -1765,14 +2287,23 @@ export class Vm {
|
|
|
1765
2287
|
server.accept();
|
|
1766
2288
|
server.binaryType = 'arraybuffer';
|
|
1767
2289
|
c.ws = server;
|
|
2290
|
+
c.counted = true;
|
|
2291
|
+
this.sockets++;
|
|
1768
2292
|
c.frames = []; // the parts of a message in fragments
|
|
2293
|
+
// The messages of the client go while less than UPLOAD_WINDOW bytes
|
|
2294
|
+
// are unread in the VM; the others wait here (Inbound).
|
|
2295
|
+
c.inbound = new Inbound(c, (b) => this.bridgeSend(c, b), () => {
|
|
2296
|
+
console.log(`beam: the app reads the WebSocket of ${c.path} too slowly: it closes (1008)`);
|
|
2297
|
+
closeSocket(server, 1008);
|
|
2298
|
+
});
|
|
1769
2299
|
server.addEventListener('message', (e) => {
|
|
1770
2300
|
const text = typeof e.data === 'string';
|
|
1771
|
-
|
|
2301
|
+
c.inbound.push(frame(text ? 1 : 2, text ? new TextEncoder().encode(e.data) : new Uint8Array(e.data)));
|
|
1772
2302
|
});
|
|
1773
2303
|
server.addEventListener('close', (e) => {
|
|
1774
|
-
|
|
1775
|
-
|
|
2304
|
+
this.socketGone(c);
|
|
2305
|
+
const code = wireCode(e.code);
|
|
2306
|
+
c.inbound.push(frame(8, new Uint8Array([code >> 8, code & 255])), true);
|
|
1776
2307
|
});
|
|
1777
2308
|
c.resolve(new Response(null, { status: 101, webSocket: client }));
|
|
1778
2309
|
c.finished();
|
|
@@ -1781,18 +2312,21 @@ export class Vm {
|
|
|
1781
2312
|
|
|
1782
2313
|
// WebSocket frames from the app (not masked) to messages for the client.
|
|
1783
2314
|
bridgeFrames(c) {
|
|
2315
|
+
const pieces = c.in;
|
|
1784
2316
|
for (;;) {
|
|
1785
|
-
|
|
2317
|
+
// The head of a frame (2 to 10 bytes), then its payload in one array.
|
|
2318
|
+
const b = pieces.peek(Math.min(pieces.size, 10));
|
|
1786
2319
|
if (b.length < 2) return;
|
|
1787
2320
|
let len = b[1] & 127, at = 2;
|
|
1788
2321
|
if (len === 126) { if (b.length < 4) return; len = (b[2] << 8) | b[3]; at = 4; }
|
|
1789
2322
|
else if (len === 127) { if (b.length < 10) return; len = Number(new DataView(b.buffer, b.byteOffset).getBigUint64(2)); at = 10; }
|
|
1790
|
-
if (
|
|
1791
|
-
const fin = b[0] & 128, op = b[0] & 15
|
|
1792
|
-
|
|
2323
|
+
if (pieces.size < at + len) return;
|
|
2324
|
+
const fin = b[0] & 128, op = b[0] & 15;
|
|
2325
|
+
pieces.drop(at);
|
|
2326
|
+
const data = pieces.take(len);
|
|
1793
2327
|
if (op === 9) { this.event({ t: 'tcp_data', id: c.id }, frame(10, data)); continue; }
|
|
1794
2328
|
if (op === 10) continue;
|
|
1795
|
-
if (op === 8) { c.ws
|
|
2329
|
+
if (op === 8) { closeSocket(c.ws, len >= 2 ? (data[0] << 8) | data[1] : 1000); continue; }
|
|
1796
2330
|
if (op !== 0) c.op = op;
|
|
1797
2331
|
c.frames.push(data);
|
|
1798
2332
|
if (!fin) continue;
|
|
@@ -1800,7 +2334,8 @@ export class Vm {
|
|
|
1800
2334
|
let i = 0;
|
|
1801
2335
|
for (const f of c.frames) { all.set(f, i); i += f.length; }
|
|
1802
2336
|
c.frames = [];
|
|
1803
|
-
|
|
2337
|
+
// The client can close at any time: a send after it is dropped.
|
|
2338
|
+
try { c.ws.send(c.op === 1 ? new TextDecoder().decode(all) : all); } catch {}
|
|
1804
2339
|
}
|
|
1805
2340
|
}
|
|
1806
2341
|
|
|
@@ -1810,17 +2345,29 @@ export class Vm {
|
|
|
1810
2345
|
const listener = this.listeners.get(port);
|
|
1811
2346
|
finished();
|
|
1812
2347
|
if (!listener) return new Response(`no listener on ${port}\n`, { status: 404 });
|
|
2348
|
+
const maxSockets = limit(this.env.BEAM_MAX_WEBSOCKETS, 0);
|
|
2349
|
+
if (maxSockets && this.sockets >= maxSockets) return this.busy('WebSockets');
|
|
1813
2350
|
const id = `w${this.nextId++}`;
|
|
1814
2351
|
const [client, server] = Object.values(new WebSocketPair());
|
|
1815
2352
|
server.accept();
|
|
1816
2353
|
server.binaryType = 'arraybuffer'; // (the default can be Blob)
|
|
1817
2354
|
if (h) h.sockets++;
|
|
1818
|
-
|
|
1819
|
-
this.
|
|
2355
|
+
const counted = { counted: true };
|
|
2356
|
+
this.sockets++;
|
|
2357
|
+
const t = { send: (b) => { try { server.send(b); } catch {} }, close: () => { this.socketGone(counted); try { server.close(); } catch {} }, h };
|
|
2358
|
+
const win = { sent: 0, read: 0 };
|
|
2359
|
+
const inbound = new Inbound(win, (b) => this.tcpIn(id, win, b), () => {
|
|
2360
|
+
console.log(`beam: the app reads the connection ${id} to port ${port} too slowly: it closes (1008)`);
|
|
2361
|
+
closeSocket(server, 1008);
|
|
2362
|
+
});
|
|
2363
|
+
t.ack = (n) => { win.read += n; inbound.flush(); };
|
|
2364
|
+
this.tcps.set(id, t);
|
|
2365
|
+
this.event({ t: 'tcp_accept', id: listener, conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: true, sent: true });
|
|
1820
2366
|
server.addEventListener('message', (e) => {
|
|
1821
|
-
|
|
2367
|
+
inbound.push(typeof e.data === 'string' ? new TextEncoder().encode(e.data) : new Uint8Array(e.data));
|
|
1822
2368
|
});
|
|
1823
2369
|
server.addEventListener('close', () => {
|
|
2370
|
+
this.socketGone(counted);
|
|
1824
2371
|
this.tcpClosed(id);
|
|
1825
2372
|
if (h) { h.sockets--; h.wake?.(); }
|
|
1826
2373
|
});
|
|
@@ -1833,7 +2380,7 @@ export class Vm {
|
|
|
1833
2380
|
// first (fetchFirst); the other protocols use connect(). direct: the
|
|
1834
2381
|
// tunnel of wasm_host_fetch, which never goes back to the fetch path.
|
|
1835
2382
|
async tcpConnect({ id, host, port, direct }, h) {
|
|
1836
|
-
let socket;
|
|
2383
|
+
let socket, inbound;
|
|
1837
2384
|
if (h) h.sockets++;
|
|
1838
2385
|
const allowed = connectAllowed(this.env.BEAM_CONNECT, host, port);
|
|
1839
2386
|
const fetchPath = this.listeners.has('fetch') && !direct;
|
|
@@ -1843,11 +2390,21 @@ export class Vm {
|
|
|
1843
2390
|
socket = net.connect({ host, port });
|
|
1844
2391
|
// A send resolves when the bytes are written; a close sends the
|
|
1845
2392
|
// bytes that wait, and then ends the socket.
|
|
1846
|
-
|
|
2393
|
+
const t = {
|
|
1847
2394
|
send: (b) => new Promise((resolve) => socket.write(b, () => resolve())),
|
|
1848
2395
|
close: () => socket.end(() => socket.destroy()),
|
|
1849
2396
|
h,
|
|
1850
|
-
}
|
|
2397
|
+
};
|
|
2398
|
+
// The data of the peer goes while less than UPLOAD_WINDOW bytes are
|
|
2399
|
+
// unread in the VM; else the socket pauses.
|
|
2400
|
+
const win = { sent: 0, read: 0 };
|
|
2401
|
+
inbound = new Inbound(win, (b) => this.tcpIn(id, win, b), () => {});
|
|
2402
|
+
t.ack = (n) => {
|
|
2403
|
+
win.read += n;
|
|
2404
|
+
inbound.flush();
|
|
2405
|
+
if (!inbound.full()) socket.resume();
|
|
2406
|
+
};
|
|
2407
|
+
this.tcps.set(id, t);
|
|
1851
2408
|
await new Promise((resolve, reject) => {
|
|
1852
2409
|
socket.once('connect', resolve);
|
|
1853
2410
|
socket.once('error', reject);
|
|
@@ -1869,9 +2426,12 @@ export class Vm {
|
|
|
1869
2426
|
if (h) { h.sockets--; h.wake?.(); }
|
|
1870
2427
|
return;
|
|
1871
2428
|
}
|
|
1872
|
-
this.event({ t: 'tcp_open', id });
|
|
2429
|
+
this.event({ t: 'tcp_open', id, ack: true, sent: true });
|
|
1873
2430
|
await new Promise((resolve) => {
|
|
1874
|
-
socket.on('data', (b) =>
|
|
2431
|
+
socket.on('data', (b) => {
|
|
2432
|
+
inbound.push(new Uint8Array(b.buffer, b.byteOffset, b.byteLength), true);
|
|
2433
|
+
if (inbound.full()) socket.pause();
|
|
2434
|
+
});
|
|
1875
2435
|
socket.on('error', () => {});
|
|
1876
2436
|
socket.once('close', resolve);
|
|
1877
2437
|
});
|
|
@@ -1908,6 +2468,9 @@ export class Vm {
|
|
|
1908
2468
|
} catch (e) {
|
|
1909
2469
|
console.log(`beam: resolve ${name}: ${e.message}`);
|
|
1910
2470
|
}
|
|
2471
|
+
// At most DNS_MAX names: a new name removes the oldest.
|
|
2472
|
+
this.dns.delete(name);
|
|
2473
|
+
if (this.dns.size >= DNS_MAX) this.dns.delete(this.dns.keys().next().value);
|
|
1911
2474
|
this.dns.set(name, { at: now, addresses });
|
|
1912
2475
|
return addresses.some((a) => inRanges(a));
|
|
1913
2476
|
}
|
|
@@ -1917,21 +2480,29 @@ export class Vm {
|
|
|
1917
2480
|
// an open connection to host:port. The pair ends when one side closes.
|
|
1918
2481
|
async fetchPair(id, host, port, h) {
|
|
1919
2482
|
const conn = `x${this.nextId++}`;
|
|
1920
|
-
let done;
|
|
2483
|
+
let done, pairEnd;
|
|
1921
2484
|
const ended = new Promise((r) => { done = r; });
|
|
1922
2485
|
const end = (other) => {
|
|
1923
2486
|
if (!this.fetchConns.has(conn)) return;
|
|
1924
2487
|
this.fetchConns.delete(conn);
|
|
1925
2488
|
this.tcps.delete(id);
|
|
1926
2489
|
this.tcps.delete(conn);
|
|
2490
|
+
// The fetches of this connection: no one reads their bodies now.
|
|
2491
|
+
for (const ac of this.fetches.values()) if (ac.conn === conn) ac.abort();
|
|
2492
|
+
pairEnd();
|
|
1927
2493
|
this.event({ t: 'tcp_closed', id: other });
|
|
1928
2494
|
done();
|
|
1929
2495
|
};
|
|
1930
2496
|
this.fetchConns.set(conn, { host, port, h });
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
this.event({ t: '
|
|
1934
|
-
this.event({ t: '
|
|
2497
|
+
// A send of one socket ends when the other socket read the bytes
|
|
2498
|
+
// (tcp_read): so the window of the sender (tcp_sent) holds them.
|
|
2499
|
+
const toConn = relay((b) => this.event({ t: 'tcp_data', id: conn }, b));
|
|
2500
|
+
const toId = relay((b) => this.event({ t: 'tcp_data', id }, b));
|
|
2501
|
+
pairEnd = () => { toConn.end(); toId.end(); };
|
|
2502
|
+
this.tcps.set(id, { send: toConn.send, close: () => end(conn), ack: toId.read, h });
|
|
2503
|
+
this.tcps.set(conn, { send: toId.send, close: () => end(id), ack: toConn.read, h });
|
|
2504
|
+
this.event({ t: 'tcp_open', id, ack: true, sent: true });
|
|
2505
|
+
this.event({ t: 'tcp_accept', id: this.listeners.get('fetch'), conn, host, port, ack: true, sent: true });
|
|
1935
2506
|
await ended;
|
|
1936
2507
|
if (h) { h.sockets--; h.wake?.(); }
|
|
1937
2508
|
}
|
|
@@ -1944,6 +2515,12 @@ export class Vm {
|
|
|
1944
2515
|
if (h) h.sockets++;
|
|
1945
2516
|
this.fetchPending++;
|
|
1946
2517
|
const ac = new AbortController();
|
|
2518
|
+
ac.conn = msg.conn;
|
|
2519
|
+
// ack: wasm_host_fetch tells the host what it read (fetch_read). The
|
|
2520
|
+
// body then goes while less than UPLOAD_WINDOW bytes are unread.
|
|
2521
|
+
ac.sent = 0;
|
|
2522
|
+
ac.read = 0;
|
|
2523
|
+
ac.signal.addEventListener('abort', () => ac.room?.());
|
|
1947
2524
|
this.fetches.set(msg.id, ac);
|
|
1948
2525
|
try {
|
|
1949
2526
|
const url = c && fetchUrl(c.host, c.port, msg.tls, msg.path);
|
|
@@ -1961,7 +2538,13 @@ export class Vm {
|
|
|
1961
2538
|
for (;;) {
|
|
1962
2539
|
const { done, value } = await reader.read();
|
|
1963
2540
|
if (done) break;
|
|
1964
|
-
if (value?.byteLength)
|
|
2541
|
+
if (!value?.byteLength) continue;
|
|
2542
|
+
this.event({ t: 'fetch_data', id: msg.id }, value);
|
|
2543
|
+
ac.sent += value.byteLength;
|
|
2544
|
+
while (msg.ack && ac.sent - ac.read >= UPLOAD_WINDOW && !ac.signal.aborted) {
|
|
2545
|
+
await new Promise((resolve) => { ac.room = resolve; });
|
|
2546
|
+
}
|
|
2547
|
+
if (ac.signal.aborted) throw new Error('the fetch stopped');
|
|
1965
2548
|
}
|
|
1966
2549
|
}
|
|
1967
2550
|
this.event({ t: 'fetch_end', id: msg.id });
|
|
@@ -1974,12 +2557,42 @@ export class Vm {
|
|
|
1974
2557
|
}
|
|
1975
2558
|
}
|
|
1976
2559
|
|
|
1977
|
-
// Data of a TCP socket: to Erlang, or to its peer after a
|
|
2560
|
+
// Data of a TCP socket: to Erlang (then true), or to its peer after a
|
|
2561
|
+
// splice.
|
|
1978
2562
|
tcpData(id, bytes) {
|
|
1979
2563
|
const t = this.tcps.get(id);
|
|
1980
2564
|
const peer = t?.peer && this.tcps.get(t.peer);
|
|
1981
2565
|
if (peer) this.run(() => peer.send(bytes), peer.h);
|
|
1982
2566
|
else this.event({ t: 'tcp_data', id }, bytes);
|
|
2567
|
+
return !peer;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// Data of a peer for the socket id, with the window win of Inbound: the
|
|
2571
|
+
// bytes that go to Erlang count, and the bytes to a spliced peer do not
|
|
2572
|
+
// (no tcp_read comes for them).
|
|
2573
|
+
tcpIn(id, win, bytes) {
|
|
2574
|
+
if (this.tcpData(id, bytes)) win.sent += bytes.length;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// A send of the VM to the socket t. The host tells the VM how many bytes
|
|
2578
|
+
// the peer took (tcp_sent), after each ACK_BYTES: wasm_tcp holds a send
|
|
2579
|
+
// while SEND_WINDOW bytes wait in the host. A send that fails counts
|
|
2580
|
+
// too, because the socket then ends.
|
|
2581
|
+
tcpSend(id, t, body) {
|
|
2582
|
+
const done = () => {
|
|
2583
|
+
t.took = (t.took ?? 0) + body.length;
|
|
2584
|
+
if (t.took >= ACK_BYTES) {
|
|
2585
|
+
this.event({ t: 'tcp_sent', id, n: t.took });
|
|
2586
|
+
t.took = 0;
|
|
2587
|
+
}
|
|
2588
|
+
};
|
|
2589
|
+
let r;
|
|
2590
|
+
try {
|
|
2591
|
+
r = t.send(body);
|
|
2592
|
+
} finally {
|
|
2593
|
+
if (typeof r?.then !== 'function') done();
|
|
2594
|
+
}
|
|
2595
|
+
if (typeof r?.then === 'function') r.then(done, done);
|
|
1983
2596
|
}
|
|
1984
2597
|
|
|
1985
2598
|
// The end of a TCP socket: to Erlang, and the end of its peer too.
|
|
@@ -1995,10 +2608,22 @@ export class Vm {
|
|
|
1995
2608
|
}
|
|
1996
2609
|
}
|
|
1997
2610
|
|
|
2611
|
+
// A message of the VM (jspi_host_send). It runs in the thread of the VM:
|
|
2612
|
+
// an exception of the host must not stop that thread, so it goes to the
|
|
2613
|
+
// log.
|
|
1998
2614
|
onsend(bytes) {
|
|
2615
|
+
try {
|
|
2616
|
+
this.onsendEvent(bytes);
|
|
2617
|
+
} catch (e) {
|
|
2618
|
+
this.log(`beam: the host failed on a message of the VM (${e?.message ?? e})`);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
onsendEvent(bytes) {
|
|
1999
2623
|
const nl = bytes.indexOf(10);
|
|
2000
2624
|
const msg = JSON.parse(new TextDecoder().decode(bytes.subarray(0, nl)));
|
|
2001
|
-
|
|
2625
|
+
// bytes is a new array (jspi_host_send): the body is a view of it.
|
|
2626
|
+
const body = bytes.subarray(nl + 1);
|
|
2002
2627
|
switch (msg.t) {
|
|
2003
2628
|
case 'ready':
|
|
2004
2629
|
this.onready();
|
|
@@ -2029,9 +2654,20 @@ export class Vm {
|
|
|
2029
2654
|
case 'fetch_cancel':
|
|
2030
2655
|
this.fetches.get(msg.id)?.abort();
|
|
2031
2656
|
break;
|
|
2657
|
+
// wasm_host_fetch read n bytes of the body of a fetch().
|
|
2658
|
+
case 'fetch_read': {
|
|
2659
|
+
const ac = this.fetches.get(msg.id);
|
|
2660
|
+
if (ac) { ac.read += msg.n; ac.room?.(); }
|
|
2661
|
+
break;
|
|
2662
|
+
}
|
|
2032
2663
|
case 'tcp_send': {
|
|
2033
2664
|
const t = this.tcps.get(msg.id);
|
|
2034
|
-
if (t) this.run(() =>
|
|
2665
|
+
if (t) this.run(() => this.tcpSend(msg.id, t, body), t.h);
|
|
2666
|
+
break;
|
|
2667
|
+
}
|
|
2668
|
+
// wasm_tcp: the app read n bytes of a socket.
|
|
2669
|
+
case 'tcp_read': {
|
|
2670
|
+
this.tcps.get(msg.id)?.ack?.(msg.n);
|
|
2035
2671
|
break;
|
|
2036
2672
|
}
|
|
2037
2673
|
case 'tcp_close': {
|
|
@@ -2115,7 +2751,195 @@ export function staticResponse(statics, request) {
|
|
|
2115
2751
|
return new Response(body, { headers });
|
|
2116
2752
|
}
|
|
2117
2753
|
|
|
2754
|
+
// The result of a wait that passed its time limit.
|
|
2755
|
+
const LATE = Symbol('late');
|
|
2756
|
+
|
|
2757
|
+
// A request body (bridgeUpload): the bytes that can be unread in the VM,
|
|
2758
|
+
// the smallest part of an event (except the last one), and the size of a
|
|
2759
|
+
// read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
|
|
2760
|
+
// costs a turn of the VM.
|
|
2761
|
+
const UPLOAD_WINDOW = 256 * 1024;
|
|
2762
|
+
const UPLOAD_PART = 16 * 1024;
|
|
2763
|
+
const UPLOAD_READ = 64 * 1024;
|
|
2764
|
+
|
|
2765
|
+
// The size bytes of the list parts, as one array.
|
|
2766
|
+
function join(parts, size) {
|
|
2767
|
+
const b = new Uint8Array(size);
|
|
2768
|
+
let at = 0;
|
|
2769
|
+
for (const p of parts) { b.set(p, at); at += p.length; }
|
|
2770
|
+
return b;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
// A chunk of a chunked body, and the end of the body.
|
|
2774
|
+
const CHUNKS_END = new TextEncoder().encode('0\r\n\r\n');
|
|
2775
|
+
function chunk(data) {
|
|
2776
|
+
const size = new TextEncoder().encode(`${data.length.toString(16)}\r\n`);
|
|
2777
|
+
const b = new Uint8Array(size.length + data.length + 2);
|
|
2778
|
+
b.set(size);
|
|
2779
|
+
b.set(data, size.length);
|
|
2780
|
+
b.set([13, 10], size.length + data.length);
|
|
2781
|
+
return b;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// A limit of a var: a number above 0, else the default (0: no limit).
|
|
2785
|
+
// "0" turns a limit with a default off.
|
|
2786
|
+
function limit(text, fallback) {
|
|
2787
|
+
if (text === undefined || text === null || text === '') return fallback;
|
|
2788
|
+
const n = Number(text);
|
|
2789
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2118
2792
|
// The position of the bytes pat in b, or -1.
|
|
2793
|
+
// The longest size line of a chunk of a response (with its extensions),
|
|
2794
|
+
// and the longest head of a response.
|
|
2795
|
+
const CHUNK_LINE = 4096;
|
|
2796
|
+
const HEAD_MAX = 1 << 20;
|
|
2797
|
+
// The longest wait of a request for a snapshot that the VM makes.
|
|
2798
|
+
const SNAPSHOT_WAIT = 10000;
|
|
2799
|
+
// Flow control of the sockets: wasm_tcp gives tcp_read after each
|
|
2800
|
+
// ACK_BYTES that the app read, and the host gives tcp_sent after each
|
|
2801
|
+
// ACK_BYTES that a peer took. Above SOCKET_QUEUE bytes from a peer that
|
|
2802
|
+
// wait for the app (Inbound), a WebSocket closes.
|
|
2803
|
+
const ACK_BYTES = 64 * 1024;
|
|
2804
|
+
const SOCKET_QUEUE = 16 * 1024 * 1024;
|
|
2805
|
+
|
|
2806
|
+
// The names of the fetch path in the cache of isCloudflare.
|
|
2807
|
+
const DNS_MAX = 1024;
|
|
2808
|
+
|
|
2809
|
+
// Bytes as text of one character for each byte (ISO-8859-1), and back.
|
|
2810
|
+
function latin1(b) {
|
|
2811
|
+
let s = '';
|
|
2812
|
+
for (let i = 0; i < b.length; i += 8192) s += String.fromCharCode(...b.subarray(i, i + 8192));
|
|
2813
|
+
return s;
|
|
2814
|
+
}
|
|
2815
|
+
function latin1Bytes(s) {
|
|
2816
|
+
const b = new Uint8Array(s.length);
|
|
2817
|
+
for (let i = 0; i < s.length; i++) b[i] = s.charCodeAt(i);
|
|
2818
|
+
return b;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// The close code of a client for the app: 1005, 1006 and 1015 are not
|
|
2822
|
+
// codes for the wire (RFC 6455).
|
|
2823
|
+
function wireCode(code) {
|
|
2824
|
+
if (code === 1006 || code === 1015) return 1001;
|
|
2825
|
+
return code && code !== 1005 ? code : 1000;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// close(code) of a WebSocket, with a code that it takes (else 1000), and
|
|
2829
|
+
// no exception for a socket that is closed already.
|
|
2830
|
+
function closeSocket(ws, code) {
|
|
2831
|
+
try {
|
|
2832
|
+
ws.close(code === 1000 || (code >= 3000 && code <= 4999) || (code >= 1001 && code <= 1014 && ![1004, 1005, 1006].includes(code)) ? code : 1000);
|
|
2833
|
+
} catch {}
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// Bytes in pieces (views of the arrays that came, no copy), joined only
|
|
2837
|
+
// when a reader needs them in one array (peek, take).
|
|
2838
|
+
// The bytes from a peer to a socket of the VM, with flow control: they go
|
|
2839
|
+
// while less than UPLOAD_WINDOW bytes are unread in the VM (win.sent, and
|
|
2840
|
+
// win.read from tcp_read), and the others wait here. Above SOCKET_QUEUE
|
|
2841
|
+
// bytes that wait, over() runs once, and the next bytes are dropped. A
|
|
2842
|
+
// forced push always waits (the end of a WebSocket).
|
|
2843
|
+
export class Inbound {
|
|
2844
|
+
constructor(win, send, over) {
|
|
2845
|
+
Object.assign(this, { win, send, over, queue: [], queued: 0, overflow: false });
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
full() {
|
|
2849
|
+
return this.win.sent - this.win.read >= UPLOAD_WINDOW;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
push(bytes, force = false) {
|
|
2853
|
+
if (!this.queue.length && !this.full()) return this.send(bytes);
|
|
2854
|
+
if (!force && (this.overflow || this.queued + bytes.length > SOCKET_QUEUE)) {
|
|
2855
|
+
if (!this.overflow) {
|
|
2856
|
+
this.overflow = true;
|
|
2857
|
+
this.over();
|
|
2858
|
+
}
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
this.queue.push(bytes);
|
|
2862
|
+
this.queued += bytes.length;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// After a tcp_read: the bytes that wait, while the window has room.
|
|
2866
|
+
flush() {
|
|
2867
|
+
while (this.queue.length && !this.full()) {
|
|
2868
|
+
const bytes = this.queue.shift();
|
|
2869
|
+
this.queued -= bytes.length;
|
|
2870
|
+
this.send(bytes);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
// The bytes from one socket of the VM to another one in the host (the
|
|
2876
|
+
// fetch path): deliver gives them to the other socket. A send ends when
|
|
2877
|
+
// the other socket read them (read, from its tcp_read), so the window of
|
|
2878
|
+
// the sender (tcp_sent) holds the bytes that wait. end() ends each send.
|
|
2879
|
+
export function relay(deliver) {
|
|
2880
|
+
let sent = 0, read = 0;
|
|
2881
|
+
const waits = [];
|
|
2882
|
+
return {
|
|
2883
|
+
send: (b) => {
|
|
2884
|
+
deliver(b);
|
|
2885
|
+
sent += b.length;
|
|
2886
|
+
if (read >= sent) return undefined;
|
|
2887
|
+
const mark = sent;
|
|
2888
|
+
return new Promise((resolve) => waits.push([mark, resolve]));
|
|
2889
|
+
},
|
|
2890
|
+
read: (n) => {
|
|
2891
|
+
read += n;
|
|
2892
|
+
while (waits.length && waits[0][0] <= read) waits.shift()[1]();
|
|
2893
|
+
},
|
|
2894
|
+
end: () => { for (const [, resolve] of waits.splice(0)) resolve(); },
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
export class Pieces {
|
|
2899
|
+
constructor() { this.list = []; this.size = 0; }
|
|
2900
|
+
push(b) {
|
|
2901
|
+
if (!b.length) return;
|
|
2902
|
+
this.list.push(b);
|
|
2903
|
+
this.size += b.length;
|
|
2904
|
+
}
|
|
2905
|
+
// The first n bytes in one array. A view when the first piece has them;
|
|
2906
|
+
// else a join, which then is the first piece.
|
|
2907
|
+
peek(n) {
|
|
2908
|
+
if (!this.list.length || this.list[0].length >= n) return (this.list[0] ?? new Uint8Array(0)).subarray(0, n);
|
|
2909
|
+
const out = new Uint8Array(n);
|
|
2910
|
+
let at = 0;
|
|
2911
|
+
for (const p of this.list) {
|
|
2912
|
+
if (at >= n) break;
|
|
2913
|
+
const k = Math.min(p.length, n - at);
|
|
2914
|
+
out.set(p.subarray(0, k), at);
|
|
2915
|
+
at += k;
|
|
2916
|
+
}
|
|
2917
|
+
this.drop(n);
|
|
2918
|
+
this.list.unshift(out);
|
|
2919
|
+
this.size += n;
|
|
2920
|
+
return out;
|
|
2921
|
+
}
|
|
2922
|
+
drop(n) {
|
|
2923
|
+
this.size -= n;
|
|
2924
|
+
while (n > 0) {
|
|
2925
|
+
const p = this.list[0];
|
|
2926
|
+
if (p.length <= n) { this.list.shift(); n -= p.length; }
|
|
2927
|
+
else { this.list[0] = p.subarray(n); n = 0; }
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
take(n) {
|
|
2931
|
+
const b = this.peek(n);
|
|
2932
|
+
this.drop(n);
|
|
2933
|
+
return b;
|
|
2934
|
+
}
|
|
2935
|
+
// At most max bytes of the first piece (a view).
|
|
2936
|
+
next(max) {
|
|
2937
|
+
const b = this.list[0].subarray(0, Math.min(max, this.list[0].length));
|
|
2938
|
+
this.drop(b.length);
|
|
2939
|
+
return b;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2119
2943
|
function indexOf(b, pat) {
|
|
2120
2944
|
outer: for (let i = 0; i + pat.length <= b.length; i++) {
|
|
2121
2945
|
for (let j = 0; j < pat.length; j++) if (b[i + j] !== pat[j]) continue outer;
|