@volter/browser-wali 0.1.166 → 0.1.168

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 (215) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +9 -0
  3. package/dist/browser-wali-command.d.ts +7 -0
  4. package/dist/browser-wali-command.d.ts.map +1 -1
  5. package/dist/browser-wali-command.js +70 -18
  6. package/dist/browser-wali-command.js.map +1 -1
  7. package/dist/bulk-memory.d.ts +4 -2
  8. package/dist/bulk-memory.d.ts.map +1 -1
  9. package/dist/bulk-memory.js +32 -9
  10. package/dist/bulk-memory.js.map +1 -1
  11. package/dist/capabilities.d.ts +1 -1
  12. package/dist/capabilities.d.ts.map +1 -1
  13. package/dist/capabilities.js +6 -2
  14. package/dist/capabilities.js.map +1 -1
  15. package/dist/child-processes.d.ts +2 -0
  16. package/dist/child-processes.d.ts.map +1 -1
  17. package/dist/child-processes.js +9 -1
  18. package/dist/child-processes.js.map +1 -1
  19. package/dist/clocks.d.ts +3 -0
  20. package/dist/clocks.d.ts.map +1 -0
  21. package/dist/clocks.js +44 -0
  22. package/dist/clocks.js.map +1 -0
  23. package/dist/coherent-file-locks.d.ts.map +1 -1
  24. package/dist/coherent-file-locks.js +2 -1
  25. package/dist/coherent-file-locks.js.map +1 -1
  26. package/dist/credentials.d.ts +4 -0
  27. package/dist/credentials.d.ts.map +1 -0
  28. package/dist/credentials.js +46 -0
  29. package/dist/credentials.js.map +1 -0
  30. package/dist/descriptors.d.ts +14 -0
  31. package/dist/descriptors.d.ts.map +1 -1
  32. package/dist/descriptors.js +45 -5
  33. package/dist/descriptors.js.map +1 -1
  34. package/dist/environment.d.ts +4 -0
  35. package/dist/environment.d.ts.map +1 -0
  36. package/dist/environment.js +23 -0
  37. package/dist/environment.js.map +1 -0
  38. package/dist/epoll-ready.d.ts +27 -0
  39. package/dist/epoll-ready.d.ts.map +1 -0
  40. package/dist/epoll-ready.js +88 -0
  41. package/dist/epoll-ready.js.map +1 -0
  42. package/dist/events.d.ts +1 -0
  43. package/dist/events.d.ts.map +1 -1
  44. package/dist/events.js +55 -24
  45. package/dist/events.js.map +1 -1
  46. package/dist/filesystem-host-session.d.ts +65 -0
  47. package/dist/filesystem-host-session.d.ts.map +1 -0
  48. package/dist/filesystem-host-session.js +951 -0
  49. package/dist/filesystem-host-session.js.map +1 -0
  50. package/dist/filesystem-host.d.ts +13 -81
  51. package/dist/filesystem-host.d.ts.map +1 -1
  52. package/dist/filesystem-host.js +14 -882
  53. package/dist/filesystem-host.js.map +1 -1
  54. package/dist/filesystem-protocol.d.ts +3 -1
  55. package/dist/filesystem-protocol.d.ts.map +1 -1
  56. package/dist/filesystem-remote.d.ts +8 -3
  57. package/dist/filesystem-remote.d.ts.map +1 -1
  58. package/dist/filesystem-remote.js +43 -20
  59. package/dist/filesystem-remote.js.map +1 -1
  60. package/dist/filesystem-service-client.d.ts +4 -0
  61. package/dist/filesystem-service-client.d.ts.map +1 -0
  62. package/dist/filesystem-service-client.js +16 -0
  63. package/dist/filesystem-service-client.js.map +1 -0
  64. package/dist/filesystem.d.ts +26 -4
  65. package/dist/filesystem.d.ts.map +1 -1
  66. package/dist/filesystem.js +633 -175
  67. package/dist/filesystem.js.map +1 -1
  68. package/dist/fork-memory.d.ts +23 -0
  69. package/dist/fork-memory.d.ts.map +1 -0
  70. package/dist/fork-memory.js +93 -0
  71. package/dist/fork-memory.js.map +1 -0
  72. package/dist/fork-worker.js +32 -11
  73. package/dist/fork-worker.js.map +1 -1
  74. package/dist/jumps.d.ts +7 -0
  75. package/dist/jumps.d.ts.map +1 -1
  76. package/dist/jumps.js +19 -1
  77. package/dist/jumps.js.map +1 -1
  78. package/dist/kernel-filesystem-worker.d.ts +2 -0
  79. package/dist/kernel-filesystem-worker.d.ts.map +1 -0
  80. package/dist/kernel-filesystem-worker.js +28 -0
  81. package/dist/kernel-filesystem-worker.js.map +1 -0
  82. package/dist/kernel-filesystem.d.ts +12 -0
  83. package/dist/kernel-filesystem.d.ts.map +1 -0
  84. package/dist/kernel-filesystem.js +45 -0
  85. package/dist/kernel-filesystem.js.map +1 -0
  86. package/dist/kernel-network-worker.d.ts +2 -0
  87. package/dist/kernel-network-worker.d.ts.map +1 -0
  88. package/dist/kernel-network-worker.js +14 -0
  89. package/dist/kernel-network-worker.js.map +1 -0
  90. package/dist/kernel-network.d.ts +14 -0
  91. package/dist/kernel-network.d.ts.map +1 -0
  92. package/dist/kernel-network.js +50 -0
  93. package/dist/kernel-network.js.map +1 -0
  94. package/dist/kernel-process-client.d.ts +6 -1
  95. package/dist/kernel-process-client.d.ts.map +1 -1
  96. package/dist/kernel-process-client.js +10 -1
  97. package/dist/kernel-process-client.js.map +1 -1
  98. package/dist/module-layout.d.ts +8 -0
  99. package/dist/module-layout.d.ts.map +1 -1
  100. package/dist/module-layout.js +263 -2
  101. package/dist/module-layout.js.map +1 -1
  102. package/dist/network-host.d.ts +6 -0
  103. package/dist/network-host.d.ts.map +1 -0
  104. package/dist/network-host.js +1582 -0
  105. package/dist/network-host.js.map +1 -0
  106. package/dist/network-protocol.d.ts +9 -2
  107. package/dist/network-protocol.d.ts.map +1 -1
  108. package/dist/network-protocol.js +3 -0
  109. package/dist/network-protocol.js.map +1 -1
  110. package/dist/network-service-client.d.ts +4 -0
  111. package/dist/network-service-client.d.ts.map +1 -0
  112. package/dist/network-service-client.js +16 -0
  113. package/dist/network-service-client.js.map +1 -0
  114. package/dist/network-worker.js +5 -1150
  115. package/dist/network-worker.js.map +1 -1
  116. package/dist/network.d.ts +13 -1
  117. package/dist/network.d.ts.map +1 -1
  118. package/dist/network.js +175 -48
  119. package/dist/network.js.map +1 -1
  120. package/dist/paths.d.ts +11 -0
  121. package/dist/paths.d.ts.map +1 -0
  122. package/dist/paths.js +97 -0
  123. package/dist/paths.js.map +1 -0
  124. package/dist/process-bridge.d.ts +12 -4
  125. package/dist/process-bridge.d.ts.map +1 -1
  126. package/dist/process-bridge.js +53 -17
  127. package/dist/process-bridge.js.map +1 -1
  128. package/dist/program.d.ts.map +1 -1
  129. package/dist/program.js +8 -0
  130. package/dist/program.js.map +1 -1
  131. package/dist/runtime.d.ts.map +1 -1
  132. package/dist/runtime.js +150 -92
  133. package/dist/runtime.js.map +1 -1
  134. package/dist/shared-buffer-pool.d.ts +18 -5
  135. package/dist/shared-buffer-pool.d.ts.map +1 -1
  136. package/dist/shared-buffer-pool.js +86 -14
  137. package/dist/shared-buffer-pool.js.map +1 -1
  138. package/dist/shared-file-layer.d.ts +3 -1
  139. package/dist/shared-file-layer.d.ts.map +1 -1
  140. package/dist/shared-file-layer.js +150 -35
  141. package/dist/shared-file-layer.js.map +1 -1
  142. package/dist/shared-pipes.d.ts +3 -1
  143. package/dist/shared-pipes.d.ts.map +1 -1
  144. package/dist/shared-pipes.js +105 -33
  145. package/dist/shared-pipes.js.map +1 -1
  146. package/dist/signal-interruption.d.ts +9 -0
  147. package/dist/signal-interruption.d.ts.map +1 -0
  148. package/dist/signal-interruption.js +6 -0
  149. package/dist/signal-interruption.js.map +1 -0
  150. package/dist/signals.d.ts +17 -6
  151. package/dist/signals.d.ts.map +1 -1
  152. package/dist/signals.js +101 -22
  153. package/dist/signals.js.map +1 -1
  154. package/dist/snapshot-filesystem.d.ts +12 -1
  155. package/dist/snapshot-filesystem.d.ts.map +1 -1
  156. package/dist/snapshot-filesystem.js +241 -73
  157. package/dist/snapshot-filesystem.js.map +1 -1
  158. package/dist/socket-flags.d.ts +3 -0
  159. package/dist/socket-flags.d.ts.map +1 -0
  160. package/dist/socket-flags.js +9 -0
  161. package/dist/socket-flags.js.map +1 -0
  162. package/dist/special-files.d.ts +6 -0
  163. package/dist/special-files.d.ts.map +1 -0
  164. package/dist/special-files.js +56 -0
  165. package/dist/special-files.js.map +1 -0
  166. package/dist/stdin.d.ts.map +1 -1
  167. package/dist/stdin.js +13 -5
  168. package/dist/stdin.js.map +1 -1
  169. package/dist/symlink.d.ts +2 -1
  170. package/dist/symlink.d.ts.map +1 -1
  171. package/dist/symlink.js +25 -93
  172. package/dist/symlink.js.map +1 -1
  173. package/dist/system.d.ts +2 -4
  174. package/dist/system.d.ts.map +1 -1
  175. package/dist/system.js +46 -29
  176. package/dist/system.js.map +1 -1
  177. package/dist/thread-coordinator-worker.js +58 -24
  178. package/dist/thread-coordinator-worker.js.map +1 -1
  179. package/dist/thread-lifecycle.d.ts +9 -0
  180. package/dist/thread-lifecycle.d.ts.map +1 -0
  181. package/dist/thread-lifecycle.js +30 -0
  182. package/dist/thread-lifecycle.js.map +1 -0
  183. package/dist/thread-worker.js +125 -44
  184. package/dist/thread-worker.js.map +1 -1
  185. package/dist/threads.d.ts +6 -0
  186. package/dist/threads.d.ts.map +1 -1
  187. package/dist/threads.js +46 -6
  188. package/dist/threads.js.map +1 -1
  189. package/dist/tty-job-control.d.ts +13 -0
  190. package/dist/tty-job-control.d.ts.map +1 -0
  191. package/dist/tty-job-control.js +56 -0
  192. package/dist/tty-job-control.js.map +1 -0
  193. package/dist/tty-settings.d.ts +3 -0
  194. package/dist/tty-settings.d.ts.map +1 -0
  195. package/dist/tty-settings.js +25 -0
  196. package/dist/tty-settings.js.map +1 -0
  197. package/dist/types.d.ts +26 -0
  198. package/dist/types.d.ts.map +1 -1
  199. package/dist/wait-state.d.ts +8 -0
  200. package/dist/wait-state.d.ts.map +1 -1
  201. package/dist/wait-state.js +15 -0
  202. package/dist/wait-state.js.map +1 -1
  203. package/dist/worker-program-protocol.d.ts +22 -2
  204. package/dist/worker-program-protocol.d.ts.map +1 -1
  205. package/dist/worker-program-worker.js +31 -9
  206. package/dist/worker-program-worker.js.map +1 -1
  207. package/dist/worker-program.d.ts +12 -2
  208. package/dist/worker-program.d.ts.map +1 -1
  209. package/dist/worker-program.js +214 -36
  210. package/dist/worker-program.js.map +1 -1
  211. package/dist/write-vector.d.ts +3 -0
  212. package/dist/write-vector.d.ts.map +1 -0
  213. package/dist/write-vector.js +35 -0
  214. package/dist/write-vector.js.map +1 -0
  215. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { KernelProcessTable, kernelProcessOwner } from "@volter/browser-runtime/kernel-processes.js";
