@volter/tabnode 0.5.16 → 0.5.18

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 (36) hide show
  1. package/dist/assets/{runtime-worker-Bw8vbYik.js → runtime-worker-C-l9pCp3.js} +4374 -365
  2. package/dist/code-transforms.d.ts +2 -1
  3. package/dist/index.mjs +4319 -322
  4. package/dist/node-lib/binding/zlib.d.ts +14 -1
  5. package/dist/node-lib/internals/amaro.d.ts +31 -0
  6. package/dist/node-lib/internals/runtime.d.ts +1 -0
  7. package/dist/node-lib/node-versions.d.ts +1 -0
  8. package/dist/node-lib/typescript-module.d.ts +11 -0
  9. package/dist/process-tokens.d.ts +2 -0
  10. package/dist/shims/perf_hooks.d.ts +0 -4
  11. package/dist/shims/process.d.ts +3 -0
  12. package/dist/shims/shell-jobs.d.ts +12 -0
  13. package/dist/shims/streamed-output.d.ts +14 -0
  14. package/dist/shims/tls.d.ts +3 -1
  15. package/dist/shims/worker_threads.d.ts +4 -0
  16. package/package.json +2 -1
  17. package/src/code-transforms.ts +28 -13
  18. package/src/node-lib/binding/zlib.ts +80 -3
  19. package/src/node-lib/internal/modules/typescript.js +183 -0
  20. package/src/node-lib/internals/amaro.ts +51 -0
  21. package/src/node-lib/internals/index.ts +2 -0
  22. package/src/node-lib/internals/runtime.ts +22 -3
  23. package/src/node-lib/node-versions.ts +4 -1
  24. package/src/node-lib/sources.ts +2 -0
  25. package/src/node-lib/typescript-module.ts +50 -0
  26. package/src/process-tokens.ts +6 -0
  27. package/src/runtime.ts +97 -19
  28. package/src/shims/child_process.ts +41 -13
  29. package/src/shims/esbuild.ts +12 -1
  30. package/src/shims/perf_hooks.ts +18 -0
  31. package/src/shims/process.ts +2 -2
  32. package/src/shims/shell-jobs.ts +451 -0
  33. package/src/shims/streamed-output.ts +25 -0
  34. package/src/shims/string_decoder.ts +5 -0
  35. package/src/shims/tls.ts +13 -1
  36. package/src/shims/worker_threads.ts +19 -0
