@secure-exec/core 0.0.0-nathan-docs-sdk-expansion.c9c2e4e

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 (132) hide show
  1. package/README.md +7 -0
  2. package/dist/binary.d.ts +4 -0
  3. package/dist/binary.js +25 -0
  4. package/dist/bytes.d.ts +2 -0
  5. package/dist/bytes.js +6 -0
  6. package/dist/callbacks.d.ts +41 -0
  7. package/dist/callbacks.js +94 -0
  8. package/dist/cargo.d.ts +2 -0
  9. package/dist/cargo.js +142 -0
  10. package/dist/correlation.d.ts +10 -0
  11. package/dist/correlation.js +49 -0
  12. package/dist/descriptors.d.ts +34 -0
  13. package/dist/descriptors.js +37 -0
  14. package/dist/event-buffer.d.ts +90 -0
  15. package/dist/event-buffer.js +313 -0
  16. package/dist/ext.d.ts +7 -0
  17. package/dist/ext.js +13 -0
  18. package/dist/filesystem.d.ts +41 -0
  19. package/dist/filesystem.js +70 -0
  20. package/dist/frame-payload-codec.d.ts +8 -0
  21. package/dist/frame-payload-codec.js +14 -0
  22. package/dist/frame-rpc.d.ts +38 -0
  23. package/dist/frame-rpc.js +73 -0
  24. package/dist/frame-stream.d.ts +27 -0
  25. package/dist/frame-stream.js +99 -0
  26. package/dist/framing.d.ts +7 -0
  27. package/dist/framing.js +22 -0
  28. package/dist/generated/AcpLimitsConfig.d.ts +4 -0
  29. package/dist/generated/AcpLimitsConfig.js +2 -0
  30. package/dist/generated/CreateVmConfig.d.ts +19 -0
  31. package/dist/generated/CreateVmConfig.js +1 -0
  32. package/dist/generated/FsPermissionRule.d.ts +6 -0
  33. package/dist/generated/FsPermissionRule.js +1 -0
  34. package/dist/generated/FsPermissionRuleSet.d.ts +6 -0
  35. package/dist/generated/FsPermissionRuleSet.js +1 -0
  36. package/dist/generated/FsPermissionScope.d.ts +3 -0
  37. package/dist/generated/FsPermissionScope.js +1 -0
  38. package/dist/generated/HttpLimitsConfig.d.ts +3 -0
  39. package/dist/generated/HttpLimitsConfig.js +2 -0
  40. package/dist/generated/JsModuleResolution.d.ts +1 -0
  41. package/dist/generated/JsModuleResolution.js +2 -0
  42. package/dist/generated/JsRuntimeConfig.d.ts +26 -0
  43. package/dist/generated/JsRuntimeConfig.js +1 -0
  44. package/dist/generated/JsRuntimeLimitsConfig.d.ts +7 -0
  45. package/dist/generated/JsRuntimeLimitsConfig.js +2 -0
  46. package/dist/generated/JsRuntimePlatform.d.ts +1 -0
  47. package/dist/generated/JsRuntimePlatform.js +2 -0
  48. package/dist/generated/MountPluginDescriptor.d.ts +4 -0
  49. package/dist/generated/MountPluginDescriptor.js +2 -0
  50. package/dist/generated/NativeRootFilesystemConfig.d.ts +5 -0
  51. package/dist/generated/NativeRootFilesystemConfig.js +1 -0
  52. package/dist/generated/PatternPermissionRule.d.ts +6 -0
  53. package/dist/generated/PatternPermissionRule.js +1 -0
  54. package/dist/generated/PatternPermissionRuleSet.d.ts +6 -0
  55. package/dist/generated/PatternPermissionRuleSet.js +1 -0
  56. package/dist/generated/PatternPermissionScope.d.ts +3 -0
  57. package/dist/generated/PatternPermissionScope.js +1 -0
  58. package/dist/generated/PermissionMode.d.ts +1 -0
  59. package/dist/generated/PermissionMode.js +2 -0
  60. package/dist/generated/PermissionsPolicy.d.ts +10 -0
  61. package/dist/generated/PermissionsPolicy.js +1 -0
  62. package/dist/generated/PluginLimitsConfig.d.ts +4 -0
  63. package/dist/generated/PluginLimitsConfig.js +2 -0
  64. package/dist/generated/PythonLimitsConfig.d.ts +5 -0
  65. package/dist/generated/PythonLimitsConfig.js +2 -0
  66. package/dist/generated/ResourceLimitsConfig.d.ts +22 -0
  67. package/dist/generated/ResourceLimitsConfig.js +2 -0
  68. package/dist/generated/RootFilesystemConfig.d.ts +9 -0
  69. package/dist/generated/RootFilesystemConfig.js +1 -0
  70. package/dist/generated/RootFilesystemEntry.d.ts +13 -0
  71. package/dist/generated/RootFilesystemEntry.js +1 -0
  72. package/dist/generated/RootFilesystemEntryEncoding.d.ts +1 -0
  73. package/dist/generated/RootFilesystemEntryEncoding.js +2 -0
  74. package/dist/generated/RootFilesystemEntryKind.d.ts +1 -0
  75. package/dist/generated/RootFilesystemEntryKind.js +2 -0
  76. package/dist/generated/RootFilesystemLowerDescriptor.d.ts +7 -0
  77. package/dist/generated/RootFilesystemLowerDescriptor.js +1 -0
  78. package/dist/generated/RootFilesystemMode.d.ts +1 -0
  79. package/dist/generated/RootFilesystemMode.js +2 -0
  80. package/dist/generated/ToolLimitsConfig.d.ts +10 -0
  81. package/dist/generated/ToolLimitsConfig.js +2 -0
  82. package/dist/generated/VmDnsConfig.d.ts +6 -0
  83. package/dist/generated/VmDnsConfig.js +2 -0
  84. package/dist/generated/VmLimitsConfig.d.ts +18 -0
  85. package/dist/generated/VmLimitsConfig.js +1 -0
  86. package/dist/generated/VmListenPolicyConfig.d.ts +5 -0
  87. package/dist/generated/VmListenPolicyConfig.js +2 -0
  88. package/dist/generated/WasmLimitsConfig.d.ts +5 -0
  89. package/dist/generated/WasmLimitsConfig.js +2 -0
  90. package/dist/generated-protocol.d.ts +1037 -0
  91. package/dist/generated-protocol.js +2887 -0
  92. package/dist/index.d.ts +24 -0
  93. package/dist/index.js +24 -0
  94. package/dist/json.d.ts +2 -0
  95. package/dist/json.js +20 -0
  96. package/dist/kernel-proxy.d.ts +149 -0
  97. package/dist/kernel-proxy.js +1733 -0
  98. package/dist/native-client.d.ts +41 -0
  99. package/dist/native-client.js +124 -0
  100. package/dist/node-runtime.d.ts +443 -0
  101. package/dist/node-runtime.js +569 -0
  102. package/dist/numbers.d.ts +1 -0
  103. package/dist/numbers.js +8 -0
  104. package/dist/ownership.d.ts +18 -0
  105. package/dist/ownership.js +77 -0
  106. package/dist/permissions.d.ts +29 -0
  107. package/dist/permissions.js +68 -0
  108. package/dist/process.d.ts +35 -0
  109. package/dist/process.js +125 -0
  110. package/dist/protocol-client.d.ts +46 -0
  111. package/dist/protocol-client.js +180 -0
  112. package/dist/protocol-frames.d.ts +68 -0
  113. package/dist/protocol-frames.js +139 -0
  114. package/dist/protocol-maps.d.ts +28 -0
  115. package/dist/protocol-maps.js +217 -0
  116. package/dist/protocol-schema.d.ts +10 -0
  117. package/dist/protocol-schema.js +11 -0
  118. package/dist/request-payloads.d.ts +137 -0
  119. package/dist/request-payloads.js +210 -0
  120. package/dist/response-payloads.d.ts +107 -0
  121. package/dist/response-payloads.js +161 -0
  122. package/dist/sidecar-client.d.ts +242 -0
  123. package/dist/sidecar-client.js +797 -0
  124. package/dist/state.d.ts +40 -0
  125. package/dist/state.js +44 -0
  126. package/dist/test-runtime.d.ts +526 -0
  127. package/dist/test-runtime.js +2119 -0
  128. package/dist/vm-config.d.ts +31 -0
  129. package/dist/vm-config.js +1 -0
  130. package/fixtures/alpine-defaults.json +520 -0
  131. package/fixtures/base-filesystem.json +528 -0
  132. package/package.json +194 -0