2
+ import { waliJumpTransfer } from "./jumps.js";
2
3
  import { forkKernelProcessIdentity } from "./kernel-process-client.js";
3
4
  import { releaseWaliSharedBuffers } from "./shared-buffer-pool.js";
4
5
  import { createSharedFileLayer, lowerTreeOf, sharedFileLayerEntries, withoutHostTrees } from "./shared-file-layer.js";
@@ -10,7 +11,7 @@ import { SharedWaliStdinWriter } from "./stdin.js";
10
11
  import { findImportedMemory } from "./module-layout.js";
11
12
  import { createWaliSignalState, defaultStops, defaultTerminates, WaliSignals } from "./signals.js";
12
13
  import { waliSharedPipeDescribesFd } from "./shared-pipes.js";
13
- import { applyProcessFilesystemPatch, closeSharedPipe, createProcessFilesystemPatch, encodeProcessPatch, publishProcessCompletion, readSharedPipe, writeSharedPipe, } from "./process-bridge.js";
14
+ import { applyProcessFilesystemPatch, closeSharedPipe, createProcessFilesystemPatch, encodeProcessPatch, waliExecCompletion, waliTerminalInput, publishProcessCompletion, readSharedPipe, writeSharedPipe, } from "./process-bridge.js";
14
15
  import { createBrowserFileLockChannel, sharedBrowserFileLockCoordinator, } from "@volter/browser-runtime/browser-file-locks.js";