@@ -0,0 +1,451 @@
1
+ /**
2
+ * The engine's shell runs a statement that ends in `&` in the background, as
3
+ * a POSIX shell does.
4
+ *
5
+ * just-bash, the engine's shell, parses `&` (a statement's `background` flag)
6
+ * and its interpreter never reads it: `node -e '…setTimeout…' & c=$!; echo
7
+ * "bg pid=$c"; wait "$c"` ran the child to its end first, printed its output
8
+ * before `bg pid=0`, `$!` was always 0, `wait` returned 0 at once, and `kill`
9
+ * was "command not found". On a machine the job starts and the script goes on,
10
+ * `$!` is the job's pid, `wait` waits for it and returns its status, and
11
+ * `kill` reaches it.
12
+ *
13
+ * This is built on the doors just-bash offers rather than on a patched copy of
14
+ * it: a transform plugin, which runs on every script the shell executes (the
15
+ * top-level line, `sh -c`, `bash -c`, a script file), and custom commands.
16
+ *
17
+ * - The plugin replaces each background statement with a call of
18
+ * `__tabnode_bg`, carrying the statement's syntax tree (and the functions the
19
+ * script defines, which a job may call) as JSON; it renames `$!` to the
20
+ * variable `__tabnode_bg` sets, and the `wait` builtin (which answers 0 at
21
+ * once and cannot be overridden) to `__tabnode_wait`.
22
+ * - `__tabnode_bg` starts the statement as a run of the engine's own: a pid
23
+ * minted in the engine's process table (the one `child_process`, `kill -0`
24
+ * and `process.kill` read), a run name that a `node` inside the job takes as
25
+ * its own, so its `process.pid` is `$!`, and a copy of the shell's variables,
26
+ * as a forked subshell has. It returns at once.
27
+ * - `__tabnode_wait` waits for the jobs of the shell it runs in (all, `-n` the
28
+ * next, or the pids named) and answers the status of the last one: an exit
29
+ * code, or 128 plus the signal that ended it. `kill` delivers a signal to any
30
+ * process of the engine by pid; `disown` forgets a job.
31
+ *
32
+ * A job's output reaches the host's streams as it is written, and the text the
33
+ * shell's own run collects at the next `wait`, since the shell's result is the
34
+ * concatenation of its commands' results. What the plugin cannot see, it leaves
35
+ * as it was: `eval`'s text is not a script the plugin is shown.
36
+ */
37
+ import { defineCommand } from 'just-bash';
38
+ import type { Bash, CommandContext } from 'just-bash';
39
+ import {
40
+ PROCESS_TOKEN_ENV, enterRun, mintPid, setRunPid, runPid, forgetRunPid, pidIsLive, signalPid,
41
+ tokenOfPid, __signalOwnedProcess, __stopOwnedProcess, type ProcessToken,
42
+ } from '../process-tokens';
43
+ import { __substrateChildren, __substrateSignals, __substrateSignalNames } from './process';
44
+ import { __releaseOwnedServers, __releaseOwnedHandles } from '../node-lib/net-module';
45
+ import type { RunStreams } from './child_process';
46
+ import { unstreamedOutput } from './streamed-output';
47
+
48
+ /** What the engine's process model gives this module. */
49
+ export interface ShellJobsHost {
50
+ runTokenOf(ctx: CommandContext): ProcessToken | null;
51
+ runStreamsFor(token: ProcessToken): RunStreams | undefined;
52
+ registerRunStreams(token: ProcessToken, streams: RunStreams): void;
53
+ releaseRunStreams(token: ProcessToken): void;
54
+ }
55
+
56
+ /** The variable `$!` is read from once the plugin has renamed it. */
57
+ const LAST_JOB_VAR = '__TABNODE_LAST_JOB_PID';
58
+ /** The variable naming the shell a job belongs to; `sh -c` does not inherit it (it is not exported), a subshell copy does. */
59
+ const SHELL_VAR = '__TABNODE_SHELL';
60
+ const BG_COMMAND = '__tabnode_bg';
61
+ const WAIT_COMMAND = '__tabnode_wait';
62
+ const JOB_PLACEHOLDER = '__tabnode_job';
63
+ /** How much of its jobs' output a shell holds for its next `wait`; the rest still reached the host's streams. */
64
+ const PENDING_LIMIT = 1 << 20;
65
+ /** Signals whose default action is to do nothing. */
66
+ const IGNORED_BY_DEFAULT = new Set(['SIGCHLD', 'SIGCONT', 'SIGURG', 'SIGWINCH']);
67
+
68
+ interface Job {
69
+ pid: number;
70
+ token: ProcessToken;
71
+ status: number | null;
72
+ done: Promise<number>;
73
+ kill(signal: string): boolean;
74
+ }
75
+ interface Shell { jobs: Map<number, Job>; order: number[]; pendingOut: string; pendingErr: string }
76
+
77
+ type Node = Record<string, unknown>;
78
+
79
+ const shells = new Map<string, Shell>();
80
+ /** A job's script, from `__tabnode_bg` to the plugin, taken once. */
81
+ const handoff = new Map<string, Node>();
82
+ let nextShell = 1;
83
+ let nextJob = 1;
84
+
85
+ function isNode(value: unknown): value is Node {
86
+ return typeof value === 'object' && value !== null;
87
+ }
88
+
89
+ function literalWord(value: string): Node {
90
+ return { type: 'Word', parts: [{ type: 'Literal', value }] };
91
+ }
92
+
93
+ function wordLiteral(word: unknown): string | null {
94
+ if (!isNode(word) || !Array.isArray(word.parts) || word.parts.length !== 1) return null;
95
+ const part = word.parts[0];
96
+ return isNode(part) && part.type === 'Literal' && typeof part.value === 'string' ? part.value : null;
97
+ }
98
+
99
+ /** A statement whose one command defines a function. */
100
+ function definesFunction(statement: Node): boolean {
101
+ const pipelines = statement.pipelines;
102
+ return Array.isArray(pipelines) && pipelines.some((pipeline) => isNode(pipeline) && Array.isArray(pipeline.commands)
103
+ && pipeline.commands.some((command) => isNode(command) && command.type === 'FunctionDef'));
104
+ }
105
+
106
+ /** Every function definition in a script, outside the background statements (a job carries its own). */
107
+ function functionDefinitions(node: unknown, found: Node[] = []): Node[] {
108
+ if (Array.isArray(node)) { for (const child of node) functionDefinitions(child, found); return found; }
109
+ if (!isNode(node)) return found;
110
+ if (node.type === 'Statement' && node.background === true) return found;
111
+ if (node.type === 'Statement' && definesFunction(node)) found.push(node);
112
+ for (const key of Object.keys(node)) functionDefinitions(node[key], found);
113
+ return found;
114
+ }
115
+
116
+ /**
117
+ * The rewrite, in place: a background statement becomes a call of
118
+ * `__tabnode_bg` (its own subtree is not descended into; the job's script is
119
+ * rewritten when it runs), `$!` reads the job variable, `wait` is the job
120
+ * table's.
121
+ */
122
+ function rewrite(node: unknown, functions: string): void {
123
+ if (Array.isArray(node)) {
124
+ for (let index = 0; index < node.length; index += 1) {
125
+ const child = node[index];
126
+ if (isNode(child) && child.type === 'Statement' && child.background === true) {
127
+ node[index] = backgroundCall(child, functions);
128
+ } else rewrite(child, functions);
129
+ }
130
+ return;
131
+ }
132
+ if (!isNode(node)) return;
133
+ if (node.type === 'ParameterExpansion' && node.parameter === '!') node.parameter = LAST_JOB_VAR;
134
+ if (node.type === 'SimpleCommand' && wordLiteral(node.name) === 'wait') node.name = literalWord(WAIT_COMMAND);
135
+ for (const key of Object.keys(node)) {
136
+ const child = node[key];
137
+ if (isNode(child) && child.type === 'Statement' && child.background === true) node[key] = backgroundCall(child, functions);
138
+ else rewrite(child, functions);
139
+ }
140
+ }
141
+
142
+ function backgroundCall(statement: Node, functions: string): Node {
143
+ const job = JSON.stringify({ ...statement, background: false });
144
+ return {
145
+ type: 'Statement',
146
+ pipelines: [{
147
+ type: 'Pipeline',
148
+ commands: [{
149
+ type: 'SimpleCommand',
150
+ name: literalWord(BG_COMMAND),
151
+ args: [{ type: 'Word', parts: [{ type: 'SingleQuoted', value: job }] }, { type: 'Word', parts: [{ type: 'SingleQuoted', value: functions }] }],
152
+ assignments: [],
153
+ redirections: [],
154
+ }],
155
+ negated: false,
156
+ timed: false,
157
+ timePosix: false,
158
+ }],
159
+ operators: [],
160
+ background: false,
161
+ ...(typeof statement.sourceText === 'string' ? { sourceText: statement.sourceText } : {}),
162
+ };
163
+ }
164
+
165
+ /** The plugin: a job's own script is taken from the handoff, and every script is rewritten. */
166
+ function transform({ ast }: { ast: Node }): { ast: Node } {
167
+ let script = ast;
168
+ const statements = Array.isArray(ast.statements) ? ast.statements : [];
169
+ if (statements.length === 1 && isNode(statements[0])) {
170
+ const pipelines = statements[0].pipelines;
171
+ const command = Array.isArray(pipelines) && isNode(pipelines[0]) && Array.isArray(pipelines[0].commands) ? pipelines[0].commands[0] : undefined;
172
+ if (isNode(command) && command.type === 'SimpleCommand' && wordLiteral(command.name) === ':' && Array.isArray(command.args)
173
+ && command.args.length === 2 && wordLiteral(command.args[0]) === JOB_PLACEHOLDER) {
174
+ const token = wordLiteral(command.args[1]);
175
+ const own = token === null ? undefined : handoff.get(token);
176
+ if (own && token !== null) { handoff.delete(token); script = own; }
177
+ }
178
+ }
179
+ if (!Array.isArray(script.statements) || !JSON.stringify(script).match(/"background":true|"parameter":"!"|"value":"wait"/u)) return { ast: script };
180
+ const functions = JSON.stringify(functionDefinitions(script.statements));
181
+ rewrite(script.statements, functions);
182
+ return { ast: script };
183
+ }
184
+
185
+ function shellOf(ctx: CommandContext): { id: string; shell: Shell } {
186
+ let id = ctx.env.get(SHELL_VAR);
187
+ if (!id) { id = `shell-${nextShell++}`; ctx.env.set(SHELL_VAR, id); }
188
+ let shell = shells.get(id);
189
+ if (!shell) { shell = { jobs: new Map(), order: [], pendingOut: '', pendingErr: '' }; shells.set(id, shell); }
190
+ return { id, shell };
191
+ }
192
+
193
+ function signalNumber(name: string): number {
194
+ return __substrateSignals[name] ?? 15;
195
+ }
196
+
197
+ /** `-TERM`, `-SIGTERM`, `TERM`, `15`, `-15` to Node's name, or null. */
198
+ function signalName(spec: string): string | null {
199
+ const bare = spec.startsWith('-') ? spec.slice(1) : spec;
200
+ if (/^\d+$/u.test(bare)) {
201
+ const number = Number(bare);
202
+ if (number === 0) return '0';
203
+ return __substrateSignalNames[number > 128 ? number - 128 : number] ?? null;
204
+ }
205
+ const upper = bare.toUpperCase();
206
+ const name = upper.startsWith('SIG') ? upper : `SIG${upper}`;
207
+ return __substrateSignals[name] !== undefined ? name : null;
208
+ }
209
+
210
+ function startJob(host: ShellJobsHost, ctx: CommandContext, shell: Shell, script: Node, sourceText: string): Job {
211
+ const token: ProcessToken = `job-${nextJob++}`;
212
+ const parent = host.runTokenOf(ctx);
213
+ const parentPid = runPid(parent)?.pid;
214
+ const pid = mintPid();
215
+ setRunPid(token, pid, parentPid !== undefined && pidIsLive(parentPid) ? parentPid : 0, { argv: ['sh', '-c', sourceText], cwd: ctx.cwd });
216
+ const controller = new AbortController();
217
+ let streamedOut: string[] = [];
218
+ let streamedErr: string[] = [];
219
+ const live = (): RunStreams | undefined => (parent === null ? undefined : host.runStreamsFor(parent));
220
+ host.registerRunStreams(token, {
221
+ onStdout: (data: string) => { streamedOut.push(data); live()?.onStdout?.(data); },
222
+ onStderr: (data: string) => { streamedErr.push(data); live()?.onStderr?.(data); },
223
+ signal: controller.signal,
224
+ held: false,
225
+ });
226
+ let settle: (status: number) => void = () => {};
227
+ const job: Job = {
228
+ pid,
229
+ token,
230
+ status: null,
231
+ done: new Promise<number>((resolve) => { settle = resolve; }),
232
+ kill(signal: string): boolean {
233
+ if (job.status !== null) return false;
234
+ // A `node` of the job, here or in a realm of its own, takes the signal
235
+ // as a process does: its listeners, else its default action, which ends
236
+ // the job with it.
237
+ if (__signalOwnedProcess(token, signal) || signalPid(pid, signal)) return true;
238
+ if (IGNORED_BY_DEFAULT.has(signal)) return true;
239
+ // Nothing of the job's receives signals (a builtin, `sleep`): it ends
240
+ // now, as a killed child's run ends, and reports the signal.
241
+ controller.abort(signal);
242
+ __releaseOwnedServers(token, false);
243
+ __releaseOwnedHandles(token);
244
+ __stopOwnedProcess(token);
245
+ end(128 + signalNumber(signal));
246
+ return true;
247
+ },
248
+ };
249
+ const end = (status: number): void => {
250
+ if (job.status !== null) return;
251
+ job.status = status;
252
+ host.releaseRunStreams(token);
253
+ forgetRunPid(token);
254
+ __substrateChildren.delete(pid);
255
+ settle(status);
256
+ };
257
+ const hold = (text: string, key: 'pendingOut' | 'pendingErr'): void => {
258
+ if (!text) return;
259
+ const room = PENDING_LIMIT - shell[key].length;
260
+ if (room > 0) shell[key] += text.slice(0, room);
261
+ };
262
+ const env: Record<string, string> = Object.fromEntries(ctx.env);
263
+ delete env[SHELL_VAR];
264
+ env[PROCESS_TOKEN_ENV] = token;
265
+ handoff.set(token, script);
266
+ __substrateChildren.set(pid, {
267
+ get exitCode() { return job.status; },
268
+ signalCode: null,
269
+ kill: (signal = 'SIGTERM') => job.kill(signal),
270
+ } as { exitCode: number | null; signalCode: string | null; kill(signal?: string): boolean });
271
+ const run = ctx.exec
272
+ ? enterRun(token, () => ctx.exec!(`: ${JOB_PLACEHOLDER} ${token}`, { env, cwd: ctx.cwd, replaceEnv: true, signal: controller.signal }))
273
+ : Promise.resolve({ stdout: '', stderr: 'bash: background jobs need a shell that can run a script\n', exitCode: 1 });
274
+ void Promise.resolve(run).then(
275
+ (result) => {
276
+ handoff.delete(token);
277
+ const stdout = result.stdout ?? '';
278
+ const stderr = result.stderr ?? '';
279
+ const restOut = unstreamedOutput(stdout, streamedOut);
280
+ const restErr = unstreamedOutput(stderr, streamedErr);
281
+ if (restOut) live()?.onStdout?.(restOut);
282
+ if (restErr) live()?.onStderr?.(restErr);
283
+ hold(stdout, 'pendingOut');
284
+ hold(stderr, 'pendingErr');
285
+ streamedOut = [];
286
+ streamedErr = [];
287
+ end(result.exitCode ?? 0);
288
+ },
289
+ (error: unknown) => {
290
+ handoff.delete(token);
291
+ const text = `${error instanceof Error ? error.message : String(error)}\n`;
292
+ live()?.onStderr?.(text);
293
+ hold(text, 'pendingErr');
294
+ end(1);
295
+ },
296
+ );
297
+ return job;
298
+ }
299
+
300
+ /** The job's output held for the shell's next `wait`, taken. */
301
+ function flush(shell: Shell): { stdout: string; stderr: string } {
302
+ const taken = { stdout: shell.pendingOut, stderr: shell.pendingErr };
303
+ shell.pendingOut = '';
304
+ shell.pendingErr = '';
305
+ return taken;
306
+ }
307
+
308
+ /** The engine's shell, given background jobs. */
309
+ export function installShellJobs(bash: Bash, host: ShellJobsHost): void {
310
+ (bash as unknown as { registerTransformPlugin(plugin: { name: string; transform: typeof transform }): void })
311
+ .registerTransformPlugin({ name: 'tabnode-background-jobs', transform });
312
+
313
+ bash.registerCommand(defineCommand(BG_COMMAND, async (args, ctx) => {
314
+ let statement: Node;
315
+ let functions: Node[];
316
+ try {
317
+ statement = JSON.parse(args[0] ?? '') as Node;
318
+ functions = JSON.parse(args[1] ?? '[]') as Node[];
319
+ } catch {
320
+ return { stdout: '', stderr: `bash: ${BG_COMMAND}: not a job\n`, exitCode: 2 };
321
+ }
322
+ const { shell } = shellOf(ctx);
323
+ const script: Node = { type: 'Script', statements: [...functions, statement] };
324
+ const job = startJob(host, ctx, shell, script, typeof statement.sourceText === 'string' ? statement.sourceText : '');
325
+ shell.jobs.set(job.pid, job);
326
+ shell.order.push(job.pid);
327
+ ctx.env.set(LAST_JOB_VAR, String(job.pid));
328
+ return { stdout: '', stderr: '', exitCode: 0 };
329
+ }));
330
+
331
+ bash.registerCommand(defineCommand(WAIT_COMMAND, async (args, ctx) => {
332
+ const { id, shell } = shellOf(ctx);
333
+ let stderr = '';
334
+ let status = 0;
335
+ let next = false;
336
+ const targets: string[] = [];
337
+ for (const arg of args) {
338
+ if (arg === '-n') next = true;
339
+ else if (arg === '-f' || arg === '--') continue;
340
+ else targets.push(arg);
341
+ }
342
+ const aborted = new Promise<'aborted'>((resolve) => {
343
+ if (ctx.signal?.aborted) resolve('aborted');
344
+ ctx.signal?.addEventListener('abort', () => resolve('aborted'), { once: true });
345
+ });
346
+ const running = (): Job[] => shell.order.map((pid) => shell.jobs.get(pid)).filter((job): job is Job => job !== undefined && job.status === null);
347
+ const reap = (job: Job): void => {
348
+ shell.jobs.delete(job.pid);
349
+ shell.order = shell.order.filter((pid) => pid !== job.pid);
350
+ };
351
+ if (next) {
352
+ const pending = targets.length > 0
353
+ ? targets.map((target) => shell.jobs.get(Number(target))).filter((job): job is Job => job !== undefined)
354
+ // A job that has ended and is not yet reaped is the next one, as bash's is.
355
+ : shell.order.map((pid) => shell.jobs.get(pid)).filter((job): job is Job => job !== undefined);
356
+ if (pending.length === 0) status = 127;
357
+ else {
358
+ const first = await Promise.race([...pending.map((job) => job.done.then(() => job)), aborted]);
359
+ if (first === 'aborted') status = 130;
360
+ else { status = first.status ?? 0; reap(first); }
361
+ }
362
+ } else if (targets.length === 0) {
363
+ // Every job the shell has, including one started while waiting on the others.
364
+ for (let jobs = running(); jobs.length > 0; jobs = running()) {
365
+ if (await Promise.race([Promise.all(jobs.map((job) => job.done)), aborted]) === 'aborted') { status = 130; break; }
366
+ }
367
+ for (const pid of [...shell.order]) { const job = shell.jobs.get(pid); if (job && job.status !== null) reap(job); }
368
+ } else {
369
+ for (const target of targets) {
370
+ if (target.startsWith('%')) { stderr += `bash: wait: ${target}: no such job\n`; status = 127; continue; }
371
+ const pid = Number(target);
372
+ const job = Number.isInteger(pid) ? shell.jobs.get(pid) : undefined;
373
+ if (!job) {
374
+ stderr += Number.isInteger(pid) ? `bash: wait: pid ${target} is not a child of this shell\n` : `bash: wait: \`${target}': not a pid or valid job spec\n`;
375
+ status = 127;
376
+ continue;
377
+ }
378
+ const outcome = await Promise.race([job.done, aborted]);
379
+ if (outcome === 'aborted') { status = 130; break; }
380
+ status = outcome;
381
+ reap(job);
382
+ }
383
+ }
384
+ const held = flush(shell);
385
+ if (shell.jobs.size === 0) shells.delete(id);
386
+ return { stdout: held.stdout, stderr: held.stderr + stderr, exitCode: status };
387
+ }));
388
+
389
+ bash.registerCommand(defineCommand('kill', async (args) => {
390
+ let signal = 'SIGTERM';
391
+ let index = 0;
392
+ if (args[0] === '-l' || args[0] === '-L') {
393
+ const names = Object.entries(__substrateSignals).sort((a, b) => a[1] - b[1]).map(([name]) => name.slice(3));
394
+ if (args[1] !== undefined) {
395
+ const number = Number(args[1]);
396
+ const name = Number.isInteger(number) ? __substrateSignalNames[number > 128 ? number - 128 : number] : signalName(args[1]);
397
+ if (!name) return { stdout: '', stderr: `bash: kill: ${args[1]}: invalid signal specification\n`, exitCode: 1 };
398
+ return { stdout: `${Number.isInteger(number) ? name.slice(3) : __substrateSignals[name]}\n`, stderr: '', exitCode: 0 };
399
+ }
400
+ return { stdout: `${names.join(' ')}\n`, stderr: '', exitCode: 0 };
401
+ }
402
+ if (args[0] === '-s' || args[0] === '-n') {
403
+ const name = args[1] === undefined ? null : signalName(args[1]);
404
+ if (!name) return { stdout: '', stderr: `bash: kill: ${args[1] ?? ''}: invalid signal specification\n`, exitCode: 1 };
405
+ signal = name;
406
+ index = 2;
407
+ } else if (args[0] !== undefined && args[0].startsWith('-') && args[0] !== '--') {
408
+ const name = signalName(args[0]);
409
+ if (!name) return { stdout: '', stderr: `bash: kill: ${args[0].slice(1)}: invalid signal specification\n`, exitCode: 1 };
410
+ signal = name;
411
+ index = 1;
412
+ }
413
+ if (args[index] === '--') index += 1;
414
+ const targets = args.slice(index);
415
+ if (targets.length === 0) return { stdout: '', stderr: 'bash: kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]\n', exitCode: 2 };
416
+ let stderr = '';
417
+ let status = 0;
418
+ for (const target of targets) {
419
+ if (target.startsWith('%')) { stderr += `bash: kill: ${target}: no such job\n`; status = 1; continue; }
420
+ if (!/^-?\d+$/u.test(target)) { stderr += `bash: kill: ${target}: arguments must be process or job IDs\n`; status = 1; continue; }
421
+ const pid = Math.abs(Number(target));
422
+ if (!deliver(pid, signal)) { stderr += `bash: kill: (${target}) - No such process\n`; status = 1; }
423
+ }
424
+ return { stdout: '', stderr, exitCode: status };
425
+ }));
426
+
427
+ bash.registerCommand(defineCommand('disown', async (args, ctx) => {
428
+ const { shell } = shellOf(ctx);
429
+ const pids = args.filter((arg) => !arg.startsWith('-')).map(Number);
430
+ const all = args.includes('-a');
431
+ const chosen = all ? [...shell.order] : pids.length > 0 ? pids : shell.order.slice(-1);
432
+ for (const pid of chosen) { shell.jobs.delete(pid); shell.order = shell.order.filter((own) => own !== pid); }
433
+ return { stdout: '', stderr: '', exitCode: 0 };
434
+ }));
435
+ }
436
+
437
+ /**
438
+ * A signal to a process of the engine by pid, as `kill(2)` delivers one:
439
+ * signal 0 asks whether it is there; a child a guest spawned and a shell's job
440
+ * take it through their handle; any other run of the engine through its
441
+ * process, or through the realm that holds it.
442
+ */
443
+ function deliver(pid: number, signal: string): boolean {
444
+ const child = __substrateChildren.get(pid);
445
+ const childLive = child !== undefined && child.exitCode === null && child.signalCode === null;
446
+ if (signal === '0') return childLive || pidIsLive(pid);
447
+ if (childLive) return child.kill(signal);
448
+ const token = tokenOfPid(pid);
449
+ if (token !== null && __signalOwnedProcess(token, signal)) return true;
450
+ return signalPid(pid, signal);
451
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What of a command's collected output was not already streamed.
3
+ *
4
+ * The engine's shell collects a command line's output as one text at its end;
5
+ * a `node` in it (or a background job) also streams its own as it writes. A
6
+ * caller that forwarded the streamed pieces sends the rest at the end. Cutting
7
+ * the collected text at the streamed length assumed the streamed pieces came
8
+ * first: `echo "pid=1"; node -e "console.log('fg out')"; echo end`, spawned by
9
+ * a guest, reached it as `fg out\ng out\nend\n` -- the builtin's line lost and
10
+ * the node's half repeated. Each streamed piece is taken out where it stands in
11
+ * the collected text, in order, and what is left is the rest; a piece the text
12
+ * does not hold is passed over.
13
+ */
14
+ export function unstreamedOutput(total: string, streamed: readonly string[]): string {
15
+ let rest = total;
16
+ let cursor = 0;
17
+ for (const piece of streamed) {
18
+ if (!piece) continue;
19
+ const at = rest.indexOf(piece, cursor);
20
+ if (at < 0) continue;
21
+ rest = rest.slice(0, at) + rest.slice(at + piece.length);
22
+ cursor = at;
23
+ }
24
+ return rest;
25
+ }
@@ -308,6 +308,7 @@ function base64End(this: StringDecoder, buf?: unknown): string {
308
308
  // ---------------------------------------------------------------------------
309
309
 
310
310
  function simpleWrite(this: StringDecoder, buf: unknown): string {
311
+ if (typeof buf === 'string') return buf;
311
312
  return decoderInput(this, buf).toString(this.encoding as BufferEncoding);
312
313
  }
313
314
 
@@ -353,6 +354,10 @@ const decoderPrototype = StringDecoderImpl.prototype as unknown as StringDecoder
353
354
 
354
355
  decoderPrototype.write = function write(this: unknown, buffer: unknown): string {
355
356
  const self = assertDecoder(this);
357
+ // Node returns a string as it is given: readline decodes every chunk of its
358
+ // input, and an input stream with an encoding (`createReadStream(path,
359
+ // 'utf8')`) hands it strings.
360
+ if (typeof buffer === 'string') return buffer;
356
361
  const buf = decoderInput(self, buffer);
357
362
  if (buf.length === 0) return '';
358
363
  let r: string | undefined;
package/src/shims/tls.ts CHANGED
@@ -102,6 +102,17 @@ const DEFAULT_MIN_VERSION = 'TLSv1.2';
102
102
 
103
103
  const rootCertificates: string[] = [];
104
104
 
105
+ // Node's certificate-store door, read by callers that build their own agents
106
+ // (VS Code's proxy agent, on every fetch it patches). The engine holds no
107
+ // store: certificates are the browser's, so every kind answers the same empty
108
+ // set `rootCertificates` does.
109
+ const getCACertificates = (type: string = 'default'): string[] => {
110
+ if (!['default', 'system', 'bundled', 'extra'].includes(type)) {
111
+ throw Object.assign(new TypeError(`The argument 'type' must be one of: 'default', 'system', 'bundled', 'extra'. Received '${String(type)}'`), { code: 'ERR_INVALID_ARG_VALUE' });
112
+ }
113
+ return [];
114
+ };
115
+
105
116
  return {
106
117
  TLSSocket,
107
118
  Server,
@@ -109,6 +120,7 @@ return {
109
120
  connect,
110
121
  createSecureContext,
111
122
  getCiphers,
123
+ getCACertificates,
112
124
  DEFAULT_ECDH_CURVE,
113
125
  DEFAULT_MAX_VERSION,
114
126
  DEFAULT_MIN_VERSION,
@@ -117,7 +129,7 @@ return {
117
129
 
118
130
  }
119
131
  const tls = createTlsModule();
120
- export const { TLSSocket, Server, createServer, connect, createSecureContext, getCiphers,
132
+ export const { TLSSocket, Server, createServer, connect, createSecureContext, getCiphers, getCACertificates,
121
133
  DEFAULT_ECDH_CURVE, DEFAULT_MAX_VERSION, DEFAULT_MIN_VERSION, rootCertificates } = tls;
122
134
  export type TLSSocket = InstanceType<typeof TLSSocket>;
123
135
  export type Server = InstanceType<typeof Server>;
@@ -88,6 +88,23 @@ export function markAsUntransferable(object: unknown): void {
88
88
  // No-op
89
89
  }
90
90
 
91
+ /**
92
+ * Node 22's `markAsUncloneable` and its query: an object so marked is refused
93
+ * by `structuredClone` and `postMessage`. undici's WebIDL layer marks every
94
+ * `CacheStorage`, `Request` and `Response` it makes, at construction, so a
95
+ * program that loads undici 7.x+ or 8.x died on `markAsUncloneable is not a
96
+ * function` before its first line. The mark is kept; this engine's message
97
+ * channels do not yet read it, so a marked object that is posted is copied
98
+ * rather than refused.
99
+ */
100
+ const uncloneable = new WeakSet<object>();
101
+ export function markAsUncloneable(object: unknown): void {
102
+ if (object !== null && (typeof object === 'object' || typeof object === 'function')) uncloneable.add(object as object);
103
+ }
104
+ export function isMarkedAsUncloneable(object: unknown): boolean {
105
+ return object !== null && (typeof object === 'object' || typeof object === 'function') && uncloneable.has(object as object);
106
+ }
107
+
91
108
  export function getEnvironmentData(key: unknown): unknown {
92
109
  return undefined;
93
110
  }
@@ -109,6 +126,8 @@ export default {
109
126
  receiveMessageOnPort,
110
127
  SHARE_ENV,
111
128
  markAsUntransferable,
129
+ markAsUncloneable,
130
+ isMarkedAsUncloneable,
112
131
  getEnvironmentData,
113
132
  setEnvironmentData,
114
133
  };