@daugasauron/dolly 0.1.4 → 0.1.5-rc.0

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.
Files changed (60) hide show
  1. package/README.md +134 -14
  2. package/coi-serviceworker.js +28 -0
  3. package/dist/dolly-build-id.mjs +1 -1
  4. package/dist/dolly-dso-0.wasm +0 -0
  5. package/dist/dolly-image-build-id.mjs +1 -1
  6. package/dist/dolly-process-0.wasm +0 -0
  7. package/dist/dolly-process-abi.mjs +1 -1
  8. package/dist/dolly-process-dso.mjs +382 -53
  9. package/dist/dolly-process-gate-0.wasm +0 -0
  10. package/dist/dolly-process-worker.mjs +164 -35
  11. package/dist/dolly.wasm +0 -0
  12. package/host/dso/abi.mjs +3 -1
  13. package/host/dso/dso.mjs +1 -2
  14. package/host/http/abi.mjs +2 -2
  15. package/host/http/broker.mjs +18 -8
  16. package/host/http/http.mjs +0 -1
  17. package/host/http/policy.mjs +8 -4
  18. package/host/input/abi.mjs +47 -0
  19. package/host/modules.mjs +4 -3
  20. package/host/nena/abi.mjs +1 -1
  21. package/host/nena/nena.mjs +16 -18
  22. package/host/packages/service.mjs +35 -15
  23. package/host/packages/worker.mjs +21 -0
  24. package/host/runtime/abi.mjs +1 -0
  25. package/host/runtime/boot-files.mjs +8 -2
  26. package/host/runtime/grower.mjs +11 -8
  27. package/host/runtime/lease-bridge.mjs +8 -5
  28. package/host/runtime/module.json +1 -1
  29. package/host/runtime/runtime.mjs +37 -18
  30. package/host/snapshot/abi.mjs +7 -9
  31. package/host/snapshot/snapshot.mjs +14 -12
  32. package/host/snapshot/transport.mjs +4 -12
  33. package/host/snapshot/ui.mjs +17 -7
  34. package/host/stack/stack.mjs +1 -2
  35. package/host/threads/abi.mjs +1 -1
  36. package/host/threads/threads.mjs +2 -1
  37. package/package.json +1 -1
  38. package/src/browser.mjs +34 -37
  39. package/src/build-log.mjs +13 -2
  40. package/src/build-worker.mjs +182 -0
  41. package/src/custom-image.mjs +59 -0
  42. package/src/dollyfile-graph.mjs +127 -0
  43. package/src/dollyfile-view.mjs +335 -0
  44. package/src/image-artifact.mjs +248 -0
  45. package/src/image-build.mjs +89 -0
  46. package/src/image-builder.mjs +63 -0
  47. package/src/images.mjs +13 -2
  48. package/src/pack-store.mjs +141 -0
  49. package/src/packaged-snapshot.mjs +263 -91
  50. package/src/page-chords.mjs +4 -2
  51. package/src/process-constants.mjs +4 -2
  52. package/src/process-supervisor.mjs +209 -122
  53. package/src/runtime-worker.mjs +9 -10
  54. package/src/session-store.mjs +5 -7
  55. package/src/sha256.mjs +65 -0
  56. package/src/snapshot-records.mjs +135 -23
  57. package/src/static-asset.mjs +29 -2
  58. package/src/terminal-text.mjs +8 -5
  59. package/src/version.mjs +1 -1
  60. package/src/worker-image.mjs +3 -5