15
16
  import { createBrowserFileCoherenceChannel, sharedBrowserFileCoherenceCoordinator, } from "@volter/browser-runtime/browser-file-coherence.js";
16
17
  import { currentEntry, patchWithContent, ProcessFilesystemBase } from "@volter/browser-runtime/browser-process-filesystem.js";
@@ -18,20 +19,43 @@ import { pageWorker } from "@volter/browser-runtime/page-workers.js";
18
19
  import { createWaliWaitAgent, startWaliWaitAgent, WaliWaitTable, waliWaitOwner } from "./wait-state.js";
19
20
  import { WaliForkPool } from "./fork-pool.js";
20
21
  import { WaliExecPool, createWaliProgramWorker } from "./exec-pool.js";
22
+ import { WaliKernelNetwork } from "./kernel-network.js";
23
+ import { WaliKernelFilesystem } from "./kernel-filesystem.js";
21
24
  const kernelProcesses = new WeakMap();
22
25
  const forkPools = new WeakMap();
23
26
  const execPools = new WeakMap();
27
+ const kernelNetworks = new WeakMap();
28
+ const kernelFilesystems = new WeakMap();
24
29
  // A pooled worker can have served a different program object. Old queued
25
30
  // replies must not match that object's first run after listeners change.
26
31
  let nextRunId = 1;
32
+ /** Host commit failures are kernel diagnostics, never bytes written by a guest. */
33
+ class WaliFilesystemCommitError extends Error {
34
+ code = "WALI_FILESYSTEM_COMMIT";
35
+ constructor(message, cause) { super(message, { cause }); }
36
+ }
37
+ function isFilesystemCommitError(error) {
38
+ const seen = new Set();
39
+ while (typeof error === "object" && error !== null && !seen.has(error)) {
40
+ seen.add(error);
41
+ if ("code" in error && error.code === "WALI_FILESYSTEM_COMMIT")
42
+ return true;
43
+ error = "cause" in error ? error.cause : undefined;
44
+ }
45
+ return false;
46
+ }
27
47
  /** Runs one unchanged WALI executable off the UI thread while retaining its compiled module. */
28
48
  /**
29
49
  * Each program's module, compiled by the first worker that ran it, for every
30
50
  * worker after: a module is compiled once per page, not once per worker. Only
31
- * a pinned artifact is kept; a `blob:` module is a URL of its own each run.
51
+ * a pinned artifact is kept, including a local Blob admitted with its digest.
32
52
  */
33
53
  const compiledModules = new Map();
34
54
  const compiledKey = (url, integrity) => integrity ? `${url}\n${integrity}` : undefined;
