beam.com 0.1.0-rc.2 → 0.1.0-rc.4
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 +924 -83
- package/runtime/deno/worker.js +924 -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 +924 -83
- package/runtime/release.json +1 -1
- package/runtime/runtime-id.js +1 -1
- package/runtime/worker.js +924 -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,198 @@ 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
|
+
// With a body, the head goes in the event of the first part of the
|
|
1976
|
+
// body (bridgeUpload): a small body is one event, as before the parts.
|
|
1977
|
+
if (body) this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1978
|
+
else this.bridgeSend(c, bytes);
|
|
1670
1979
|
});
|
|
1671
1980
|
}
|
|
1672
1981
|
|
|
1982
|
+
// Bytes to the app on the connection c.
|
|
1983
|
+
bridgeSend(c, bytes) {
|
|
1984
|
+
c.sent += bytes.length;
|
|
1985
|
+
this.event({ t: 'tcp_data', id: c.id }, bytes);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// The body of a request to the app, in parts of UPLOAD_PART bytes or more
|
|
1989
|
+
// (or the rest of the body): one event for each part, not for each small
|
|
1990
|
+
// chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
|
|
1991
|
+
// unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
|
|
1992
|
+
// the memory of the VM, and a slow app slows the client. The head of the
|
|
1993
|
+
// request goes with the bytes of the first read of the client, with no
|
|
1994
|
+
// wait for UPLOAD_PART bytes.
|
|
1995
|
+
async bridgeUpload(c, body, chunked, max, head) {
|
|
1996
|
+
// workerd has readAtLeast. The option min of the standard BYOB read is
|
|
1997
|
+
// not safe: when the stream closes with fewer bytes than min, the read
|
|
1998
|
+
// errors the stream (Node.js 26, Chromium). So the other hosts use the
|
|
1999
|
+
// default reader, and the parts come from the chunks here.
|
|
2000
|
+
let byob = null;
|
|
2001
|
+
try { byob = body.getReader({ mode: 'byob' }); } catch {}
|
|
2002
|
+
if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
|
|
2003
|
+
const reader = byob ?? body.getReader();
|
|
2004
|
+
const read = byob ? (min) => byob.readAtLeast(min, new Uint8Array(UPLOAD_READ)) : () => reader.read();
|
|
2005
|
+
let total = 0;
|
|
2006
|
+
let part = [];
|
|
2007
|
+
let size = 0;
|
|
2008
|
+
// The gathered part to the app, when less than UPLOAD_WINDOW bytes are
|
|
2009
|
+
// unread. False when the connection ended while it waited.
|
|
2010
|
+
const flush = async () => {
|
|
2011
|
+
while (c.sent - c.read >= UPLOAD_WINDOW && !c.done && !this.dead) {
|
|
2012
|
+
await new Promise((resolve) => { c.room = resolve; });
|
|
2013
|
+
}
|
|
2014
|
+
if (c.done || this.dead) return false;
|
|
2015
|
+
const bytes = part.length === 1 ? part[0] : join(part, size);
|
|
2016
|
+
part = [];
|
|
2017
|
+
size = 0;
|
|
2018
|
+
out(chunked ? chunk(bytes) : bytes);
|
|
2019
|
+
return true;
|
|
2020
|
+
};
|
|
2021
|
+
// Bytes to the app, after the head when it did not go yet.
|
|
2022
|
+
const out = (bytes) => {
|
|
2023
|
+
clearTimeout(alone);
|
|
2024
|
+
this.bridgeSend(c, head ? join([head, bytes], head.length + bytes.length) : bytes);
|
|
2025
|
+
head = null;
|
|
2026
|
+
};
|
|
2027
|
+
// A client that sends no body yet: the head goes alone after HEAD_WAIT
|
|
2028
|
+
// ms, so the app sees the request (and its own time limits run).
|
|
2029
|
+
const alone = setTimeout(() => { if (head && !c.done && !this.dead) out(new Uint8Array(0)); }, HEAD_WAIT);
|
|
2030
|
+
try {
|
|
2031
|
+
for (;;) {
|
|
2032
|
+
// A read can end the stream with its last bytes (done and a value).
|
|
2033
|
+
const { value, done } = await read(head ? 1 : UPLOAD_PART);
|
|
2034
|
+
if (c.done || this.dead) break;
|
|
2035
|
+
if (value?.length) {
|
|
2036
|
+
total += value.length;
|
|
2037
|
+
if (max && total > max) {
|
|
2038
|
+
this.bridgeRefuse(c, this.tooLarge(max));
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
// A chunk of the client can be large (Deno): it goes in pieces of
|
|
2043
|
+
// UPLOAD_READ bytes or less, so the window stays small.
|
|
2044
|
+
let ok = true;
|
|
2045
|
+
for (let at = 0; ok && value && at < value.length; at += UPLOAD_READ) {
|
|
2046
|
+
const piece = value.subarray(at, at + UPLOAD_READ);
|
|
2047
|
+
part.push(piece);
|
|
2048
|
+
size += piece.length;
|
|
2049
|
+
if (size >= UPLOAD_PART) ok = await flush();
|
|
2050
|
+
}
|
|
2051
|
+
if (ok && (done || head) && size) ok = await flush();
|
|
2052
|
+
if (!ok) break;
|
|
2053
|
+
if (done) {
|
|
2054
|
+
if (chunked) out(CHUNKS_END);
|
|
2055
|
+
else if (head) out(new Uint8Array(0));
|
|
2056
|
+
this.bridgeClock(c, c.timeout * 1000);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
reader.cancel().catch(() => {});
|
|
2061
|
+
} catch {
|
|
2062
|
+
// The client stopped the upload.
|
|
2063
|
+
this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
|
|
2068
|
+
bridgeClock(c, ms) {
|
|
2069
|
+
if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// The app read n bytes of the connection c (tcp_read).
|
|
2073
|
+
bridgeRead(c, n) {
|
|
2074
|
+
c.read += n;
|
|
2075
|
+
this.bridgeRoom(c);
|
|
2076
|
+
c.inbound?.flush();
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// The upload of c waits no more (more room, or the end of c).
|
|
2080
|
+
bridgeRoom(c) {
|
|
2081
|
+
const room = c.room;
|
|
2082
|
+
c.room = null;
|
|
2083
|
+
room?.();
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// The answer of the host in place of the app (when the app has not
|
|
2087
|
+
// answered yet), and the end of the connection for the app.
|
|
2088
|
+
bridgeRefuse(c, response) {
|
|
2089
|
+
if (!c.status) {
|
|
2090
|
+
clearTimeout(c.timer);
|
|
2091
|
+
c.status = response.status;
|
|
2092
|
+
c.resolve(response);
|
|
2093
|
+
c.finished();
|
|
2094
|
+
}
|
|
2095
|
+
this.bridgeDone(c);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// The answer above BEAM_MAX_BODY.
|
|
2099
|
+
tooLarge(max) {
|
|
2100
|
+
this.log(`beam: a request body above ${max} bytes: 413`);
|
|
2101
|
+
return new Response(`The body is larger than ${max} bytes.\n`, { status: 413, headers: { 'content-type': 'text/plain' } });
|
|
2102
|
+
}
|
|
2103
|
+
|
|
1673
2104
|
// Bytes from the HTTP server of the app to the connection c of bridge().
|
|
1674
2105
|
bridgeData(c, data) {
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
2106
|
+
c.in.push(data);
|
|
2107
|
+
while (!c.status) {
|
|
2108
|
+
// The head of the response: it is small, so the bytes are joined.
|
|
2109
|
+
const head = c.in.peek(c.in.size);
|
|
2110
|
+
const end = indexOf(head, [13, 10, 13, 10]);
|
|
2111
|
+
if (end < 0) {
|
|
2112
|
+
if (head.length > HEAD_MAX) throw new Error(`a head of a response above ${HEAD_MAX} bytes`);
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
// The bytes of a header value are characters of one byte (Headers
|
|
2116
|
+
// takes no character above U+00FF).
|
|
2117
|
+
const lines = latin1(head.subarray(0, end)).split('\r\n');
|
|
2118
|
+
c.in.drop(end + 4);
|
|
2119
|
+
const status = Number(lines[0].split(' ')[1]);
|
|
2120
|
+
// An informational head (100 Continue, 103 Early Hints): the head of
|
|
2121
|
+
// the response comes after it.
|
|
2122
|
+
if (status >= 100 && status < 200 && !(status === 101 && c.upgrade)) continue;
|
|
1684
2123
|
const headers = new Headers();
|
|
1685
2124
|
for (const l of lines.slice(1)) {
|
|
1686
2125
|
const i = l.indexOf(':');
|
|
1687
2126
|
headers.append(l.slice(0, i).trim(), l.slice(i + 1).trim());
|
|
1688
2127
|
}
|
|
1689
|
-
c.
|
|
1690
|
-
if (
|
|
1691
|
-
|
|
2128
|
+
clearTimeout(c.timer);
|
|
2129
|
+
if (status === 101) {
|
|
2130
|
+
c.status = 101;
|
|
2131
|
+
return this.bridgeUpgrade(c);
|
|
2132
|
+
}
|
|
2133
|
+
if (c.upgrade && status === 403) {
|
|
1692
2134
|
console.log(`beam: the app refused the WebSocket of ${c.path} (403) from the origin ${c.origin}. ` +
|
|
1693
2135
|
`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
2136
|
}
|
|
@@ -1696,13 +2138,21 @@ export class Vm {
|
|
|
1696
2138
|
c.chunked = /chunked/i.test(headers.get('transfer-encoding') ?? '');
|
|
1697
2139
|
headers.delete('transfer-encoding');
|
|
1698
2140
|
headers.delete('connection');
|
|
1699
|
-
if (c.head ||
|
|
2141
|
+
if (c.head || status === 204 || status === 304) { c.length = 0; c.chunked = false; }
|
|
2142
|
+
c.left = 0;
|
|
1700
2143
|
const { readable, writable } = new TransformStream();
|
|
1701
|
-
c.writer = writable.getWriter();
|
|
1702
2144
|
// The app compressed the body (Bandit: gzip, deflate): the runtime
|
|
1703
2145
|
// must send it as it is, not compress it again.
|
|
1704
2146
|
const encodeBody = headers.has('content-encoding') ? 'manual' : 'automatic';
|
|
1705
|
-
|
|
2147
|
+
// The Response first: a status that it refuses ends the connection
|
|
2148
|
+
// with 502 (bridgeGuard), and the client gets an answer.
|
|
2149
|
+
const response = new Response(c.length === 0 ? null : readable, { status, headers, encodeBody });
|
|
2150
|
+
c.status = status;
|
|
2151
|
+
c.writer = writable.getWriter();
|
|
2152
|
+
// The client went away (or the stream failed): the app gets the end
|
|
2153
|
+
// of the connection.
|
|
2154
|
+
c.writer.closed.catch(() => this.bridgeDone(c));
|
|
2155
|
+
c.resolve(response);
|
|
1706
2156
|
c.finished();
|
|
1707
2157
|
}
|
|
1708
2158
|
if (c.ws) return this.bridgeFrames(c);
|
|
@@ -1710,32 +2160,95 @@ export class Vm {
|
|
|
1710
2160
|
}
|
|
1711
2161
|
|
|
1712
2162
|
// The body of a response: by content-length, chunked, or until the end.
|
|
2163
|
+
// The pieces of the app go to the stream as they are (views, no copy):
|
|
2164
|
+
// also the data of a large chunk, before its end.
|
|
1713
2165
|
bridgeBody(c) {
|
|
1714
2166
|
if (c.done) return;
|
|
2167
|
+
const pieces = c.in;
|
|
1715
2168
|
if (c.chunked) {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
2169
|
+
while (pieces.size && !c.done) {
|
|
2170
|
+
if (c.left > 0) {
|
|
2171
|
+
const part = pieces.next(c.left);
|
|
2172
|
+
c.left -= part.length;
|
|
2173
|
+
this.bridgeWrite(c, part);
|
|
2174
|
+
if (c.left === 0) c.left = -2; // then the CRLF of the chunk
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
if (c.left < 0) {
|
|
2178
|
+
const n = Math.min(-c.left, pieces.size);
|
|
2179
|
+
pieces.drop(n);
|
|
2180
|
+
c.left += n;
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
// The size line of the next chunk (a chunk extension after ";").
|
|
2184
|
+
const line = pieces.peek(Math.min(pieces.size, CHUNK_LINE));
|
|
2185
|
+
const nl = line.indexOf(10);
|
|
2186
|
+
if (nl < 0) {
|
|
2187
|
+
if (line.length >= CHUNK_LINE) this.bridgeBad(c);
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const size = parseInt(new TextDecoder().decode(line.subarray(0, nl)), 16);
|
|
2191
|
+
pieces.drop(nl + 1);
|
|
1720
2192
|
if (size === 0) return this.bridgeDone(c);
|
|
1721
|
-
if (
|
|
1722
|
-
c.
|
|
1723
|
-
c.buf = c.buf.slice(nl + 2 + size + 2);
|
|
2193
|
+
if (!(size > 0)) return this.bridgeBad(c);
|
|
2194
|
+
c.left = size;
|
|
1724
2195
|
}
|
|
2196
|
+
return;
|
|
1725
2197
|
}
|
|
1726
|
-
|
|
1727
|
-
const part = c.length
|
|
1728
|
-
if (part.length) c.writer.write(part.slice());
|
|
2198
|
+
while (pieces.size && c.length !== 0) {
|
|
2199
|
+
const part = pieces.next(c.length ?? Infinity);
|
|
1729
2200
|
if (c.length !== null) c.length -= part.length;
|
|
1730
|
-
c
|
|
2201
|
+
this.bridgeWrite(c, part);
|
|
1731
2202
|
}
|
|
1732
2203
|
if (c.length === 0) this.bridgeDone(c);
|
|
1733
2204
|
}
|
|
1734
2205
|
|
|
2206
|
+
// A part of the body to the client. A write to a stream that the client
|
|
2207
|
+
// closed fails, and writer.closed then ends the connection.
|
|
2208
|
+
bridgeWrite(c, part) {
|
|
2209
|
+
if (c.writer) c.wrote = c.writer.write(part).catch(() => {});
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// fn for the connection c (an event of the app). An exception ends c:
|
|
2213
|
+
// 502 before the head of the response, else the end of the stream or of
|
|
2214
|
+
// the WebSocket. Before, it stopped the thread of the VM, and so the VM.
|
|
2215
|
+
bridgeGuard(c, fn) {
|
|
2216
|
+
try {
|
|
2217
|
+
fn();
|
|
2218
|
+
} catch (e) {
|
|
2219
|
+
this.log(`beam: the response of ${c.path} failed (${e.message}): the connection ends`);
|
|
2220
|
+
if (c.ws) {
|
|
2221
|
+
try { c.ws.close(1011); } catch {}
|
|
2222
|
+
this.socketGone(c);
|
|
2223
|
+
this.conns.delete(c);
|
|
2224
|
+
this.tcps.delete(c.id);
|
|
2225
|
+
this.event({ t: 'tcp_closed', id: c.id });
|
|
2226
|
+
this.bridgeRelease(c);
|
|
2227
|
+
} else if (!c.status) {
|
|
2228
|
+
this.bridgeRefuse(c, new Response('bad gateway\n', { status: 502 }));
|
|
2229
|
+
} else {
|
|
2230
|
+
c.writer?.abort(e).catch(() => {});
|
|
2231
|
+
c.writer = null;
|
|
2232
|
+
this.bridgeDone(c);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// A chunked body that is not HTTP/1.1: the stream of the response
|
|
2238
|
+
// fails, and the app gets the end of the connection.
|
|
2239
|
+
bridgeBad(c) {
|
|
2240
|
+
console.log(`beam: a bad chunked body from the app for ${c.path}: the response stops`);
|
|
2241
|
+
c.writer?.abort(new Error('a bad chunked body from the app')).catch(() => {});
|
|
2242
|
+
c.writer = null;
|
|
2243
|
+
this.bridgeDone(c);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
1735
2246
|
bridgeDone(c) {
|
|
1736
2247
|
if (c.done) return;
|
|
1737
2248
|
c.done = true;
|
|
2249
|
+
this.bridgeRoom(c);
|
|
1738
2250
|
c.writer?.close().catch(() => {});
|
|
2251
|
+
this.conns.delete(c);
|
|
1739
2252
|
this.tcps.delete(c.id);
|
|
1740
2253
|
this.event({ t: 'tcp_closed', id: c.id });
|
|
1741
2254
|
this.bridgeRelease(c);
|
|
@@ -1746,17 +2259,40 @@ export class Vm {
|
|
|
1746
2259
|
if (c.released) return;
|
|
1747
2260
|
c.released = true;
|
|
1748
2261
|
if (c.h) { c.h.sockets--; c.h.wake?.(); }
|
|
2262
|
+
this.notePeak();
|
|
1749
2263
|
}
|
|
1750
2264
|
|
|
1751
2265
|
// The app closed the connection.
|
|
1752
2266
|
bridgeEnd(c) {
|
|
2267
|
+
clearTimeout(c.timer);
|
|
1753
2268
|
this.tcps.delete(c.id);
|
|
1754
2269
|
if (!c.status) {
|
|
2270
|
+
c.status = 502;
|
|
1755
2271
|
c.resolve(new Response('bad gateway\n', { status: 502 }));
|
|
1756
2272
|
c.finished();
|
|
1757
2273
|
}
|
|
1758
|
-
if (c.ws) {
|
|
1759
|
-
|
|
2274
|
+
if (c.ws) {
|
|
2275
|
+
try { c.ws.close(); } catch {}
|
|
2276
|
+
this.socketGone(c);
|
|
2277
|
+
this.conns.delete(c);
|
|
2278
|
+
this.bridgeRelease(c);
|
|
2279
|
+
} else this.bridgeDone(c);
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// No head of a response in BEAM_REQUEST_TIMEOUT seconds: 504 for the
|
|
2283
|
+
// client, and the end of the connection for the app (tcp_closed).
|
|
2284
|
+
bridgeTimeout(c, seconds) {
|
|
2285
|
+
if (c.status || c.done) return;
|
|
2286
|
+
c.status = 504;
|
|
2287
|
+
c.resolve(this.late(`no response for ${c.path}`, seconds));
|
|
2288
|
+
c.finished();
|
|
2289
|
+
this.bridgeDone(c);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// The answer of BEAM_REQUEST_TIMEOUT.
|
|
2293
|
+
late(what, seconds) {
|
|
2294
|
+
console.log(`beam: ${what} in ${seconds} s: 504`);
|
|
2295
|
+
return new Response('The app did not answer in time.\n', { status: 504, headers: { 'content-type': 'text/plain' } });
|
|
1760
2296
|
}
|
|
1761
2297
|
|
|
1762
2298
|
// A WebSocket: the client end to the browser, frames to the app.
|
|
@@ -1765,14 +2301,23 @@ export class Vm {
|
|
|
1765
2301
|
server.accept();
|
|
1766
2302
|
server.binaryType = 'arraybuffer';
|
|
1767
2303
|
c.ws = server;
|
|
2304
|
+
c.counted = true;
|
|
2305
|
+
this.sockets++;
|
|
1768
2306
|
c.frames = []; // the parts of a message in fragments
|
|
2307
|
+
// The messages of the client go while less than UPLOAD_WINDOW bytes
|
|
2308
|
+
// are unread in the VM; the others wait here (Inbound).
|
|
2309
|
+
c.inbound = new Inbound(c, (b) => this.bridgeSend(c, b), () => {
|
|
2310
|
+
console.log(`beam: the app reads the WebSocket of ${c.path} too slowly: it closes (1008)`);
|
|
2311
|
+
closeSocket(server, 1008);
|
|
2312
|
+
});
|
|
1769
2313
|
server.addEventListener('message', (e) => {
|
|
1770
2314
|
const text = typeof e.data === 'string';
|
|
1771
|
-
|
|
2315
|
+
c.inbound.push(frame(text ? 1 : 2, text ? new TextEncoder().encode(e.data) : new Uint8Array(e.data)));
|
|
1772
2316
|
});
|
|
1773
2317
|
server.addEventListener('close', (e) => {
|
|
1774
|
-
|
|
1775
|
-
|
|
2318
|
+
this.socketGone(c);
|
|
2319
|
+
const code = wireCode(e.code);
|
|
2320
|
+
c.inbound.push(frame(8, new Uint8Array([code >> 8, code & 255])), true);
|
|
1776
2321
|
});
|
|
1777
2322
|
c.resolve(new Response(null, { status: 101, webSocket: client }));
|
|
1778
2323
|
c.finished();
|
|
@@ -1781,18 +2326,21 @@ export class Vm {
|
|
|
1781
2326
|
|
|
1782
2327
|
// WebSocket frames from the app (not masked) to messages for the client.
|
|
1783
2328
|
bridgeFrames(c) {
|
|
2329
|
+
const pieces = c.in;
|
|
1784
2330
|
for (;;) {
|
|
1785
|
-
|
|
2331
|
+
// The head of a frame (2 to 10 bytes), then its payload in one array.
|
|
2332
|
+
const b = pieces.peek(Math.min(pieces.size, 10));
|
|
1786
2333
|
if (b.length < 2) return;
|
|
1787
2334
|
let len = b[1] & 127, at = 2;
|
|
1788
2335
|
if (len === 126) { if (b.length < 4) return; len = (b[2] << 8) | b[3]; at = 4; }
|
|
1789
2336
|
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
|
-
|
|
2337
|
+
if (pieces.size < at + len) return;
|
|
2338
|
+
const fin = b[0] & 128, op = b[0] & 15;
|
|
2339
|
+
pieces.drop(at);
|
|
2340
|
+
const data = pieces.take(len);
|
|
1793
2341
|
if (op === 9) { this.event({ t: 'tcp_data', id: c.id }, frame(10, data)); continue; }
|
|
1794
2342
|
if (op === 10) continue;
|
|
1795
|
-
if (op === 8) { c.ws
|
|
2343
|
+
if (op === 8) { closeSocket(c.ws, len >= 2 ? (data[0] << 8) | data[1] : 1000); continue; }
|
|
1796
2344
|
if (op !== 0) c.op = op;
|
|
1797
2345
|
c.frames.push(data);
|
|
1798
2346
|
if (!fin) continue;
|
|
@@ -1800,7 +2348,8 @@ export class Vm {
|
|
|
1800
2348
|
let i = 0;
|
|
1801
2349
|
for (const f of c.frames) { all.set(f, i); i += f.length; }
|
|
1802
2350
|
c.frames = [];
|
|
1803
|
-
|
|
2351
|
+
// The client can close at any time: a send after it is dropped.
|
|
2352
|
+
try { c.ws.send(c.op === 1 ? new TextDecoder().decode(all) : all); } catch {}
|
|
1804
2353
|
}
|
|
1805
2354
|
}
|
|
1806
2355
|
|
|
@@ -1810,17 +2359,29 @@ export class Vm {
|
|
|
1810
2359
|
const listener = this.listeners.get(port);
|
|
1811
2360
|
finished();
|
|
1812
2361
|
if (!listener) return new Response(`no listener on ${port}\n`, { status: 404 });
|
|
2362
|
+
const maxSockets = limit(this.env.BEAM_MAX_WEBSOCKETS, 0);
|
|
2363
|
+
if (maxSockets && this.sockets >= maxSockets) return this.busy('WebSockets');
|
|
1813
2364
|
const id = `w${this.nextId++}`;
|
|
1814
2365
|
const [client, server] = Object.values(new WebSocketPair());
|
|
1815
2366
|
server.accept();
|
|
1816
2367
|
server.binaryType = 'arraybuffer'; // (the default can be Blob)
|
|
1817
2368
|
if (h) h.sockets++;
|
|
1818
|
-
|
|
1819
|
-
this.
|
|
2369
|
+
const counted = { counted: true };
|
|
2370
|
+
this.sockets++;
|
|
2371
|
+
const t = { send: (b) => { try { server.send(b); } catch {} }, close: () => { this.socketGone(counted); try { server.close(); } catch {} }, h };
|
|
2372
|
+
const win = { sent: 0, read: 0 };
|
|
2373
|
+
const inbound = new Inbound(win, (b) => this.tcpIn(id, win, b), () => {
|
|
2374
|
+
console.log(`beam: the app reads the connection ${id} to port ${port} too slowly: it closes (1008)`);
|
|
2375
|
+
closeSocket(server, 1008);
|
|
2376
|
+
});
|
|
2377
|
+
t.ack = (n) => { win.read += n; inbound.flush(); };
|
|
2378
|
+
this.tcps.set(id, t);
|
|
2379
|
+
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
2380
|
server.addEventListener('message', (e) => {
|
|
1821
|
-
|
|
2381
|
+
inbound.push(typeof e.data === 'string' ? new TextEncoder().encode(e.data) : new Uint8Array(e.data));
|
|
1822
2382
|
});
|
|
1823
2383
|
server.addEventListener('close', () => {
|
|
2384
|
+
this.socketGone(counted);
|
|
1824
2385
|
this.tcpClosed(id);
|
|
1825
2386
|
if (h) { h.sockets--; h.wake?.(); }
|
|
1826
2387
|
});
|
|
@@ -1833,7 +2394,7 @@ export class Vm {
|
|
|
1833
2394
|
// first (fetchFirst); the other protocols use connect(). direct: the
|
|
1834
2395
|
// tunnel of wasm_host_fetch, which never goes back to the fetch path.
|
|
1835
2396
|
async tcpConnect({ id, host, port, direct }, h) {
|
|
1836
|
-
let socket;
|
|
2397
|
+
let socket, inbound;
|
|
1837
2398
|
if (h) h.sockets++;
|
|
1838
2399
|
const allowed = connectAllowed(this.env.BEAM_CONNECT, host, port);
|
|
1839
2400
|
const fetchPath = this.listeners.has('fetch') && !direct;
|
|
@@ -1843,11 +2404,21 @@ export class Vm {
|
|
|
1843
2404
|
socket = net.connect({ host, port });
|
|
1844
2405
|
// A send resolves when the bytes are written; a close sends the
|
|
1845
2406
|
// bytes that wait, and then ends the socket.
|
|
1846
|
-
|
|
2407
|
+
const t = {
|
|
1847
2408
|
send: (b) => new Promise((resolve) => socket.write(b, () => resolve())),
|
|
1848
2409
|
close: () => socket.end(() => socket.destroy()),
|
|
1849
2410
|
h,
|
|
1850
|
-
}
|
|
2411
|
+
};
|
|
2412
|
+
// The data of the peer goes while less than UPLOAD_WINDOW bytes are
|
|
2413
|
+
// unread in the VM; else the socket pauses.
|
|
2414
|
+
const win = { sent: 0, read: 0 };
|
|
2415
|
+
inbound = new Inbound(win, (b) => this.tcpIn(id, win, b), () => {});
|
|
2416
|
+
t.ack = (n) => {
|
|
2417
|
+
win.read += n;
|
|
2418
|
+
inbound.flush();
|
|
2419
|
+
if (!inbound.full()) socket.resume();
|
|
2420
|
+
};
|
|
2421
|
+
this.tcps.set(id, t);
|
|
1851
2422
|
await new Promise((resolve, reject) => {
|
|
1852
2423
|
socket.once('connect', resolve);
|
|
1853
2424
|
socket.once('error', reject);
|
|
@@ -1869,9 +2440,12 @@ export class Vm {
|
|
|
1869
2440
|
if (h) { h.sockets--; h.wake?.(); }
|
|
1870
2441
|
return;
|
|
1871
2442
|
}
|
|
1872
|
-
this.event({ t: 'tcp_open', id });
|
|
2443
|
+
this.event({ t: 'tcp_open', id, ack: true, sent: true });
|
|
1873
2444
|
await new Promise((resolve) => {
|
|
1874
|
-
socket.on('data', (b) =>
|
|
2445
|
+
socket.on('data', (b) => {
|
|
2446
|
+
inbound.push(new Uint8Array(b.buffer, b.byteOffset, b.byteLength), true);
|
|
2447
|
+
if (inbound.full()) socket.pause();
|
|
2448
|
+
});
|
|
1875
2449
|
socket.on('error', () => {});
|
|
1876
2450
|
socket.once('close', resolve);
|
|
1877
2451
|
});
|
|
@@ -1908,6 +2482,9 @@ export class Vm {
|
|
|
1908
2482
|
} catch (e) {
|
|
1909
2483
|
console.log(`beam: resolve ${name}: ${e.message}`);
|
|
1910
2484
|
}
|
|
2485
|
+
// At most DNS_MAX names: a new name removes the oldest.
|
|
2486
|
+
this.dns.delete(name);
|
|
2487
|
+
if (this.dns.size >= DNS_MAX) this.dns.delete(this.dns.keys().next().value);
|
|
1911
2488
|
this.dns.set(name, { at: now, addresses });
|
|
1912
2489
|
return addresses.some((a) => inRanges(a));
|
|
1913
2490
|
}
|
|
@@ -1917,21 +2494,29 @@ export class Vm {
|
|
|
1917
2494
|
// an open connection to host:port. The pair ends when one side closes.
|
|
1918
2495
|
async fetchPair(id, host, port, h) {
|
|
1919
2496
|
const conn = `x${this.nextId++}`;
|
|
1920
|
-
let done;
|
|
2497
|
+
let done, pairEnd;
|
|
1921
2498
|
const ended = new Promise((r) => { done = r; });
|
|
1922
2499
|
const end = (other) => {
|
|
1923
2500
|
if (!this.fetchConns.has(conn)) return;
|
|
1924
2501
|
this.fetchConns.delete(conn);
|
|
1925
2502
|
this.tcps.delete(id);
|
|
1926
2503
|
this.tcps.delete(conn);
|
|
2504
|
+
// The fetches of this connection: no one reads their bodies now.
|
|
2505
|
+
for (const ac of this.fetches.values()) if (ac.conn === conn) ac.abort();
|
|
2506
|
+
pairEnd();
|
|
1927
2507
|
this.event({ t: 'tcp_closed', id: other });
|
|
1928
2508
|
done();
|
|
1929
2509
|
};
|
|
1930
2510
|
this.fetchConns.set(conn, { host, port, h });
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
this.event({ t: '
|
|
1934
|
-
this.event({ t: '
|
|
2511
|
+
// A send of one socket ends when the other socket read the bytes
|
|
2512
|
+
// (tcp_read): so the window of the sender (tcp_sent) holds them.
|
|
2513
|
+
const toConn = relay((b) => this.event({ t: 'tcp_data', id: conn }, b));
|
|
2514
|
+
const toId = relay((b) => this.event({ t: 'tcp_data', id }, b));
|
|
2515
|
+
pairEnd = () => { toConn.end(); toId.end(); };
|
|
2516
|
+
this.tcps.set(id, { send: toConn.send, close: () => end(conn), ack: toId.read, h });
|
|
2517
|
+
this.tcps.set(conn, { send: toId.send, close: () => end(id), ack: toConn.read, h });
|
|
2518
|
+
this.event({ t: 'tcp_open', id, ack: true, sent: true });
|
|
2519
|
+
this.event({ t: 'tcp_accept', id: this.listeners.get('fetch'), conn, host, port, ack: true, sent: true });
|
|
1935
2520
|
await ended;
|
|
1936
2521
|
if (h) { h.sockets--; h.wake?.(); }
|
|
1937
2522
|
}
|
|
@@ -1944,6 +2529,12 @@ export class Vm {
|
|
|
1944
2529
|
if (h) h.sockets++;
|
|
1945
2530
|
this.fetchPending++;
|
|
1946
2531
|
const ac = new AbortController();
|
|
2532
|
+
ac.conn = msg.conn;
|
|
2533
|
+
// ack: wasm_host_fetch tells the host what it read (fetch_read). The
|
|
2534
|
+
// body then goes while less than UPLOAD_WINDOW bytes are unread.
|
|
2535
|
+
ac.sent = 0;
|
|
2536
|
+
ac.read = 0;
|
|
2537
|
+
ac.signal.addEventListener('abort', () => ac.room?.());
|
|
1947
2538
|
this.fetches.set(msg.id, ac);
|
|
1948
2539
|
try {
|
|
1949
2540
|
const url = c && fetchUrl(c.host, c.port, msg.tls, msg.path);
|
|
@@ -1961,7 +2552,13 @@ export class Vm {
|
|
|
1961
2552
|
for (;;) {
|
|
1962
2553
|
const { done, value } = await reader.read();
|
|
1963
2554
|
if (done) break;
|
|
1964
|
-
if (value?.byteLength)
|
|
2555
|
+
if (!value?.byteLength) continue;
|
|
2556
|
+
this.event({ t: 'fetch_data', id: msg.id }, value);
|
|
2557
|
+
ac.sent += value.byteLength;
|
|
2558
|
+
while (msg.ack && ac.sent - ac.read >= UPLOAD_WINDOW && !ac.signal.aborted) {
|
|
2559
|
+
await new Promise((resolve) => { ac.room = resolve; });
|
|
2560
|
+
}
|
|
2561
|
+
if (ac.signal.aborted) throw new Error('the fetch stopped');
|
|
1965
2562
|
}
|
|
1966
2563
|
}
|
|
1967
2564
|
this.event({ t: 'fetch_end', id: msg.id });
|
|
@@ -1974,12 +2571,42 @@ export class Vm {
|
|
|
1974
2571
|
}
|
|
1975
2572
|
}
|
|
1976
2573
|
|
|
1977
|
-
// Data of a TCP socket: to Erlang, or to its peer after a
|
|
2574
|
+
// Data of a TCP socket: to Erlang (then true), or to its peer after a
|
|
2575
|
+
// splice.
|
|
1978
2576
|
tcpData(id, bytes) {
|
|
1979
2577
|
const t = this.tcps.get(id);
|
|
1980
2578
|
const peer = t?.peer && this.tcps.get(t.peer);
|
|
1981
2579
|
if (peer) this.run(() => peer.send(bytes), peer.h);
|
|
1982
2580
|
else this.event({ t: 'tcp_data', id }, bytes);
|
|
2581
|
+
return !peer;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// Data of a peer for the socket id, with the window win of Inbound: the
|
|
2585
|
+
// bytes that go to Erlang count, and the bytes to a spliced peer do not
|
|
2586
|
+
// (no tcp_read comes for them).
|
|
2587
|
+
tcpIn(id, win, bytes) {
|
|
2588
|
+
if (this.tcpData(id, bytes)) win.sent += bytes.length;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// A send of the VM to the socket t. The host tells the VM how many bytes
|
|
2592
|
+
// the peer took (tcp_sent), after each ACK_BYTES: wasm_tcp holds a send
|
|
2593
|
+
// while SEND_WINDOW bytes wait in the host. A send that fails counts
|
|
2594
|
+
// too, because the socket then ends.
|
|
2595
|
+
tcpSend(id, t, body) {
|
|
2596
|
+
const done = () => {
|
|
2597
|
+
t.took = (t.took ?? 0) + body.length;
|
|
2598
|
+
if (t.took >= ACK_BYTES) {
|
|
2599
|
+
this.event({ t: 'tcp_sent', id, n: t.took });
|
|
2600
|
+
t.took = 0;
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2603
|
+
let r;
|
|
2604
|
+
try {
|
|
2605
|
+
r = t.send(body);
|
|
2606
|
+
} finally {
|
|
2607
|
+
if (typeof r?.then !== 'function') done();
|
|
2608
|
+
}
|
|
2609
|
+
if (typeof r?.then === 'function') r.then(done, done);
|
|
1983
2610
|
}
|
|
1984
2611
|
|
|
1985
2612
|
// The end of a TCP socket: to Erlang, and the end of its peer too.
|
|
@@ -1995,10 +2622,22 @@ export class Vm {
|
|
|
1995
2622
|
}
|
|
1996
2623
|
}
|
|
1997
2624
|
|
|
2625
|
+
// A message of the VM (jspi_host_send). It runs in the thread of the VM:
|
|
2626
|
+
// an exception of the host must not stop that thread, so it goes to the
|
|
2627
|
+
// log.
|
|
1998
2628
|
onsend(bytes) {
|
|
2629
|
+
try {
|
|
2630
|
+
this.onsendEvent(bytes);
|
|
2631
|
+
} catch (e) {
|
|
2632
|
+
this.log(`beam: the host failed on a message of the VM (${e?.message ?? e})`);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
onsendEvent(bytes) {
|
|
1999
2637
|
const nl = bytes.indexOf(10);
|
|
2000
2638
|
const msg = JSON.parse(new TextDecoder().decode(bytes.subarray(0, nl)));
|
|
2001
|
-
|
|
2639
|
+
// bytes is a new array (jspi_host_send): the body is a view of it.
|
|
2640
|
+
const body = bytes.subarray(nl + 1);
|
|
2002
2641
|
switch (msg.t) {
|
|
2003
2642
|
case 'ready':
|
|
2004
2643
|
this.onready();
|
|
@@ -2029,9 +2668,20 @@ export class Vm {
|
|
|
2029
2668
|
case 'fetch_cancel':
|
|
2030
2669
|
this.fetches.get(msg.id)?.abort();
|
|
2031
2670
|
break;
|
|
2671
|
+
// wasm_host_fetch read n bytes of the body of a fetch().
|
|
2672
|
+
case 'fetch_read': {
|
|
2673
|
+
const ac = this.fetches.get(msg.id);
|
|
2674
|
+
if (ac) { ac.read += msg.n; ac.room?.(); }
|
|
2675
|
+
break;
|
|
2676
|
+
}
|
|
2032
2677
|
case 'tcp_send': {
|
|
2033
2678
|
const t = this.tcps.get(msg.id);
|
|
2034
|
-
if (t) this.run(() =>
|
|
2679
|
+
if (t) this.run(() => this.tcpSend(msg.id, t, body), t.h);
|
|
2680
|
+
break;
|
|
2681
|
+
}
|
|
2682
|
+
// wasm_tcp: the app read n bytes of a socket.
|
|
2683
|
+
case 'tcp_read': {
|
|
2684
|
+
this.tcps.get(msg.id)?.ack?.(msg.n);
|
|
2035
2685
|
break;
|
|
2036
2686
|
}
|
|
2037
2687
|
case 'tcp_close': {
|
|
@@ -2115,7 +2765,198 @@ export function staticResponse(statics, request) {
|
|
|
2115
2765
|
return new Response(body, { headers });
|
|
2116
2766
|
}
|
|
2117
2767
|
|
|
2768
|
+
// The result of a wait that passed its time limit.
|
|
2769
|
+
const LATE = Symbol('late');
|
|
2770
|
+
|
|
2771
|
+
// A request body (bridgeUpload): the bytes that can be unread in the VM,
|
|
2772
|
+
// the smallest part of an event (except the last one), and the size of a
|
|
2773
|
+
// read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
|
|
2774
|
+
// costs a turn of the VM.
|
|
2775
|
+
const UPLOAD_WINDOW = 256 * 1024;
|
|
2776
|
+
const UPLOAD_PART = 32 * 1024;
|
|
2777
|
+
// The time that the head of a request waits for the first bytes of its
|
|
2778
|
+
// body (bridgeUpload), in ms.
|
|
2779
|
+
const HEAD_WAIT = 20;
|
|
2780
|
+
const UPLOAD_READ = 64 * 1024;
|
|
2781
|
+
|
|
2782
|
+
// The size bytes of the list parts, as one array.
|
|
2783
|
+
function join(parts, size) {
|
|
2784
|
+
const b = new Uint8Array(size);
|
|
2785
|
+
let at = 0;
|
|
2786
|
+
for (const p of parts) { b.set(p, at); at += p.length; }
|
|
2787
|
+
return b;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// A chunk of a chunked body, and the end of the body.
|
|
2791
|
+
const CHUNKS_END = new TextEncoder().encode('0\r\n\r\n');
|
|
2792
|
+
function chunk(data) {
|
|
2793
|
+
const size = new TextEncoder().encode(`${data.length.toString(16)}\r\n`);
|
|
2794
|
+
const b = new Uint8Array(size.length + data.length + 2);
|
|
2795
|
+
b.set(size);
|
|
2796
|
+
b.set(data, size.length);
|
|
2797
|
+
b.set([13, 10], size.length + data.length);
|
|
2798
|
+
return b;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// A limit of a var: a number above 0, else the default (0: no limit).
|
|
2802
|
+
// "0" turns a limit with a default off.
|
|
2803
|
+
function limit(text, fallback) {
|
|
2804
|
+
if (text === undefined || text === null || text === '') return fallback;
|
|
2805
|
+
const n = Number(text);
|
|
2806
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2118
2809
|
// The position of the bytes pat in b, or -1.
|
|
2810
|
+
// The longest size line of a chunk of a response (with its extensions),
|
|
2811
|
+
// and the longest head of a response.
|
|
2812
|
+
const CHUNK_LINE = 4096;
|
|
2813
|
+
const HEAD_MAX = 1 << 20;
|
|
2814
|
+
// The longest wait of a request for a snapshot that the VM makes.
|
|
2815
|
+
const SNAPSHOT_WAIT = 10000;
|
|
2816
|
+
// Flow control of the sockets: wasm_tcp gives tcp_read after each
|
|
2817
|
+
// ACK_BYTES that the app read, and the host gives tcp_sent after each
|
|
2818
|
+
// ACK_BYTES that a peer took. Above SOCKET_QUEUE bytes from a peer that
|
|
2819
|
+
// wait for the app (Inbound), a WebSocket closes.
|
|
2820
|
+
const ACK_BYTES = 64 * 1024;
|
|
2821
|
+
const SOCKET_QUEUE = 16 * 1024 * 1024;
|
|
2822
|
+
|
|
2823
|
+
// The names of the fetch path in the cache of isCloudflare.
|
|
2824
|
+
const DNS_MAX = 1024;
|
|
2825
|
+
|
|
2826
|
+
// Bytes as text of one character for each byte (ISO-8859-1), and back.
|
|
2827
|
+
function latin1(b) {
|
|
2828
|
+
let s = '';
|
|
2829
|
+
for (let i = 0; i < b.length; i += 8192) s += String.fromCharCode(...b.subarray(i, i + 8192));
|
|
2830
|
+
return s;
|
|
2831
|
+
}
|
|
2832
|
+
function latin1Bytes(s) {
|
|
2833
|
+
const b = new Uint8Array(s.length);
|
|
2834
|
+
for (let i = 0; i < s.length; i++) b[i] = s.charCodeAt(i);
|
|
2835
|
+
return b;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// The close code of a client for the app: 1005, 1006 and 1015 are not
|
|
2839
|
+
// codes for the wire (RFC 6455).
|
|
2840
|
+
function wireCode(code) {
|
|
2841
|
+
if (code === 1006 || code === 1015) return 1001;
|
|
2842
|
+
return code && code !== 1005 ? code : 1000;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
// close(code) of a WebSocket, with a code that it takes (else 1000), and
|
|
2846
|
+
// no exception for a socket that is closed already.
|
|
2847
|
+
function closeSocket(ws, code) {
|
|
2848
|
+
try {
|
|
2849
|
+
ws.close(code === 1000 || (code >= 3000 && code <= 4999) || (code >= 1001 && code <= 1014 && ![1004, 1005, 1006].includes(code)) ? code : 1000);
|
|
2850
|
+
} catch {}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// Bytes in pieces (views of the arrays that came, no copy), joined only
|
|
2854
|
+
// when a reader needs them in one array (peek, take).
|
|
2855
|
+
// The bytes from a peer to a socket of the VM, with flow control: they go
|
|
2856
|
+
// while less than UPLOAD_WINDOW bytes are unread in the VM (win.sent, and
|
|
2857
|
+
// win.read from tcp_read), and the others wait here. Above SOCKET_QUEUE
|
|
2858
|
+
// bytes that wait, over() runs once, and the next bytes are dropped. A
|
|
2859
|
+
// forced push always waits (the end of a WebSocket).
|
|
2860
|
+
export class Inbound {
|
|
2861
|
+
constructor(win, send, over) {
|
|
2862
|
+
Object.assign(this, { win, send, over, queue: [], queued: 0, overflow: false });
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
full() {
|
|
2866
|
+
return this.win.sent - this.win.read >= UPLOAD_WINDOW;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
push(bytes, force = false) {
|
|
2870
|
+
if (!this.queue.length && !this.full()) return this.send(bytes);
|
|
2871
|
+
if (!force && (this.overflow || this.queued + bytes.length > SOCKET_QUEUE)) {
|
|
2872
|
+
if (!this.overflow) {
|
|
2873
|
+
this.overflow = true;
|
|
2874
|
+
this.over();
|
|
2875
|
+
}
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
this.queue.push(bytes);
|
|
2879
|
+
this.queued += bytes.length;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
// After a tcp_read: the bytes that wait, while the window has room.
|
|
2883
|
+
flush() {
|
|
2884
|
+
while (this.queue.length && !this.full()) {
|
|
2885
|
+
const bytes = this.queue.shift();
|
|
2886
|
+
this.queued -= bytes.length;
|
|
2887
|
+
this.send(bytes);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// The bytes from one socket of the VM to another one in the host (the
|
|
2893
|
+
// fetch path): deliver gives them to the other socket. A send ends when
|
|
2894
|
+
// the other socket read them (read, from its tcp_read), so the window of
|
|
2895
|
+
// the sender (tcp_sent) holds the bytes that wait. end() ends each send.
|
|
2896
|
+
export function relay(deliver) {
|
|
2897
|
+
let sent = 0, read = 0;
|
|
2898
|
+
const waits = [];
|
|
2899
|
+
return {
|
|
2900
|
+
send: (b) => {
|
|
2901
|
+
deliver(b);
|
|
2902
|
+
sent += b.length;
|
|
2903
|
+
if (read >= sent) return undefined;
|
|
2904
|
+
const mark = sent;
|
|
2905
|
+
return new Promise((resolve) => waits.push([mark, resolve]));
|
|
2906
|
+
},
|
|
2907
|
+
read: (n) => {
|
|
2908
|
+
read += n;
|
|
2909
|
+
while (waits.length && waits[0][0] <= read) waits.shift()[1]();
|
|
2910
|
+
},
|
|
2911
|
+
end: () => { for (const [, resolve] of waits.splice(0)) resolve(); },
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
export class Pieces {
|
|
2916
|
+
constructor() { this.list = []; this.size = 0; }
|
|
2917
|
+
push(b) {
|
|
2918
|
+
if (!b.length) return;
|
|
2919
|
+
this.list.push(b);
|
|
2920
|
+
this.size += b.length;
|
|
2921
|
+
}
|
|
2922
|
+
// The first n bytes in one array. A view when the first piece has them;
|
|
2923
|
+
// else a join, which then is the first piece.
|
|
2924
|
+
peek(n) {
|
|
2925
|
+
if (!this.list.length || this.list[0].length >= n) return (this.list[0] ?? new Uint8Array(0)).subarray(0, n);
|
|
2926
|
+
const out = new Uint8Array(n);
|
|
2927
|
+
let at = 0;
|
|
2928
|
+
for (const p of this.list) {
|
|
2929
|
+
if (at >= n) break;
|
|
2930
|
+
const k = Math.min(p.length, n - at);
|
|
2931
|
+
out.set(p.subarray(0, k), at);
|
|
2932
|
+
at += k;
|
|
2933
|
+
}
|
|
2934
|
+
this.drop(n);
|
|
2935
|
+
this.list.unshift(out);
|
|
2936
|
+
this.size += n;
|
|
2937
|
+
return out;
|
|
2938
|
+
}
|
|
2939
|
+
drop(n) {
|
|
2940
|
+
this.size -= n;
|
|
2941
|
+
while (n > 0) {
|
|
2942
|
+
const p = this.list[0];
|
|
2943
|
+
if (p.length <= n) { this.list.shift(); n -= p.length; }
|
|
2944
|
+
else { this.list[0] = p.subarray(n); n = 0; }
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
take(n) {
|
|
2948
|
+
const b = this.peek(n);
|
|
2949
|
+
this.drop(n);
|
|
2950
|
+
return b;
|
|
2951
|
+
}
|
|
2952
|
+
// At most max bytes of the first piece (a view).
|
|
2953
|
+
next(max) {
|
|
2954
|
+
const b = this.list[0].subarray(0, Math.min(max, this.list[0].length));
|
|
2955
|
+
this.drop(b.length);
|
|
2956
|
+
return b;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2119
2960
|
function indexOf(b, pat) {
|
|
2120
2961
|
outer: for (let i = 0; i + pat.length <= b.length; i++) {
|
|
2121
2962
|
for (let j = 0; j < pat.length; j++) if (b[i + j] !== pat[j]) continue outer;
|