@@ -103,7 +103,7 @@ var DOLLY_PROCESS_SIZEOF = Object.freeze({
103
103
  dolly_process_directory_request: 24,
104
104
  dolly_process_stat_response: 88,
105
105
  dolly_process_filesystem_stat_response: 96,
106
- dolly_process_spawn_request: 56,
106
+ dolly_process_spawn_request: 72,
107
107
  dolly_process_fd_mapping: 8,
108
108
  dolly_process_exec_state: 16,
109
109
  dolly_process_exec_request: 48,
@@ -118,18 +118,24 @@ var DOLLY_PROCESS_SIZEOF = Object.freeze({
118
118
  dolly_process_wait_response: 16,
119
119
  dolly_process_signal_request: 8,
120
120
  dolly_process_alarm: 16,
121
+ dolly_process_signal_dispositions: 8,
121
122
  dolly_process_fd_seek_request: 16,
122
123
  dolly_process_clock_request: 16,
123
124
  dolly_process_path_request: 16
124
125
  });
125
126
 
126
127
  // host/threads/abi.mjs
128
+ var DOLLY_THREAD_SPAWN = 144;
127
129
  var DOLLY_THREAD_EXIT = 146;
128
130
  var DOLLY_ERRNO2 = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
129
131
 
130
132
  // src/process-worker.mjs
131
133
  var PROCESS_EXIT = Symbol("Dolly process exit");
132
134
  var THREAD_EXIT = Symbol("Dolly thread exit");
135
+ var UNSTARTED = Symbol("Dolly thread ended before it started");
136
+ var deferredResult = -(1n << 63n);
137
+ var reservationRetryMilliseconds = 50;
138
+ var reservationPatienceMilliseconds = 5e3;
133
139
  var configuration = await new Promise((resolve, reject) => {
134
140
  const timeout = setTimeout(
135
141
  () => reject(new Error("Dolly process configuration was not provided")),
@@ -142,19 +148,92 @@ var configuration = await new Promise((resolve, reject) => {
142
148
  resolve(event.data);
143
149
  });
144
150
  });
145
- if (!(configuration.module instanceof WebAssembly.Module) || !(configuration.memory instanceof WebAssembly.Memory) || !(configuration.control instanceof SharedArrayBuffer) || !Array.isArray(configuration.local) || configuration.control.byteLength !== 16 || !Number.isFinite(configuration.clockOrigin) || !Number.isInteger(configuration.pid) || configuration.pid <= 0) {
151
+ if (!(configuration.module instanceof WebAssembly.Module || configuration.module instanceof MessagePort) || !(configuration.gateModule instanceof WebAssembly.Module) || !(configuration.kernelMemory instanceof WebAssembly.Memory) || !(configuration.control instanceof SharedArrayBuffer) || !Array.isArray(configuration.local) || configuration.control.byteLength !== 32 || !Number.isFinite(configuration.clockOrigin) || !Number.isSafeInteger(configuration.mailbox) || configuration.mailbox <= 0 || typeof configuration.memory?.initial !== "bigint" || typeof configuration.memory?.maximum !== "bigint" || configuration.port !== void 0 && !(configuration.port instanceof MessagePort) || !Number.isInteger(configuration.pid) || configuration.pid <= 0) {
146
152
  throw new Error("Dolly process received an invalid configuration");
147
153
  }
148
154
  var control = new Int32Array(configuration.control);
155
+ var mailbox = BigInt(configuration.mailbox);
156
+ var maximumBytes = configuration.memory.maximum * 65536n;
149
157
  var clockOffset = performance.timeOrigin - configuration.clockOrigin;
150
158
  var lastSignalCheck = -Infinity;
151
159
  var threadResult;
160
+ var ending = null;
161
+ var ended = (how) => {
162
+ ending = how;
163
+ return deferredResult;
164
+ };
152
165
  var instance;
166
+ var memory;
167
+ var gate;
153
168
  var localModules = [];
169
+ var awaitHost = (wait) => wait();
170
+ var openChannels = [];
171
+ var receiverPatienceMilliseconds = 1e4;
172
+ function closeReceivedChannels() {
173
+ for (let index = openChannels.length; index--; ) {
174
+ const { port, received } = openChannels[index];
175
+ if (Atomics.load(received, 0)) {
176
+ port.close();
177
+ openChannels.splice(index, 1);
178
+ }
179
+ }
180
+ }
181
+ function reportThreadFinished() {
182
+ for (const { received } of openChannels) {
183
+ while (Atomics.load(received, 0) === 0 && Atomics.wait(received, 0, 0, receiverPatienceMilliseconds) !== "timed-out") ;
184
+ }
185
+ self.postMessage({ type: "thread-finished", pid: configuration.pid, tid: configuration.tid, result: threadResult });
186
+ }
187
+ async function processMemory() {
188
+ if (configuration.port) {
189
+ const ended2 = Atomics.waitAsync(control, 5, 0);
190
+ const { memory: memory2, received } = await new Promise((resolve, reject) => {
191
+ const timeout = setTimeout(() => reject(new Error("the process memory was not provided")), 1e4);
192
+ configuration.port.onmessage = ({ data }) => {
193
+ clearTimeout(timeout);
194
+ resolve(data ?? {});
195
+ };
196
+ Promise.resolve(ended2.value).then(() => {
197
+ clearTimeout(timeout);
198
+ reject(UNSTARTED);
199
+ });
200
+ });
201
+ configuration.port.close();
202
+ if (!(memory2 instanceof WebAssembly.Memory) || !(received instanceof Int32Array)) {
203
+ throw new TypeError("Dolly thread received an invalid memory");
204
+ }
205
+ Atomics.store(received, 0, 1);
206
+ Atomics.notify(received, 0);
207
+ return memory2;
208
+ }
209
+ const { initial, maximum } = configuration.memory;
210
+ for (let waited = 0; ; waited += reservationRetryMilliseconds) {
211
+ try {
212
+ return new WebAssembly.Memory({ initial, maximum, shared: true, address: "i64" });
213
+ } catch (error) {
214
+ if (!(error instanceof RangeError) || waited >= reservationPatienceMilliseconds) throw error;
215
+ }
216
+ await new Promise((resolve) => setTimeout(resolve, reservationRetryMilliseconds));
217
+ }
218
+ }
219
+ async function programModule() {
220
+ const { module } = configuration;
221
+ if (module instanceof WebAssembly.Module) return module;
222
+ const received = await new Promise((resolve, reject) => {
223
+ const timeout = setTimeout(() => reject(new Error("the program's module was not provided")), 1e4);
224
+ module.onmessage = ({ data }) => {
225
+ clearTimeout(timeout);
226
+ resolve(data);
227
+ };
228
+ });
229
+ module.close();
230
+ if (!(received instanceof WebAssembly.Module)) throw new TypeError("Dolly process received an invalid module");
231
+ return received;
232
+ }
154
233
  function packetRange(addressValue, sizeValue) {
155
234
  const address = Number(addressValue);
156
235
  const size = Number(sizeValue);
157
- const length = configuration.memory.buffer.byteLength;
236
+ const length = memory.buffer.byteLength;
158
237
  return !Number.isSafeInteger(address) || !Number.isSafeInteger(size) || address < 0 || size < 0 || address > length - size || size !== 0 && address === 0 ? null : { address, size };
159
238
  }
160
239
  function decodeResult() {
@@ -166,9 +245,29 @@ function clockResponse(clock, response, now) {
166
245
  const nanoseconds = Math.round(
167
246
  (clock === DOLLY_PROCESS_CLOCK_REALTIME ? Date.now() : now + clockOffset) * 1e6
168
247
  );
169
- new DataView(configuration.memory.buffer, response.address, 8).setBigUint64(0, BigInt(nanoseconds), true);
248
+ new DataView(memory.buffer, response.address, 8).setBigUint64(0, BigInt(nanoseconds), true);
170
249
  return 8n;
171
250
  }
251
+ function copyRequest(request) {
252
+ if (Atomics.compareExchange(control, 4, 0, 1) !== 0) return false;
253
+ try {
254
+ gate.exports.request(BigInt(request.address), mailbox, BigInt(request.size));
255
+ } finally {
256
+ Atomics.store(control, 4, 0);
257
+ Atomics.notify(control, 4);
258
+ }
259
+ return true;
260
+ }
261
+ function reserveMaximum() {
262
+ const size = BigInt(memory.buffer.byteLength);
263
+ if (size >= maximumBytes) return true;
264
+ try {
265
+ memory.grow(configuration.memory.maximum - size / 65536n);
266
+ } catch {
267
+ return false;
268
+ }
269
+ return true;
270
+ }
172
271
  function call(operation, requestAddressValue, requestSizeValue, responseAddressValue, responseCapacityValue) {
173
272
  operation >>>= 0;
174
273
  const request = packetRange(requestAddressValue, requestSizeValue);
@@ -177,55 +276,83 @@ function call(operation, requestAddressValue, requestSizeValue, responseAddressV
177
276
  for (const module of localModules) {
178
277
  if (module.handles(operation)) return module.call(operation, request, response);
179
278
  }
180
- const exitingResult = operation === DOLLY_THREAD_EXIT && request.size === 8 ? new DataView(configuration.memory.buffer, request.address, 8).getBigUint64(0, true) : void 0;
279
+ const exitingResult = operation === DOLLY_THREAD_EXIT && request.size === 8 ? new DataView(memory.buffer, request.address, 8).getBigUint64(0, true) : void 0;
181
280
  let clock;
182
281
  if (operation === DOLLY_PROCESS_CLOCK_TIME && request.size === 16 && response.size >= 8 && response.size <= DOLLY_PROCESS_PACKET_LIMIT) {
183
- const packet = new DataView(configuration.memory.buffer, request.address, 16);
282
+ const packet = new DataView(memory.buffer, request.address, 16);
184
283
  const id = packet.getUint32(0, true);
185
284
  if ((id === DOLLY_PROCESS_CLOCK_REALTIME || id === DOLLY_PROCESS_CLOCK_MONOTONIC) && packet.getUint32(4, true) === 0) clock = id;
186
285
  const now = performance.now();
187
286
  if (clock !== void 0 && now - lastSignalCheck < 1) return clockResponse(clock, response, now);
188
287
  }
189
288
  if (operation === DOLLY_PROCESS_INTERRUPT_POLL && request.size === 0 && response.size >= 4 && performance.now() - lastSignalCheck < 1) {
190
- new DataView(configuration.memory.buffer, response.address, 4).setInt32(0, 0, true);
289
+ new DataView(memory.buffer, response.address, 4).setInt32(0, 0, true);
191
290
  return 4n;
192
291
  }
193
292
  if (request.size > DOLLY_PROCESS_PACKET_LIMIT || response.size > DOLLY_PROCESS_PACKET_LIMIT) {
194
293
  return -BigInt(DOLLY_ERRNO.E2BIG);
195
294
  }
196
- const sequence = Atomics.load(control, 0) % 2147483647 + 1;
197
- Atomics.store(control, 0, sequence);
198
- self.postMessage({
199
- type: "syscall",
200
- pid: configuration.pid,
201
- tid: configuration.tid,
202
- sequence,
203
- operation,
204
- requestAddress: request.address,
205
- requestSize: request.size,
206
- responseAddress: response.address,
207
- responseCapacity: response.size
295
+ const memoryBytes = memory.buffer.byteLength;
296
+ let channel;
297
+ if (openChannels.length) closeReceivedChannels();
298
+ if (operation === DOLLY_THREAD_SPAWN && configuration.threaded) {
299
+ if (!reserveMaximum()) return -BigInt(DOLLY_ERRNO.EAGAIN);
300
+ channel = new MessageChannel();
301
+ }
302
+ const result = awaitHost(() => {
303
+ const sequence = Atomics.load(control, 0) % 2147483647 + 1;
304
+ Atomics.store(control, 0, sequence);
305
+ if (!copyRequest(request)) return deferredResult;
306
+ self.postMessage({
307
+ type: "syscall",
308
+ pid: configuration.pid,
309
+ tid: configuration.tid,
310
+ sequence,
311
+ operation,
312
+ requestAddress: request.address,
313
+ requestSize: request.size,
314
+ responseAddress: response.address,
315
+ responseCapacity: response.size,
316
+ memoryBytes,
317
+ port: channel?.port1
318
+ }, channel ? [channel.port1] : []);
319
+ for (; ; ) {
320
+ const observed = Atomics.load(control, 1);
321
+ if (observed === sequence) break;
322
+ Atomics.wait(control, 1, observed);
323
+ }
324
+ return decodeResult();
208
325
  });
209
- for (; ; ) {
210
- const observed = Atomics.load(control, 1);
211
- if (observed === sequence) break;
212
- Atomics.wait(control, 1, observed);
326
+ if (channel) {
327
+ if (result < 0n) channel.port2.close();
328
+ else {
329
+ const received = new Int32Array(new SharedArrayBuffer(4));
330
+ channel.port2.postMessage({ memory, received });
331
+ openChannels.push({ port: channel.port2, received });
332
+ }
213
333
  }
214
- const result = decodeResult();
215
- if (result === -(1n << 63n)) throw PROCESS_EXIT;
334
+ if (result === deferredResult) return ended(PROCESS_EXIT);
335
+ if (result > 0n) gate.exports.response(mailbox, BigInt(response.address), result);
216
336
  lastSignalCheck = result === -BigInt(DOLLY_ERRNO.EINTR) ? -Infinity : performance.now();
217
337
  if (operation === DOLLY_THREAD_EXIT && result >= 0n) {
218
338
  threadResult = exitingResult;
219
- throw THREAD_EXIT;
339
+ for (const module of localModules) module.leaving?.();
340
+ return ended(THREAD_EXIT);
220
341
  }
221
342
  if (clock !== void 0 && result === 8n) return clockResponse(clock, response, lastSignalCheck);
222
- if (operation === DOLLY_PROCESS_EXIT && result >= 0n) throw PROCESS_EXIT;
343
+ if (operation === DOLLY_PROCESS_EXIT && result >= 0n) return ended(PROCESS_EXIT);
223
344
  return result;
224
345
  }
225
346
  try {
226
- instance = await WebAssembly.instantiate(configuration.module, {
227
- env: { memory: configuration.memory },
228
- dolly_process_0: { call }
347
+ memory = await processMemory();
348
+ if (configuration.port && Atomics.compareExchange(control, 5, 0, 1) !== 0) throw UNSTARTED;
349
+ gate = new WebAssembly.Instance(configuration.gateModule, {
350
+ process: { memory, call },
351
+ kernel: { memory: configuration.kernelMemory }
352
+ });
353
+ instance = await WebAssembly.instantiate(await programModule(), {
354
+ env: { memory },
355
+ dolly_process_0: { call: gate.exports.call }
229
356
  });
230
357
  if (typeof instance.exports._start !== "function") {
231
358
  throw new TypeError("Dolly process does not export _start");
@@ -236,7 +363,7 @@ try {
236
363
  return (await import(url)).serve({
237
364
  ...options,
238
365
  instance,
239
- memory: configuration.memory,
366
+ memory,
240
367
  processInterface: configuration.processInterface,
241
368
  threaded: configuration.threaded,
242
369
  stack: { promisingEntry: configuration.promisingEntry, depths: configuration.stackDepths }
@@ -245,19 +372,21 @@ try {
245
372
  URL.revokeObjectURL(url);
246
373
  }
247
374
  }));
375
+ awaitHost = localModules.find((module) => module.awaitHost)?.awaitHost ?? awaitHost;
248
376
  self.postMessage({ type: "started", pid: configuration.pid, tid: configuration.tid });
249
377
  const enter = configuration.promisingEntry ? WebAssembly.promising : (entry) => entry;
250
378
  if (configuration.argument !== void 0) {
251
379
  threadResult = await enter(instance.exports.dolly_thread_start)(configuration.tid, configuration.argument);
252
- self.postMessage({ type: "thread-finished", pid: configuration.pid, tid: configuration.tid, result: threadResult });
380
+ reportThreadFinished();
253
381
  } else {
254
382
  await enter(instance.exports._start)();
255
383
  self.postMessage({ type: "finished", pid: configuration.pid, tid: configuration.tid });
256
384
  }
257
385
  } catch (caught) {
258
- if (caught === THREAD_EXIT && configuration.threaded && typeof threadResult === "bigint") {
259
- self.postMessage({ type: "thread-finished", pid: configuration.pid, tid: configuration.tid, result: threadResult });
260
- } else if (caught === PROCESS_EXIT) {
386
+ if (caught === UNSTARTED) {
387
+ } else if (ending === THREAD_EXIT && configuration.threaded && typeof threadResult === "bigint") {
388
+ reportThreadFinished();
389
+ } else if (ending === PROCESS_EXIT) {
261
390
  self.postMessage({ type: "finished", pid: configuration.pid, tid: configuration.tid });
262
391
  } else {
263
392
  await null;
package/dist/dolly.wasm CHANGED
Binary file
package/host/dso/abi.mjs CHANGED
@@ -2,12 +2,14 @@
2
2
  export const DOLLY_DSO_OPEN = 112;
3
3
  export const DOLLY_DSO_SYMBOL = 113;
4
4
  export const DOLLY_DSO_CLOSE = 114;
5
+ export const DOLLY_DSO_WAIT = 115;
5
6
  export const DOLLY_DSO_GLOBAL = 1;
6
7
  export const DOLLY_DSO_LIMIT = 536870912;
7
8
  export const DOLLY_DSO_ERROR_CAPACITY = 240;
9
+ export const DOLLY_DSO_THREADS_SIZE = 272;
8
10
  export const DOLLY_FFI_CALL = 120;
9
11
  export const DOLLY_FFI_CLOSURE_ALLOC = 121;
10
12
  export const DOLLY_FFI_CLOSURE_FREE = 122;
11
13
  export const DOLLY_FFI_CLOSURE_PREP = 123;
12
14
  export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
13
- export const DOLLY_DSO_ABI_DIGEST = "f620970cf60972fe67e4d14dd3441edb9652092ed2ebc27a511c24d2931fb800";
15
+ export const DOLLY_DSO_ABI_DIGEST = "6758333327df36a58f1dea5e0741eca482bb92cbede2b525a67a5e5f85bb35fd";
package/host/dso/dso.mjs CHANGED
@@ -17,9 +17,8 @@ export function worker({ get, abi, runtimeBase }) {
17
17
  }));
18
18
  // abi is the live set of modules the image admits; a loaded library's
19
19
  // own records are checked against it.
20
- // threads: it serves each thread's Worker of a program using threads@0.
21
20
  get("runtime").serveInProcess("dso@0", { bundle: new Blob([bundle], { type: "text/javascript" }),
22
- configuration: { contract: parseWasmInterface(contract, "dolly-dso-0"), hostAbi: abi }, threads: true });
21
+ configuration: { contract: parseWasmInterface(contract, "dolly-dso-0"), hostAbi: abi } });
23
22
  },
24
23
  disabled() { get("runtime").serveInProcess("dso@0"); },
25
24
  };
package/host/http/abi.mjs CHANGED
@@ -4,7 +4,7 @@ export const DOLLY_HTTP_CHUNK_CAPACITY = 65536;
4
4
  export const DOLLY_HTTP_MAX_METHOD = 32;
5
5
  export const DOLLY_HTTP_MAX_URL = 8192;
6
6
  export const DOLLY_HTTP_MAX_HEADERS = 65536;
7
- export const DOLLY_HTTP_MAX_BODY = 8388608;
7
+ export const DOLLY_HTTP_MAX_BODY = 67108864;
8
8
  export const DOLLY_HTTP_HEADER_SIZE = 64;
9
9
  export const DOLLY_HTTP_WORD_STATE = 0;
10
10
  export const DOLLY_HTTP_WORD_SEQUENCE = 1;
@@ -25,4 +25,4 @@ export const DOLLY_HTTP_POLL = 81;
25
25
  export const DOLLY_HTTP_CANCEL = 82;
26
26
  export const DOLLY_HTTP_BODY_WRITE = 83;
27
27
  export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
28
- export const DOLLY_HTTP_ABI_DIGEST = "429f0fdb280240a4429fdeb550e6710453d81ac51b7eb662dd43b024fcfaaaed";
28
+ export const DOLLY_HTTP_ABI_DIGEST = "9e3dd4f36f656a23f477c67ce6fe723130caa9b4bf69f6af8670859ea653470f";
@@ -218,7 +218,19 @@ class HttpTransfer {
218
218
  check() {
219
219
  if (this.terminalError) throw new HttpError(this.terminalError, "HTTP transfer ended");
220
220
  if (this.controller.signal.aborted || performance.now() >= this.deadline)
221
- throw new HttpError(errno.ETIMEDOUT, "HTTP deadline exceeded");
221
+ throw new HttpError(errno.ETIMEDOUT, "HTTP request timed out");
222
+ }
223
+
224
+ // A transfer lasts as long as it moves. It times out when it has waited the
225
+ // rule's timeout for its next record: the response's headers, its next bytes
226
+ // or the guest's reading of the record before.
227
+ moved() {
228
+ clearTimeout(this.timer);
229
+ this.deadline = performance.now() + this.timeout;
230
+ this.timer = setTimeout(() => {
231
+ this.fail(errno.ETIMEDOUT);
232
+ this.controller.abort();
233
+ }, this.timeout);
222
234
  }
223
235
 
224
236
  // The mailbox ends the slot's transfer once, and only while the guest's
@@ -242,10 +254,11 @@ class HttpTransfer {
242
254
  const error = await this.broker.mailbox.publish({ index: this.index, transfer: this.id, sequence: this.sequence,
243
255
  bytes: bytes.slice(), status, eof, kind, deadline: now() + this.deadline - performance.now() });
244
256
  if (error) throw new HttpError(error, "HTTP transfer ended");
257
+ this.moved();
245
258
  }
246
259
 
247
260
  async run({ method, url, headers: headerBlock, body, flags }) {
248
- let timeout, failure = errno.EINVAL;
261
+ let failure = errno.EINVAL;
249
262
  try {
250
263
  this.check();
251
264
  const target = this.broker.target(url);
@@ -266,11 +279,8 @@ class HttpTransfer {
266
279
  stripDollyBrowserOwnedHeaders(headers);
267
280
  const upperMethod = method.toUpperCase();
268
281
  const rule = this.broker.policy.authorize(target, upperMethod, headers, body?.byteLength ?? 0);
269
- this.deadline = performance.now() + rule.timeoutMilliseconds;
270
- timeout = setTimeout(() => {
271
- this.fail(errno.ETIMEDOUT);
272
- this.controller.abort();
273
- }, rule.timeoutMilliseconds);
282
+ this.timeout = rule.timeoutMilliseconds;
283
+ this.moved();
274
284
  failure = errno.EIO;
275
285
  const init = {
276
286
  method: upperMethod, headers,
@@ -318,7 +328,7 @@ class HttpTransfer {
318
328
  this.fail(error instanceof HttpError ? error.errno :
319
329
  this.controller.signal.aborted ? errno.ETIMEDOUT : failure);
320
330
  } finally {
321
- clearTimeout(timeout);
331
+ clearTimeout(this.timer);
322
332
  this.controller.abort();
323
333
  }
324
334
  }
@@ -12,7 +12,6 @@ export function browser({ applicationBase, configuration, send, ask }) {
12
12
  const services = {};
13
13
  const instance = {
14
14
  services,
15
- get transport() { return transport; },
16
15
  page: {
17
16
  get httpActive() { return transport?.active ?? false; },
18
17
  get httpRequestCount() { return transport?.requestCount ?? 0; },
@@ -1,4 +1,4 @@
1
- import { DOLLY_ERRNO } from "./abi.mjs";
1
+ import { DOLLY_ERRNO, DOLLY_HTTP_MAX_BODY } from "./abi.mjs";
2
2
 
3
3
  export class HttpError extends Error {
4
4
  constructor(errno, message) { super(message); this.errno = errno; }
@@ -38,14 +38,17 @@ export function stripDollyBrowserOwnedHeaders(headers) {
38
38
 
39
39
  const defaultLimits = Object.freeze({
40
40
  maxRequests: 256,
41
- maxRequestBytes: 8 * 1024 * 1024,
41
+ maxRequestBytes: DOLLY_HTTP_MAX_BODY,
42
42
  maxResponseBytes: Infinity,
43
43
  timeoutMilliseconds: 600_000,
44
44
  });
45
45
 
46
- function positiveInteger(value, fallback, name) {
46
+ // Timers wait at most this long; a longer timeout would fire early.
47
+ const longestTimeout = 2 ** 31 - 1;
48
+
49
+ function positiveInteger(value, fallback, name, maximum = Number.MAX_SAFE_INTEGER) {
47
50
  const result = value ?? fallback;
48
- if (!Number.isSafeInteger(result) || result <= 0) {
51
+ if (!Number.isSafeInteger(result) || result <= 0 || result > maximum) {
49
52
  throw new TypeError(`invalid Dolly HTTP policy ${name}`);
50
53
  }
51
54
  return result;
@@ -105,6 +108,7 @@ function normalizeRule(rule) {
105
108
  rule.timeoutMilliseconds,
106
109
  defaultLimits.timeoutMilliseconds,
107
110
  "timeoutMilliseconds",
111
+ longestTimeout,
108
112
  ),
109
113
  });
110
114
  }
@@ -0,0 +1,47 @@
1
+ // Generated from host/input/dolly-input-0.wat, host/input/input.h, include/dolly/process.h.
2
+ export const DOLLY_INPUT_HEADER_SIZE = 28;
3
+ export const DOLLY_INPUT_EVENT_SIZE = 128;
4
+ export const DOLLY_INPUT_EVENT_DATA_SIZE = 88;
5
+ export const DOLLY_INPUT_EVENT_CAPACITY = 256;
6
+ export const DOLLY_INPUT_WORD_EVENT_READ = 0;
7
+ export const DOLLY_INPUT_WORD_EVENT_WRITE = 1;
8
+ export const DOLLY_INPUT_WORD_FLAGS = 2;
9
+ export const DOLLY_INPUT_WORD_PASTE_SEQUENCE = 3;
10
+ export const DOLLY_INPUT_WORD_PASTE_CONSUMED_SEQUENCE = 4;
11
+ export const DOLLY_INPUT_WORD_PASTE_LENGTH = 5;
12
+ export const DOLLY_INPUT_WORD_ENABLED = 6;
13
+ export const DOLLY_INPUT_LEASED = 1;
14
+ export const DOLLY_INPUT_POINTER_RELATIVE = 2;
15
+ export const DOLLY_INPUT_EVENT_KEY = 1;
16
+ export const DOLLY_INPUT_EVENT_TEXT = 2;
17
+ export const DOLLY_INPUT_EVENT_FOCUS = 4;
18
+ export const DOLLY_INPUT_EVENT_PASTE = 5;
19
+ export const DOLLY_INPUT_EVENT_POINTER = 6;
20
+ export const DOLLY_INPUT_EVENT_SCROLL = 7;
21
+ export const DOLLY_INPUT_EVENT_POINTER_MOTION = 8;
22
+ export const DOLLY_INPUT_EVENT_POINTER_CAPTURE = 9;
23
+ export const DOLLY_INPUT_EVENT_POINTER_PRESENCE = 10;
24
+ export const DOLLY_INPUT_EVENT_DROPPED = 11;
25
+ export const DOLLY_KEY_ACTION_RELEASE = 0;
26
+ export const DOLLY_KEY_ACTION_PRESS = 1;
27
+ export const DOLLY_KEY_ACTION_REPEAT = 2;
28
+ export const DOLLY_POINTER_ACTION_RELEASE = 0;
29
+ export const DOLLY_POINTER_ACTION_PRESS = 1;
30
+ export const DOLLY_POINTER_ACTION_DRAG = 2;
31
+ export const DOLLY_SCROLL_UNIT_PIXEL = 0;
32
+ export const DOLLY_SCROLL_UNIT_LINE = 1;
33
+ export const DOLLY_SCROLL_UNIT_PAGE = 2;
34
+ export const DOLLY_INPUT_MOD_SHIFT = 1;
35
+ export const DOLLY_INPUT_MOD_CONTROL = 2;
36
+ export const DOLLY_INPUT_MOD_ALT = 4;
37
+ export const DOLLY_INPUT_MOD_META = 8;
38
+ export const DOLLY_INPUT_MOD_CAPS_LOCK = 16;
39
+ export const DOLLY_INPUT_MOD_NUM_LOCK = 32;
40
+ export const DOLLY_INPUT_FLAG_COMPOSING = 1;
41
+ export const DOLLY_INPUT_PASTE_CAPACITY = 262144;
42
+ export const DOLLY_INPUT_ACQUIRE = 88;
43
+ export const DOLLY_INPUT_NEXT_EVENT = 89;
44
+ export const DOLLY_INPUT_SET_POINTER = 90;
45
+ export const DOLLY_INPUT_RELEASE = 91;
46
+ export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
47
+ export const DOLLY_INPUT_ABI_DIGEST = "862edb42d7dbb04c1c49411d740a97503fe387678a1b660f19b3b2dc4f18b9c6";
package/host/modules.mjs CHANGED
@@ -2,9 +2,10 @@ import { hostRequirement, hostRequirements } from "./requirements.mjs";
2
2
  import { DOLLY_ERRNO as E } from "../src/process-constants.mjs";
3
3
 
4
4
  // What a build enables beside the runtime its image declares: the broker the
5
- // engine fetches sources through, the threads its toolchain uses and the
6
- // modules its compilers and interpreters load.
7
- export const buildHost = Object.freeze(["http@0", "threads@0", "dso@0"]);
5
+ // engine fetches sources through, the threads its toolchain uses, the
6
+ // modules its compilers and interpreters load and the fresh stacks of the
7
+ // tools stack-wrap rewrote.
8
+ export const buildHost = Object.freeze(["http@0", "threads@0", "dso@0", "stack@0"]);
8
9
 
9
10
  // A page's host modules, from the URLs of their manifests. The embedding, trusted
10
11
  // code, names them; images select no JS or Worker URLs, and each module's provider
package/host/nena/abi.mjs CHANGED
@@ -16,4 +16,4 @@ export const DOLLY_NENA_ENABLED = 1;
16
16
  export const DOLLY_NENA_DECLARED = 2;
17
17
  export const DOLLY_NENA_FIXED = 4;
18
18
  export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
19
- export const DOLLY_NENA_ABI_DIGEST = "3b6540395986cc839e4ef67cdfd0a0510780e26cdf7071522b60730c7b05c58e";
19
+ export const DOLLY_NENA_ABI_DIGEST = "cc920703d81c6e25656402683fc32180bd232f9206d5fbffabdca4c9cb24649a";
@@ -5,7 +5,7 @@ import { DOLLY_ERRNO as E } from "./abi.mjs";
5
5
  export { DOLLY_NENA_ABI_DIGEST as digest } from "./abi.mjs";
6
6
 
7
7
  // The device as the lease bridge sees it.
8
- export const lease = { type: "nena", slots: N.DOLLY_NENA_SLOTS, packetBytes: N.DOLLY_NENA_PACKET_BYTES,
8
+ export const lease = { type: "nena", version: N.DOLLY_NENA_VERSION, slots: N.DOLLY_NENA_SLOTS, packetBytes: N.DOLLY_NENA_PACKET_BYTES,
9
9
  replyBytes: N.DOLLY_NENA_REPLY_BYTES, open: N.DOLLY_NENA_OPEN, close: N.DOLLY_NENA_CLOSE };
10
10
  // The session's own record of the modules it enabled, one name@version a line.
11
11
  // The Worker writes it after every switch and the page enables what it lists
@@ -36,32 +36,30 @@ export function browser({ send, modules, showStatus }) {
36
36
  }
37
37
  // What the session enabled beyond what the page started with: its record.
38
38
  const enabledSince = () => modules.list().filter(module => module.enabled && !module.declared).map(requirement);
39
- async function dispatch(packet) {
40
- if (!(packet instanceof Uint8Array) || packet.byteLength < 32 || packet.byteLength > N.DOLLY_NENA_PACKET_BYTES) fail(E.EINVAL);
41
- const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
42
- const operation = view.getUint32(4, true), scope = view.getUint32(8, true);
43
- if (view.getUint32(0, true) !== N.DOLLY_NENA_VERSION || view.getUint32(12, true) || view.getUint32(20, true) ||
44
- !scope || view.getUint32(24, true) !== packet.length - 32 || view.getUint32(28, true)) fail(E.EINVAL);
45
- const sized = bytes => { if (packet.length !== bytes) fail(E.EINVAL); };
39
+ // A request the lease bridge checked: the packet's operation and scope
40
+ // (dolly_device_header) and its body.
41
+ async function dispatch({ operation, scope, body }) {
42
+ const view = new DataView(body.buffer, body.byteOffset, body.byteLength);
43
+ const sized = bytes => { if (body.length !== bytes) fail(E.EINVAL); };
46
44
  if (operation === N.DOLLY_NENA_OPEN) {
47
- sized(32);
45
+ sized(0);
48
46
  const reply = new Uint8Array(8);
49
47
  new DataView(reply.buffer).setBigUint64(0, BigInt(scope), true);
50
48
  return { bytes: reply };
51
49
  }
52
- if (operation === N.DOLLY_NENA_CLOSE) { sized(32); return { bytes: new Uint8Array() }; }
50
+ if (operation === N.DOLLY_NENA_CLOSE) { sized(0); return { bytes: new Uint8Array() }; }
53
51
  if (operation === N.DOLLY_NENA_MODULE) {
54
- sized(40);
55
- if (view.getUint32(36, true)) fail(E.EINVAL);
56
- return { bytes: moduleRecord(modules.list()[view.getUint32(32, true)] ?? fail(E.ENOENT)) };
52
+ sized(8);
53
+ if (view.getUint32(4, true)) fail(E.EINVAL);
54
+ return { bytes: moduleRecord(modules.list()[view.getUint32(0, true)] ?? fail(E.ENOENT)) };
57
55
  }
58
56
  if (operation !== N.DOLLY_NENA_ENABLE && operation !== N.DOLLY_NENA_DISABLE) fail(E.EINVAL);
59
- sized(72);
60
- const field = packet.subarray(32, 32 + N.DOLLY_NENA_NAME_BYTES), end = field.indexOf(0);
61
- if (end < 1 || field.subarray(end).some(byte => byte) || view.getUint32(68, true)) fail(E.EINVAL);
57
+ sized(40);
58
+ const field = body.subarray(0, N.DOLLY_NENA_NAME_BYTES), end = field.indexOf(0);
59
+ if (end < 1 || field.subarray(end).some(byte => byte) || view.getUint32(36, true)) fail(E.EINVAL);
62
60
  let name;
63
61
  try { name = decoder.decode(field.subarray(0, end)); } catch { fail(E.EINVAL); }
64
- const version = view.getUint32(64, true), enabling = operation === N.DOLLY_NENA_ENABLE;
62
+ const version = view.getUint32(32, true), enabling = operation === N.DOLLY_NENA_ENABLE;
65
63
  const found = () => modules.list().find(module => module.name === name && module.version === version) ?? fail(E.ENOENT);
66
64
  const value = requirement(found());
67
65
  // The session gives the privilege up: answered first, since the answer
@@ -74,7 +72,7 @@ export function browser({ send, modules, showStatus }) {
74
72
  messages: {
75
73
  async "nena-request"(message) {
76
74
  let result;
77
- try { result = await dispatch(message.packet); }
75
+ try { result = await dispatch(message); }
78
76
  catch (error) { result = { error: error.errno ?? E.EIO, bytes: new Uint8Array() }; }
79
77
  if (result.enabled) send({ type: "nena-record", enabled: result.enabled });
80
78
  send({ type: "nena-complete", scope: message.scope, sequence: message.sequence, error: result.error ?? 0,