55
+ /** Artifact owner retirement, after its last active image has ended. */
56
+ export function forgetWaliModule(url, integrity) {
57
+ compiledModules.delete(compiledKey(url, integrity));
58
+ }
35
59
  /** `work`, or sooner when `signal` aborts: a run waiting on a preload is still one that can be cancelled (review of da64b88). */
36
60
  async function untilAborted(work, signal) {
37
61
  if (!signal) {
@@ -87,16 +111,23 @@ export function preloadWaliModule(url, integrity) {
87
111
  export class BrowserWaliWorkerProgram {
88
112
  filesystem;
89
113
  program;
114
+ /** Host continuations outlive a retired guest image until its successor finishes. */
115
+ childRuns = new Set();
90
116
  capabilities = Object.freeze({ filesystemPolicy: true });
91
117
  /** Started by the first run, so a program the page offers and never runs costs no worker. */
92
118
  worker;
93
119
  pending;
94
120
  stdin;
121
+ terminalInput;
95
122
  terminalSize = { columns: 80, rows: 24 };
96
123
  detachWorker;
97
124
  execPool;
98
125
  ownsExecPool = false;
99
126
  workerLeased = false;
127
+ kernelNetwork;
128
+ releaseKernelNetwork;
129
+ kernelFilesystem;
130
+ releaseKernelFilesystem;
100
131
  forkWorkers = new Map();
101
132
  forkCleanup = new Map();
102
133
  forkPool;
@@ -173,6 +204,26 @@ export class BrowserWaliWorkerProgram {
173
204
  this.execPool = new WaliExecPool();
174
205
  execPools.set(this.waitOwner.table, this.execPool);
175
206
  }
207
+ this.kernelNetwork = kernelNetworks.get(kernel);
208
+ if (!this.kernelNetwork || this.kernelNetwork.isDisposed) {
209
+ this.kernelNetwork = new WaliKernelNetwork();
210
+ kernelNetworks.set(kernel, this.kernelNetwork);
211
+ }
212
+ this.releaseKernelNetwork = this.kernelNetwork.retain(error => {
213
+ this.pending?.reject(error);
214
+ this.finishPending();
215
+ this.resetWorker();
216
+ });
217
+ this.kernelFilesystem = kernelFilesystems.get(kernel);
218
+ if (!this.kernelFilesystem || this.kernelFilesystem.isDisposed) {
219
+ this.kernelFilesystem = new WaliKernelFilesystem();
220
+ kernelFilesystems.set(kernel, this.kernelFilesystem);
221
+ }
222
+ this.releaseKernelFilesystem = this.kernelFilesystem.retain(error => {
223
+ this.pending?.reject(error);
224
+ this.finishPending();
225
+ this.resetWorker();
226
+ });
176
227
  // Started here so its boot overlaps the run's preparation.
177
228
  this.worker ??= this.createWorker();
178
229
  this.preparing = true;
@@ -210,7 +261,7 @@ export class BrowserWaliWorkerProgram {
210
261
  if (layer?.vanished) {
211
262
  const vanished = new Set(layer.vanished);
212
263
  image = { files: image.files.filter((entry) => !vanished.has(entry.path)) };
213
- delete layer.vanished;
264
+ // The cached layer is immutable and may already serve another run.
214
265
  }
215
266
  // The host's own layers are described by size in the image and the base,
216
267
  // as a store's files are: the program holds them, and a file it did not
@@ -259,6 +310,7 @@ export class BrowserWaliWorkerProgram {
259
310
  const size = options.terminal ?? this.terminalSize;
260
311
  stdin.resize(size.columns, size.rows);
261
312
  this.stdin = stdin;
313
+ this.terminalInput = options[waliTerminalInput] ?? (options.terminal ? stdin.buffer : undefined);
262
314
  const signalState = createWaliSignalState();
263
315
  return await new Promise((resolve, reject) => {
264
316
  // What `kill` sent (the abort reason's `signal`; an abort that names
@@ -280,7 +332,15 @@ export class BrowserWaliWorkerProgram {
280
332
  const survives = (signal) => {
281
333
  if (signal === 9)
282
334
  return false;
283
- const disposition = new WaliSignals(signalState).raise(signal);
335
+ if (signal === 19) {
336
+ new WaliSignals(signalState).raise(signal);
337
+ kernel.stop(processPid, signal);
338
+ return true;
339
+ }
340
+ // Default-ignored signals also remain pending while blocked. Explicit
341
+ // SIG_IGN is still discarded by raise; unblocked defaults act now.
342
+ const disposition = new WaliSignals(signalState).raise(signal, 0, 0, 0, "masked");
343
+ this.waitOwner?.table.notifyProcess(processPid);
284
344
  if (disposition === "default" && defaultStops(signal))
285
345
  kernel.stop(processPid, signal);
286
346
  return disposition !== "default" || !defaultTerminates(signal);
@@ -343,6 +403,7 @@ export class BrowserWaliWorkerProgram {
343
403
  childPidAllocator: lineage?.childPidAllocator ?? kernel.allocator,
344
404
  id,
345
405
  ...(options.descriptorTable ? { descriptorTable: options.descriptorTable } : {}),
406
+ ...(options[waliExecCompletion] ? { execCompletion: options[waliExecCompletion] } : {}),
346
407
  url: this.program.url,
347
408
  ...(this.program.integrity ? { integrity: this.program.integrity } : {}),
348
409
  ...(() => {
@@ -366,6 +427,8 @@ export class BrowserWaliWorkerProgram {
366
427
  } } : {}),
367
428
  ...(loopbackChannel ? { loopbackPort: loopbackChannel.port2 } : {}),
368
429
  stdin: stdin.buffer,
430
+ ...(this.terminalInput ? { terminalStdin: this.terminalInput } : {}),
431
+ ...(options.terminal?.write ? { terminalOutput: true } : {}),
369
432
  terminal: options.terminal !== undefined,
370
433
  filesystem,
371
434
  ...(store ? { store: { projectId: store } } : {}),
@@ -383,7 +446,7 @@ export class BrowserWaliWorkerProgram {
383
446
  if (!active || active.id !== id)
384
447
  return;
385
448
  if (event.data.type === "spawn") {
386
- void this.spawnChild({ ...event.data, id, type: "spawn" }, active);
449
+ this.launchChild({ ...event.data, id, type: "spawn" }, active);
387
450
  }
388
451
  else
389
452
  void this.handleMessage({ ...event.data, id });
@@ -411,7 +474,9 @@ export class BrowserWaliWorkerProgram {
411
474
  active.options.onDiagnostic?.(event.data.message);
412
475
  return;
413
476
  }
414
- if (event.data.type === "stdout")
477
+ if (event.data.type === "terminal-output")
478
+ active.options.terminal?.write?.(new TextDecoder().decode(event.data.bytes));
479
+ else if (event.data.type === "stdout")
415
480
  this.emitStdout(active, event.data.bytes);
416
481
  else
417
482
  this.emitStderr(active, new TextDecoder().decode(event.data.bytes));
@@ -598,6 +663,14 @@ export class BrowserWaliWorkerProgram {
598
663
  const pending = this.pending;
599
664
  if (!pending || message.id !== pending.id)
600
665
  return;
666
+ if (message.type === "network-service") {
667
+ this.kernelNetwork.connect(message.reply);
668
+ return;
669
+ }
670
+ if (message.type === "filesystem-service") {
671
+ this.kernelFilesystem.connect(message.reply);
672
+ return;
673
+ }
601
674
  if (message.type === "compiled") {
602
675
  const key = compiledKey(this.program.url, this.program.integrity);
603
676
  if (key)
@@ -616,6 +689,10 @@ export class BrowserWaliWorkerProgram {
616
689
  this.emitStdout(pending, message.chunk);
617
690
  return;
618
691
  }
692
+ if (message.type === "terminal-output") {
693
+ pending.options.terminal?.write?.(new TextDecoder().decode(message.chunk));
694
+ return;
695
+ }
619
696
  if (message.type === "stderr") {
620
697
  this.emitStderr(pending, message.chunk);
621
698
  return;
@@ -625,8 +702,15 @@ export class BrowserWaliWorkerProgram {
625
702
  return;
626
703
  }
627
704
  if (message.type === "process") {
628
- const result = this.processOwner?.table.call(message.callerPid, message.operation, message.first, message.second) ?? -3;
705
+ const table = this.processOwner?.table;
706
+ const settings = message.operation === "termios"
707
+ ? table?.terminalSettings(message.callerPid, message.first === 1, message.second, message.data ?? new Uint8Array())
708
+ : undefined;
709
+ const result = message.operation === "termios" ? settings?.result ?? -3
710
+ : table?.call(message.callerPid, message.operation, message.first, message.second) ?? -3;
629
711
  const reply = new Int32Array(message.reply);
712
+ if (settings?.result === 0 && settings.settings)
713
+ new Uint8Array(message.reply, 8).set(settings.settings);
630
714
  Atomics.store(reply, 1, result);
631
715
  Atomics.store(reply, 0, 1);
632
716
  Atomics.notify(reply, 0);
@@ -637,7 +721,7 @@ export class BrowserWaliWorkerProgram {
637
721
  return;
638
722
  }
639
723
  if (message.type === "spawn") {
640
- void this.spawnChild(message, pending);
724
+ this.launchChild(message, pending);
641
725
  return;
642
726
  }
643
727
  if (message.type === "kill") {
@@ -654,7 +738,8 @@ export class BrowserWaliWorkerProgram {
654
738
  return;
655
739
  }
656
740
  console.log("[wali-boot]", JSON.stringify({ stage: "result-received", run: this.traceId, at: Date.now() }));
657
- const safeFilesystem = this.program.sanitizeFilesystem
741
+ const delegated = pending.options.descriptorTable !== undefined;
742
+ const safeFilesystem = !delegated && this.program.sanitizeFilesystem
658
743
  ? await this.program.sanitizeFilesystem(message.filesystem)
659
744
  : message.filesystem;
660
745
  if (message.filesystemFinalized)
@@ -663,19 +748,36 @@ export class BrowserWaliWorkerProgram {
663
748
  console.log("[wali-boot]", JSON.stringify({ stage: "filesystem-settled", run: this.traceId, at: Date.now() }));
664
749
  if (this.pending !== pending)
665
750
  return;
751
+ if (message.result?.replaced) {
752
+ // Return the old worker now, while its host continuation awaits the
753
+ // successor. A backend caller may terminate this program in finally;
754
+ // do not resolve that caller until the exec chain has closed its pipes.
755
+ this.resetWorker(true, true);
756
+ await Promise.all([...this.childRuns]);
757
+ if (this.pending !== pending)
758
+ return;
759
+ }
666
760
  // A threaded process commits its final delta on the ordered update port.
667
761
  // Its full snapshot may predate a peer import acknowledged on that port;
668
762
  // diffing it against the advanced base would manufacture a deletion.
669
- const patch = message.filesystemFinalized ? { files: [], removed: [] }
763
+ // Exec/fork images that inherit the owner cannot replay their private
764
+ // artifact snapshot over writes the owner has already published.
765
+ const patch = message.filesystemFinalized || delegated ? { files: [], removed: [] }
670
766
  : this.withoutHostTrees(createProcessFilesystemPatch(pending.filesystemBase, safeFilesystem, { written: true }));
671
- if (!message.filesystemFinalized)
672
- applyProcessFilesystemPatch(this.filesystem, patch, pending.filesystemBase);
767
+ if (!message.filesystemFinalized && !delegated) {
768
+ try {
769
+ applyProcessFilesystemPatch(this.filesystem, patch, pending.filesystemBase);
770
+ }
771
+ catch (error) {
772
+ throw new WaliFilesystemCommitError("WALI exit filesystem commit failed", error);
773
+ }
774
+ }
673
775
  try {
674
776
  await this.program.onFilesystemCommit?.(this.filesystem, patch);
675
777
  }
676
778
  catch (error) {
677
779
  this.finishPending();
678
- pending.reject(new Error(`WALI filesystem durability barrier failed: ${error instanceof Error ? error.message : String(error)}`));
780
+ pending.reject(new WaliFilesystemCommitError(`WALI filesystem durability barrier failed: ${error instanceof Error ? error.message : String(error)}`, error));
679
781
  return;
680
782
  }
681
783
  this.finishPending();
@@ -683,7 +785,7 @@ export class BrowserWaliWorkerProgram {
683
785
  // this image's listeners and children before a different image borrows
684
786
  // the worker; a caller's finally/terminate then has no lease to cancel.
685
787
  if (!this.ownsExecPool)
686
- this.resetWorker(message.result !== undefined);
788
+ this.resetWorker(message.result !== undefined, message.result?.replaced === true);
687
789
  console.log("[wali-boot]", JSON.stringify({ stage: "done", run: this.traceId, at: Date.now() }));
688
790
  if (message.result)
689
791
  pending.resolve({ ...message.result, stdout: pending.stdout, stderr: pending.stderr });
@@ -711,7 +813,7 @@ export class BrowserWaliWorkerProgram {
711
813
  }) : Promise.resolve();
712
814
  let ended = false;
713
815
  let processImage = 0;
714
- const finish = async (signal) => {
816
+ const finish = async (signal, replaced = false) => {
715
817
  if (ended)
716
818
  return;
717
819
  ended = true;
@@ -734,15 +836,20 @@ export class BrowserWaliWorkerProgram {
734
836
  publishProcessCompletion({ ...message, processIdentity: identity });
735
837
  releaseWaliSharedBuffers(message.bufferLease);
736
838
  }
737
- else
839
+ else {
738
840
  pool.release(worker);
841
+ if (!replaced) {
842
+ publishProcessCompletion({ ...message, processIdentity: identity });
843
+ releaseWaliSharedBuffers(message.bufferLease);
844
+ }
845
+ }
739
846
  table?.removeImage(agent.image);
740
847
  };
741
848
  const receive = (event) => {
742
849
  if (this.forkWorkers.get(message.pid) !== worker)
743
850
  return;
744
851
  if (event.data.type === "fork-done") {
745
- void finish();
852
+ void finish(undefined, event.data.replaced);
746
853
  return;
747
854
  }
748
855
  if (event.data.type === "stderr" && event.data.chunk.includes("could not allocate memory")) {
@@ -765,7 +872,14 @@ export class BrowserWaliWorkerProgram {
765
872
  processImage = owner.table.image(message.pid, signal => {
766
873
  if (ended)
767
874
  return;
768
- const disposition = signal === 9 ? "default" : new WaliSignals(message.forkState.signalState).raise(signal);
875
+ if (signal === 19) {
876
+ new WaliSignals(message.forkState.signalState).raise(signal);
877
+ owner.table.stop(message.pid, signal);
878
+ return;
879
+ }
880
+ const disposition = signal === 9 ? "default"
881
+ : new WaliSignals(message.forkState.signalState).raise(signal, 0, 0, 0, "masked");
882
+ table?.notifyProcess(message.pid);
769
883
  if (disposition === "default" && defaultStops(signal))
770
884
  owner.table.stop(message.pid, signal);
771
885
  if (disposition === "default" && defaultTerminates(signal))
@@ -775,7 +889,23 @@ export class BrowserWaliWorkerProgram {
775
889
  worker.addEventListener("error", failed);
776
890
  const descriptorTable = { ...message.descriptorTable };
777
891
  delete descriptorTable.releasePort;
778
- worker.postMessage({ ...message, processIdentity: identity, descriptorTable, waitAgent: agent }, [descriptorTable.port, message.forkState.memory]);
892
+ // The child's reactor channel travels with the fork: a request for it
893
+ // would be one more round trip through this thread before the child runs.
894
+ const networkServicePort = this.kernelNetwork?.open();
895
+ worker.postMessage({ ...message, processIdentity: identity, descriptorTable, waitAgent: agent,
896
+ ...(networkServicePort ? { networkServicePort } : {}) }, [descriptorTable.port, message.forkState.memory, ...waliJumpTransfer(message.forkState.jumps),
897
+ ...(networkServicePort ? [networkServicePort] : [])]);
898
+ }
899
+ launchChild(message, pending) {
900
+ const work = this.spawnChild(message, pending);
901
+ this.childRuns.add(work);
902
+ void work.finally(() => this.childRuns.delete(work)).catch(error => {
903
+ if (this.pending !== pending)
904
+ return;
905
+ pending.reject(error);
906
+ this.finishPending();
907
+ this.resetWorker();
908
+ });
779
909
  }
780
910
  async spawnChild(message, pending) {
781
911
  const table = this.waitOwner?.table;
@@ -803,6 +933,7 @@ export class BrowserWaliWorkerProgram {
803
933
  const descriptorTable = message.descriptorTable && {
804
934
  port: message.descriptorTable.port, control: message.descriptorTable.control,
805
935
  payload: message.descriptorTable.payload,
936
+ ...(message.descriptorTable.descriptorLimits ? { descriptorLimits: message.descriptorTable.descriptorLimits } : {}),
806
937
  ...(message.descriptorTable.sharedPipes ? { sharedPipes: message.descriptorTable.sharedPipes } : {}),
807
938
  };
808
939
  let released;
@@ -814,6 +945,7 @@ export class BrowserWaliWorkerProgram {
814
945
  let stderrWrites = Promise.resolve();
815
946
  let stdoutStreamed = false;
816
947
  let stderrStreamed = false;
948
+ let replaced = false;
817
949
  // The child's patch is a diff of the tree before and after it ran; the
818
950
  // diff needs sizes and times, and only the entries it names need bytes.
819
951
  const metadataSnapshot = () => readBrowserFileSystemSnapshot(this.filesystem, "/", { metadataRoots: ["/"] });
@@ -874,18 +1006,33 @@ export class BrowserWaliWorkerProgram {
874
1006
  const safePatch = this.withoutHostTrees(this.program.sanitizeFilesystemPatch
875
1007
  ? await this.program.sanitizeFilesystemPatch(message.filesystemPatch)
876
1008
  : message.filesystemPatch);
877
- applyProcessFilesystemPatch(this.filesystem, safePatch, pending.filesystemBase);
878
- pending.filesystemBase.apply(safePatch);
1009
+ // A descriptor handoff already flushed the owner before exec/spawn.
1010
+ // The old image's private snapshot cannot compete with that owner.
1011
+ if (!message.descriptorTable) {
1012
+ try {
1013
+ applyProcessFilesystemPatch(this.filesystem, safePatch, pending.filesystemBase);
1014
+ }
1015
+ catch (error) {
1016
+ throw new WaliFilesystemCommitError("WALI spawn filesystem commit failed", error);
1017
+ }
1018
+ pending.filesystemBase.apply(safePatch);
1019
+ }
879
1020
  childFilesystem = await metadataSnapshot();
880
1021
  trace("spawn-patched");
881
1022
  const stdinStream = message.stdin ? streamBrowserPipe(message.stdin, controller.signal) : undefined;
882
1023
  const spawned = browserCommand(message);
883
- const result = await backend.run(spawned.command, {
1024
+ const runOptions = {
1025
+ ...(this.terminalInput ? { [waliTerminalInput]: this.terminalInput } : {}),
884
1026
  [kernelProcessOwner]: { table: owner.table, identity },
885
1027
  ...(table ? { [waliWaitOwner]: { table, agent } } : {}),
886
1028
  [browserProcessLineage]: { pid: message.pid, parentPid: message.parentPid, childPidAllocator: message.childPidAllocator },
887
1029
  ...(filesystemPolicy ? { filesystemPolicy } : {}),
888
1030
  ...(descriptorTable ? { descriptorTable } : {}),
1031
+ ...(message.replaceImage ? { [waliExecCompletion]: {
1032
+ control: message.control, response: message.response,
1033
+ ...(message.bufferLease ? { bufferLease: message.bufferLease } : {}),
1034
+ processIdentity: identity,
1035
+ } } : {}),
889
1036
  cwd: message.cwd,
890
1037
  ...(pending.options.terminal ? { terminal: pending.options.terminal } : {}),
891
1038
  ...(spawned.executablePath ? { executablePath: spawned.executablePath } : {}),
@@ -895,7 +1042,9 @@ export class BrowserWaliWorkerProgram {
895
1042
  ...(message.stdout ? { onStdoutBytes: stdoutBytes } : {}),
896
1043
  onStdout: stdout,
897
1044
  onStderr: stderr,
898
- });
1045
+ };
1046
+ const result = await backend.run(spawned.command, runOptions);
1047
+ replaced = result.replaced === true;
899
1048
  // Tabnode streams long-running commands, but may only return buffered
900
1049
  // output for short commands. Deliver exactly one form to the child pipe.
901
1050
  if (result.stdout && !stdoutStreamed)
@@ -905,7 +1054,10 @@ export class BrowserWaliWorkerProgram {
905
1054
  trace("spawn-ran");
906
1055
  await Promise.all([stdoutWrites, stderrWrites]);
907
1056
  await releaseDescriptors();
908
- encodeProcessPatch(message, await patchAgainst(childFilesystem), result.exitCode);
1057
+ // exec transferred this same completion record. The successor is its
1058
+ // only producer; publishing here would reap a still-running process.
1059
+ if (!replaced)
1060
+ encodeProcessPatch(message, await patchAgainst(childFilesystem), result.exitCode);
909
1061
  trace("spawn-returned");
910
1062
  }
911
1063
  catch (error) {
@@ -914,7 +1066,12 @@ export class BrowserWaliWorkerProgram {
914
1066
  && controller.signal.reason?.handled !== true;
915
1067
  const signal = typeof delivered === "number" && Number.isInteger(delivered) && delivered > 0 && delivered <= 64
916
1068
  ? delivered : cancellation ? abortSignalNumber(controller.signal.reason) : undefined;
917
- if (signal === undefined) {
1069
+ const kernelFailure = isFilesystemCommitError(error);
1070
+ if (signal === undefined && kernelFailure) {
1071
+ pending.options.onDiagnostic?.(`[WALI filesystem] ${error instanceof Error ? error.message : String(error)}`);
1072
+ await Promise.allSettled([stdoutWrites, stderrWrites]);
1073
+ }
1074
+ else if (signal === undefined) {
918
1075
  const text = `${error instanceof Error ? error.message : String(error)}\n`;
919
1076
  stderr(text);
920
1077
  await stderrWrites;
@@ -925,7 +1082,7 @@ export class BrowserWaliWorkerProgram {
925
1082
  await Promise.allSettled([stdoutWrites, stderrWrites]);
926
1083
  }
927
1084
  await releaseDescriptors();
928
- encodeProcessPatch(message, await patchAgainst(childFilesystem), signal === undefined ? 127 : 128 + signal, signal);
1085
+ encodeProcessPatch(message, kernelFailure ? { files: [], removed: [] } : await patchAgainst(childFilesystem), signal === undefined ? 127 : 128 + signal, signal);
929
1086
  }
930
1087
  finally {
931
1088
  table?.removeImage(agent.image);
@@ -935,7 +1092,8 @@ export class BrowserWaliWorkerProgram {
935
1092
  closeExternalPipe(message.stdin);
936
1093
  closeExternalPipe(message.stdout);
937
1094
  closeExternalPipe(message.stderr);
938
- releaseWaliSharedBuffers(message.bufferLease);
1095
+ if (!replaced)
1096
+ releaseWaliSharedBuffers(message.bufferLease);
939
1097
  }
940
1098
  }
941
1099
  async applyLiveFilesystemPatch(id, message) {
@@ -963,6 +1121,10 @@ export class BrowserWaliWorkerProgram {
963
1121
  this.scheduleFilesystemImport(pending);
964
1122
  return;
965
1123
  }
1124
+ // An inherited owner's updates arrive on that owner's port, not this
1125
+ // image's bootstrap port. Imported/checkpoint acknowledgements still run.
1126
+ if (pending.options.descriptorTable && message.type === "filesystem")
1127
+ return;
966
1128
  try {
967
1129
  if (message.type === "import-error")
968
1130
  throw new Error(message.message);
@@ -1007,7 +1169,7 @@ export class BrowserWaliWorkerProgram {
1007
1169
  return;
1008
1170
  this.finishPending();
1009
1171
  this.resetWorker();
1010
- pending.reject(new Error(`WALI filesystem update could not be applied: ${error instanceof Error ? error.message : String(error)}`));
1172
+ pending.reject(new WaliFilesystemCommitError(`WALI filesystem update could not be applied: ${error instanceof Error ? error.message : String(error)}`, error));
1011
1173
  return;
1012
1174
  }
1013
1175
  try {
@@ -1120,6 +1282,10 @@ export class BrowserWaliWorkerProgram {
1120
1282
  if (this.waitOwner)
1121
1283
  execPools.delete(this.waitOwner.table);
1122
1284
  }
1285
+ this.releaseKernelNetwork?.();
1286
+ this.releaseKernelNetwork = undefined;
1287
+ this.releaseKernelFilesystem?.();
1288
+ this.releaseKernelFilesystem = undefined;
1123
1289
  this.pending?.resolveFilesystemFinalization();
1124
1290
  this.pending?.removeAbortListener();
1125
1291
  this.pending?.releaseLoopback();
@@ -1130,6 +1296,7 @@ export class BrowserWaliWorkerProgram {
1130
1296
  this.waitOwner = undefined;
1131
1297
  this.stdin?.close();
1132
1298
  this.stdin = undefined;
1299
+ this.terminalInput = undefined;
1133
1300
  for (const port of this.processPorts)
1134
1301
  port.close();
1135
1302
  this.processPorts = [];
@@ -1166,14 +1333,19 @@ export class BrowserWaliWorkerProgram {
1166
1333
  }
1167
1334
  this.urlScanTail = text.slice(-2048);
1168
1335
  }
1169
- resetWorker(reusable = false) {
1170
- for (const finish of this.forkCleanup.values())
1171
- finish();
1172
- this.forkCleanup.clear();
1173
- this.forkWorkers.clear();
1174
- for (const controller of this.childControllers.values())
1175
- controller.abort();
1176
- this.childControllers.clear();
1336
+ resetWorker(reusable = false, preserveChildren = false) {
1337
+ // Image replacement retires a worker, not its process's children or the
1338
+ // successor now running through spawnChild. Their owner callbacks remain
1339
+ // alive until their normal completion; cancellation still tears them down.
1340
+ if (!preserveChildren) {
1341
+ for (const finish of this.forkCleanup.values())
1342
+ finish();
1343
+ this.forkCleanup.clear();
1344
+ this.forkWorkers.clear();
1345
+ for (const controller of this.childControllers.values())
1346
+ controller.abort();
1347
+ this.childControllers.clear();
1348
+ }
1177
1349
  // A retained program object must not retain its retired worker realm.
1178
1350
  // The next run starts lazily; late events are rejected by createWorker.
1179
1351
  const worker = this.worker;
@@ -1211,6 +1383,12 @@ function abortSignalNumber(reason) {
1211
1383
  * shell, not what the line runs, so it does not go with it.
1212
1384
  */
1213
1385
  function browserCommand(message) {
1386
+ // execve already resolved an image. Its argv is data, including argv[0];
1387
+ // it must not be renamed or interpreted as another shell command.
1388
+ if (message.replaceImage)
1389
+ return {
1390
+ command: message.argv.map(shellQuote).join(" "), executablePath: message.executablePath,
1391
+ };
1214
1392
  const [program = "", ...args] = message.argv;
1215
1393
  const executable = message.executablePath || program;
1216
1394
  const basename = executable.slice(executable.lastIndexOf("/") + 1);