@scelar/nodepod 1.0.3 → 1.0.4
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/__sw__.js +642 -642
- package/dist/{child_process-D6oDN2MX.js → child_process-53fMkug_.js} +4 -4
- package/dist/child_process-53fMkug_.js.map +1 -0
- package/dist/{child_process-hmVqFcF7.cjs → child_process-lxSKECHq.cjs} +5 -5
- package/dist/child_process-lxSKECHq.cjs.map +1 -0
- package/dist/{index-Ale2oba_.js → index-B8lyh_ti.js} +1081 -428
- package/dist/index-B8lyh_ti.js.map +1 -0
- package/dist/{index-BO1i013L.cjs → index-C-TQIrdG.cjs} +1195 -430
- package/dist/index-C-TQIrdG.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/memory-volume.d.ts +1 -1
- package/dist/polyfills/wasi.d.ts +45 -4
- package/dist/sdk/nodepod.d.ts +4 -3
- package/dist/sdk/types.d.ts +6 -0
- package/dist/threading/process-manager.d.ts +1 -1
- package/dist/threading/worker-protocol.d.ts +1 -1
- package/package.json +97 -97
- package/src/index.ts +192 -192
- package/src/memory-volume.ts +10 -4
- package/src/packages/version-resolver.ts +12 -2
- package/src/polyfills/child_process.ts +2288 -2288
- package/src/polyfills/fs.ts +2888 -2888
- package/src/polyfills/http.ts +1450 -1449
- package/src/polyfills/process.ts +27 -1
- package/src/polyfills/stream.ts +1620 -1620
- package/src/polyfills/wasi.ts +1284 -22
- package/src/request-proxy.ts +716 -716
- package/src/script-engine.ts +3694 -3396
- package/src/sdk/nodepod.ts +525 -509
- package/src/sdk/types.ts +7 -0
- package/src/syntax-transforms.ts +2 -2
- package/src/threading/offload-worker.ts +383 -383
- package/src/threading/offload.ts +271 -271
- package/src/threading/process-manager.ts +956 -956
- package/src/threading/process-worker-entry.ts +858 -854
- package/src/threading/worker-protocol.ts +1 -1
- package/dist/child_process-D6oDN2MX.js.map +0 -1
- package/dist/child_process-hmVqFcF7.cjs.map +0 -1
- package/dist/index-Ale2oba_.js.map +0 -1
- package/dist/index-BO1i013L.cjs.map +0 -1
|
@@ -1,956 +1,956 @@
|
|
|
1
|
-
// ProcessManager — main-thread process lifecycle manager.
|
|
2
|
-
// Spawns Web Worker processes, routes I/O, handles VFS sync, tracks process tree.
|
|
3
|
-
|
|
4
|
-
import { EventEmitter } from "../polyfills/events";
|
|
5
|
-
import type { MemoryVolume } from "../memory-volume";
|
|
6
|
-
import { ProcessHandle } from "./process-handle";
|
|
7
|
-
import type {
|
|
8
|
-
SpawnConfig,
|
|
9
|
-
ProcessInfo,
|
|
10
|
-
MainToWorker_Init,
|
|
11
|
-
VFSBinarySnapshot,
|
|
12
|
-
WorkerToMain_SpawnRequest,
|
|
13
|
-
WorkerToMain_ForkRequest,
|
|
14
|
-
WorkerToMain_WorkerThreadRequest,
|
|
15
|
-
WorkerToMain_SpawnSync,
|
|
16
|
-
WorkerToMain_HttpResponse,
|
|
17
|
-
} from "./worker-protocol";
|
|
18
|
-
import type { VFSBridge } from "./vfs-bridge";
|
|
19
|
-
import { PROCESS_WORKER_BUNDLE } from "virtual:process-worker-bundle";
|
|
20
|
-
import { SLOT_SIZE } from "./sync-channel";
|
|
21
|
-
|
|
22
|
-
/* ------------------------------------------------------------------ */
|
|
23
|
-
/* Constants */
|
|
24
|
-
/* ------------------------------------------------------------------ */
|
|
25
|
-
|
|
26
|
-
const MAX_PROCESS_DEPTH = 10;
|
|
27
|
-
const MAX_PROCESSES = 50;
|
|
28
|
-
|
|
29
|
-
/* ------------------------------------------------------------------ */
|
|
30
|
-
/* ProcessManager */
|
|
31
|
-
/* ------------------------------------------------------------------ */
|
|
32
|
-
|
|
33
|
-
export class ProcessManager extends EventEmitter {
|
|
34
|
-
private _processes = new Map<number, ProcessHandle>();
|
|
35
|
-
private _nextPid = 100;
|
|
36
|
-
private _volume: MemoryVolume;
|
|
37
|
-
private _vfsBridge: VFSBridge | null = null;
|
|
38
|
-
private _sharedBuffer: SharedArrayBuffer | null = null;
|
|
39
|
-
private _syncBuffer: SharedArrayBuffer | null = null;
|
|
40
|
-
|
|
41
|
-
// port → owning PID
|
|
42
|
-
private _serverPorts = new Map<number, number>();
|
|
43
|
-
// parent PID → child PIDs (for exit deferral)
|
|
44
|
-
private _childPids = new Map<number, Set<number>>();
|
|
45
|
-
// pending HTTP request callbacks
|
|
46
|
-
private _httpCallbacks = new Map<number, (resp: WorkerToMain_HttpResponse) => void>();
|
|
47
|
-
private _nextHttpRequestId = 1;
|
|
48
|
-
|
|
49
|
-
constructor(volume: MemoryVolume) {
|
|
50
|
-
super();
|
|
51
|
-
this._volume = volume;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
setVFSBridge(bridge: VFSBridge): void {
|
|
55
|
-
this._vfsBridge = bridge;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
setSharedBuffer(buf: SharedArrayBuffer): void {
|
|
59
|
-
this._sharedBuffer = buf;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
setSyncBuffer(buf: SharedArrayBuffer): void {
|
|
63
|
-
this._syncBuffer = buf;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
spawn(config: {
|
|
67
|
-
command: string;
|
|
68
|
-
args?: string[];
|
|
69
|
-
cwd?: string;
|
|
70
|
-
env?: Record<string, string>;
|
|
71
|
-
parentPid?: number;
|
|
72
|
-
}): ProcessHandle {
|
|
73
|
-
if (this._processes.size >= MAX_PROCESSES) {
|
|
74
|
-
throw new Error(`Process limit exceeded (max ${MAX_PROCESSES})`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (config.parentPid !== undefined) {
|
|
78
|
-
let depth = 0;
|
|
79
|
-
let pid: number | undefined = config.parentPid;
|
|
80
|
-
while (pid !== undefined && depth < MAX_PROCESS_DEPTH) {
|
|
81
|
-
const parent = this._processes.get(pid);
|
|
82
|
-
pid = parent?.parentPid;
|
|
83
|
-
depth++;
|
|
84
|
-
}
|
|
85
|
-
if (depth >= MAX_PROCESS_DEPTH) {
|
|
86
|
-
throw new Error(`Process tree depth limit exceeded (max ${MAX_PROCESS_DEPTH})`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const pid = this._nextPid++;
|
|
91
|
-
|
|
92
|
-
const snapshot = this._vfsBridge
|
|
93
|
-
? this._vfsBridge.createSnapshot()
|
|
94
|
-
: this._createEmptySnapshot();
|
|
95
|
-
|
|
96
|
-
const spawnConfig: SpawnConfig = {
|
|
97
|
-
command: config.command,
|
|
98
|
-
args: config.args ?? [],
|
|
99
|
-
cwd: config.cwd ?? "/",
|
|
100
|
-
env: config.env ?? {},
|
|
101
|
-
snapshot,
|
|
102
|
-
sharedBuffer: this._sharedBuffer ?? undefined,
|
|
103
|
-
syncBuffer: this._syncBuffer ?? undefined,
|
|
104
|
-
parentPid: config.parentPid,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const worker = this._createWorker();
|
|
108
|
-
const handle = new ProcessHandle(worker, spawnConfig);
|
|
109
|
-
handle._setPid(pid);
|
|
110
|
-
|
|
111
|
-
this._processes.set(pid, handle);
|
|
112
|
-
this._wireHandleEvents(handle);
|
|
113
|
-
|
|
114
|
-
const initMsg: MainToWorker_Init = {
|
|
115
|
-
type: "init",
|
|
116
|
-
pid,
|
|
117
|
-
cwd: spawnConfig.cwd,
|
|
118
|
-
env: spawnConfig.env,
|
|
119
|
-
snapshot: spawnConfig.snapshot,
|
|
120
|
-
sharedBuffer: spawnConfig.sharedBuffer,
|
|
121
|
-
syncBuffer: spawnConfig.syncBuffer,
|
|
122
|
-
};
|
|
123
|
-
handle.init(initMsg);
|
|
124
|
-
|
|
125
|
-
this.emit("spawn", pid, config.command, config.args);
|
|
126
|
-
return handle;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
getProcess(pid: number): ProcessHandle | undefined {
|
|
130
|
-
return this._processes.get(pid);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
listProcesses(): ProcessInfo[] {
|
|
134
|
-
const result: ProcessInfo[] = [];
|
|
135
|
-
for (const [pid, handle] of this._processes) {
|
|
136
|
-
result.push({
|
|
137
|
-
pid,
|
|
138
|
-
command: handle.command,
|
|
139
|
-
args: handle.args,
|
|
140
|
-
state: handle.state,
|
|
141
|
-
exitCode: handle.exitCode,
|
|
142
|
-
parentPid: handle.parentPid,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Kills process and ALL descendants recursively, cleans up server ports
|
|
149
|
-
kill(pid: number, signal: string = "SIGTERM"): boolean {
|
|
150
|
-
const handle = this._processes.get(pid);
|
|
151
|
-
if (!handle) return false;
|
|
152
|
-
handle.kill(signal);
|
|
153
|
-
this._killDescendants(pid, signal);
|
|
154
|
-
this._cleanupServerPorts(pid);
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private _cleanupServerPorts(pid: number): void {
|
|
159
|
-
for (const [port, ownerPid] of this._serverPorts) {
|
|
160
|
-
if (ownerPid === pid) {
|
|
161
|
-
this._serverPorts.delete(port);
|
|
162
|
-
this.emit("server-close", pid, port);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const children = this._childPids.get(pid);
|
|
166
|
-
if (children) {
|
|
167
|
-
for (const childPid of children) {
|
|
168
|
-
this._cleanupServerPorts(childPid);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private _killDescendants(pid: number, signal: string): void {
|
|
174
|
-
const children = this._childPids.get(pid);
|
|
175
|
-
if (!children) return;
|
|
176
|
-
for (const childPid of children) {
|
|
177
|
-
const childHandle = this._processes.get(childPid);
|
|
178
|
-
if (childHandle && childHandle.state !== "exited") {
|
|
179
|
-
childHandle.kill(signal);
|
|
180
|
-
// Prevent stale output from dying workers leaking into the terminal
|
|
181
|
-
childHandle.removeAllListeners("stdout");
|
|
182
|
-
childHandle.removeAllListeners("stderr");
|
|
183
|
-
}
|
|
184
|
-
this._killDescendants(childPid, signal);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
teardown(): void {
|
|
189
|
-
for (const [pid, handle] of this._processes) {
|
|
190
|
-
try { handle.kill("SIGKILL"); } catch {
|
|
191
|
-
/* ignore */
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
this._processes.clear();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
get processCount(): number {
|
|
198
|
-
return this._processes.size;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
registerServerPort(port: number, pid: number): void {
|
|
202
|
-
this._serverPorts.set(port, pid);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
unregisterServerPort(port: number): void {
|
|
206
|
-
this._serverPorts.delete(port);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
getServerPorts(): number[] {
|
|
210
|
-
return [...this._serverPorts.keys()];
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Dispatch HTTP request to the worker owning the port
|
|
214
|
-
dispatchHttpRequest(
|
|
215
|
-
port: number,
|
|
216
|
-
method: string,
|
|
217
|
-
path: string,
|
|
218
|
-
headers: Record<string, string>,
|
|
219
|
-
body?: string | null,
|
|
220
|
-
): Promise<{ statusCode: number; statusMessage: string; headers: Record<string, string>; body: string }> {
|
|
221
|
-
const pid = this._serverPorts.get(port);
|
|
222
|
-
if (pid === undefined) {
|
|
223
|
-
return Promise.resolve({
|
|
224
|
-
statusCode: 503,
|
|
225
|
-
statusMessage: "Service Unavailable",
|
|
226
|
-
headers: { "Content-Type": "text/plain" },
|
|
227
|
-
body: `No server on port ${port}`,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const handle = this._processes.get(pid);
|
|
232
|
-
if (!handle || handle.state === "exited") {
|
|
233
|
-
this._serverPorts.delete(port);
|
|
234
|
-
return Promise.resolve({
|
|
235
|
-
statusCode: 503,
|
|
236
|
-
statusMessage: "Service Unavailable",
|
|
237
|
-
headers: { "Content-Type": "text/plain" },
|
|
238
|
-
body: `Server process exited (pid ${pid})`,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const requestId = this._nextHttpRequestId++;
|
|
243
|
-
// console.log(`[PM] dispatchHttpRequest #${requestId} ${method} ${path} → pid ${pid}`);
|
|
244
|
-
return new Promise((resolve) => {
|
|
245
|
-
this._httpCallbacks.set(requestId, (resp) => {
|
|
246
|
-
this._httpCallbacks.delete(requestId);
|
|
247
|
-
// console.log(`[PM] http-response #${requestId} status=${resp.statusCode}`);
|
|
248
|
-
resolve({
|
|
249
|
-
statusCode: resp.statusCode,
|
|
250
|
-
statusMessage: resp.statusMessage,
|
|
251
|
-
headers: resp.headers,
|
|
252
|
-
body: resp.body,
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
handle.postMessage({
|
|
257
|
-
type: "http-request",
|
|
258
|
-
requestId,
|
|
259
|
-
port,
|
|
260
|
-
method,
|
|
261
|
-
path,
|
|
262
|
-
headers,
|
|
263
|
-
body: body ?? null,
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Returns owning PID, or -1 if no server found
|
|
269
|
-
dispatchWsUpgrade(
|
|
270
|
-
port: number,
|
|
271
|
-
uid: string,
|
|
272
|
-
path: string,
|
|
273
|
-
headers: Record<string, string>,
|
|
274
|
-
): number {
|
|
275
|
-
const pid = this._serverPorts.get(port);
|
|
276
|
-
if (pid === undefined) return -1;
|
|
277
|
-
|
|
278
|
-
const handle = this._processes.get(pid);
|
|
279
|
-
if (!handle || handle.state === "exited") {
|
|
280
|
-
this._serverPorts.delete(port);
|
|
281
|
-
return -1;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
handle.postMessage({ type: "ws-upgrade", uid, port, path, headers });
|
|
285
|
-
return pid;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
dispatchWsData(pid: number, uid: string, frame: number[]): void {
|
|
289
|
-
const handle = this._processes.get(pid);
|
|
290
|
-
if (!handle || handle.state === "exited") return;
|
|
291
|
-
handle.postMessage({ type: "ws-data", uid, frame });
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
dispatchWsClose(pid: number, uid: string, code: number): void {
|
|
295
|
-
const handle = this._processes.get(pid);
|
|
296
|
-
if (!handle || handle.state === "exited") return;
|
|
297
|
-
handle.postMessage({ type: "ws-close", uid, code });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/* ---- Internal ---- */
|
|
301
|
-
|
|
302
|
-
private static _workerBlobUrl: string | null = null;
|
|
303
|
-
|
|
304
|
-
private _createWorker(): Worker {
|
|
305
|
-
// Blob URL from pre-bundled source — works in any environment
|
|
306
|
-
if (!ProcessManager._workerBlobUrl) {
|
|
307
|
-
const blob = new Blob([PROCESS_WORKER_BUNDLE], { type: "application/javascript" });
|
|
308
|
-
ProcessManager._workerBlobUrl = URL.createObjectURL(blob);
|
|
309
|
-
}
|
|
310
|
-
return new Worker(ProcessManager._workerBlobUrl);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
private _wireHandleEvents(handle: ProcessHandle): void {
|
|
314
|
-
// Forward signals to all descendants (handles both running and exited parent)
|
|
315
|
-
handle.on("signal", (signal: string) => {
|
|
316
|
-
this._killDescendants(handle.pid, signal);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// Forward stdin to children (even if parent is blocked on Atomics.wait)
|
|
320
|
-
handle.on("stdin-forward", (data: string) => {
|
|
321
|
-
const children = this._childPids.get(handle.pid);
|
|
322
|
-
if (children) {
|
|
323
|
-
for (const childPid of children) {
|
|
324
|
-
const childHandle = this._processes.get(childPid);
|
|
325
|
-
if (childHandle && childHandle.state !== "exited") {
|
|
326
|
-
childHandle.sendStdin(data);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
handle.on("exit", (exitCode: number) => {
|
|
333
|
-
for (const [port, pid] of this._serverPorts) {
|
|
334
|
-
if (pid === handle.pid) {
|
|
335
|
-
this._serverPorts.delete(port);
|
|
336
|
-
this.emit("server-close", handle.pid, port);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
this.emit("exit", handle.pid, exitCode);
|
|
340
|
-
// Delay removal so event handlers finish
|
|
341
|
-
setTimeout(() => {
|
|
342
|
-
this._processes.delete(handle.pid);
|
|
343
|
-
}, 100);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
handle.on("vfs-write", (path: string, content: ArrayBuffer, isDirectory: boolean) => {
|
|
347
|
-
if (this._vfsBridge) {
|
|
348
|
-
if (isDirectory) {
|
|
349
|
-
this._vfsBridge.handleWorkerMkdir(path);
|
|
350
|
-
} else {
|
|
351
|
-
this._vfsBridge.handleWorkerWrite(path, new Uint8Array(content));
|
|
352
|
-
}
|
|
353
|
-
this._vfsBridge.broadcastChange(path, content, handle.pid);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
handle.on("vfs-delete", (path: string) => {
|
|
358
|
-
if (this._vfsBridge) {
|
|
359
|
-
this._vfsBridge.handleWorkerDelete(path);
|
|
360
|
-
this._vfsBridge.broadcastChange(path, null, handle.pid);
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
handle.on("vfs-read", (requestId: number, path: string) => {
|
|
365
|
-
try {
|
|
366
|
-
if (this._volume.existsSync(path)) {
|
|
367
|
-
const stat = this._volume.statSync(path);
|
|
368
|
-
if (stat.isDirectory()) {
|
|
369
|
-
handle.postMessage({
|
|
370
|
-
type: "vfs-sync",
|
|
371
|
-
path,
|
|
372
|
-
content: null,
|
|
373
|
-
isDirectory: true,
|
|
374
|
-
});
|
|
375
|
-
} else {
|
|
376
|
-
const data = this._volume.readFileSync(path);
|
|
377
|
-
const buffer = (data.buffer as ArrayBuffer).slice(
|
|
378
|
-
data.byteOffset,
|
|
379
|
-
data.byteOffset + data.byteLength,
|
|
380
|
-
);
|
|
381
|
-
handle.postMessage({
|
|
382
|
-
type: "vfs-sync",
|
|
383
|
-
path,
|
|
384
|
-
content: buffer,
|
|
385
|
-
isDirectory: false,
|
|
386
|
-
}, [buffer]);
|
|
387
|
-
}
|
|
388
|
-
} else {
|
|
389
|
-
handle.postMessage({
|
|
390
|
-
type: "vfs-sync",
|
|
391
|
-
path,
|
|
392
|
-
content: null,
|
|
393
|
-
isDirectory: false,
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
} catch {
|
|
397
|
-
// Send null so worker doesn't hang
|
|
398
|
-
try {
|
|
399
|
-
handle.postMessage({
|
|
400
|
-
type: "vfs-sync",
|
|
401
|
-
path,
|
|
402
|
-
content: null,
|
|
403
|
-
isDirectory: false,
|
|
404
|
-
});
|
|
405
|
-
} catch { /* worker may have died */ }
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
handle.on("spawn-request", (msg: WorkerToMain_SpawnRequest) => {
|
|
410
|
-
const fullCmd = msg.args.length ? `${msg.command} ${msg.args.join(" ")}` : msg.command;
|
|
411
|
-
try {
|
|
412
|
-
const childHandle = this.spawn({
|
|
413
|
-
command: msg.command,
|
|
414
|
-
args: msg.args,
|
|
415
|
-
cwd: msg.cwd,
|
|
416
|
-
env: msg.env,
|
|
417
|
-
parentPid: handle.pid,
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
if (!this._childPids.has(handle.pid)) {
|
|
421
|
-
this._childPids.set(handle.pid, new Set());
|
|
422
|
-
}
|
|
423
|
-
this._childPids.get(handle.pid)!.add(childHandle.pid);
|
|
424
|
-
|
|
425
|
-
// Defer parent exit/done until child finishes (e.g. create-vite -> vite dev)
|
|
426
|
-
handle.holdExit();
|
|
427
|
-
handle.holdShellDone();
|
|
428
|
-
|
|
429
|
-
handle.postMessage({
|
|
430
|
-
type: "spawn-result",
|
|
431
|
-
requestId: msg.requestId,
|
|
432
|
-
pid: childHandle.pid,
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
// Detect bare node commands and send as direct execution
|
|
436
|
-
const isNodeBin = /(?:^|\/)node$/.test(msg.command);
|
|
437
|
-
const sendExec = () => {
|
|
438
|
-
if (isNodeBin && msg.args.length > 0) {
|
|
439
|
-
childHandle.exec({
|
|
440
|
-
type: "exec",
|
|
441
|
-
filePath: msg.args[0],
|
|
442
|
-
args: msg.args.slice(1),
|
|
443
|
-
cwd: msg.cwd,
|
|
444
|
-
env: msg.env,
|
|
445
|
-
isShell: false,
|
|
446
|
-
});
|
|
447
|
-
} else {
|
|
448
|
-
childHandle.exec({
|
|
449
|
-
type: "exec",
|
|
450
|
-
filePath: "",
|
|
451
|
-
args: msg.args,
|
|
452
|
-
cwd: msg.cwd,
|
|
453
|
-
env: msg.env,
|
|
454
|
-
isShell: true,
|
|
455
|
-
shellCommand: fullCmd,
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
if (childHandle.state === "running") {
|
|
461
|
-
sendExec();
|
|
462
|
-
} else {
|
|
463
|
-
childHandle.on("ready", sendExec);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Relay child output: direct emit if parent done, postMessage if still running
|
|
467
|
-
childHandle.on("stdout", (data: string) => {
|
|
468
|
-
if (handle.workerExited || handle.shellCommandDone) {
|
|
469
|
-
handle.emit("stdout", data);
|
|
470
|
-
} else {
|
|
471
|
-
handle.postMessage({
|
|
472
|
-
type: "child-output",
|
|
473
|
-
requestId: msg.requestId,
|
|
474
|
-
stream: "stdout",
|
|
475
|
-
data,
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
childHandle.on("stderr", (data: string) => {
|
|
480
|
-
if (handle.workerExited || handle.shellCommandDone) {
|
|
481
|
-
handle.emit("stderr", data);
|
|
482
|
-
} else {
|
|
483
|
-
handle.postMessage({
|
|
484
|
-
type: "child-output",
|
|
485
|
-
requestId: msg.requestId,
|
|
486
|
-
stream: "stderr",
|
|
487
|
-
data,
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
childHandle.on("stdin-raw-status", (isRaw: boolean) => {
|
|
493
|
-
handle.emit("stdin-raw-status", isRaw);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
childHandle.on("exit", (exitCode: number) => {
|
|
497
|
-
if (!handle.workerExited) {
|
|
498
|
-
handle.postMessage({
|
|
499
|
-
type: "child-exit",
|
|
500
|
-
requestId: msg.requestId,
|
|
501
|
-
exitCode,
|
|
502
|
-
stdout: childHandle.stdout,
|
|
503
|
-
stderr: childHandle.stderr,
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
const children = this._childPids.get(handle.pid);
|
|
507
|
-
if (children) {
|
|
508
|
-
children.delete(childHandle.pid);
|
|
509
|
-
if (children.size === 0) this._childPids.delete(handle.pid);
|
|
510
|
-
}
|
|
511
|
-
handle.releaseExit();
|
|
512
|
-
handle.releaseShellDone();
|
|
513
|
-
});
|
|
514
|
-
} catch (e) {
|
|
515
|
-
handle.postMessage({
|
|
516
|
-
type: "spawn-result",
|
|
517
|
-
requestId: msg.requestId,
|
|
518
|
-
pid: -1,
|
|
519
|
-
error: e instanceof Error ? e.message : String(e),
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
handle.on("fork-request", (msg: WorkerToMain_ForkRequest) => {
|
|
525
|
-
try {
|
|
526
|
-
const childHandle = this.spawn({
|
|
527
|
-
command: "node",
|
|
528
|
-
args: [msg.modulePath, ...msg.args],
|
|
529
|
-
cwd: msg.cwd,
|
|
530
|
-
env: msg.env,
|
|
531
|
-
parentPid: handle.pid,
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
if (!this._childPids.has(handle.pid)) {
|
|
535
|
-
this._childPids.set(handle.pid, new Set());
|
|
536
|
-
}
|
|
537
|
-
this._childPids.get(handle.pid)!.add(childHandle.pid);
|
|
538
|
-
handle.holdExit();
|
|
539
|
-
handle.holdShellDone();
|
|
540
|
-
|
|
541
|
-
handle.postMessage({
|
|
542
|
-
type: "spawn-result",
|
|
543
|
-
requestId: msg.requestId,
|
|
544
|
-
pid: childHandle.pid,
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
const sendExec = () => {
|
|
548
|
-
childHandle.exec({
|
|
549
|
-
type: "exec",
|
|
550
|
-
filePath: msg.modulePath,
|
|
551
|
-
args: msg.args,
|
|
552
|
-
cwd: msg.cwd,
|
|
553
|
-
env: msg.env,
|
|
554
|
-
isShell: false,
|
|
555
|
-
isFork: true,
|
|
556
|
-
});
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
if (childHandle.state === "running") {
|
|
560
|
-
sendExec();
|
|
561
|
-
} else {
|
|
562
|
-
childHandle.on("ready", sendExec);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
childHandle.on("stdout", (data: string) => {
|
|
566
|
-
if (handle.workerExited || handle.shellCommandDone) {
|
|
567
|
-
handle.emit("stdout", data);
|
|
568
|
-
} else {
|
|
569
|
-
handle.postMessage({
|
|
570
|
-
type: "child-output",
|
|
571
|
-
requestId: msg.requestId,
|
|
572
|
-
stream: "stdout",
|
|
573
|
-
data,
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
childHandle.on("stderr", (data: string) => {
|
|
578
|
-
if (handle.workerExited || handle.shellCommandDone) {
|
|
579
|
-
handle.emit("stderr", data);
|
|
580
|
-
} else {
|
|
581
|
-
handle.postMessage({
|
|
582
|
-
type: "child-output",
|
|
583
|
-
requestId: msg.requestId,
|
|
584
|
-
stream: "stderr",
|
|
585
|
-
data,
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
// IPC: child -> parent
|
|
591
|
-
childHandle.on("ipc-message", (ipcMsg: any) => {
|
|
592
|
-
const payload = ipcMsg?.data ?? ipcMsg;
|
|
593
|
-
if (!handle.workerExited) {
|
|
594
|
-
handle.postMessage({
|
|
595
|
-
type: "ipc-message",
|
|
596
|
-
targetRequestId: msg.requestId,
|
|
597
|
-
data: payload,
|
|
598
|
-
} as any);
|
|
599
|
-
}
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// IPC: parent -> child
|
|
603
|
-
handle.on("ipc-message", (ipcMsg: any) => {
|
|
604
|
-
if (ipcMsg.targetRequestId === msg.requestId) {
|
|
605
|
-
childHandle.postMessage({
|
|
606
|
-
type: "ipc-message",
|
|
607
|
-
data: ipcMsg.data,
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
childHandle.on("exit", (exitCode: number) => {
|
|
613
|
-
if (!handle.workerExited) {
|
|
614
|
-
handle.postMessage({
|
|
615
|
-
type: "child-exit",
|
|
616
|
-
requestId: msg.requestId,
|
|
617
|
-
exitCode,
|
|
618
|
-
stdout: childHandle.stdout,
|
|
619
|
-
stderr: childHandle.stderr,
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
const children = this._childPids.get(handle.pid);
|
|
623
|
-
if (children) {
|
|
624
|
-
children.delete(childHandle.pid);
|
|
625
|
-
if (children.size === 0) this._childPids.delete(handle.pid);
|
|
626
|
-
}
|
|
627
|
-
handle.releaseExit();
|
|
628
|
-
handle.releaseShellDone();
|
|
629
|
-
});
|
|
630
|
-
} catch (e) {
|
|
631
|
-
handle.postMessage({
|
|
632
|
-
type: "spawn-result",
|
|
633
|
-
requestId: msg.requestId,
|
|
634
|
-
pid: -1,
|
|
635
|
-
error: e instanceof Error ? e.message : String(e),
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
handle.on("workerthread-request", (msg: WorkerToMain_WorkerThreadRequest) => {
|
|
641
|
-
try {
|
|
642
|
-
let modulePath = msg.modulePath;
|
|
643
|
-
// eval mode: write code to temp VFS file
|
|
644
|
-
if (msg.isEval) {
|
|
645
|
-
const evalPath = `/__wt_eval_${msg.threadId}__.js`;
|
|
646
|
-
this._volume.writeFileSync(evalPath, msg.modulePath);
|
|
647
|
-
modulePath = evalPath;
|
|
648
|
-
if (this._vfsBridge) {
|
|
649
|
-
const encoder = new TextEncoder();
|
|
650
|
-
const content = encoder.encode(msg.modulePath).buffer as ArrayBuffer;
|
|
651
|
-
this._vfsBridge.handleWorkerWrite(evalPath, new Uint8Array(content));
|
|
652
|
-
this._vfsBridge.broadcastChange(evalPath, content, handle.pid);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const childHandle = this.spawn({
|
|
657
|
-
command: "node",
|
|
658
|
-
args: [modulePath],
|
|
659
|
-
cwd: msg.cwd,
|
|
660
|
-
env: msg.env,
|
|
661
|
-
parentPid: handle.pid,
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
if (!this._childPids.has(handle.pid)) {
|
|
665
|
-
this._childPids.set(handle.pid, new Set());
|
|
666
|
-
}
|
|
667
|
-
this._childPids.get(handle.pid)!.add(childHandle.pid);
|
|
668
|
-
handle.holdExit();
|
|
669
|
-
handle.holdShellDone();
|
|
670
|
-
|
|
671
|
-
handle.postMessage({
|
|
672
|
-
type: "spawn-result",
|
|
673
|
-
requestId: msg.requestId,
|
|
674
|
-
pid: childHandle.pid,
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
const sendExec = () => {
|
|
678
|
-
childHandle.exec({
|
|
679
|
-
type: "exec",
|
|
680
|
-
filePath: modulePath,
|
|
681
|
-
args: msg.args || [],
|
|
682
|
-
cwd: msg.cwd,
|
|
683
|
-
env: msg.env,
|
|
684
|
-
isShell: false,
|
|
685
|
-
isFork: true,
|
|
686
|
-
isWorkerThread: true,
|
|
687
|
-
workerData: msg.workerData,
|
|
688
|
-
threadId: msg.threadId,
|
|
689
|
-
});
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
if (childHandle.state === "running") {
|
|
693
|
-
sendExec();
|
|
694
|
-
} else {
|
|
695
|
-
childHandle.on("ready", sendExec);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
childHandle.on("stdout", (data: string) => {
|
|
699
|
-
if (handle.workerExited || handle.shellCommandDone) {
|
|
700
|
-
handle.emit("stdout", data);
|
|
701
|
-
} else {
|
|
702
|
-
handle.postMessage({
|
|
703
|
-
type: "child-output",
|
|
704
|
-
requestId: msg.requestId,
|
|
705
|
-
stream: "stdout",
|
|
706
|
-
data,
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
childHandle.on("stderr", (data: string) => {
|
|
711
|
-
if (handle.workerExited || handle.shellCommandDone) {
|
|
712
|
-
handle.emit("stderr", data);
|
|
713
|
-
} else {
|
|
714
|
-
handle.postMessage({
|
|
715
|
-
type: "child-output",
|
|
716
|
-
requestId: msg.requestId,
|
|
717
|
-
stream: "stderr",
|
|
718
|
-
data,
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
childHandle.on("ipc-message", (ipcMsg: any) => {
|
|
724
|
-
const payload = ipcMsg?.data ?? ipcMsg;
|
|
725
|
-
if (!handle.workerExited) {
|
|
726
|
-
handle.postMessage({
|
|
727
|
-
type: "ipc-message",
|
|
728
|
-
targetRequestId: msg.requestId,
|
|
729
|
-
data: payload,
|
|
730
|
-
} as any);
|
|
731
|
-
}
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
handle.on("ipc-message", (ipcMsg: any) => {
|
|
735
|
-
if (ipcMsg.targetRequestId === msg.requestId) {
|
|
736
|
-
childHandle.postMessage({
|
|
737
|
-
type: "ipc-message",
|
|
738
|
-
data: ipcMsg.data,
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
childHandle.on("exit", (exitCode: number) => {
|
|
744
|
-
if (msg.isEval) {
|
|
745
|
-
try {
|
|
746
|
-
this._volume.unlinkSync(`/__wt_eval_${msg.threadId}__.js`);
|
|
747
|
-
} catch {
|
|
748
|
-
/* ignore */
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
if (!handle.workerExited) {
|
|
753
|
-
handle.postMessage({
|
|
754
|
-
type: "child-exit",
|
|
755
|
-
requestId: msg.requestId,
|
|
756
|
-
exitCode,
|
|
757
|
-
stdout: childHandle.stdout,
|
|
758
|
-
stderr: childHandle.stderr,
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
const children = this._childPids.get(handle.pid);
|
|
762
|
-
if (children) {
|
|
763
|
-
children.delete(childHandle.pid);
|
|
764
|
-
if (children.size === 0) this._childPids.delete(handle.pid);
|
|
765
|
-
}
|
|
766
|
-
handle.releaseExit();
|
|
767
|
-
handle.releaseShellDone();
|
|
768
|
-
});
|
|
769
|
-
} catch (e) {
|
|
770
|
-
handle.postMessage({
|
|
771
|
-
type: "spawn-result",
|
|
772
|
-
requestId: msg.requestId,
|
|
773
|
-
pid: -1,
|
|
774
|
-
error: e instanceof Error ? e.message : String(e),
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
handle.on("spawn-sync", (msg: WorkerToMain_SpawnSync) => {
|
|
780
|
-
if (!this._syncBuffer) {
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const fullCmd = msg.shellCommand ??
|
|
785
|
-
(msg.args.length ? `${msg.command} ${msg.args.join(" ")}` : msg.command);
|
|
786
|
-
const maxStdoutLen = (SLOT_SIZE - 3) * 4;
|
|
787
|
-
|
|
788
|
-
const signalError = (exitCode: number) => {
|
|
789
|
-
try {
|
|
790
|
-
const int32 = new Int32Array(this._syncBuffer!);
|
|
791
|
-
const slotBase = msg.syncSlot * SLOT_SIZE;
|
|
792
|
-
Atomics.store(int32, slotBase + 1, exitCode);
|
|
793
|
-
Atomics.store(int32, slotBase + 2, 0);
|
|
794
|
-
Atomics.store(int32, slotBase, 2); // STATUS_ERROR
|
|
795
|
-
Atomics.notify(int32, slotBase);
|
|
796
|
-
} catch {
|
|
797
|
-
// Buffer unusable — worker will time out
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
|
-
|
|
801
|
-
try {
|
|
802
|
-
const childHandle = this.spawn({
|
|
803
|
-
command: msg.command,
|
|
804
|
-
args: msg.args,
|
|
805
|
-
cwd: msg.cwd,
|
|
806
|
-
env: msg.env,
|
|
807
|
-
parentPid: handle.pid,
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
// Must track for Ctrl+C signal propagation via _killDescendants
|
|
811
|
-
if (!this._childPids.has(handle.pid)) {
|
|
812
|
-
this._childPids.set(handle.pid, new Set());
|
|
813
|
-
}
|
|
814
|
-
this._childPids.get(handle.pid)!.add(childHandle.pid);
|
|
815
|
-
|
|
816
|
-
handle.holdExit();
|
|
817
|
-
handle.holdShellDone();
|
|
818
|
-
handle.holdSync(); // parent is blocked on Atomics.wait — stdin must bypass
|
|
819
|
-
|
|
820
|
-
const sendExec = () => {
|
|
821
|
-
childHandle.exec({
|
|
822
|
-
type: "exec",
|
|
823
|
-
filePath: "",
|
|
824
|
-
args: msg.args,
|
|
825
|
-
cwd: msg.cwd,
|
|
826
|
-
env: msg.env,
|
|
827
|
-
isShell: true,
|
|
828
|
-
shellCommand: fullCmd,
|
|
829
|
-
});
|
|
830
|
-
};
|
|
831
|
-
|
|
832
|
-
if (childHandle.state === "running") {
|
|
833
|
-
sendExec();
|
|
834
|
-
} else {
|
|
835
|
-
childHandle.on("ready", sendExec);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Parent is blocked on Atomics.wait — can't process postMessage, emit directly
|
|
839
|
-
childHandle.on("stdout", (data: string) => {
|
|
840
|
-
handle.emit("stdout", data);
|
|
841
|
-
});
|
|
842
|
-
childHandle.on("stderr", (data: string) => {
|
|
843
|
-
handle.emit("stderr", data);
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
childHandle.on("stdin-raw-status", (isRaw: boolean) => {
|
|
847
|
-
handle.emit("stdin-raw-status", isRaw);
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
childHandle.on("exit", (exitCode: number) => {
|
|
851
|
-
try {
|
|
852
|
-
const int32 = new Int32Array(this._syncBuffer!);
|
|
853
|
-
const encoder = new TextEncoder();
|
|
854
|
-
const slotBase = msg.syncSlot * SLOT_SIZE;
|
|
855
|
-
const stdoutBytes = encoder.encode(childHandle.stdout);
|
|
856
|
-
const truncatedLen = Math.min(stdoutBytes.byteLength, maxStdoutLen);
|
|
857
|
-
|
|
858
|
-
Atomics.store(int32, slotBase + 1, exitCode);
|
|
859
|
-
Atomics.store(int32, slotBase + 2, truncatedLen);
|
|
860
|
-
|
|
861
|
-
const uint8 = new Uint8Array(this._syncBuffer!);
|
|
862
|
-
const dataOffset = (slotBase + 3) * 4;
|
|
863
|
-
uint8.set(stdoutBytes.subarray(0, truncatedLen), dataOffset);
|
|
864
|
-
|
|
865
|
-
// Must be last — wakes the waiting worker
|
|
866
|
-
Atomics.store(int32, slotBase, 1);
|
|
867
|
-
Atomics.notify(int32, slotBase);
|
|
868
|
-
} catch {
|
|
869
|
-
signalError(1);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const children = this._childPids.get(handle.pid);
|
|
873
|
-
if (children) {
|
|
874
|
-
children.delete(childHandle.pid);
|
|
875
|
-
if (children.size === 0) this._childPids.delete(handle.pid);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
handle.releaseSync();
|
|
879
|
-
handle.releaseExit();
|
|
880
|
-
handle.releaseShellDone();
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
childHandle.on("worker-error", () => {
|
|
884
|
-
signalError(1);
|
|
885
|
-
const children = this._childPids.get(handle.pid);
|
|
886
|
-
if (children) {
|
|
887
|
-
children.delete(childHandle.pid);
|
|
888
|
-
if (children.size === 0) this._childPids.delete(handle.pid);
|
|
889
|
-
}
|
|
890
|
-
handle.releaseSync();
|
|
891
|
-
handle.releaseExit();
|
|
892
|
-
handle.releaseShellDone();
|
|
893
|
-
});
|
|
894
|
-
} catch {
|
|
895
|
-
signalError(1);
|
|
896
|
-
}
|
|
897
|
-
});
|
|
898
|
-
|
|
899
|
-
handle.on("server-listen", (port: number, hostname: string) => {
|
|
900
|
-
this.registerServerPort(port, handle.pid);
|
|
901
|
-
this.emit("server-listen", handle.pid, port, hostname);
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
handle.on("server-close", (port: number) => {
|
|
905
|
-
this.unregisterServerPort(port);
|
|
906
|
-
this.emit("server-close", handle.pid, port);
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
handle.on("http-response", (msg: WorkerToMain_HttpResponse) => {
|
|
910
|
-
const cb = this._httpCallbacks.get(msg.requestId);
|
|
911
|
-
if (cb) cb(msg);
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
handle.on("ws-frame", (msg: any) => {
|
|
915
|
-
this.emit("ws-frame", msg);
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
handle.on("cwd-change", (cwd: string) => {
|
|
919
|
-
this.emit("cwd-change", handle.pid, cwd);
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
handle.on("stdin-raw-status", (isRaw: boolean) => {
|
|
923
|
-
this.emit("stdin-raw-status", handle.pid, isRaw);
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
handle.on("worker-error", (message: string, stack?: string) => {
|
|
927
|
-
this.emit("error", handle.pid, message, stack);
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
private _createEmptySnapshot(): VFSBinarySnapshot {
|
|
932
|
-
return {
|
|
933
|
-
manifest: [],
|
|
934
|
-
data: new ArrayBuffer(0),
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
broadcastVFSChange(path: string, content: ArrayBuffer | null, isDirectory: boolean, excludePid: number): void {
|
|
939
|
-
for (const [pid, handle] of this._processes) {
|
|
940
|
-
if (pid === excludePid || handle.state === "exited") continue;
|
|
941
|
-
try {
|
|
942
|
-
// ArrayBuffer can only be transferred once, clone for each recipient
|
|
943
|
-
const clonedContent = content ? content.slice(0) : null;
|
|
944
|
-
handle.postMessage({
|
|
945
|
-
type: "vfs-sync",
|
|
946
|
-
path,
|
|
947
|
-
content: clonedContent,
|
|
948
|
-
isDirectory,
|
|
949
|
-
}, clonedContent ? [clonedContent] : []);
|
|
950
|
-
} catch {
|
|
951
|
-
/* ignore */
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
1
|
+
// ProcessManager — main-thread process lifecycle manager.
|
|
2
|
+
// Spawns Web Worker processes, routes I/O, handles VFS sync, tracks process tree.
|
|
3
|
+
|
|
4
|
+
import { EventEmitter } from "../polyfills/events";
|
|
5
|
+
import type { MemoryVolume } from "../memory-volume";
|
|
6
|
+
import { ProcessHandle } from "./process-handle";
|
|
7
|
+
import type {
|
|
8
|
+
SpawnConfig,
|
|
9
|
+
ProcessInfo,
|
|
10
|
+
MainToWorker_Init,
|
|
11
|
+
VFSBinarySnapshot,
|
|
12
|
+
WorkerToMain_SpawnRequest,
|
|
13
|
+
WorkerToMain_ForkRequest,
|
|
14
|
+
WorkerToMain_WorkerThreadRequest,
|
|
15
|
+
WorkerToMain_SpawnSync,
|
|
16
|
+
WorkerToMain_HttpResponse,
|
|
17
|
+
} from "./worker-protocol";
|
|
18
|
+
import type { VFSBridge } from "./vfs-bridge";
|
|
19
|
+
import { PROCESS_WORKER_BUNDLE } from "virtual:process-worker-bundle";
|
|
20
|
+
import { SLOT_SIZE } from "./sync-channel";
|
|
21
|
+
|
|
22
|
+
/* ------------------------------------------------------------------ */
|
|
23
|
+
/* Constants */
|
|
24
|
+
/* ------------------------------------------------------------------ */
|
|
25
|
+
|
|
26
|
+
const MAX_PROCESS_DEPTH = 10;
|
|
27
|
+
const MAX_PROCESSES = 50;
|
|
28
|
+
|
|
29
|
+
/* ------------------------------------------------------------------ */
|
|
30
|
+
/* ProcessManager */
|
|
31
|
+
/* ------------------------------------------------------------------ */
|
|
32
|
+
|
|
33
|
+
export class ProcessManager extends EventEmitter {
|
|
34
|
+
private _processes = new Map<number, ProcessHandle>();
|
|
35
|
+
private _nextPid = 100;
|
|
36
|
+
private _volume: MemoryVolume;
|
|
37
|
+
private _vfsBridge: VFSBridge | null = null;
|
|
38
|
+
private _sharedBuffer: SharedArrayBuffer | null = null;
|
|
39
|
+
private _syncBuffer: SharedArrayBuffer | null = null;
|
|
40
|
+
|
|
41
|
+
// port → owning PID
|
|
42
|
+
private _serverPorts = new Map<number, number>();
|
|
43
|
+
// parent PID → child PIDs (for exit deferral)
|
|
44
|
+
private _childPids = new Map<number, Set<number>>();
|
|
45
|
+
// pending HTTP request callbacks
|
|
46
|
+
private _httpCallbacks = new Map<number, (resp: WorkerToMain_HttpResponse) => void>();
|
|
47
|
+
private _nextHttpRequestId = 1;
|
|
48
|
+
|
|
49
|
+
constructor(volume: MemoryVolume) {
|
|
50
|
+
super();
|
|
51
|
+
this._volume = volume;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setVFSBridge(bridge: VFSBridge): void {
|
|
55
|
+
this._vfsBridge = bridge;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setSharedBuffer(buf: SharedArrayBuffer): void {
|
|
59
|
+
this._sharedBuffer = buf;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setSyncBuffer(buf: SharedArrayBuffer): void {
|
|
63
|
+
this._syncBuffer = buf;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
spawn(config: {
|
|
67
|
+
command: string;
|
|
68
|
+
args?: string[];
|
|
69
|
+
cwd?: string;
|
|
70
|
+
env?: Record<string, string>;
|
|
71
|
+
parentPid?: number;
|
|
72
|
+
}): ProcessHandle {
|
|
73
|
+
if (this._processes.size >= MAX_PROCESSES) {
|
|
74
|
+
throw new Error(`Process limit exceeded (max ${MAX_PROCESSES})`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (config.parentPid !== undefined) {
|
|
78
|
+
let depth = 0;
|
|
79
|
+
let pid: number | undefined = config.parentPid;
|
|
80
|
+
while (pid !== undefined && depth < MAX_PROCESS_DEPTH) {
|
|
81
|
+
const parent = this._processes.get(pid);
|
|
82
|
+
pid = parent?.parentPid;
|
|
83
|
+
depth++;
|
|
84
|
+
}
|
|
85
|
+
if (depth >= MAX_PROCESS_DEPTH) {
|
|
86
|
+
throw new Error(`Process tree depth limit exceeded (max ${MAX_PROCESS_DEPTH})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const pid = this._nextPid++;
|
|
91
|
+
|
|
92
|
+
const snapshot = this._vfsBridge
|
|
93
|
+
? this._vfsBridge.createSnapshot()
|
|
94
|
+
: this._createEmptySnapshot();
|
|
95
|
+
|
|
96
|
+
const spawnConfig: SpawnConfig = {
|
|
97
|
+
command: config.command,
|
|
98
|
+
args: config.args ?? [],
|
|
99
|
+
cwd: config.cwd ?? "/",
|
|
100
|
+
env: config.env ?? {},
|
|
101
|
+
snapshot,
|
|
102
|
+
sharedBuffer: this._sharedBuffer ?? undefined,
|
|
103
|
+
syncBuffer: this._syncBuffer ?? undefined,
|
|
104
|
+
parentPid: config.parentPid,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const worker = this._createWorker();
|
|
108
|
+
const handle = new ProcessHandle(worker, spawnConfig);
|
|
109
|
+
handle._setPid(pid);
|
|
110
|
+
|
|
111
|
+
this._processes.set(pid, handle);
|
|
112
|
+
this._wireHandleEvents(handle);
|
|
113
|
+
|
|
114
|
+
const initMsg: MainToWorker_Init = {
|
|
115
|
+
type: "init",
|
|
116
|
+
pid,
|
|
117
|
+
cwd: spawnConfig.cwd,
|
|
118
|
+
env: spawnConfig.env,
|
|
119
|
+
snapshot: spawnConfig.snapshot,
|
|
120
|
+
sharedBuffer: spawnConfig.sharedBuffer,
|
|
121
|
+
syncBuffer: spawnConfig.syncBuffer,
|
|
122
|
+
};
|
|
123
|
+
handle.init(initMsg);
|
|
124
|
+
|
|
125
|
+
this.emit("spawn", pid, config.command, config.args);
|
|
126
|
+
return handle;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getProcess(pid: number): ProcessHandle | undefined {
|
|
130
|
+
return this._processes.get(pid);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
listProcesses(): ProcessInfo[] {
|
|
134
|
+
const result: ProcessInfo[] = [];
|
|
135
|
+
for (const [pid, handle] of this._processes) {
|
|
136
|
+
result.push({
|
|
137
|
+
pid,
|
|
138
|
+
command: handle.command,
|
|
139
|
+
args: handle.args,
|
|
140
|
+
state: handle.state,
|
|
141
|
+
exitCode: handle.exitCode,
|
|
142
|
+
parentPid: handle.parentPid,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Kills process and ALL descendants recursively, cleans up server ports
|
|
149
|
+
kill(pid: number, signal: string = "SIGTERM"): boolean {
|
|
150
|
+
const handle = this._processes.get(pid);
|
|
151
|
+
if (!handle) return false;
|
|
152
|
+
handle.kill(signal);
|
|
153
|
+
this._killDescendants(pid, signal);
|
|
154
|
+
this._cleanupServerPorts(pid);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private _cleanupServerPorts(pid: number): void {
|
|
159
|
+
for (const [port, ownerPid] of this._serverPorts) {
|
|
160
|
+
if (ownerPid === pid) {
|
|
161
|
+
this._serverPorts.delete(port);
|
|
162
|
+
this.emit("server-close", pid, port);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const children = this._childPids.get(pid);
|
|
166
|
+
if (children) {
|
|
167
|
+
for (const childPid of children) {
|
|
168
|
+
this._cleanupServerPorts(childPid);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private _killDescendants(pid: number, signal: string): void {
|
|
174
|
+
const children = this._childPids.get(pid);
|
|
175
|
+
if (!children) return;
|
|
176
|
+
for (const childPid of children) {
|
|
177
|
+
const childHandle = this._processes.get(childPid);
|
|
178
|
+
if (childHandle && childHandle.state !== "exited") {
|
|
179
|
+
childHandle.kill(signal);
|
|
180
|
+
// Prevent stale output from dying workers leaking into the terminal
|
|
181
|
+
childHandle.removeAllListeners("stdout");
|
|
182
|
+
childHandle.removeAllListeners("stderr");
|
|
183
|
+
}
|
|
184
|
+
this._killDescendants(childPid, signal);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
teardown(): void {
|
|
189
|
+
for (const [pid, handle] of this._processes) {
|
|
190
|
+
try { handle.kill("SIGKILL"); } catch {
|
|
191
|
+
/* ignore */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this._processes.clear();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
get processCount(): number {
|
|
198
|
+
return this._processes.size;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
registerServerPort(port: number, pid: number): void {
|
|
202
|
+
this._serverPorts.set(port, pid);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
unregisterServerPort(port: number): void {
|
|
206
|
+
this._serverPorts.delete(port);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getServerPorts(): number[] {
|
|
210
|
+
return [...this._serverPorts.keys()];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Dispatch HTTP request to the worker owning the port
|
|
214
|
+
dispatchHttpRequest(
|
|
215
|
+
port: number,
|
|
216
|
+
method: string,
|
|
217
|
+
path: string,
|
|
218
|
+
headers: Record<string, string>,
|
|
219
|
+
body?: string | null,
|
|
220
|
+
): Promise<{ statusCode: number; statusMessage: string; headers: Record<string, string>; body: string | ArrayBuffer }> {
|
|
221
|
+
const pid = this._serverPorts.get(port);
|
|
222
|
+
if (pid === undefined) {
|
|
223
|
+
return Promise.resolve({
|
|
224
|
+
statusCode: 503,
|
|
225
|
+
statusMessage: "Service Unavailable",
|
|
226
|
+
headers: { "Content-Type": "text/plain" },
|
|
227
|
+
body: `No server on port ${port}`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const handle = this._processes.get(pid);
|
|
232
|
+
if (!handle || handle.state === "exited") {
|
|
233
|
+
this._serverPorts.delete(port);
|
|
234
|
+
return Promise.resolve({
|
|
235
|
+
statusCode: 503,
|
|
236
|
+
statusMessage: "Service Unavailable",
|
|
237
|
+
headers: { "Content-Type": "text/plain" },
|
|
238
|
+
body: `Server process exited (pid ${pid})`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const requestId = this._nextHttpRequestId++;
|
|
243
|
+
// console.log(`[PM] dispatchHttpRequest #${requestId} ${method} ${path} → pid ${pid}`);
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
this._httpCallbacks.set(requestId, (resp) => {
|
|
246
|
+
this._httpCallbacks.delete(requestId);
|
|
247
|
+
// console.log(`[PM] http-response #${requestId} status=${resp.statusCode}`);
|
|
248
|
+
resolve({
|
|
249
|
+
statusCode: resp.statusCode,
|
|
250
|
+
statusMessage: resp.statusMessage,
|
|
251
|
+
headers: resp.headers,
|
|
252
|
+
body: resp.body,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
handle.postMessage({
|
|
257
|
+
type: "http-request",
|
|
258
|
+
requestId,
|
|
259
|
+
port,
|
|
260
|
+
method,
|
|
261
|
+
path,
|
|
262
|
+
headers,
|
|
263
|
+
body: body ?? null,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Returns owning PID, or -1 if no server found
|
|
269
|
+
dispatchWsUpgrade(
|
|
270
|
+
port: number,
|
|
271
|
+
uid: string,
|
|
272
|
+
path: string,
|
|
273
|
+
headers: Record<string, string>,
|
|
274
|
+
): number {
|
|
275
|
+
const pid = this._serverPorts.get(port);
|
|
276
|
+
if (pid === undefined) return -1;
|
|
277
|
+
|
|
278
|
+
const handle = this._processes.get(pid);
|
|
279
|
+
if (!handle || handle.state === "exited") {
|
|
280
|
+
this._serverPorts.delete(port);
|
|
281
|
+
return -1;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
handle.postMessage({ type: "ws-upgrade", uid, port, path, headers });
|
|
285
|
+
return pid;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
dispatchWsData(pid: number, uid: string, frame: number[]): void {
|
|
289
|
+
const handle = this._processes.get(pid);
|
|
290
|
+
if (!handle || handle.state === "exited") return;
|
|
291
|
+
handle.postMessage({ type: "ws-data", uid, frame });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
dispatchWsClose(pid: number, uid: string, code: number): void {
|
|
295
|
+
const handle = this._processes.get(pid);
|
|
296
|
+
if (!handle || handle.state === "exited") return;
|
|
297
|
+
handle.postMessage({ type: "ws-close", uid, code });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* ---- Internal ---- */
|
|
301
|
+
|
|
302
|
+
private static _workerBlobUrl: string | null = null;
|
|
303
|
+
|
|
304
|
+
private _createWorker(): Worker {
|
|
305
|
+
// Blob URL from pre-bundled source — works in any environment
|
|
306
|
+
if (!ProcessManager._workerBlobUrl) {
|
|
307
|
+
const blob = new Blob([PROCESS_WORKER_BUNDLE], { type: "application/javascript" });
|
|
308
|
+
ProcessManager._workerBlobUrl = URL.createObjectURL(blob);
|
|
309
|
+
}
|
|
310
|
+
return new Worker(ProcessManager._workerBlobUrl);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private _wireHandleEvents(handle: ProcessHandle): void {
|
|
314
|
+
// Forward signals to all descendants (handles both running and exited parent)
|
|
315
|
+
handle.on("signal", (signal: string) => {
|
|
316
|
+
this._killDescendants(handle.pid, signal);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Forward stdin to children (even if parent is blocked on Atomics.wait)
|
|
320
|
+
handle.on("stdin-forward", (data: string) => {
|
|
321
|
+
const children = this._childPids.get(handle.pid);
|
|
322
|
+
if (children) {
|
|
323
|
+
for (const childPid of children) {
|
|
324
|
+
const childHandle = this._processes.get(childPid);
|
|
325
|
+
if (childHandle && childHandle.state !== "exited") {
|
|
326
|
+
childHandle.sendStdin(data);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
handle.on("exit", (exitCode: number) => {
|
|
333
|
+
for (const [port, pid] of this._serverPorts) {
|
|
334
|
+
if (pid === handle.pid) {
|
|
335
|
+
this._serverPorts.delete(port);
|
|
336
|
+
this.emit("server-close", handle.pid, port);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
this.emit("exit", handle.pid, exitCode);
|
|
340
|
+
// Delay removal so event handlers finish
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
this._processes.delete(handle.pid);
|
|
343
|
+
}, 100);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
handle.on("vfs-write", (path: string, content: ArrayBuffer, isDirectory: boolean) => {
|
|
347
|
+
if (this._vfsBridge) {
|
|
348
|
+
if (isDirectory) {
|
|
349
|
+
this._vfsBridge.handleWorkerMkdir(path);
|
|
350
|
+
} else {
|
|
351
|
+
this._vfsBridge.handleWorkerWrite(path, new Uint8Array(content));
|
|
352
|
+
}
|
|
353
|
+
this._vfsBridge.broadcastChange(path, content, handle.pid);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
handle.on("vfs-delete", (path: string) => {
|
|
358
|
+
if (this._vfsBridge) {
|
|
359
|
+
this._vfsBridge.handleWorkerDelete(path);
|
|
360
|
+
this._vfsBridge.broadcastChange(path, null, handle.pid);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
handle.on("vfs-read", (requestId: number, path: string) => {
|
|
365
|
+
try {
|
|
366
|
+
if (this._volume.existsSync(path)) {
|
|
367
|
+
const stat = this._volume.statSync(path);
|
|
368
|
+
if (stat.isDirectory()) {
|
|
369
|
+
handle.postMessage({
|
|
370
|
+
type: "vfs-sync",
|
|
371
|
+
path,
|
|
372
|
+
content: null,
|
|
373
|
+
isDirectory: true,
|
|
374
|
+
});
|
|
375
|
+
} else {
|
|
376
|
+
const data = this._volume.readFileSync(path);
|
|
377
|
+
const buffer = (data.buffer as ArrayBuffer).slice(
|
|
378
|
+
data.byteOffset,
|
|
379
|
+
data.byteOffset + data.byteLength,
|
|
380
|
+
);
|
|
381
|
+
handle.postMessage({
|
|
382
|
+
type: "vfs-sync",
|
|
383
|
+
path,
|
|
384
|
+
content: buffer,
|
|
385
|
+
isDirectory: false,
|
|
386
|
+
}, [buffer]);
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
handle.postMessage({
|
|
390
|
+
type: "vfs-sync",
|
|
391
|
+
path,
|
|
392
|
+
content: null,
|
|
393
|
+
isDirectory: false,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
// Send null so worker doesn't hang
|
|
398
|
+
try {
|
|
399
|
+
handle.postMessage({
|
|
400
|
+
type: "vfs-sync",
|
|
401
|
+
path,
|
|
402
|
+
content: null,
|
|
403
|
+
isDirectory: false,
|
|
404
|
+
});
|
|
405
|
+
} catch { /* worker may have died */ }
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
handle.on("spawn-request", (msg: WorkerToMain_SpawnRequest) => {
|
|
410
|
+
const fullCmd = msg.args.length ? `${msg.command} ${msg.args.join(" ")}` : msg.command;
|
|
411
|
+
try {
|
|
412
|
+
const childHandle = this.spawn({
|
|
413
|
+
command: msg.command,
|
|
414
|
+
args: msg.args,
|
|
415
|
+
cwd: msg.cwd,
|
|
416
|
+
env: msg.env,
|
|
417
|
+
parentPid: handle.pid,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (!this._childPids.has(handle.pid)) {
|
|
421
|
+
this._childPids.set(handle.pid, new Set());
|
|
422
|
+
}
|
|
423
|
+
this._childPids.get(handle.pid)!.add(childHandle.pid);
|
|
424
|
+
|
|
425
|
+
// Defer parent exit/done until child finishes (e.g. create-vite -> vite dev)
|
|
426
|
+
handle.holdExit();
|
|
427
|
+
handle.holdShellDone();
|
|
428
|
+
|
|
429
|
+
handle.postMessage({
|
|
430
|
+
type: "spawn-result",
|
|
431
|
+
requestId: msg.requestId,
|
|
432
|
+
pid: childHandle.pid,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Detect bare node commands and send as direct execution
|
|
436
|
+
const isNodeBin = /(?:^|\/)node$/.test(msg.command);
|
|
437
|
+
const sendExec = () => {
|
|
438
|
+
if (isNodeBin && msg.args.length > 0) {
|
|
439
|
+
childHandle.exec({
|
|
440
|
+
type: "exec",
|
|
441
|
+
filePath: msg.args[0],
|
|
442
|
+
args: msg.args.slice(1),
|
|
443
|
+
cwd: msg.cwd,
|
|
444
|
+
env: msg.env,
|
|
445
|
+
isShell: false,
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
childHandle.exec({
|
|
449
|
+
type: "exec",
|
|
450
|
+
filePath: "",
|
|
451
|
+
args: msg.args,
|
|
452
|
+
cwd: msg.cwd,
|
|
453
|
+
env: msg.env,
|
|
454
|
+
isShell: true,
|
|
455
|
+
shellCommand: fullCmd,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
if (childHandle.state === "running") {
|
|
461
|
+
sendExec();
|
|
462
|
+
} else {
|
|
463
|
+
childHandle.on("ready", sendExec);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Relay child output: direct emit if parent done, postMessage if still running
|
|
467
|
+
childHandle.on("stdout", (data: string) => {
|
|
468
|
+
if (handle.workerExited || handle.shellCommandDone) {
|
|
469
|
+
handle.emit("stdout", data);
|
|
470
|
+
} else {
|
|
471
|
+
handle.postMessage({
|
|
472
|
+
type: "child-output",
|
|
473
|
+
requestId: msg.requestId,
|
|
474
|
+
stream: "stdout",
|
|
475
|
+
data,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
childHandle.on("stderr", (data: string) => {
|
|
480
|
+
if (handle.workerExited || handle.shellCommandDone) {
|
|
481
|
+
handle.emit("stderr", data);
|
|
482
|
+
} else {
|
|
483
|
+
handle.postMessage({
|
|
484
|
+
type: "child-output",
|
|
485
|
+
requestId: msg.requestId,
|
|
486
|
+
stream: "stderr",
|
|
487
|
+
data,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
childHandle.on("stdin-raw-status", (isRaw: boolean) => {
|
|
493
|
+
handle.emit("stdin-raw-status", isRaw);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
childHandle.on("exit", (exitCode: number) => {
|
|
497
|
+
if (!handle.workerExited) {
|
|
498
|
+
handle.postMessage({
|
|
499
|
+
type: "child-exit",
|
|
500
|
+
requestId: msg.requestId,
|
|
501
|
+
exitCode,
|
|
502
|
+
stdout: childHandle.stdout,
|
|
503
|
+
stderr: childHandle.stderr,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
const children = this._childPids.get(handle.pid);
|
|
507
|
+
if (children) {
|
|
508
|
+
children.delete(childHandle.pid);
|
|
509
|
+
if (children.size === 0) this._childPids.delete(handle.pid);
|
|
510
|
+
}
|
|
511
|
+
handle.releaseExit();
|
|
512
|
+
handle.releaseShellDone();
|
|
513
|
+
});
|
|
514
|
+
} catch (e) {
|
|
515
|
+
handle.postMessage({
|
|
516
|
+
type: "spawn-result",
|
|
517
|
+
requestId: msg.requestId,
|
|
518
|
+
pid: -1,
|
|
519
|
+
error: e instanceof Error ? e.message : String(e),
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
handle.on("fork-request", (msg: WorkerToMain_ForkRequest) => {
|
|
525
|
+
try {
|
|
526
|
+
const childHandle = this.spawn({
|
|
527
|
+
command: "node",
|
|
528
|
+
args: [msg.modulePath, ...msg.args],
|
|
529
|
+
cwd: msg.cwd,
|
|
530
|
+
env: msg.env,
|
|
531
|
+
parentPid: handle.pid,
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
if (!this._childPids.has(handle.pid)) {
|
|
535
|
+
this._childPids.set(handle.pid, new Set());
|
|
536
|
+
}
|
|
537
|
+
this._childPids.get(handle.pid)!.add(childHandle.pid);
|
|
538
|
+
handle.holdExit();
|
|
539
|
+
handle.holdShellDone();
|
|
540
|
+
|
|
541
|
+
handle.postMessage({
|
|
542
|
+
type: "spawn-result",
|
|
543
|
+
requestId: msg.requestId,
|
|
544
|
+
pid: childHandle.pid,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const sendExec = () => {
|
|
548
|
+
childHandle.exec({
|
|
549
|
+
type: "exec",
|
|
550
|
+
filePath: msg.modulePath,
|
|
551
|
+
args: msg.args,
|
|
552
|
+
cwd: msg.cwd,
|
|
553
|
+
env: msg.env,
|
|
554
|
+
isShell: false,
|
|
555
|
+
isFork: true,
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
if (childHandle.state === "running") {
|
|
560
|
+
sendExec();
|
|
561
|
+
} else {
|
|
562
|
+
childHandle.on("ready", sendExec);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
childHandle.on("stdout", (data: string) => {
|
|
566
|
+
if (handle.workerExited || handle.shellCommandDone) {
|
|
567
|
+
handle.emit("stdout", data);
|
|
568
|
+
} else {
|
|
569
|
+
handle.postMessage({
|
|
570
|
+
type: "child-output",
|
|
571
|
+
requestId: msg.requestId,
|
|
572
|
+
stream: "stdout",
|
|
573
|
+
data,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
childHandle.on("stderr", (data: string) => {
|
|
578
|
+
if (handle.workerExited || handle.shellCommandDone) {
|
|
579
|
+
handle.emit("stderr", data);
|
|
580
|
+
} else {
|
|
581
|
+
handle.postMessage({
|
|
582
|
+
type: "child-output",
|
|
583
|
+
requestId: msg.requestId,
|
|
584
|
+
stream: "stderr",
|
|
585
|
+
data,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// IPC: child -> parent
|
|
591
|
+
childHandle.on("ipc-message", (ipcMsg: any) => {
|
|
592
|
+
const payload = ipcMsg?.data ?? ipcMsg;
|
|
593
|
+
if (!handle.workerExited) {
|
|
594
|
+
handle.postMessage({
|
|
595
|
+
type: "ipc-message",
|
|
596
|
+
targetRequestId: msg.requestId,
|
|
597
|
+
data: payload,
|
|
598
|
+
} as any);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// IPC: parent -> child
|
|
603
|
+
handle.on("ipc-message", (ipcMsg: any) => {
|
|
604
|
+
if (ipcMsg.targetRequestId === msg.requestId) {
|
|
605
|
+
childHandle.postMessage({
|
|
606
|
+
type: "ipc-message",
|
|
607
|
+
data: ipcMsg.data,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
childHandle.on("exit", (exitCode: number) => {
|
|
613
|
+
if (!handle.workerExited) {
|
|
614
|
+
handle.postMessage({
|
|
615
|
+
type: "child-exit",
|
|
616
|
+
requestId: msg.requestId,
|
|
617
|
+
exitCode,
|
|
618
|
+
stdout: childHandle.stdout,
|
|
619
|
+
stderr: childHandle.stderr,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const children = this._childPids.get(handle.pid);
|
|
623
|
+
if (children) {
|
|
624
|
+
children.delete(childHandle.pid);
|
|
625
|
+
if (children.size === 0) this._childPids.delete(handle.pid);
|
|
626
|
+
}
|
|
627
|
+
handle.releaseExit();
|
|
628
|
+
handle.releaseShellDone();
|
|
629
|
+
});
|
|
630
|
+
} catch (e) {
|
|
631
|
+
handle.postMessage({
|
|
632
|
+
type: "spawn-result",
|
|
633
|
+
requestId: msg.requestId,
|
|
634
|
+
pid: -1,
|
|
635
|
+
error: e instanceof Error ? e.message : String(e),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
handle.on("workerthread-request", (msg: WorkerToMain_WorkerThreadRequest) => {
|
|
641
|
+
try {
|
|
642
|
+
let modulePath = msg.modulePath;
|
|
643
|
+
// eval mode: write code to temp VFS file
|
|
644
|
+
if (msg.isEval) {
|
|
645
|
+
const evalPath = `/__wt_eval_${msg.threadId}__.js`;
|
|
646
|
+
this._volume.writeFileSync(evalPath, msg.modulePath);
|
|
647
|
+
modulePath = evalPath;
|
|
648
|
+
if (this._vfsBridge) {
|
|
649
|
+
const encoder = new TextEncoder();
|
|
650
|
+
const content = encoder.encode(msg.modulePath).buffer as ArrayBuffer;
|
|
651
|
+
this._vfsBridge.handleWorkerWrite(evalPath, new Uint8Array(content));
|
|
652
|
+
this._vfsBridge.broadcastChange(evalPath, content, handle.pid);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const childHandle = this.spawn({
|
|
657
|
+
command: "node",
|
|
658
|
+
args: [modulePath],
|
|
659
|
+
cwd: msg.cwd,
|
|
660
|
+
env: msg.env,
|
|
661
|
+
parentPid: handle.pid,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
if (!this._childPids.has(handle.pid)) {
|
|
665
|
+
this._childPids.set(handle.pid, new Set());
|
|
666
|
+
}
|
|
667
|
+
this._childPids.get(handle.pid)!.add(childHandle.pid);
|
|
668
|
+
handle.holdExit();
|
|
669
|
+
handle.holdShellDone();
|
|
670
|
+
|
|
671
|
+
handle.postMessage({
|
|
672
|
+
type: "spawn-result",
|
|
673
|
+
requestId: msg.requestId,
|
|
674
|
+
pid: childHandle.pid,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const sendExec = () => {
|
|
678
|
+
childHandle.exec({
|
|
679
|
+
type: "exec",
|
|
680
|
+
filePath: modulePath,
|
|
681
|
+
args: msg.args || [],
|
|
682
|
+
cwd: msg.cwd,
|
|
683
|
+
env: msg.env,
|
|
684
|
+
isShell: false,
|
|
685
|
+
isFork: true,
|
|
686
|
+
isWorkerThread: true,
|
|
687
|
+
workerData: msg.workerData,
|
|
688
|
+
threadId: msg.threadId,
|
|
689
|
+
});
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
if (childHandle.state === "running") {
|
|
693
|
+
sendExec();
|
|
694
|
+
} else {
|
|
695
|
+
childHandle.on("ready", sendExec);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
childHandle.on("stdout", (data: string) => {
|
|
699
|
+
if (handle.workerExited || handle.shellCommandDone) {
|
|
700
|
+
handle.emit("stdout", data);
|
|
701
|
+
} else {
|
|
702
|
+
handle.postMessage({
|
|
703
|
+
type: "child-output",
|
|
704
|
+
requestId: msg.requestId,
|
|
705
|
+
stream: "stdout",
|
|
706
|
+
data,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
childHandle.on("stderr", (data: string) => {
|
|
711
|
+
if (handle.workerExited || handle.shellCommandDone) {
|
|
712
|
+
handle.emit("stderr", data);
|
|
713
|
+
} else {
|
|
714
|
+
handle.postMessage({
|
|
715
|
+
type: "child-output",
|
|
716
|
+
requestId: msg.requestId,
|
|
717
|
+
stream: "stderr",
|
|
718
|
+
data,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
childHandle.on("ipc-message", (ipcMsg: any) => {
|
|
724
|
+
const payload = ipcMsg?.data ?? ipcMsg;
|
|
725
|
+
if (!handle.workerExited) {
|
|
726
|
+
handle.postMessage({
|
|
727
|
+
type: "ipc-message",
|
|
728
|
+
targetRequestId: msg.requestId,
|
|
729
|
+
data: payload,
|
|
730
|
+
} as any);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
handle.on("ipc-message", (ipcMsg: any) => {
|
|
735
|
+
if (ipcMsg.targetRequestId === msg.requestId) {
|
|
736
|
+
childHandle.postMessage({
|
|
737
|
+
type: "ipc-message",
|
|
738
|
+
data: ipcMsg.data,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
childHandle.on("exit", (exitCode: number) => {
|
|
744
|
+
if (msg.isEval) {
|
|
745
|
+
try {
|
|
746
|
+
this._volume.unlinkSync(`/__wt_eval_${msg.threadId}__.js`);
|
|
747
|
+
} catch {
|
|
748
|
+
/* ignore */
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (!handle.workerExited) {
|
|
753
|
+
handle.postMessage({
|
|
754
|
+
type: "child-exit",
|
|
755
|
+
requestId: msg.requestId,
|
|
756
|
+
exitCode,
|
|
757
|
+
stdout: childHandle.stdout,
|
|
758
|
+
stderr: childHandle.stderr,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
const children = this._childPids.get(handle.pid);
|
|
762
|
+
if (children) {
|
|
763
|
+
children.delete(childHandle.pid);
|
|
764
|
+
if (children.size === 0) this._childPids.delete(handle.pid);
|
|
765
|
+
}
|
|
766
|
+
handle.releaseExit();
|
|
767
|
+
handle.releaseShellDone();
|
|
768
|
+
});
|
|
769
|
+
} catch (e) {
|
|
770
|
+
handle.postMessage({
|
|
771
|
+
type: "spawn-result",
|
|
772
|
+
requestId: msg.requestId,
|
|
773
|
+
pid: -1,
|
|
774
|
+
error: e instanceof Error ? e.message : String(e),
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
handle.on("spawn-sync", (msg: WorkerToMain_SpawnSync) => {
|
|
780
|
+
if (!this._syncBuffer) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const fullCmd = msg.shellCommand ??
|
|
785
|
+
(msg.args.length ? `${msg.command} ${msg.args.join(" ")}` : msg.command);
|
|
786
|
+
const maxStdoutLen = (SLOT_SIZE - 3) * 4;
|
|
787
|
+
|
|
788
|
+
const signalError = (exitCode: number) => {
|
|
789
|
+
try {
|
|
790
|
+
const int32 = new Int32Array(this._syncBuffer!);
|
|
791
|
+
const slotBase = msg.syncSlot * SLOT_SIZE;
|
|
792
|
+
Atomics.store(int32, slotBase + 1, exitCode);
|
|
793
|
+
Atomics.store(int32, slotBase + 2, 0);
|
|
794
|
+
Atomics.store(int32, slotBase, 2); // STATUS_ERROR
|
|
795
|
+
Atomics.notify(int32, slotBase);
|
|
796
|
+
} catch {
|
|
797
|
+
// Buffer unusable — worker will time out
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
const childHandle = this.spawn({
|
|
803
|
+
command: msg.command,
|
|
804
|
+
args: msg.args,
|
|
805
|
+
cwd: msg.cwd,
|
|
806
|
+
env: msg.env,
|
|
807
|
+
parentPid: handle.pid,
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// Must track for Ctrl+C signal propagation via _killDescendants
|
|
811
|
+
if (!this._childPids.has(handle.pid)) {
|
|
812
|
+
this._childPids.set(handle.pid, new Set());
|
|
813
|
+
}
|
|
814
|
+
this._childPids.get(handle.pid)!.add(childHandle.pid);
|
|
815
|
+
|
|
816
|
+
handle.holdExit();
|
|
817
|
+
handle.holdShellDone();
|
|
818
|
+
handle.holdSync(); // parent is blocked on Atomics.wait — stdin must bypass
|
|
819
|
+
|
|
820
|
+
const sendExec = () => {
|
|
821
|
+
childHandle.exec({
|
|
822
|
+
type: "exec",
|
|
823
|
+
filePath: "",
|
|
824
|
+
args: msg.args,
|
|
825
|
+
cwd: msg.cwd,
|
|
826
|
+
env: msg.env,
|
|
827
|
+
isShell: true,
|
|
828
|
+
shellCommand: fullCmd,
|
|
829
|
+
});
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
if (childHandle.state === "running") {
|
|
833
|
+
sendExec();
|
|
834
|
+
} else {
|
|
835
|
+
childHandle.on("ready", sendExec);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Parent is blocked on Atomics.wait — can't process postMessage, emit directly
|
|
839
|
+
childHandle.on("stdout", (data: string) => {
|
|
840
|
+
handle.emit("stdout", data);
|
|
841
|
+
});
|
|
842
|
+
childHandle.on("stderr", (data: string) => {
|
|
843
|
+
handle.emit("stderr", data);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
childHandle.on("stdin-raw-status", (isRaw: boolean) => {
|
|
847
|
+
handle.emit("stdin-raw-status", isRaw);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
childHandle.on("exit", (exitCode: number) => {
|
|
851
|
+
try {
|
|
852
|
+
const int32 = new Int32Array(this._syncBuffer!);
|
|
853
|
+
const encoder = new TextEncoder();
|
|
854
|
+
const slotBase = msg.syncSlot * SLOT_SIZE;
|
|
855
|
+
const stdoutBytes = encoder.encode(childHandle.stdout);
|
|
856
|
+
const truncatedLen = Math.min(stdoutBytes.byteLength, maxStdoutLen);
|
|
857
|
+
|
|
858
|
+
Atomics.store(int32, slotBase + 1, exitCode);
|
|
859
|
+
Atomics.store(int32, slotBase + 2, truncatedLen);
|
|
860
|
+
|
|
861
|
+
const uint8 = new Uint8Array(this._syncBuffer!);
|
|
862
|
+
const dataOffset = (slotBase + 3) * 4;
|
|
863
|
+
uint8.set(stdoutBytes.subarray(0, truncatedLen), dataOffset);
|
|
864
|
+
|
|
865
|
+
// Must be last — wakes the waiting worker
|
|
866
|
+
Atomics.store(int32, slotBase, 1);
|
|
867
|
+
Atomics.notify(int32, slotBase);
|
|
868
|
+
} catch {
|
|
869
|
+
signalError(1);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const children = this._childPids.get(handle.pid);
|
|
873
|
+
if (children) {
|
|
874
|
+
children.delete(childHandle.pid);
|
|
875
|
+
if (children.size === 0) this._childPids.delete(handle.pid);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
handle.releaseSync();
|
|
879
|
+
handle.releaseExit();
|
|
880
|
+
handle.releaseShellDone();
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
childHandle.on("worker-error", () => {
|
|
884
|
+
signalError(1);
|
|
885
|
+
const children = this._childPids.get(handle.pid);
|
|
886
|
+
if (children) {
|
|
887
|
+
children.delete(childHandle.pid);
|
|
888
|
+
if (children.size === 0) this._childPids.delete(handle.pid);
|
|
889
|
+
}
|
|
890
|
+
handle.releaseSync();
|
|
891
|
+
handle.releaseExit();
|
|
892
|
+
handle.releaseShellDone();
|
|
893
|
+
});
|
|
894
|
+
} catch {
|
|
895
|
+
signalError(1);
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
handle.on("server-listen", (port: number, hostname: string) => {
|
|
900
|
+
this.registerServerPort(port, handle.pid);
|
|
901
|
+
this.emit("server-listen", handle.pid, port, hostname);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
handle.on("server-close", (port: number) => {
|
|
905
|
+
this.unregisterServerPort(port);
|
|
906
|
+
this.emit("server-close", handle.pid, port);
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
handle.on("http-response", (msg: WorkerToMain_HttpResponse) => {
|
|
910
|
+
const cb = this._httpCallbacks.get(msg.requestId);
|
|
911
|
+
if (cb) cb(msg);
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
handle.on("ws-frame", (msg: any) => {
|
|
915
|
+
this.emit("ws-frame", msg);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
handle.on("cwd-change", (cwd: string) => {
|
|
919
|
+
this.emit("cwd-change", handle.pid, cwd);
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
handle.on("stdin-raw-status", (isRaw: boolean) => {
|
|
923
|
+
this.emit("stdin-raw-status", handle.pid, isRaw);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
handle.on("worker-error", (message: string, stack?: string) => {
|
|
927
|
+
this.emit("error", handle.pid, message, stack);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private _createEmptySnapshot(): VFSBinarySnapshot {
|
|
932
|
+
return {
|
|
933
|
+
manifest: [],
|
|
934
|
+
data: new ArrayBuffer(0),
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
broadcastVFSChange(path: string, content: ArrayBuffer | null, isDirectory: boolean, excludePid: number): void {
|
|
939
|
+
for (const [pid, handle] of this._processes) {
|
|
940
|
+
if (pid === excludePid || handle.state === "exited") continue;
|
|
941
|
+
try {
|
|
942
|
+
// ArrayBuffer can only be transferred once, clone for each recipient
|
|
943
|
+
const clonedContent = content ? content.slice(0) : null;
|
|
944
|
+
handle.postMessage({
|
|
945
|
+
type: "vfs-sync",
|
|
946
|
+
path,
|
|
947
|
+
content: clonedContent,
|
|
948
|
+
isDirectory,
|
|
949
|
+
}, clonedContent ? [clonedContent] : []);
|
|
950
|
+
} catch {
|
|
951
|
+
/* ignore */
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|