@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.
- package/dist/assets/{runtime-worker-Bw8vbYik.js → runtime-worker-C-l9pCp3.js} +4374 -365
- package/dist/code-transforms.d.ts +2 -1
- package/dist/index.mjs +4319 -322
- package/dist/node-lib/binding/zlib.d.ts +14 -1
- package/dist/node-lib/internals/amaro.d.ts +31 -0
- package/dist/node-lib/internals/runtime.d.ts +1 -0
- package/dist/node-lib/node-versions.d.ts +1 -0
- package/dist/node-lib/typescript-module.d.ts +11 -0
- package/dist/process-tokens.d.ts +2 -0
- package/dist/shims/perf_hooks.d.ts +0 -4
- package/dist/shims/process.d.ts +3 -0
- package/dist/shims/shell-jobs.d.ts +12 -0
- package/dist/shims/streamed-output.d.ts +14 -0
- package/dist/shims/tls.d.ts +3 -1
- package/dist/shims/worker_threads.d.ts +4 -0
- package/package.json +2 -1
- package/src/code-transforms.ts +28 -13
- package/src/node-lib/binding/zlib.ts +80 -3
- package/src/node-lib/internal/modules/typescript.js +183 -0
- package/src/node-lib/internals/amaro.ts +51 -0
- package/src/node-lib/internals/index.ts +2 -0
- package/src/node-lib/internals/runtime.ts +22 -3
- package/src/node-lib/node-versions.ts +4 -1
- package/src/node-lib/sources.ts +2 -0
- package/src/node-lib/typescript-module.ts +50 -0
- package/src/process-tokens.ts +6 -0
- package/src/runtime.ts +97 -19
- package/src/shims/child_process.ts +41 -13
- package/src/shims/esbuild.ts +12 -1
- package/src/shims/perf_hooks.ts +18 -0
- package/src/shims/process.ts +2 -2
- package/src/shims/shell-jobs.ts +451 -0
- package/src/shims/streamed-output.ts +25 -0
- package/src/shims/string_decoder.ts +5 -0
- package/src/shims/tls.ts +13 -1
- 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
|
};
|