@@ -0,0 +1,1733 @@
1
+ import { constants as osConstants } from "node:os";
2
+ import { posix as posixPath } from "node:path";
3
+ export function serializeMountConfigForSidecar(mount) {
4
+ if ("driver" in mount) {
5
+ return {
6
+ guestPath: mount.path,
7
+ readOnly: mount.readOnly ?? false,
8
+ plugin: {
9
+ id: "js_bridge",
10
+ config: {},
11
+ },
12
+ };
13
+ }
14
+ return {
15
+ guestPath: mount.path,
16
+ readOnly: mount.readOnly ?? false,
17
+ plugin: {
18
+ id: mount.plugin.id,
19
+ config: mount.plugin.config ?? {},
20
+ },
21
+ };
22
+ }
23
+ const SYNTHETIC_PID_BASE = 1_000_000;
24
+ const MISSING_EXIT_EVENT_GRACE_MS = 500;
25
+ const PROTECTED_READ_ONLY_GUEST_ROOTS = ["/etc/secure-exec"];
26
+ const TRAILING_OUTPUT_DRAIN_INTERVAL_MS = 10;
27
+ const TRAILING_OUTPUT_DRAIN_MAX_MS = 250;
28
+ const TRAILING_OUTPUT_DRAIN_QUIET_TURNS = 2;
29
+ async function drainTrailingProcessOutputTurn(delayMs = 0) {
30
+ // Native-sidecar `process_output` events can lag one macrotask behind the
31
+ // terminal `process_exited` notification for very short-lived processes, and
32
+ // under suite load the sidecar event pump can need a little extra time to
33
+ // flush delayed output through its listener callbacks.
34
+ await new Promise((resolve) => {
35
+ setTimeout(resolve, delayMs);
36
+ });
37
+ }
38
+ const PREFERRED_SIGNAL_NAMES = [
39
+ "SIGHUP",
40
+ "SIGINT",
41
+ "SIGQUIT",
42
+ "SIGILL",
43
+ "SIGTRAP",
44
+ "SIGABRT",
45
+ "SIGBUS",
46
+ "SIGFPE",
47
+ "SIGKILL",
48
+ "SIGUSR1",
49
+ "SIGSEGV",
50
+ "SIGUSR2",
51
+ "SIGPIPE",
52
+ "SIGALRM",
53
+ "SIGTERM",
54
+ "SIGSTKFLT",
55
+ "SIGCHLD",
56
+ "SIGCONT",
57
+ "SIGSTOP",
58
+ "SIGTSTP",
59
+ "SIGTTIN",
60
+ "SIGTTOU",
61
+ "SIGURG",
62
+ "SIGXCPU",
63
+ "SIGXFSZ",
64
+ "SIGVTALRM",
65
+ "SIGPROF",
66
+ "SIGWINCH",
67
+ "SIGIO",
68
+ "SIGPWR",
69
+ "SIGSYS",
70
+ "SIGEMT",
71
+ "SIGINFO",
72
+ ];
73
+ const NON_CANONICAL_SIGNAL_NAMES = new Set([
74
+ "SIGCLD",
75
+ "SIGIOT",
76
+ "SIGPOLL",
77
+ "SIGUNUSED",
78
+ ]);
79
+ const SIGNAL_NAME_BY_NUMBER = buildSignalNameByNumber();
80
+ const DOUBLE_QUOTE_ESCAPABLE_CHARACTERS = new Set(['"', "\\", "$", "`"]);
81
+ function appendDoubleQuotedEscape(current, character) {
82
+ if (DOUBLE_QUOTE_ESCAPABLE_CHARACTERS.has(character)) {
83
+ return current + character;
84
+ }
85
+ if (character === "\n") {
86
+ return current;
87
+ }
88
+ return `${current}\\${character}`;
89
+ }
90
+ function parseSimpleExecCommand(command) {
91
+ const tokens = [];
92
+ let current = "";
93
+ let quote = null;
94
+ let escaped = false;
95
+ for (const character of command) {
96
+ if (quote === null) {
97
+ if (escaped) {
98
+ current += character;
99
+ escaped = false;
100
+ continue;
101
+ }
102
+ if (character === "\\") {
103
+ escaped = true;
104
+ continue;
105
+ }
106
+ if (character === "'" || character === '"') {
107
+ quote = character;
108
+ continue;
109
+ }
110
+ if (/\s/.test(character)) {
111
+ if (current) {
112
+ tokens.push(current);
113
+ current = "";
114
+ }
115
+ continue;
116
+ }
117
+ if ("|&;<>()$`*?[]{}~!".includes(character)) {
118
+ return null;
119
+ }
120
+ current += character;
121
+ continue;
122
+ }
123
+ if (quote === "'") {
124
+ if (character === "'") {
125
+ quote = null;
126
+ continue;
127
+ }
128
+ current += character;
129
+ continue;
130
+ }
131
+ if (escaped) {
132
+ current = appendDoubleQuotedEscape(current, character);
133
+ escaped = false;
134
+ continue;
135
+ }
136
+ if (character === "\\") {
137
+ escaped = true;
138
+ continue;
139
+ }
140
+ if (character === '"') {
141
+ quote = null;
142
+ continue;
143
+ }
144
+ if (character === "$" || character === "`") {
145
+ return null;
146
+ }
147
+ current += character;
148
+ }
149
+ if (quote !== null || escaped) {
150
+ return null;
151
+ }
152
+ if (current) {
153
+ tokens.push(current);
154
+ }
155
+ if (tokens.length === 0) {
156
+ return null;
157
+ }
158
+ if (tokens.some((token) => token.length === 0)) {
159
+ return null;
160
+ }
161
+ return tokens;
162
+ }
163
+ function canUseDirectExec(driver, commandName) {
164
+ return driver === "wasmvm" || (driver === "node" && commandName === "node");
165
+ }
166
+ function shellSingleQuote(value) {
167
+ if (value.length === 0) {
168
+ return "''";
169
+ }
170
+ return `'${value.replace(/'/g, `'\"'\"'`)}'`;
171
+ }
172
+ function buildSignalNameByNumber() {
173
+ const signals = osConstants.signals;
174
+ const names = new Map();
175
+ for (const name of PREFERRED_SIGNAL_NAMES) {
176
+ const value = signals[name];
177
+ if (typeof value === "number") {
178
+ names.set(value, name);
179
+ }
180
+ }
181
+ for (const [name, value] of Object.entries(signals)) {
182
+ if (typeof value === "number" &&
183
+ !NON_CANONICAL_SIGNAL_NAMES.has(name) &&
184
+ !names.has(value)) {
185
+ names.set(value, name);
186
+ }
187
+ }
188
+ return names;
189
+ }
190
+ export function toSidecarSignalName(signal) {
191
+ return SIGNAL_NAME_BY_NUMBER.get(signal) ?? String(signal);
192
+ }
193
+ export class NativeSidecarKernelProxy {
194
+ env;
195
+ cwd;
196
+ commands;
197
+ vfs;
198
+ processes = new Map();
199
+ defaultExecCwd;
200
+ client;
201
+ session;
202
+ vm;
203
+ localMounts;
204
+ commandDrivers;
205
+ onWasmCommandResolved;
206
+ onDispose;
207
+ trackedProcesses = new Map();
208
+ trackedProcessesById = new Map();
209
+ listenerLookups = new Map();
210
+ boundUdpLookups = new Map();
211
+ signalStates = new Map();
212
+ signalRefreshes = new Map();
213
+ sidecarProcessSnapshot = [];
214
+ processSnapshotRefresh = null;
215
+ observedProcessStartTimes = new Map();
216
+ rootView;
217
+ zombieTimerCountValue = 0;
218
+ zombieTimerCountRefresh = null;
219
+ disposed = false;
220
+ pumpError = null;
221
+ nextSyntheticPid = SYNTHETIC_PID_BASE;
222
+ eventPumpAbortController = new AbortController();
223
+ eventPump;
224
+ constructor(options) {
225
+ this.client = options.client;
226
+ this.session = options.session;
227
+ this.vm = options.vm;
228
+ this.env = { ...options.env };
229
+ this.cwd = options.cwd;
230
+ this.defaultExecCwd = options.defaultExecCwd;
231
+ this.localMounts = [...options.localMounts].sort((left, right) => right.path.length - left.path.length);
232
+ this.commandDrivers = buildCommandMap(options.commandGuestPaths);
233
+ this.onWasmCommandResolved = options.onWasmCommandResolved;
234
+ this.onDispose = options.onDispose;
235
+ this.commands = this.commandDrivers;
236
+ this.vfs = this.createFilesystemView(true);
237
+ this.rootView = this.createFilesystemView(false);
238
+ this.eventPump = this.runEventPump();
239
+ void this.eventPump.catch(() => { });
240
+ }
241
+ createRootView() {
242
+ return this.rootView;
243
+ }
244
+ get zombieTimerCount() {
245
+ if (!this.zombieTimerCountRefresh) {
246
+ this.zombieTimerCountRefresh = this.refreshZombieTimerCount();
247
+ }
248
+ return this.zombieTimerCountValue;
249
+ }
250
+ registerCommandGuestPaths(commandGuestPaths) {
251
+ for (const name of commandGuestPaths.keys()) {
252
+ this.commandDrivers.set(name, "wasmvm");
253
+ }
254
+ }
255
+ async dispose() {
256
+ if (this.disposed) {
257
+ return;
258
+ }
259
+ this.disposed = true;
260
+ this.eventPumpAbortController.abort();
261
+ const liveProcesses = [...this.trackedProcesses.values()].filter((entry) => entry.exitCode === null);
262
+ await Promise.allSettled(liveProcesses.map((entry) => this.signalProcess(entry, 15)));
263
+ await this.client.disposeVm(this.session, this.vm).catch(() => { });
264
+ for (const entry of liveProcesses) {
265
+ if (entry.exitCode === null) {
266
+ // The sidecar dispose path already performs TERM/KILL escalation for any
267
+ // guest executions that are still live. Resolve local waiters eagerly so
268
+ // VM teardown does not hang on killed ACP adapter processes that never
269
+ // surface a terminal process_exited event back to the JS bridge.
270
+ this.finishProcess(entry, 143);
271
+ }
272
+ }
273
+ await this.client.dispose().catch(() => { });
274
+ await this.eventPump.catch(() => { });
275
+ await this.onDispose?.().catch(() => { });
276
+ }
277
+ async exec(command, options) {
278
+ if (!this.commands.has("sh")) {
279
+ throw new Error(`native sidecar exec requires guest shell command 'sh': ${command}`);
280
+ }
281
+ const stdoutChunks = [];
282
+ const stderrChunks = [];
283
+ const effectiveCwd = options?.cwd ?? this.defaultExecCwd ?? this.cwd;
284
+ const parsedCommand = parseSimpleExecCommand(command);
285
+ const resolveExecPath = (targetPath) => targetPath.startsWith("/")
286
+ ? posixPath.normalize(targetPath)
287
+ : posixPath.normalize(posixPath.join(effectiveCwd, targetPath));
288
+ const runAndCapture = async (proc, stdinOverride, readExitCode) => {
289
+ if (stdinOverride !== undefined) {
290
+ proc.writeStdin(stdinOverride);
291
+ }
292
+ else if (options?.stdin !== undefined) {
293
+ proc.writeStdin(options.stdin);
294
+ }
295
+ // `kernel.exec()` is a non-interactive run-to-completion API: when the
296
+ // caller does not opt into a streaming stdin handle, the guest process
297
+ // should observe EOF after any provided input so commands like
298
+ // `node -e ...` do not linger behind an inherited open stdin pipe.
299
+ proc.closeStdin();
300
+ const waitPromise = proc.wait();
301
+ const shellExitCode = typeof options?.timeout === "number"
302
+ ? await new Promise((resolve) => {
303
+ const timer = setTimeout(() => {
304
+ proc.kill(9);
305
+ void proc.wait().then(resolve);
306
+ }, options.timeout);
307
+ void waitPromise.then((code) => {
308
+ clearTimeout(timer);
309
+ resolve(code);
310
+ });
311
+ })
312
+ : await waitPromise;
313
+ const exitCode = readExitCode
314
+ ? await readExitCode().catch(() => shellExitCode)
315
+ : shellExitCode;
316
+ await drainTrailingProcessOutputTurn();
317
+ return {
318
+ exitCode,
319
+ stdout: Buffer.concat(stdoutChunks.map((chunk) => Buffer.from(chunk))).toString("utf8"),
320
+ stderr: Buffer.concat(stderrChunks.map((chunk) => Buffer.from(chunk))).toString("utf8"),
321
+ };
322
+ };
323
+ if (parsedCommand &&
324
+ (parsedCommand[0] === "sh" || parsedCommand[0] === "/bin/sh") &&
325
+ parsedCommand[1] === "-c" &&
326
+ parsedCommand.length === 3) {
327
+ const shellScript = parsedCommand[2].trim();
328
+ const exitMatch = shellScript.match(/^exit(?:\s+(-?\d+))?$/);
329
+ if (exitMatch) {
330
+ return {
331
+ exitCode: Number.parseInt(exitMatch[1] ?? "0", 10),
332
+ stdout: "",
333
+ stderr: "",
334
+ };
335
+ }
336
+ return this.exec(parsedCommand[2], options);
337
+ }
338
+ if (parsedCommand &&
339
+ parsedCommand[0] === "chmod" &&
340
+ parsedCommand.length >= 3 &&
341
+ /^[0-7]{3,4}$/.test(parsedCommand[1] ?? "")) {
342
+ const mode = Number.parseInt(parsedCommand[1], 8);
343
+ for (const target of parsedCommand.slice(2)) {
344
+ await this.client.chmod(this.session, this.vm, resolveExecPath(target), mode);
345
+ }
346
+ return { exitCode: 0, stdout: "", stderr: "" };
347
+ }
348
+ if (parsedCommand &&
349
+ parsedCommand[0] === "stat" &&
350
+ parsedCommand.length === 4 &&
351
+ parsedCommand[1] === "-c" &&
352
+ parsedCommand[2] === "%a") {
353
+ const stat = await this.stat(resolveExecPath(parsedCommand[3]));
354
+ return {
355
+ exitCode: 0,
356
+ stdout: `${(stat.mode & 0o777).toString(8)}\n`,
357
+ stderr: "",
358
+ };
359
+ }
360
+ const parsedCommandDriver = parsedCommand
361
+ ? this.commands.get(parsedCommand[0])
362
+ : undefined;
363
+ const requiresShellWrappedWasmCwd = parsedCommandDriver === "wasmvm" && parsedCommand?.[0] === "pwd";
364
+ if (parsedCommand &&
365
+ parsedCommandDriver &&
366
+ canUseDirectExec(parsedCommandDriver, parsedCommand[0]) &&
367
+ !requiresShellWrappedWasmCwd) {
368
+ if (parsedCommandDriver === "wasmvm") {
369
+ this.onWasmCommandResolved?.(parsedCommand[0]);
370
+ }
371
+ return runAndCapture(this.spawn(parsedCommand[0], parsedCommand.slice(1), {
372
+ ...options,
373
+ cwd: effectiveCwd,
374
+ onStdout: (chunk) => {
375
+ stdoutChunks.push(chunk);
376
+ options?.onStdout?.(chunk);
377
+ },
378
+ onStderr: (chunk) => {
379
+ stderrChunks.push(chunk);
380
+ options?.onStderr?.(chunk);
381
+ },
382
+ }));
383
+ }
384
+ const proc = this.spawn("sh", ["-c", command], {
385
+ ...options,
386
+ cwd: effectiveCwd,
387
+ onStdout: (chunk) => {
388
+ stdoutChunks.push(chunk);
389
+ options?.onStdout?.(chunk);
390
+ },
391
+ onStderr: (chunk) => {
392
+ stderrChunks.push(chunk);
393
+ options?.onStderr?.(chunk);
394
+ },
395
+ });
396
+ return runAndCapture(proc);
397
+ }
398
+ spawn(command, args, options) {
399
+ let spawnCommand = command;
400
+ let spawnArgs = [...args];
401
+ const shellOption = options
402
+ ?.shell;
403
+ if (shellOption === true || typeof shellOption === "string") {
404
+ // Node's shell mode hands the raw command line to the shell. Shell
405
+ // grammar belongs to the guest shell, so the bridge never parses it.
406
+ if (!this.commands.has("sh")) {
407
+ throw new Error(`native sidecar shell-mode spawn requires guest shell command 'sh': ${command}`);
408
+ }
409
+ spawnCommand = "sh";
410
+ spawnArgs = ["-c", [command, ...args].join(" ")];
411
+ }
412
+ const pid = this.nextSyntheticPid++;
413
+ const processId = `proc-${pid}`;
414
+ let resolveWait;
415
+ let rejectWait;
416
+ const waitPromise = new Promise((resolve, reject) => {
417
+ resolveWait = resolve;
418
+ rejectWait = reject;
419
+ });
420
+ const entry = {
421
+ pid,
422
+ processId,
423
+ command: spawnCommand,
424
+ args: spawnArgs,
425
+ driver: spawnCommand === "node" ? "node" : "wasmvm",
426
+ cwd: options?.cwd ?? this.cwd,
427
+ env: {
428
+ ...(options?.env ?? {}),
429
+ ...(options?.streamStdin ? { SECURE_EXEC_KEEP_STDIN_OPEN: "1" } : {}),
430
+ },
431
+ startTime: Date.now(),
432
+ exitTime: null,
433
+ hostPid: null,
434
+ exitCode: null,
435
+ started: false,
436
+ startPromise: Promise.resolve(),
437
+ waitPromise,
438
+ resolveWait,
439
+ rejectWait,
440
+ onStdout: new Set(options?.onStdout ? [options.onStdout] : []),
441
+ onStderr: new Set(options?.onStderr ? [options.onStderr] : []),
442
+ pendingStdin: [],
443
+ stdinFlushPromise: null,
444
+ pendingCloseStdin: false,
445
+ pendingKillSignal: null,
446
+ waitWithFallbackPromise: null,
447
+ hostExitObservedAt: null,
448
+ outputGeneration: 0,
449
+ };
450
+ this.trackedProcesses.set(pid, entry);
451
+ this.trackedProcessesById.set(processId, entry);
452
+ this.updateTrackedProcessSnapshot(entry);
453
+ const proc = {
454
+ pid,
455
+ writeStdin: (data) => {
456
+ if (entry.exitCode !== null) {
457
+ return;
458
+ }
459
+ entry.pendingStdin.push(data);
460
+ void this.flushPendingStdin(entry).catch((error) => {
461
+ this.handleBackgroundProcessError(entry, error);
462
+ });
463
+ },
464
+ closeStdin: () => {
465
+ entry.pendingCloseStdin = true;
466
+ void this.closeTrackedStdin(entry).catch((error) => {
467
+ this.handleBackgroundProcessError(entry, error);
468
+ });
469
+ },
470
+ kill: (signal = 15) => {
471
+ if (entry.exitCode !== null) {
472
+ return;
473
+ }
474
+ entry.pendingKillSignal = signal;
475
+ void entry.startPromise.then(async () => {
476
+ if (entry.exitCode !== null || entry.pendingKillSignal === null) {
477
+ return;
478
+ }
479
+ const pendingSignal = entry.pendingKillSignal;
480
+ entry.pendingKillSignal = null;
481
+ await this.signalProcess(entry, pendingSignal);
482
+ });
483
+ },
484
+ wait: async () => {
485
+ const exitCode = await this.waitForTrackedProcess(entry);
486
+ await this.drainTrailingProcessOutput(entry);
487
+ return exitCode;
488
+ },
489
+ get exitCode() {
490
+ return entry.exitCode;
491
+ },
492
+ };
493
+ entry.startPromise = this.startTrackedProcess(entry).catch((error) => {
494
+ const normalized = error instanceof Error ? error : new Error(String(error));
495
+ const stderr = new TextEncoder().encode(`${normalized.message}\n`);
496
+ for (const handler of entry.onStderr) {
497
+ handler(stderr);
498
+ }
499
+ this.finishProcess(entry, 1);
500
+ });
501
+ return proc;
502
+ }
503
+ openShell(options) {
504
+ const stdoutHandlers = new Set();
505
+ const stderrHandlers = new Set();
506
+ const command = options?.command ?? "sh";
507
+ const args = options?.args ??
508
+ (command === "sh" || command === "/bin/sh" ? ["-i"] : []);
509
+ const synthesizePrompt = !options?.command && !options?.args;
510
+ const autoCloseExplicitCommandStdin = Boolean(options?.command) &&
511
+ !["sh", "/bin/sh", "bash"].includes(command);
512
+ const promptText = "sh-0.4$ ";
513
+ const textEncoder = new TextEncoder();
514
+ const textDecoder = new TextDecoder();
515
+ const execCommand = this.exec.bind(this);
516
+ const spawnCommand = this.spawn.bind(this);
517
+ const sanitizeSyntheticShellText = (value) => value
518
+ .replace(/\u001b\[[0-9;]*m/g, "")
519
+ .replace(/^.*WARN could not retrieve pid for child process\n?/gm, "")
520
+ .replace(/^ProcessExitError:.*\n(?:\s+at .*\n)*/gm, "");
521
+ let bufferedInput = "";
522
+ let bufferedCommand = "";
523
+ let activeForegroundProcess = null;
524
+ let shellEnv = { ...(options?.env ?? {}) };
525
+ let shellCwd = options?.cwd ?? this.cwd;
526
+ let syntheticCommandQueue = Promise.resolve();
527
+ let promptTimer = null;
528
+ let closeStdinTimer = null;
529
+ let commandInFlight = false;
530
+ let syntheticCursorAtLineStart = true;
531
+ const syntheticPid = this.nextSyntheticPid++;
532
+ let syntheticExitCode = null;
533
+ let resolveSyntheticWait;
534
+ const syntheticWaitPromise = new Promise((resolve) => {
535
+ resolveSyntheticWait = resolve;
536
+ });
537
+ const clearPromptTimer = () => {
538
+ if (promptTimer !== null) {
539
+ clearTimeout(promptTimer);
540
+ promptTimer = null;
541
+ }
542
+ };
543
+ const clearCloseStdinTimer = () => {
544
+ if (closeStdinTimer !== null) {
545
+ clearTimeout(closeStdinTimer);
546
+ closeStdinTimer = null;
547
+ }
548
+ };
549
+ const normalizeSyntheticTerminalText = (text) => text.replace(/\r?\n/g, "\r\n");
550
+ const updateSyntheticCursor = (text) => {
551
+ if (!text) {
552
+ return;
553
+ }
554
+ syntheticCursorAtLineStart = /(?:\r\n)$/.test(text);
555
+ };
556
+ const emitSyntheticStdout = (text) => {
557
+ if (!text) {
558
+ return;
559
+ }
560
+ const normalized = normalizeSyntheticTerminalText(text);
561
+ updateSyntheticCursor(normalized);
562
+ const chunk = textEncoder.encode(normalized);
563
+ for (const handler of stdoutHandlers) {
564
+ handler(chunk);
565
+ }
566
+ };
567
+ const emitSyntheticTerminal = (text) => {
568
+ if (!text) {
569
+ return;
570
+ }
571
+ const normalized = normalizeSyntheticTerminalText(text);
572
+ updateSyntheticCursor(normalized);
573
+ const chunk = textEncoder.encode(normalized);
574
+ for (const handler of stdoutHandlers) {
575
+ handler(chunk);
576
+ }
577
+ };
578
+ const finishSyntheticShell = (exitCode) => {
579
+ if (syntheticExitCode !== null) {
580
+ return;
581
+ }
582
+ syntheticExitCode = exitCode;
583
+ clearPromptTimer();
584
+ resolveSyntheticWait(exitCode);
585
+ };
586
+ const commandNeedsContinuation = (source) => {
587
+ let singleQuoted = false;
588
+ let doubleQuoted = false;
589
+ let escaped = false;
590
+ for (const character of source) {
591
+ if (escaped) {
592
+ escaped = false;
593
+ continue;
594
+ }
595
+ if (character === "\\") {
596
+ escaped = true;
597
+ continue;
598
+ }
599
+ if (!doubleQuoted && character === "'") {
600
+ singleQuoted = !singleQuoted;
601
+ continue;
602
+ }
603
+ if (!singleQuoted && character === '"') {
604
+ doubleQuoted = !doubleQuoted;
605
+ }
606
+ }
607
+ return singleQuoted || doubleQuoted || escaped;
608
+ };
609
+ const emitPrompt = () => {
610
+ if (!synthesizePrompt) {
611
+ return;
612
+ }
613
+ if (syntheticExitCode !== null) {
614
+ return;
615
+ }
616
+ commandInFlight = false;
617
+ const promptPrefix = syntheticCursorAtLineStart ? "" : "\r\n";
618
+ const promptChunk = textEncoder.encode(`${promptPrefix}${promptText}`);
619
+ for (const handler of stdoutHandlers) {
620
+ handler(promptChunk);
621
+ }
622
+ syntheticCursorAtLineStart = false;
623
+ };
624
+ const schedulePrompt = (delayMs) => {
625
+ if (!synthesizePrompt) {
626
+ return;
627
+ }
628
+ clearPromptTimer();
629
+ promptTimer = setTimeout(() => {
630
+ promptTimer = null;
631
+ emitPrompt();
632
+ }, delayMs);
633
+ };
634
+ const parseForegroundCommand = (source) => {
635
+ const parsed = parseSimpleExecCommand(source);
636
+ const driver = parsed ? this.commands.get(parsed[0]) : undefined;
637
+ if (!parsed ||
638
+ !canUseDirectExec(driver, parsed[0]) ||
639
+ (driver === "wasmvm" && parsed[0] === "pwd")) {
640
+ return null;
641
+ }
642
+ return parsed;
643
+ };
644
+ const writeForegroundInput = (proc, data) => {
645
+ if (typeof data === "string") {
646
+ for (const character of data) {
647
+ proc.writeStdin(character);
648
+ }
649
+ return;
650
+ }
651
+ for (const byte of data) {
652
+ proc.writeStdin(new Uint8Array([byte]));
653
+ }
654
+ };
655
+ let onData = null;
656
+ stdoutHandlers.add((data) => onData?.(data));
657
+ if (options?.onStderr) {
658
+ stderrHandlers.add(options.onStderr);
659
+ }
660
+ if (synthesizePrompt) {
661
+ schedulePrompt(0);
662
+ return {
663
+ pid: syntheticPid,
664
+ write(data) {
665
+ if (syntheticExitCode !== null) {
666
+ return;
667
+ }
668
+ if (activeForegroundProcess) {
669
+ const rawText = typeof data === "string"
670
+ ? data
671
+ : Buffer.from(data).toString("utf8");
672
+ if (rawText.includes("\u0003")) {
673
+ const [beforeInterrupt] = rawText.split("\u0003");
674
+ if (beforeInterrupt) {
675
+ writeForegroundInput(activeForegroundProcess, beforeInterrupt);
676
+ }
677
+ emitSyntheticTerminal("^C\n");
678
+ activeForegroundProcess.kill(2);
679
+ return;
680
+ }
681
+ writeForegroundInput(activeForegroundProcess, data);
682
+ return;
683
+ }
684
+ const rawText = typeof data === "string"
685
+ ? data
686
+ : Buffer.from(data).toString("utf8");
687
+ let text = rawText;
688
+ if (rawText.includes("\u0003")) {
689
+ const segments = rawText.split("\u0003");
690
+ bufferedInput = "";
691
+ bufferedCommand = "";
692
+ for (let index = 0; index < segments.length - 1; index += 1) {
693
+ emitSyntheticTerminal("^C\n");
694
+ emitPrompt();
695
+ }
696
+ text = segments[segments.length - 1] ?? "";
697
+ }
698
+ if (text.includes("\u0004") &&
699
+ bufferedInput.length === 0 &&
700
+ bufferedCommand.length === 0) {
701
+ finishSyntheticShell(0);
702
+ return;
703
+ }
704
+ bufferedInput += text.replace(/\u0004/g, "");
705
+ while (true) {
706
+ const newlineIndex = bufferedInput.indexOf("\n");
707
+ if (newlineIndex < 0) {
708
+ break;
709
+ }
710
+ const line = bufferedInput.slice(0, newlineIndex).replace(/\r$/, "");
711
+ bufferedInput = bufferedInput.slice(newlineIndex + 1);
712
+ emitSyntheticStdout(`${line}\n`);
713
+ const nextCommand = bufferedCommand
714
+ ? `${bufferedCommand}\n${line}`
715
+ : line;
716
+ if (commandNeedsContinuation(nextCommand)) {
717
+ bufferedCommand = nextCommand;
718
+ continue;
719
+ }
720
+ bufferedCommand = "";
721
+ syntheticCommandQueue = syntheticCommandQueue
722
+ .then(async () => {
723
+ const trimmed = nextCommand.trim();
724
+ if (!trimmed) {
725
+ emitPrompt();
726
+ return;
727
+ }
728
+ const exitMatch = trimmed.match(/^exit(?:\s+(-?\d+))?$/);
729
+ if (exitMatch) {
730
+ finishSyntheticShell(Number.parseInt(exitMatch[1] ?? "0", 10));
731
+ return;
732
+ }
733
+ const exportMatch = trimmed.match(/^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
734
+ if (exportMatch) {
735
+ shellEnv = {
736
+ ...shellEnv,
737
+ [exportMatch[1]]: exportMatch[2],
738
+ };
739
+ emitPrompt();
740
+ return;
741
+ }
742
+ const cdMatch = trimmed.match(/^cd(?:\s+(.*))?$/);
743
+ if (cdMatch) {
744
+ const target = cdMatch[1]?.trim() || "/";
745
+ shellCwd = target.startsWith("/")
746
+ ? posixPath.normalize(target)
747
+ : posixPath.normalize(posixPath.join(shellCwd, target));
748
+ emitPrompt();
749
+ return;
750
+ }
751
+ const foregroundCommand = parseForegroundCommand(trimmed);
752
+ if (foregroundCommand) {
753
+ const proc = spawnCommand(foregroundCommand[0], foregroundCommand.slice(1), {
754
+ env: shellEnv,
755
+ cwd: shellCwd,
756
+ streamStdin: true,
757
+ onStdout: (chunk) => emitSyntheticTerminal(textDecoder.decode(chunk)),
758
+ onStderr: (chunk) => emitSyntheticTerminal(textDecoder.decode(chunk)),
759
+ });
760
+ activeForegroundProcess = proc;
761
+ try {
762
+ await proc.wait();
763
+ }
764
+ finally {
765
+ if (activeForegroundProcess === proc) {
766
+ activeForegroundProcess = null;
767
+ }
768
+ }
769
+ emitPrompt();
770
+ return;
771
+ }
772
+ const result = await execCommand(nextCommand, {
773
+ env: shellEnv,
774
+ cwd: shellCwd,
775
+ });
776
+ const sanitizedStdout = sanitizeSyntheticShellText(result.stdout);
777
+ if (sanitizedStdout) {
778
+ emitSyntheticStdout(sanitizedStdout);
779
+ }
780
+ const sanitizedStderr = sanitizeSyntheticShellText(result.stderr).replace(/^error: failed to execute command '([^']+)': .*$/gm, "error: command not found: $1");
781
+ if (sanitizedStderr) {
782
+ emitSyntheticTerminal(sanitizedStderr);
783
+ }
784
+ emitPrompt();
785
+ })
786
+ .catch((error) => {
787
+ const message = error instanceof Error ? error.message : String(error);
788
+ emitSyntheticTerminal(`${message}\n`);
789
+ emitPrompt();
790
+ });
791
+ }
792
+ },
793
+ get onData() {
794
+ return onData;
795
+ },
796
+ set onData(handler) {
797
+ onData = handler;
798
+ },
799
+ resize() {
800
+ // Synthetic shells are terminal-less.
801
+ },
802
+ kill(signal = 15) {
803
+ finishSyntheticShell(128 + signal);
804
+ },
805
+ wait() {
806
+ return syntheticWaitPromise;
807
+ },
808
+ };
809
+ }
810
+ const proc = this.spawn(command, args, {
811
+ env: options?.env,
812
+ cwd: options?.cwd,
813
+ streamStdin: true,
814
+ onStdout: (chunk) => {
815
+ for (const handler of stdoutHandlers) {
816
+ handler(chunk);
817
+ }
818
+ if (commandInFlight) {
819
+ schedulePrompt(120);
820
+ }
821
+ },
822
+ onStderr: (chunk) => {
823
+ for (const handler of stderrHandlers) {
824
+ handler(chunk);
825
+ }
826
+ if (commandInFlight) {
827
+ schedulePrompt(120);
828
+ }
829
+ },
830
+ });
831
+ return {
832
+ pid: proc.pid,
833
+ write(data) {
834
+ if (synthesizePrompt) {
835
+ return;
836
+ }
837
+ proc.writeStdin(data);
838
+ if (autoCloseExplicitCommandStdin) {
839
+ clearCloseStdinTimer();
840
+ closeStdinTimer = setTimeout(() => {
841
+ closeStdinTimer = null;
842
+ proc.closeStdin();
843
+ }, 100);
844
+ }
845
+ if (synthesizePrompt &&
846
+ typeof data === "string" &&
847
+ (data.includes("\n") || data.includes("\r"))) {
848
+ commandInFlight = true;
849
+ schedulePrompt(120);
850
+ }
851
+ },
852
+ get onData() {
853
+ return onData;
854
+ },
855
+ set onData(handler) {
856
+ onData = handler;
857
+ },
858
+ resize() {
859
+ // The current stdio-native path is process-backed rather than PTY-backed.
860
+ },
861
+ kill(signal) {
862
+ clearCloseStdinTimer();
863
+ clearPromptTimer();
864
+ proc.kill(signal);
865
+ },
866
+ wait() {
867
+ clearPromptTimer();
868
+ return proc.wait();
869
+ },
870
+ };
871
+ }
872
+ async connectTerminal(options) {
873
+ const stdin = process.stdin;
874
+ const stdout = process.stdout;
875
+ const { onData, ...shellOptions } = options ?? {};
876
+ const shell = this.openShell({
877
+ ...shellOptions,
878
+ onStderr: shellOptions.onStderr ??
879
+ ((data) => {
880
+ process.stderr.write(data);
881
+ }),
882
+ });
883
+ const outputHandler = onData ??
884
+ ((data) => {
885
+ stdout.write(data);
886
+ });
887
+ const restoreRawMode = stdin.isTTY && typeof stdin.setRawMode === "function";
888
+ const onStdinData = (data) => {
889
+ shell.write(data);
890
+ };
891
+ const onResize = () => {
892
+ shell.resize(stdout.columns, stdout.rows);
893
+ };
894
+ let cleanedUp = false;
895
+ const cleanup = () => {
896
+ if (cleanedUp) {
897
+ return;
898
+ }
899
+ cleanedUp = true;
900
+ stdin.removeListener("data", onStdinData);
901
+ stdin.pause();
902
+ if (restoreRawMode) {
903
+ stdin.setRawMode(false);
904
+ }
905
+ if (stdout.isTTY) {
906
+ stdout.removeListener("resize", onResize);
907
+ }
908
+ };
909
+ try {
910
+ if (restoreRawMode) {
911
+ stdin.setRawMode(true);
912
+ }
913
+ stdin.on("data", onStdinData);
914
+ stdin.resume();
915
+ shell.onData = outputHandler;
916
+ if (stdout.isTTY) {
917
+ stdout.on("resize", onResize);
918
+ shell.resize(stdout.columns, stdout.rows);
919
+ }
920
+ }
921
+ catch (error) {
922
+ cleanup();
923
+ shell.kill();
924
+ throw error;
925
+ }
926
+ void shell.wait().finally(() => {
927
+ cleanup();
928
+ });
929
+ return shell.pid;
930
+ }
931
+ readFile(path) {
932
+ return this.dispatchRead(path, (mount, relativePath) => mount.fs.readFile(relativePath));
933
+ }
934
+ writeFile(path, content) {
935
+ return this.dispatchWrite(path, (mount, relativePath) => mount.fs.writeFile(relativePath, content), () => this.client.writeFile(this.session, this.vm, path, content));
936
+ }
937
+ async mkdir(path, recursive = true) {
938
+ return this.dispatchWrite(path, (mount, relativePath) => mount.fs.mkdir(relativePath, { recursive }), () => this.client.mkdir(this.session, this.vm, path, { recursive }));
939
+ }
940
+ async exists(path) {
941
+ const local = this.resolveLocalMount(path);
942
+ if (local) {
943
+ return local.mount.fs.exists(local.relativePath);
944
+ }
945
+ return this.client.exists(this.session, this.vm, path);
946
+ }
947
+ async stat(path) {
948
+ const local = this.resolveLocalMount(path);
949
+ if (local) {
950
+ return local.mount.fs.stat(local.relativePath);
951
+ }
952
+ return toVirtualStat(await this.client.stat(this.session, this.vm, path));
953
+ }
954
+ async readdir(path) {
955
+ const local = this.resolveLocalMount(path);
956
+ if (local) {
957
+ return local.mount.fs.readDir(local.relativePath);
958
+ }
959
+ const entries = await this.client.readdir(this.session, this.vm, path);
960
+ return [...new Set([...entries, ...this.mountedChildNames(path)])].sort((a, b) => a.localeCompare(b));
961
+ }
962
+ async removeFile(path) {
963
+ return this.dispatchWrite(path, (mount, relativePath) => mount.fs.removeFile(relativePath), () => this.client.removeFile(this.session, this.vm, path));
964
+ }
965
+ async removeDir(path) {
966
+ return this.dispatchWrite(path, (mount, relativePath) => mount.fs.removeDir(relativePath), () => this.client.removeDir(this.session, this.vm, path));
967
+ }
968
+ async rename(oldPath, newPath) {
969
+ const from = this.resolveLocalMount(oldPath);
970
+ const to = this.resolveLocalMount(newPath);
971
+ if (!!from !== !!to) {
972
+ throw errnoError("EXDEV", "cross-device link not permitted");
973
+ }
974
+ if (from && to) {
975
+ if (from.mount.path !== to.mount.path) {
976
+ throw errnoError("EXDEV", "cross-device link not permitted");
977
+ }
978
+ this.assertLocalWritable(from.mount);
979
+ return from.mount.fs.rename(from.relativePath, to.relativePath);
980
+ }
981
+ return this.client.rename(this.session, this.vm, oldPath, newPath);
982
+ }
983
+ mountFs(path, driver, options) {
984
+ this.localMounts.unshift({
985
+ path: posixPath.normalize(path),
986
+ fs: driver,
987
+ readOnly: options?.readOnly ?? false,
988
+ });
989
+ this.localMounts.sort((left, right) => right.path.length - left.path.length);
990
+ }
991
+ unmountFs(path) {
992
+ const normalized = posixPath.normalize(path);
993
+ const index = this.localMounts.findIndex((mount) => mount.path === normalized);
994
+ if (index >= 0) {
995
+ this.localMounts.splice(index, 1);
996
+ }
997
+ }
998
+ snapshotProcesses() {
999
+ return this.buildProcessSnapshot();
1000
+ }
1001
+ findListener(request) {
1002
+ const key = socketLookupKey("listener", request);
1003
+ const cached = this.listenerLookups.get(key);
1004
+ if (!cached?.pending) {
1005
+ this.listenerLookups.set(key, {
1006
+ value: cached?.value ?? null,
1007
+ pending: this.refreshSocketLookup(this.listenerLookups, key, () => this.client.findListener(this.session, this.vm, request)),
1008
+ });
1009
+ }
1010
+ return this.listenerLookups.get(key)?.value ?? null;
1011
+ }
1012
+ findBoundUdp(request) {
1013
+ const key = socketLookupKey("udp", request);
1014
+ const cached = this.boundUdpLookups.get(key);
1015
+ if (!cached?.pending) {
1016
+ this.boundUdpLookups.set(key, {
1017
+ value: cached?.value ?? null,
1018
+ pending: this.refreshSocketLookup(this.boundUdpLookups, key, () => this.client.findBoundUdp(this.session, this.vm, request)),
1019
+ });
1020
+ }
1021
+ return this.boundUdpLookups.get(key)?.value ?? null;
1022
+ }
1023
+ /**
1024
+ * Drive an HTTP request to a guest-bound TCP port inside the VM and return
1025
+ * the raw JSON response payload from the sidecar. The caller decodes the
1026
+ * JSON into a structured response.
1027
+ */
1028
+ async vmFetch(request) {
1029
+ return this.client.vmFetch(this.session, this.vm, request);
1030
+ }
1031
+ getSignalState(pid) {
1032
+ const entry = this.trackedProcesses.get(pid);
1033
+ if (entry && !this.signalRefreshes.has(pid)) {
1034
+ this.signalRefreshes.set(pid, this.refreshSignalState(entry));
1035
+ }
1036
+ return this.signalStates.get(pid) ?? { handlers: new Map() };
1037
+ }
1038
+ async refreshSocketLookup(cache, key, lookup) {
1039
+ try {
1040
+ const socket = await lookup();
1041
+ cache.set(key, {
1042
+ value: socket ? toKernelSocketSnapshot(socket) : null,
1043
+ pending: null,
1044
+ });
1045
+ }
1046
+ catch {
1047
+ cache.set(key, {
1048
+ value: cache.get(key)?.value ?? null,
1049
+ pending: null,
1050
+ });
1051
+ }
1052
+ }
1053
+ async refreshSignalState(entry) {
1054
+ try {
1055
+ const signalState = await this.client.getSignalState(this.session, this.vm, entry.processId);
1056
+ this.signalStates.set(entry.pid, toKernelSignalState(signalState.handlers));
1057
+ }
1058
+ catch {
1059
+ this.signalStates.set(entry.pid, this.signalStates.get(entry.pid) ?? { handlers: new Map() });
1060
+ }
1061
+ finally {
1062
+ this.signalRefreshes.delete(entry.pid);
1063
+ }
1064
+ }
1065
+ async refreshProcessSnapshot() {
1066
+ if (this.processSnapshotRefresh) {
1067
+ await this.processSnapshotRefresh;
1068
+ return;
1069
+ }
1070
+ this.processSnapshotRefresh = (async () => {
1071
+ try {
1072
+ this.sidecarProcessSnapshot = await this.client.getProcessSnapshot(this.session, this.vm);
1073
+ }
1074
+ finally {
1075
+ this.processSnapshotRefresh = null;
1076
+ }
1077
+ })();
1078
+ await this.processSnapshotRefresh;
1079
+ }
1080
+ async refreshZombieTimerCount() {
1081
+ try {
1082
+ const snapshot = await this.client.getZombieTimerCount(this.session, this.vm);
1083
+ this.zombieTimerCountValue = snapshot.count;
1084
+ }
1085
+ catch {
1086
+ // Keep the last known value if the sidecar query fails.
1087
+ }
1088
+ finally {
1089
+ this.zombieTimerCountRefresh = null;
1090
+ }
1091
+ }
1092
+ async drainTrailingProcessOutput(entry) {
1093
+ if (entry.onStdout.size === 0 && entry.onStderr.size === 0) {
1094
+ return;
1095
+ }
1096
+ let observedGeneration = entry.outputGeneration;
1097
+ let quietTurns = 0;
1098
+ let delayMs = 0;
1099
+ const deadline = Date.now() + TRAILING_OUTPUT_DRAIN_MAX_MS;
1100
+ while (quietTurns < TRAILING_OUTPUT_DRAIN_QUIET_TURNS) {
1101
+ const remainingMs = deadline - Date.now();
1102
+ if (remainingMs <= 0) {
1103
+ return;
1104
+ }
1105
+ await drainTrailingProcessOutputTurn(Math.min(delayMs, remainingMs));
1106
+ if (entry.outputGeneration === observedGeneration) {
1107
+ quietTurns += 1;
1108
+ }
1109
+ else {
1110
+ observedGeneration = entry.outputGeneration;
1111
+ quietTurns = 0;
1112
+ }
1113
+ delayMs = TRAILING_OUTPUT_DRAIN_INTERVAL_MS;
1114
+ }
1115
+ }
1116
+ async startTrackedProcess(entry) {
1117
+ const started = await this.client.execute(this.session, this.vm, {
1118
+ processId: entry.processId,
1119
+ command: entry.command,
1120
+ args: entry.args,
1121
+ env: entry.env,
1122
+ cwd: entry.cwd,
1123
+ });
1124
+ entry.hostPid = started.pid;
1125
+ entry.started = true;
1126
+ this.updateTrackedProcessSnapshot(entry);
1127
+ void this.refreshProcessSnapshot().catch(() => { });
1128
+ await this.refreshSignalState(entry);
1129
+ void this.flushPendingStdin(entry).catch((error) => {
1130
+ this.handleBackgroundProcessError(entry, error);
1131
+ });
1132
+ void this.closeTrackedStdin(entry).catch((error) => {
1133
+ this.handleBackgroundProcessError(entry, error);
1134
+ });
1135
+ if (entry.pendingKillSignal !== null) {
1136
+ const signal = entry.pendingKillSignal;
1137
+ entry.pendingKillSignal = null;
1138
+ await this.signalProcess(entry, signal);
1139
+ }
1140
+ }
1141
+ async runEventPump() {
1142
+ while (!this.disposed) {
1143
+ try {
1144
+ const event = await this.client.waitForEvent({ any: true }, undefined, {
1145
+ signal: this.eventPumpAbortController.signal,
1146
+ });
1147
+ if (event.payload.type === "process_output") {
1148
+ const entry = this.trackedProcessesById.get(event.payload.process_id);
1149
+ if (!entry) {
1150
+ continue;
1151
+ }
1152
+ entry.outputGeneration += 1;
1153
+ void this.refreshProcessSnapshot().catch(() => { });
1154
+ if (!this.signalRefreshes.has(entry.pid)) {
1155
+ this.signalRefreshes.set(entry.pid, this.refreshSignalState(entry));
1156
+ await this.signalRefreshes.get(entry.pid);
1157
+ }
1158
+ const chunk = event.payload.chunk;
1159
+ const listeners = event.payload.channel === "stdout"
1160
+ ? entry.onStdout
1161
+ : entry.onStderr;
1162
+ for (const listener of listeners) {
1163
+ listener(chunk);
1164
+ }
1165
+ continue;
1166
+ }
1167
+ if (event.payload.type === "process_exited") {
1168
+ const entry = this.trackedProcessesById.get(event.payload.process_id);
1169
+ if (!entry) {
1170
+ continue;
1171
+ }
1172
+ void this.refreshProcessSnapshot().catch(() => { });
1173
+ this.signalRefreshes.delete(entry.pid);
1174
+ this.finishProcess(entry, event.payload.exit_code);
1175
+ }
1176
+ }
1177
+ catch (error) {
1178
+ if (this.disposed) {
1179
+ return;
1180
+ }
1181
+ this.pumpError =
1182
+ error instanceof Error ? error : new Error(String(error));
1183
+ for (const entry of this.trackedProcesses.values()) {
1184
+ if (entry.exitCode !== null) {
1185
+ continue;
1186
+ }
1187
+ const stderr = new TextEncoder().encode(`${this.pumpError.message}\n`);
1188
+ for (const listener of entry.onStderr) {
1189
+ listener(stderr);
1190
+ }
1191
+ this.finishProcess(entry, 1);
1192
+ }
1193
+ return;
1194
+ }
1195
+ }
1196
+ }
1197
+ finishProcess(entry, exitCode) {
1198
+ if (entry.exitCode !== null) {
1199
+ return;
1200
+ }
1201
+ entry.exitCode = exitCode;
1202
+ entry.exitTime = Date.now();
1203
+ this.updateTrackedProcessSnapshot(entry);
1204
+ entry.resolveWait(exitCode);
1205
+ }
1206
+ waitForTrackedProcess(entry) {
1207
+ if (entry.exitCode !== null) {
1208
+ return Promise.resolve(entry.exitCode);
1209
+ }
1210
+ if (entry.waitWithFallbackPromise !== null) {
1211
+ return entry.waitWithFallbackPromise;
1212
+ }
1213
+ entry.waitWithFallbackPromise = (async () => {
1214
+ await entry.startPromise.catch(() => { });
1215
+ while (entry.exitCode === null && !this.disposed) {
1216
+ const maybeExit = await Promise.race([
1217
+ entry.waitPromise.then((exitCode) => exitCode),
1218
+ new Promise((resolve) => setTimeout(() => resolve(null), 50)),
1219
+ ]);
1220
+ if (maybeExit !== null) {
1221
+ return maybeExit;
1222
+ }
1223
+ try {
1224
+ await this.refreshProcessSnapshot();
1225
+ const snapshot = this.sidecarProcessSnapshot.find((candidate) => candidate.processId === entry.processId);
1226
+ if (snapshot?.status === "exited") {
1227
+ this.finishProcess(entry, snapshot.exitCode ?? 0);
1228
+ break;
1229
+ }
1230
+ if (snapshot) {
1231
+ entry.hostExitObservedAt = null;
1232
+ continue;
1233
+ }
1234
+ // Fast guest processes can exit before the sidecar emits a
1235
+ // `process_exited` event. Once a started process disappears from the
1236
+ // authoritative VM snapshot for a full grace window, treat it as
1237
+ // reaped even if the `pid` returned at launch was only a kernel/shared
1238
+ // runtime identifier rather than a probeable host PID.
1239
+ if (!snapshot) {
1240
+ const now = Date.now();
1241
+ if (entry.hostExitObservedAt === null) {
1242
+ entry.hostExitObservedAt = now;
1243
+ continue;
1244
+ }
1245
+ if (now - entry.hostExitObservedAt >= MISSING_EXIT_EVENT_GRACE_MS) {
1246
+ this.finishProcess(entry, 0);
1247
+ break;
1248
+ }
1249
+ continue;
1250
+ }
1251
+ }
1252
+ catch {
1253
+ // Fall back to the next wait interval if the sidecar snapshot query fails.
1254
+ }
1255
+ }
1256
+ return entry.waitPromise;
1257
+ })().finally(() => {
1258
+ entry.waitWithFallbackPromise = null;
1259
+ });
1260
+ return entry.waitWithFallbackPromise;
1261
+ }
1262
+ async signalProcess(entry, signal) {
1263
+ try {
1264
+ await this.client.killProcess(this.session, this.vm, entry.processId, toSidecarSignalName(signal));
1265
+ }
1266
+ catch (error) {
1267
+ if (isNoSuchProcessError(error) || isUnknownVmError(error)) {
1268
+ return;
1269
+ }
1270
+ throw error;
1271
+ }
1272
+ }
1273
+ flushPendingStdin(entry) {
1274
+ if (entry.stdinFlushPromise !== null) {
1275
+ return entry.stdinFlushPromise;
1276
+ }
1277
+ entry.stdinFlushPromise = entry.startPromise
1278
+ .then(async () => {
1279
+ if (entry.exitCode !== null) {
1280
+ return;
1281
+ }
1282
+ while (entry.pendingStdin.length > 0) {
1283
+ const chunk = entry.pendingStdin.shift();
1284
+ if (chunk === undefined) {
1285
+ break;
1286
+ }
1287
+ await this.client.writeStdin(this.session, this.vm, entry.processId, chunk);
1288
+ }
1289
+ })
1290
+ .catch((error) => {
1291
+ if (isNoSuchProcessError(error) || isUnknownVmError(error)) {
1292
+ return;
1293
+ }
1294
+ throw error;
1295
+ })
1296
+ .finally(() => {
1297
+ entry.stdinFlushPromise = null;
1298
+ if (entry.pendingStdin.length > 0 && entry.exitCode === null) {
1299
+ void this.flushPendingStdin(entry).catch((error) => {
1300
+ this.handleBackgroundProcessError(entry, error);
1301
+ });
1302
+ }
1303
+ });
1304
+ return entry.stdinFlushPromise;
1305
+ }
1306
+ async closeTrackedStdin(entry) {
1307
+ await entry.startPromise;
1308
+ await this.flushPendingStdin(entry);
1309
+ if (entry.exitCode !== null || !entry.pendingCloseStdin) {
1310
+ return;
1311
+ }
1312
+ entry.pendingCloseStdin = false;
1313
+ try {
1314
+ await this.client.closeStdin(this.session, this.vm, entry.processId);
1315
+ }
1316
+ catch (error) {
1317
+ if (isNoSuchProcessError(error) || isUnknownVmError(error)) {
1318
+ return;
1319
+ }
1320
+ throw error;
1321
+ }
1322
+ }
1323
+ handleBackgroundProcessError(entry, error) {
1324
+ if (this.disposed || isNoSuchProcessError(error) || isUnknownVmError(error)) {
1325
+ return;
1326
+ }
1327
+ if (entry.exitCode !== null) {
1328
+ this.recordCompletedProcessError(entry, error);
1329
+ return;
1330
+ }
1331
+ this.emitBackgroundProcessError(entry, error);
1332
+ this.finishProcess(entry, 1);
1333
+ }
1334
+ recordCompletedProcessError(entry, error) {
1335
+ if (this.disposed || isNoSuchProcessError(error) || isUnknownVmError(error)) {
1336
+ return entry.exitCode ?? 1;
1337
+ }
1338
+ this.emitBackgroundProcessError(entry, error);
1339
+ entry.exitCode =
1340
+ entry.exitCode === null || entry.exitCode === 0 ? 1 : entry.exitCode;
1341
+ entry.exitTime ??= Date.now();
1342
+ this.updateTrackedProcessSnapshot(entry);
1343
+ return entry.exitCode;
1344
+ }
1345
+ emitBackgroundProcessError(entry, error) {
1346
+ const normalized = error instanceof Error ? error : new Error(String(error));
1347
+ const stderr = new TextEncoder().encode(`${normalized.message}\n`);
1348
+ for (const handler of entry.onStderr) {
1349
+ handler(stderr);
1350
+ }
1351
+ }
1352
+ createFilesystemView(includeLocalMounts) {
1353
+ return {
1354
+ readFile: (path) => this.dispatchRead(path, (mount, relativePath) => mount.fs.readFile(relativePath), includeLocalMounts),
1355
+ readTextFile: async (path) => new TextDecoder().decode(await this.dispatchRead(path, (mount, relativePath) => mount.fs.readFile(relativePath), includeLocalMounts)),
1356
+ readDir: async (path) => {
1357
+ const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1358
+ if (local) {
1359
+ return local.mount.fs.readDir(local.relativePath);
1360
+ }
1361
+ const entries = await this.client.readdir(this.session, this.vm, path);
1362
+ return includeLocalMounts
1363
+ ? [...new Set([...entries, ...this.mountedChildNames(path)])].sort((a, b) => a.localeCompare(b))
1364
+ : entries;
1365
+ },
1366
+ readDirWithTypes: async (path) => {
1367
+ const entries = await this.createFilesystemView(includeLocalMounts).readDir(path);
1368
+ return Promise.all(entries.map(async (name) => {
1369
+ const stat = await this.createFilesystemView(includeLocalMounts).lstat(posixPath.join(path, name));
1370
+ return {
1371
+ name,
1372
+ isDirectory: stat.isDirectory,
1373
+ isSymbolicLink: stat.isSymbolicLink,
1374
+ };
1375
+ }));
1376
+ },
1377
+ writeFile: (path, content) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.writeFile(relativePath, content), () => this.client.writeFile(this.session, this.vm, path, content), includeLocalMounts),
1378
+ createDir: (path) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.createDir(relativePath), async () => {
1379
+ try {
1380
+ await this.client.mkdir(this.session, this.vm, path, {
1381
+ recursive: false,
1382
+ });
1383
+ }
1384
+ catch (error) {
1385
+ if (!isAlreadyExistsError(error)) {
1386
+ throw error;
1387
+ }
1388
+ }
1389
+ }, includeLocalMounts),
1390
+ mkdir: (path, options) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.mkdir(relativePath, {
1391
+ recursive: options?.recursive ?? true,
1392
+ }), () => this.client.mkdir(this.session, this.vm, path, {
1393
+ recursive: options?.recursive ?? true,
1394
+ }), includeLocalMounts),
1395
+ exists: async (path) => {
1396
+ const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1397
+ if (local) {
1398
+ return local.mount.fs.exists(local.relativePath);
1399
+ }
1400
+ return this.client.exists(this.session, this.vm, path);
1401
+ },
1402
+ stat: async (path) => {
1403
+ const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1404
+ if (local) {
1405
+ return local.mount.fs.stat(local.relativePath);
1406
+ }
1407
+ return toVirtualStat(await this.client.stat(this.session, this.vm, path));
1408
+ },
1409
+ removeFile: (path) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.removeFile(relativePath), () => this.client.removeFile(this.session, this.vm, path), includeLocalMounts),
1410
+ removeDir: (path) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.removeDir(relativePath), () => this.client.removeDir(this.session, this.vm, path), includeLocalMounts),
1411
+ rename: async (oldPath, newPath) => {
1412
+ const from = includeLocalMounts
1413
+ ? this.resolveLocalMount(oldPath)
1414
+ : null;
1415
+ const to = includeLocalMounts ? this.resolveLocalMount(newPath) : null;
1416
+ if (!!from !== !!to) {
1417
+ throw errnoError("EXDEV", "cross-device link not permitted");
1418
+ }
1419
+ if (from && to) {
1420
+ if (from.mount.path !== to.mount.path) {
1421
+ throw errnoError("EXDEV", "cross-device link not permitted");
1422
+ }
1423
+ this.assertLocalWritable(from.mount);
1424
+ return from.mount.fs.rename(from.relativePath, to.relativePath);
1425
+ }
1426
+ return this.client.rename(this.session, this.vm, oldPath, newPath);
1427
+ },
1428
+ realpath: async (path) => {
1429
+ const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1430
+ if (local) {
1431
+ return local.mount.fs.realpath(local.relativePath);
1432
+ }
1433
+ return this.client.realpath(this.session, this.vm, path);
1434
+ },
1435
+ symlink: (target, linkPath) => this.dispatchWrite(linkPath, (mount, relativePath) => mount.fs.symlink(target, relativePath), () => this.client.symlink(this.session, this.vm, target, linkPath), includeLocalMounts),
1436
+ readlink: async (path) => {
1437
+ const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1438
+ if (local) {
1439
+ return local.mount.fs.readlink(local.relativePath);
1440
+ }
1441
+ return this.client.readLink(this.session, this.vm, path);
1442
+ },
1443
+ lstat: async (path) => {
1444
+ const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1445
+ if (local) {
1446
+ return local.mount.fs.lstat(local.relativePath);
1447
+ }
1448
+ return toVirtualStat(await this.client.lstat(this.session, this.vm, path));
1449
+ },
1450
+ link: async (oldPath, newPath) => {
1451
+ const from = includeLocalMounts
1452
+ ? this.resolveLocalMount(oldPath)
1453
+ : null;
1454
+ const to = includeLocalMounts ? this.resolveLocalMount(newPath) : null;
1455
+ if (!!from !== !!to) {
1456
+ throw errnoError("EXDEV", "cross-device link not permitted");
1457
+ }
1458
+ if (from && to) {
1459
+ if (from.mount.path !== to.mount.path) {
1460
+ throw errnoError("EXDEV", "cross-device link not permitted");
1461
+ }
1462
+ this.assertLocalWritable(from.mount);
1463
+ return from.mount.fs.link(from.relativePath, to.relativePath);
1464
+ }
1465
+ return this.client.link(this.session, this.vm, oldPath, newPath);
1466
+ },
1467
+ chmod: (path, mode) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.chmod(relativePath, mode), () => this.client.chmod(this.session, this.vm, path, mode), includeLocalMounts),
1468
+ chown: (path, uid, gid) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.chown(relativePath, uid, gid), () => this.client.chown(this.session, this.vm, path, uid, gid), includeLocalMounts),
1469
+ utimes: (path, atimeMs, mtimeMs) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.utimes(relativePath, atimeMs, mtimeMs), () => this.client.utimes(this.session, this.vm, path, atimeMs, mtimeMs), includeLocalMounts),
1470
+ truncate: (path, length) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.truncate(relativePath, length), () => this.client.truncate(this.session, this.vm, path, length), includeLocalMounts),
1471
+ pread: async (path, offset, length) => {
1472
+ const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1473
+ if (local) {
1474
+ return local.mount.fs.pread(local.relativePath, offset, length);
1475
+ }
1476
+ return this.client.pread(this.session, this.vm, path, offset, length);
1477
+ },
1478
+ pwrite: async (path, offset, data) => {
1479
+ const bytes = await this.createFilesystemView(includeLocalMounts).readFile(path);
1480
+ const nextSize = Math.max(bytes.length, offset + data.length);
1481
+ const updated = new Uint8Array(nextSize);
1482
+ updated.set(bytes);
1483
+ updated.set(data, offset);
1484
+ await this.createFilesystemView(includeLocalMounts).writeFile(path, updated);
1485
+ },
1486
+ };
1487
+ }
1488
+ buildProcessSnapshot() {
1489
+ void this.refreshProcessSnapshot().catch(() => { });
1490
+ const processMap = new Map();
1491
+ const displayPidByKernelPid = new Map();
1492
+ for (const entry of this.sidecarProcessSnapshot) {
1493
+ const tracked = this.trackedProcessesById.get(entry.processId);
1494
+ if (tracked) {
1495
+ displayPidByKernelPid.set(entry.pid, tracked.pid);
1496
+ }
1497
+ }
1498
+ for (const entry of this.sidecarProcessSnapshot) {
1499
+ const tracked = this.trackedProcessesById.get(entry.processId);
1500
+ const displayPid = displayPidByKernelPid.get(entry.pid) ?? entry.pid;
1501
+ const displayPpid = displayPidByKernelPid.get(entry.ppid) ?? entry.ppid;
1502
+ const displayPgid = displayPidByKernelPid.get(entry.pgid) ?? entry.pgid;
1503
+ const displaySid = displayPidByKernelPid.get(entry.sid) ?? entry.sid;
1504
+ const processKey = `${entry.processId}:${entry.pid}`;
1505
+ const startTime = tracked?.startTime ??
1506
+ this.observedProcessStartTimes.get(processKey) ??
1507
+ Date.now();
1508
+ this.observedProcessStartTimes.set(processKey, startTime);
1509
+ processMap.set(displayPid, {
1510
+ pid: displayPid,
1511
+ ppid: displayPpid,
1512
+ pgid: displayPgid,
1513
+ sid: displaySid,
1514
+ driver: tracked?.driver ?? entry.driver,
1515
+ command: tracked?.command ?? entry.command,
1516
+ args: tracked?.args ?? entry.args,
1517
+ cwd: tracked?.cwd ?? entry.cwd,
1518
+ status: tracked?.exitCode !== null
1519
+ ? "exited"
1520
+ : tracked
1521
+ ? "running"
1522
+ : entry.status === "exited"
1523
+ ? "exited"
1524
+ : "running",
1525
+ exitCode: tracked?.exitCode ?? entry.exitCode,
1526
+ startTime,
1527
+ exitTime: tracked?.exitTime ?? null,
1528
+ });
1529
+ }
1530
+ for (const entry of this.trackedProcesses.values()) {
1531
+ if (processMap.has(entry.pid)) {
1532
+ continue;
1533
+ }
1534
+ processMap.set(entry.pid, {
1535
+ pid: entry.pid,
1536
+ ppid: 0,
1537
+ pgid: entry.pid,
1538
+ sid: entry.pid,
1539
+ driver: entry.driver,
1540
+ command: entry.command,
1541
+ args: entry.args,
1542
+ cwd: entry.cwd,
1543
+ status: entry.exitCode === null ? "running" : "exited",
1544
+ exitCode: entry.exitCode,
1545
+ startTime: entry.startTime,
1546
+ exitTime: entry.exitTime,
1547
+ });
1548
+ }
1549
+ this.processes.clear();
1550
+ for (const process of processMap.values()) {
1551
+ this.processes.set(process.pid, process);
1552
+ }
1553
+ return [...processMap.values()].sort((left, right) => left.pid - right.pid);
1554
+ }
1555
+ dispatchRead(path, handler, includeLocalMounts = true) {
1556
+ const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1557
+ if (local) {
1558
+ return handler(local.mount, local.relativePath);
1559
+ }
1560
+ return this.dispatchNativeRead(path);
1561
+ }
1562
+ dispatchNativeRead(path) {
1563
+ return this.client.readFile(this.session, this.vm, path);
1564
+ }
1565
+ async dispatchWrite(path, handler, nativeHandler, includeLocalMounts = true) {
1566
+ this.assertGuestPathWritable(path);
1567
+ const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1568
+ if (local) {
1569
+ this.assertLocalWritable(local.mount);
1570
+ await handler(local.mount, local.relativePath);
1571
+ return;
1572
+ }
1573
+ await nativeHandler();
1574
+ }
1575
+ resolveLocalMount(path) {
1576
+ const normalizedPath = posixPath.normalize(path);
1577
+ for (const mount of this.localMounts) {
1578
+ if (normalizedPath !== mount.path &&
1579
+ !normalizedPath.startsWith(`${mount.path}/`)) {
1580
+ continue;
1581
+ }
1582
+ const relativePath = normalizedPath === mount.path
1583
+ ? "/"
1584
+ : `/${normalizedPath.slice(mount.path.length + 1)}`;
1585
+ return {
1586
+ mount,
1587
+ relativePath,
1588
+ };
1589
+ }
1590
+ return null;
1591
+ }
1592
+ assertGuestPathWritable(path) {
1593
+ const normalizedPath = posixPath.normalize(path);
1594
+ for (const root of PROTECTED_READ_ONLY_GUEST_ROOTS) {
1595
+ if (normalizedPath === root ||
1596
+ normalizedPath.startsWith(`${root}/`)) {
1597
+ throw errnoError("EROFS", "read-only file system");
1598
+ }
1599
+ }
1600
+ }
1601
+ mountedChildNames(path) {
1602
+ const normalizedPath = posixPath.normalize(path);
1603
+ const names = new Set();
1604
+ for (const mount of this.localMounts) {
1605
+ if (mount.path === normalizedPath) {
1606
+ continue;
1607
+ }
1608
+ if (!mount.path.startsWith(`${normalizedPath}/`) &&
1609
+ normalizedPath !== "/") {
1610
+ continue;
1611
+ }
1612
+ const relative = normalizedPath === "/"
1613
+ ? mount.path.slice(1)
1614
+ : mount.path.slice(normalizedPath.length + 1);
1615
+ const name = relative.split("/").find(Boolean);
1616
+ if (name) {
1617
+ names.add(name);
1618
+ }
1619
+ }
1620
+ return [...names];
1621
+ }
1622
+ assertLocalWritable(mount) {
1623
+ if (mount.readOnly) {
1624
+ throw errnoError("EROFS", "read-only file system");
1625
+ }
1626
+ }
1627
+ updateTrackedProcessSnapshot(entry) {
1628
+ this.processes.set(entry.pid, {
1629
+ pid: entry.pid,
1630
+ ppid: 0,
1631
+ pgid: entry.pid,
1632
+ sid: entry.pid,
1633
+ driver: entry.driver,
1634
+ command: entry.command,
1635
+ args: entry.args,
1636
+ cwd: entry.cwd,
1637
+ status: entry.exitCode === null ? "running" : "exited",
1638
+ exitCode: entry.exitCode,
1639
+ startTime: entry.startTime,
1640
+ exitTime: entry.exitTime,
1641
+ });
1642
+ }
1643
+ }
1644
+ function buildCommandMap(commandGuestPaths) {
1645
+ const commands = new Map([
1646
+ ["node", "node"],
1647
+ ["npm", "node"],
1648
+ ["npx", "node"],
1649
+ ]);
1650
+ for (const name of commandGuestPaths.keys()) {
1651
+ commands.set(name, "wasmvm");
1652
+ }
1653
+ return commands;
1654
+ }
1655
+ function isNoSuchProcessError(error) {
1656
+ if (!(error instanceof Error)) {
1657
+ return false;
1658
+ }
1659
+ const message = error.message.toLowerCase();
1660
+ return (error.message.includes("ESRCH") ||
1661
+ message.includes("no such process") ||
1662
+ message.includes("has no active process"));
1663
+ }
1664
+ function isUnknownVmError(error) {
1665
+ if (!(error instanceof Error)) {
1666
+ return false;
1667
+ }
1668
+ return error.message.toLowerCase().includes("unknown sidecar vm");
1669
+ }
1670
+ function isAlreadyExistsError(error) {
1671
+ if (!(error instanceof Error)) {
1672
+ return false;
1673
+ }
1674
+ const message = error.message.toLowerCase();
1675
+ return error.message.includes("EEXIST") || message.includes("file exists");
1676
+ }
1677
+ function isMissingHostProcessError(error) {
1678
+ return (typeof error === "object" &&
1679
+ error !== null &&
1680
+ "code" in error &&
1681
+ error.code === "ESRCH");
1682
+ }
1683
+ function errnoError(code, message) {
1684
+ return Object.assign(new Error(`${code}: ${message}`), { code });
1685
+ }
1686
+ function toVirtualStat(stat) {
1687
+ return {
1688
+ mode: stat.mode,
1689
+ size: stat.size,
1690
+ blocks: stat.blocks,
1691
+ dev: stat.dev,
1692
+ rdev: stat.rdev,
1693
+ isDirectory: stat.is_directory,
1694
+ isSymbolicLink: stat.is_symbolic_link,
1695
+ atimeMs: stat.atime_ms,
1696
+ mtimeMs: stat.mtime_ms,
1697
+ ctimeMs: stat.ctime_ms,
1698
+ birthtimeMs: stat.birthtime_ms,
1699
+ ino: stat.ino,
1700
+ nlink: stat.nlink,
1701
+ uid: stat.uid,
1702
+ gid: stat.gid,
1703
+ };
1704
+ }
1705
+ function toKernelSocketSnapshot(socket) {
1706
+ return {
1707
+ processId: socket.processId,
1708
+ ...(socket.host !== undefined ? { host: socket.host } : {}),
1709
+ ...(socket.port !== undefined ? { port: socket.port } : {}),
1710
+ ...(socket.path !== undefined ? { path: socket.path } : {}),
1711
+ };
1712
+ }
1713
+ function toKernelSignalState(handlers) {
1714
+ return {
1715
+ handlers: new Map([...handlers.entries()].map(([signal, registration]) => [
1716
+ signal,
1717
+ {
1718
+ action: registration.action,
1719
+ mask: new Set(registration.mask),
1720
+ flags: registration.flags,
1721
+ },
1722
+ ])),
1723
+ };
1724
+ }
1725
+ function socketLookupKey(kind, request) {
1726
+ return JSON.stringify({
1727
+ kind,
1728
+ host: request.host ?? null,
1729
+ port: request.port ?? null,
1730
+ path: request.path ?? null,
1731
+ });
1732
+ }
1733
+ export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, NativeSidecarProcessClient, SidecarEventBufferOverflow, SidecarProcessError, SidecarProcessExited, } from "./sidecar-client.js";