@scelar/nodepod 1.0.0
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/LICENSE +43 -0
- package/README.md +240 -0
- package/dist/child_process-BJOMsZje.js +8233 -0
- package/dist/child_process-BJOMsZje.js.map +1 -0
- package/dist/child_process-Cj8vOcuc.cjs +7434 -0
- package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
- package/dist/index-Cb1Cgdnd.js +35308 -0
- package/dist/index-Cb1Cgdnd.js.map +1 -0
- package/dist/index-DsMGS-xc.cjs +37195 -0
- package/dist/index-DsMGS-xc.cjs.map +1 -0
- package/dist/index.cjs +65 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +59 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +95 -0
- package/src/__tests__/smoke.test.ts +11 -0
- package/src/constants/cdn-urls.ts +18 -0
- package/src/constants/config.ts +236 -0
- package/src/cross-origin.ts +26 -0
- package/src/engine-factory.ts +176 -0
- package/src/engine-types.ts +56 -0
- package/src/helpers/byte-encoding.ts +39 -0
- package/src/helpers/digest.ts +9 -0
- package/src/helpers/event-loop.ts +96 -0
- package/src/helpers/wasm-cache.ts +133 -0
- package/src/iframe-sandbox.ts +141 -0
- package/src/index.ts +192 -0
- package/src/isolation-helpers.ts +148 -0
- package/src/memory-volume.ts +941 -0
- package/src/module-transformer.ts +368 -0
- package/src/packages/archive-extractor.ts +248 -0
- package/src/packages/browser-bundler.ts +284 -0
- package/src/packages/installer.ts +396 -0
- package/src/packages/registry-client.ts +131 -0
- package/src/packages/version-resolver.ts +411 -0
- package/src/polyfills/assert.ts +384 -0
- package/src/polyfills/async_hooks.ts +144 -0
- package/src/polyfills/buffer.ts +628 -0
- package/src/polyfills/child_process.ts +2288 -0
- package/src/polyfills/chokidar.ts +336 -0
- package/src/polyfills/cluster.ts +106 -0
- package/src/polyfills/console.ts +136 -0
- package/src/polyfills/constants.ts +123 -0
- package/src/polyfills/crypto.ts +885 -0
- package/src/polyfills/dgram.ts +87 -0
- package/src/polyfills/diagnostics_channel.ts +76 -0
- package/src/polyfills/dns.ts +134 -0
- package/src/polyfills/domain.ts +68 -0
- package/src/polyfills/esbuild.ts +854 -0
- package/src/polyfills/events.ts +276 -0
- package/src/polyfills/fs.ts +2888 -0
- package/src/polyfills/fsevents.ts +79 -0
- package/src/polyfills/http.ts +1449 -0
- package/src/polyfills/http2.ts +199 -0
- package/src/polyfills/https.ts +76 -0
- package/src/polyfills/inspector.ts +62 -0
- package/src/polyfills/lightningcss.ts +105 -0
- package/src/polyfills/module.ts +191 -0
- package/src/polyfills/net.ts +353 -0
- package/src/polyfills/os.ts +238 -0
- package/src/polyfills/path.ts +206 -0
- package/src/polyfills/perf_hooks.ts +102 -0
- package/src/polyfills/process.ts +690 -0
- package/src/polyfills/punycode.ts +159 -0
- package/src/polyfills/querystring.ts +93 -0
- package/src/polyfills/quic.ts +118 -0
- package/src/polyfills/readdirp.ts +229 -0
- package/src/polyfills/readline.ts +692 -0
- package/src/polyfills/repl.ts +134 -0
- package/src/polyfills/rollup.ts +119 -0
- package/src/polyfills/sea.ts +33 -0
- package/src/polyfills/sqlite.ts +78 -0
- package/src/polyfills/stream.ts +1620 -0
- package/src/polyfills/string_decoder.ts +25 -0
- package/src/polyfills/tailwindcss-oxide.ts +309 -0
- package/src/polyfills/test.ts +197 -0
- package/src/polyfills/timers.ts +32 -0
- package/src/polyfills/tls.ts +105 -0
- package/src/polyfills/trace_events.ts +50 -0
- package/src/polyfills/tty.ts +71 -0
- package/src/polyfills/url.ts +174 -0
- package/src/polyfills/util.ts +559 -0
- package/src/polyfills/v8.ts +126 -0
- package/src/polyfills/vm.ts +132 -0
- package/src/polyfills/volume-registry.ts +15 -0
- package/src/polyfills/wasi.ts +44 -0
- package/src/polyfills/worker_threads.ts +326 -0
- package/src/polyfills/ws.ts +595 -0
- package/src/polyfills/zlib.ts +881 -0
- package/src/request-proxy.ts +716 -0
- package/src/script-engine.ts +3375 -0
- package/src/sdk/nodepod-fs.ts +93 -0
- package/src/sdk/nodepod-process.ts +86 -0
- package/src/sdk/nodepod-terminal.ts +350 -0
- package/src/sdk/nodepod.ts +509 -0
- package/src/sdk/types.ts +70 -0
- package/src/shell/commands/bun.ts +121 -0
- package/src/shell/commands/directory.ts +297 -0
- package/src/shell/commands/file-ops.ts +525 -0
- package/src/shell/commands/git.ts +2142 -0
- package/src/shell/commands/node.ts +80 -0
- package/src/shell/commands/npm.ts +198 -0
- package/src/shell/commands/pm-types.ts +45 -0
- package/src/shell/commands/pnpm.ts +82 -0
- package/src/shell/commands/search.ts +264 -0
- package/src/shell/commands/shell-env.ts +352 -0
- package/src/shell/commands/text-processing.ts +1152 -0
- package/src/shell/commands/yarn.ts +84 -0
- package/src/shell/shell-builtins.ts +19 -0
- package/src/shell/shell-helpers.ts +250 -0
- package/src/shell/shell-interpreter.ts +514 -0
- package/src/shell/shell-parser.ts +429 -0
- package/src/shell/shell-types.ts +85 -0
- package/src/syntax-transforms.ts +561 -0
- package/src/threading/engine-worker.ts +64 -0
- package/src/threading/inline-worker.ts +372 -0
- package/src/threading/offload-types.ts +112 -0
- package/src/threading/offload-worker.ts +383 -0
- package/src/threading/offload.ts +271 -0
- package/src/threading/process-context.ts +92 -0
- package/src/threading/process-handle.ts +275 -0
- package/src/threading/process-manager.ts +956 -0
- package/src/threading/process-worker-entry.ts +854 -0
- package/src/threading/shared-vfs.ts +352 -0
- package/src/threading/sync-channel.ts +135 -0
- package/src/threading/task-queue.ts +177 -0
- package/src/threading/vfs-bridge.ts +231 -0
- package/src/threading/worker-pool.ts +233 -0
- package/src/threading/worker-protocol.ts +358 -0
- package/src/threading/worker-vfs.ts +218 -0
- package/src/types/externals.d.ts +38 -0
- package/src/types/fs-streams.ts +142 -0
- package/src/types/manifest.ts +17 -0
- package/src/worker-sandbox.ts +90 -0
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
// Per-process Web Worker entry point.
|
|
2
|
+
// Each worker gets its own MemoryVolume, ScriptEngine, and NodepodShell.
|
|
3
|
+
// I/O flows via postMessage using the protocol in worker-protocol.ts.
|
|
4
|
+
|
|
5
|
+
import { MemoryVolume } from "../memory-volume";
|
|
6
|
+
import { ScriptEngine, setChildProcessPolyfill } from "../script-engine";
|
|
7
|
+
import { SyncChannelWorker } from "./sync-channel";
|
|
8
|
+
import type {
|
|
9
|
+
MainToWorkerMessage,
|
|
10
|
+
MainToWorker_Init,
|
|
11
|
+
MainToWorker_Exec,
|
|
12
|
+
WorkerToMainMessage,
|
|
13
|
+
} from "./worker-protocol";
|
|
14
|
+
|
|
15
|
+
// --- Worker state ---
|
|
16
|
+
|
|
17
|
+
let _pid = 0;
|
|
18
|
+
let _cwd = "/";
|
|
19
|
+
let _env: Record<string, string> = {};
|
|
20
|
+
let _volume: MemoryVolume | null = null;
|
|
21
|
+
let _initialized = false;
|
|
22
|
+
let _abortController: AbortController | null = null;
|
|
23
|
+
|
|
24
|
+
// Prevents echo loops: suppress vfs-write/delete while applying inbound vfs-sync
|
|
25
|
+
let _suppressVFSWatch = false;
|
|
26
|
+
|
|
27
|
+
let _shellInitialized = false;
|
|
28
|
+
let _shellMod: typeof import("../polyfills/child_process") | null = null;
|
|
29
|
+
let _syncChannelWorker: SyncChannelWorker | null = null;
|
|
30
|
+
let _ipcMessageHandler: ((data: unknown) => void) | null = null;
|
|
31
|
+
let _cols = 80;
|
|
32
|
+
let _rows = 24;
|
|
33
|
+
|
|
34
|
+
const _spawnCallbacks = new Map<number, (result: any) => void>();
|
|
35
|
+
const _childOutputCallbacks = new Map<number, (stream: string, data: string) => void>();
|
|
36
|
+
const _childExitCallbacks = new Map<number, (exitCode: number, stdout: string, stderr: string) => void>();
|
|
37
|
+
const _ipcCallbacks = new Map<number, (data: unknown) => void>();
|
|
38
|
+
let _nextRequestId = 1;
|
|
39
|
+
|
|
40
|
+
// --- Post to main thread ---
|
|
41
|
+
|
|
42
|
+
function post(msg: WorkerToMainMessage, transfer?: Transferable[]): void {
|
|
43
|
+
(self as unknown as Worker).postMessage(msg, transfer ?? []);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function postStdout(data: string): void {
|
|
47
|
+
post({ type: "stdout", data });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function postStderr(data: string): void {
|
|
51
|
+
post({ type: "stderr", data });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function postExit(exitCode: number, stdout: string, stderr: string): void {
|
|
55
|
+
post({ type: "exit", exitCode, stdout, stderr });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function postError(message: string, stack?: string): void {
|
|
59
|
+
post({ type: "error", message, stack });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function postCwdChange(cwd: string): void {
|
|
63
|
+
post({ type: "cwd-change", cwd });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function postStdinRawStatus(isRaw: boolean): void {
|
|
67
|
+
post({ type: "stdin-raw-status", isRaw });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- Message handler ---
|
|
71
|
+
|
|
72
|
+
self.addEventListener("message", (ev: MessageEvent) => {
|
|
73
|
+
if (!ev?.data?.type) return;
|
|
74
|
+
const msg = ev.data as MainToWorkerMessage;
|
|
75
|
+
|
|
76
|
+
switch (msg.type) {
|
|
77
|
+
case "init":
|
|
78
|
+
handleInit(msg);
|
|
79
|
+
break;
|
|
80
|
+
case "exec":
|
|
81
|
+
handleExec(msg);
|
|
82
|
+
break;
|
|
83
|
+
case "stdin":
|
|
84
|
+
handleStdin(msg.data);
|
|
85
|
+
break;
|
|
86
|
+
case "signal":
|
|
87
|
+
handleSignal(msg);
|
|
88
|
+
break;
|
|
89
|
+
case "resize":
|
|
90
|
+
_cols = msg.cols;
|
|
91
|
+
_rows = msg.rows;
|
|
92
|
+
break;
|
|
93
|
+
case "vfs-sync":
|
|
94
|
+
handleVFSSync(msg);
|
|
95
|
+
break;
|
|
96
|
+
case "vfs-chunk":
|
|
97
|
+
handleVFSChunk(msg);
|
|
98
|
+
break;
|
|
99
|
+
case "spawn-result":
|
|
100
|
+
_spawnCallbacks.get(msg.requestId)?.(msg);
|
|
101
|
+
_spawnCallbacks.delete(msg.requestId);
|
|
102
|
+
break;
|
|
103
|
+
case "child-output": {
|
|
104
|
+
const cb = _childOutputCallbacks.get(msg.requestId);
|
|
105
|
+
if (cb) cb(msg.stream, msg.data);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "child-exit": {
|
|
109
|
+
const cb = _childExitCallbacks.get(msg.requestId);
|
|
110
|
+
if (cb) {
|
|
111
|
+
_childExitCallbacks.delete(msg.requestId);
|
|
112
|
+
_childOutputCallbacks.delete(msg.requestId);
|
|
113
|
+
cb(msg.exitCode, msg.stdout, msg.stderr);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "ipc-message": {
|
|
118
|
+
const ipcMsg = msg as any;
|
|
119
|
+
if (ipcMsg.targetRequestId !== undefined) {
|
|
120
|
+
// This worker is a PARENT — route to the fork callback for a child
|
|
121
|
+
const ipcCb = _ipcCallbacks.get(ipcMsg.targetRequestId);
|
|
122
|
+
if (ipcCb) ipcCb(ipcMsg.data);
|
|
123
|
+
} else {
|
|
124
|
+
// This worker IS a forked child — emit on process object
|
|
125
|
+
handleIPCMessage(ipcMsg.data);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "http-request":
|
|
130
|
+
handleHttpRequest(msg as any);
|
|
131
|
+
break;
|
|
132
|
+
case "ws-upgrade":
|
|
133
|
+
handleWsUpgrade(msg as any);
|
|
134
|
+
break;
|
|
135
|
+
case "ws-data":
|
|
136
|
+
handleWsData(msg as any);
|
|
137
|
+
break;
|
|
138
|
+
case "ws-close":
|
|
139
|
+
handleWsClose(msg as any);
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// --- Init ---
|
|
147
|
+
|
|
148
|
+
function handleInit(msg: MainToWorker_Init): void {
|
|
149
|
+
_pid = msg.pid;
|
|
150
|
+
_cwd = msg.cwd || "/";
|
|
151
|
+
_env = msg.env || {};
|
|
152
|
+
|
|
153
|
+
_volume = MemoryVolume.fromBinarySnapshot(msg.snapshot);
|
|
154
|
+
|
|
155
|
+
// Watch local writes → forward to main. Suppressed during inbound vfs-sync to prevent echo.
|
|
156
|
+
_volume.watch("/", { recursive: true }, (event, filename) => {
|
|
157
|
+
if (!filename || _suppressVFSWatch) return;
|
|
158
|
+
try {
|
|
159
|
+
if (_volume!.existsSync(filename)) {
|
|
160
|
+
const stat = _volume!.statSync(filename);
|
|
161
|
+
if (stat.isDirectory()) {
|
|
162
|
+
post({
|
|
163
|
+
type: "vfs-write",
|
|
164
|
+
path: filename,
|
|
165
|
+
content: new ArrayBuffer(0),
|
|
166
|
+
isDirectory: true,
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
const data = _volume!.readFileSync(filename);
|
|
170
|
+
const buffer = (data.buffer as ArrayBuffer).slice(
|
|
171
|
+
data.byteOffset,
|
|
172
|
+
data.byteOffset + data.byteLength,
|
|
173
|
+
);
|
|
174
|
+
post({ type: "vfs-write", path: filename, content: buffer, isDirectory: false }, [buffer]);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
post({ type: "vfs-delete", path: filename });
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
/* ignore */
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (msg.syncBuffer) {
|
|
185
|
+
_syncChannelWorker = new SyncChannelWorker(msg.syncBuffer);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
_initialized = true;
|
|
189
|
+
post({ type: "ready", pid: _pid });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Shell init (lazy) ---
|
|
193
|
+
|
|
194
|
+
async function ensureShell(): Promise<typeof import("../polyfills/child_process")> {
|
|
195
|
+
if (!_shellMod) {
|
|
196
|
+
_shellMod = await import("../polyfills/child_process");
|
|
197
|
+
// Must be eager — sync require('child_process') needs this before any microtask fires
|
|
198
|
+
setChildProcessPolyfill(_shellMod);
|
|
199
|
+
}
|
|
200
|
+
if (!_shellInitialized && _volume) {
|
|
201
|
+
_shellMod.initShellExec(_volume, { cwd: _cwd, env: _env });
|
|
202
|
+
if (_syncChannelWorker) {
|
|
203
|
+
_shellMod.setSyncChannel(_syncChannelWorker);
|
|
204
|
+
}
|
|
205
|
+
_shellMod.setSpawnChildCallback(spawnChild);
|
|
206
|
+
_shellMod.setForkChildCallback(forkChild);
|
|
207
|
+
const wtMod = await import("../polyfills/worker_threads");
|
|
208
|
+
wtMod.setWorkerThreadForkCallback(workerThreadFork);
|
|
209
|
+
const httpMod = await import("../polyfills/http");
|
|
210
|
+
httpMod.setServerListenCallback((port: number) => {
|
|
211
|
+
post({ type: "server-listen", port, hostname: "0.0.0.0" });
|
|
212
|
+
});
|
|
213
|
+
httpMod.setServerCloseCallback((port: number) => {
|
|
214
|
+
post({ type: "server-close", port });
|
|
215
|
+
});
|
|
216
|
+
_shellInitialized = true;
|
|
217
|
+
}
|
|
218
|
+
return _shellMod;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- Exec ---
|
|
222
|
+
|
|
223
|
+
async function handleExec(msg: MainToWorker_Exec): Promise<void> {
|
|
224
|
+
if (!_volume) {
|
|
225
|
+
postStderr("Error: VFS not initialized\n");
|
|
226
|
+
postExit(1, "", "Error: VFS not initialized\n");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
_abortController = new AbortController();
|
|
231
|
+
|
|
232
|
+
if (msg.cwd) {
|
|
233
|
+
_cwd = msg.cwd;
|
|
234
|
+
// Must sync shell cwd too, otherwise it keeps its old cwd
|
|
235
|
+
if (_shellMod) _shellMod.setShellCwd(msg.cwd);
|
|
236
|
+
}
|
|
237
|
+
if (msg.env) Object.assign(_env, msg.env);
|
|
238
|
+
|
|
239
|
+
if (msg.isShell) {
|
|
240
|
+
await handleShellExec(msg);
|
|
241
|
+
} else {
|
|
242
|
+
await handleFileExec(msg);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- Shell command execution ---
|
|
247
|
+
|
|
248
|
+
async function handleShellExec(msg: MainToWorker_Exec): Promise<void> {
|
|
249
|
+
const shellCmd = msg.shellCommand || "";
|
|
250
|
+
const persistent = !!msg.persistent;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const shell = await ensureShell();
|
|
254
|
+
|
|
255
|
+
shell.setStreamingCallbacks({
|
|
256
|
+
onStdout: postStdout,
|
|
257
|
+
onStderr: postStderr,
|
|
258
|
+
signal: _abortController!.signal,
|
|
259
|
+
getCols: () => _cols,
|
|
260
|
+
getRows: () => _rows,
|
|
261
|
+
onRawModeChange: postStdinRawStatus,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// shellExec() NOT child_process.exec() — the latter spawns a new worker, causing recursion
|
|
265
|
+
shell.shellExec(shellCmd, {}, (error, stdout, stderr) => {
|
|
266
|
+
// Don't clearStreamingCallbacks — background children (e.g. vite dev)
|
|
267
|
+
// may still need the output sinks. Cleaned up on worker termination.
|
|
268
|
+
|
|
269
|
+
const newCwd = shell.getShellCwd();
|
|
270
|
+
if (newCwd !== _cwd) {
|
|
271
|
+
_cwd = newCwd;
|
|
272
|
+
postCwdChange(newCwd);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
postStdinRawStatus(false);
|
|
276
|
+
|
|
277
|
+
const exitCode = error ? ((error as any).code ?? 1) : 0;
|
|
278
|
+
const outStr = String(stdout ?? "");
|
|
279
|
+
const errStr = String(stderr ?? "");
|
|
280
|
+
|
|
281
|
+
if (persistent) {
|
|
282
|
+
post({ type: "shell-done", exitCode, stdout: outStr, stderr: errStr });
|
|
283
|
+
} else {
|
|
284
|
+
postExit(exitCode, outStr, errStr);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
} catch (e: any) {
|
|
288
|
+
postError(e?.message || String(e), e?.stack);
|
|
289
|
+
if (persistent) {
|
|
290
|
+
post({ type: "shell-done", exitCode: 1, stdout: "", stderr: e?.message || String(e) });
|
|
291
|
+
} else {
|
|
292
|
+
postExit(1, "", e?.message || String(e));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- IPC message handler ---
|
|
298
|
+
|
|
299
|
+
function handleIPCMessage(data: unknown): void {
|
|
300
|
+
if (_shellMod) {
|
|
301
|
+
_shellMod.handleIPCFromParent(data);
|
|
302
|
+
} else if (_ipcMessageHandler) {
|
|
303
|
+
_ipcMessageHandler(data);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// --- File execution (node script.js) ---
|
|
308
|
+
|
|
309
|
+
async function handleFileExec(msg: MainToWorker_Exec): Promise<void> {
|
|
310
|
+
const filePath = msg.filePath;
|
|
311
|
+
const args = msg.args || [];
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const shell = await ensureShell();
|
|
315
|
+
|
|
316
|
+
// Enable IPC for forks: process.send() → postMessage to main
|
|
317
|
+
if (msg.isFork) {
|
|
318
|
+
shell.setIPCSend((data: unknown) => {
|
|
319
|
+
post({ type: "ipc-message", data });
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let workerThreadsOverride: {
|
|
324
|
+
isMainThread: boolean;
|
|
325
|
+
parentPort: unknown;
|
|
326
|
+
workerData: unknown;
|
|
327
|
+
threadId: number;
|
|
328
|
+
} | undefined;
|
|
329
|
+
|
|
330
|
+
if (msg.isWorkerThread) {
|
|
331
|
+
const { MessagePort } = await import("../polyfills/worker_threads");
|
|
332
|
+
const pp = new MessagePort();
|
|
333
|
+
pp.postMessage = (data: unknown) => {
|
|
334
|
+
post({ type: "ipc-message", data });
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
shell.setIPCReceiveHandler((data: unknown) => {
|
|
338
|
+
pp.emit("message", data);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
workerThreadsOverride = {
|
|
342
|
+
isMainThread: false,
|
|
343
|
+
parentPort: pp,
|
|
344
|
+
workerData: msg.workerData ?? null,
|
|
345
|
+
threadId: msg.threadId ?? 0,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
shell.setStreamingCallbacks({
|
|
350
|
+
onStdout: postStdout,
|
|
351
|
+
onStderr: postStderr,
|
|
352
|
+
signal: _abortController!.signal,
|
|
353
|
+
getCols: () => _cols,
|
|
354
|
+
getRows: () => _rows,
|
|
355
|
+
onRawModeChange: postStdinRawStatus,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const ctx = {
|
|
359
|
+
cwd: _cwd,
|
|
360
|
+
env: _env,
|
|
361
|
+
volume: _volume!,
|
|
362
|
+
exec: async (cmd: string, opts?: { cwd?: string; env?: Record<string, string> }) => {
|
|
363
|
+
// Needed for ShellContext type compat
|
|
364
|
+
return new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve) => {
|
|
365
|
+
shell.exec(cmd, opts ?? {}, (error: any, stdout: any, stderr: any) => {
|
|
366
|
+
resolve({
|
|
367
|
+
stdout: String(stdout ?? ""),
|
|
368
|
+
stderr: String(stderr ?? ""),
|
|
369
|
+
exitCode: error ? (error.code ?? 1) : 0,
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const result = await shell.executeNodeBinary(filePath, args, ctx, {
|
|
377
|
+
isFork: !!msg.isFork,
|
|
378
|
+
workerThreadsOverride,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
shell.clearStreamingCallbacks();
|
|
382
|
+
postExit(result.exitCode, result.stdout, result.stderr);
|
|
383
|
+
} catch (e: any) {
|
|
384
|
+
// Safety net — executeNodeBinary handles its own errors, but just in case:
|
|
385
|
+
const errMsg = e?.message || String(e);
|
|
386
|
+
postStderr(`Error: ${errMsg}\n`);
|
|
387
|
+
postExit(1, "", errMsg);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// --- Stdin ---
|
|
392
|
+
|
|
393
|
+
function handleStdin(data: string): void {
|
|
394
|
+
if (!_shellMod) return;
|
|
395
|
+
try {
|
|
396
|
+
_shellMod.sendStdin(data);
|
|
397
|
+
} catch {
|
|
398
|
+
/* ignore */
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// --- Signal ---
|
|
403
|
+
|
|
404
|
+
function handleSignal(msg: { signal: string }): void {
|
|
405
|
+
if (msg.signal === "SIGINT" || msg.signal === "SIGTERM") {
|
|
406
|
+
if (_abortController) {
|
|
407
|
+
_abortController.abort();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// --- VFS sync ---
|
|
413
|
+
|
|
414
|
+
function handleVFSSync(msg: { path: string; content: ArrayBuffer | null; isDirectory: boolean }): void {
|
|
415
|
+
if (!_volume) return;
|
|
416
|
+
// Suppress watcher — these came from another worker, don't echo back
|
|
417
|
+
_suppressVFSWatch = true;
|
|
418
|
+
try {
|
|
419
|
+
if (msg.content === null) {
|
|
420
|
+
if (_volume.existsSync(msg.path)) {
|
|
421
|
+
const stat = _volume.statSync(msg.path);
|
|
422
|
+
if (stat.isDirectory()) {
|
|
423
|
+
_volume.rmdirSync(msg.path);
|
|
424
|
+
} else {
|
|
425
|
+
_volume.unlinkSync(msg.path);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
} else if (msg.isDirectory) {
|
|
429
|
+
if (!_volume.existsSync(msg.path)) {
|
|
430
|
+
_volume.mkdirSync(msg.path, { recursive: true });
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
const parentDir = msg.path.substring(0, msg.path.lastIndexOf("/")) || "/";
|
|
434
|
+
if (parentDir !== "/" && !_volume.existsSync(parentDir)) {
|
|
435
|
+
_volume.mkdirSync(parentDir, { recursive: true });
|
|
436
|
+
}
|
|
437
|
+
_volume.writeFileSync(msg.path, new Uint8Array(msg.content));
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
/* ignore */
|
|
441
|
+
} finally {
|
|
442
|
+
_suppressVFSWatch = false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let _vfsChunks: Array<{ data: ArrayBuffer; manifest: Array<{ path: string; offset: number; length: number; isDirectory: boolean }> } | null> = [];
|
|
447
|
+
|
|
448
|
+
function handleVFSChunk(msg: { chunkIndex: number; totalChunks: number; data: ArrayBuffer; manifest: Array<{ path: string; offset: number; length: number; isDirectory: boolean }> }): void {
|
|
449
|
+
_vfsChunks[msg.chunkIndex] = { data: msg.data, manifest: msg.manifest };
|
|
450
|
+
const received = _vfsChunks.filter(Boolean).length;
|
|
451
|
+
if (received === msg.totalChunks) {
|
|
452
|
+
// All chunks received — apply to volume
|
|
453
|
+
_suppressVFSWatch = true;
|
|
454
|
+
try {
|
|
455
|
+
if (_volume) {
|
|
456
|
+
for (const chunk of _vfsChunks) {
|
|
457
|
+
if (!chunk) continue;
|
|
458
|
+
const data = new Uint8Array(chunk.data);
|
|
459
|
+
for (const entry of chunk.manifest) {
|
|
460
|
+
if (entry.isDirectory) {
|
|
461
|
+
if (!_volume.existsSync(entry.path)) {
|
|
462
|
+
_volume.mkdirSync(entry.path, { recursive: true });
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
const parentDir = entry.path.substring(0, entry.path.lastIndexOf("/")) || "/";
|
|
466
|
+
if (parentDir !== "/" && !_volume.existsSync(parentDir)) {
|
|
467
|
+
_volume.mkdirSync(parentDir, { recursive: true });
|
|
468
|
+
}
|
|
469
|
+
_volume.writeFileSync(entry.path, data.slice(entry.offset, entry.offset + entry.length));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} finally {
|
|
475
|
+
_suppressVFSWatch = false;
|
|
476
|
+
}
|
|
477
|
+
_vfsChunks = [];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// --- HTTP request dispatch ---
|
|
482
|
+
|
|
483
|
+
async function handleHttpRequest(msg: {
|
|
484
|
+
requestId: number;
|
|
485
|
+
port: number;
|
|
486
|
+
method: string;
|
|
487
|
+
path: string;
|
|
488
|
+
headers: Record<string, string>;
|
|
489
|
+
body: string | null;
|
|
490
|
+
}): Promise<void> {
|
|
491
|
+
try {
|
|
492
|
+
const httpMod = await import("../polyfills/http");
|
|
493
|
+
const server = httpMod.getServer(msg.port);
|
|
494
|
+
if (!server) {
|
|
495
|
+
post({
|
|
496
|
+
type: "http-response",
|
|
497
|
+
requestId: msg.requestId,
|
|
498
|
+
statusCode: 503,
|
|
499
|
+
statusMessage: "Service Unavailable",
|
|
500
|
+
headers: { "Content-Type": "text/plain" },
|
|
501
|
+
body: `No server on port ${msg.port}`,
|
|
502
|
+
} as any);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const { Buffer } = await import("../polyfills/buffer");
|
|
507
|
+
const bodyBuf = msg.body ? Buffer.from(msg.body) : undefined;
|
|
508
|
+
const result = await server.dispatchRequest(
|
|
509
|
+
msg.method,
|
|
510
|
+
msg.path,
|
|
511
|
+
msg.headers,
|
|
512
|
+
bodyBuf,
|
|
513
|
+
);
|
|
514
|
+
// Can't transfer Buffer via postMessage, convert to string
|
|
515
|
+
let bodyStr = "";
|
|
516
|
+
if (result.body) {
|
|
517
|
+
if (typeof result.body === "string") {
|
|
518
|
+
bodyStr = result.body;
|
|
519
|
+
} else if (result.body instanceof Uint8Array || Buffer.isBuffer(result.body)) {
|
|
520
|
+
bodyStr = new TextDecoder().decode(result.body);
|
|
521
|
+
} else {
|
|
522
|
+
bodyStr = String(result.body);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
post({
|
|
527
|
+
type: "http-response",
|
|
528
|
+
requestId: msg.requestId,
|
|
529
|
+
statusCode: result.statusCode,
|
|
530
|
+
statusMessage: result.statusMessage,
|
|
531
|
+
headers: result.headers,
|
|
532
|
+
body: bodyStr,
|
|
533
|
+
} as any);
|
|
534
|
+
} catch (e: any) {
|
|
535
|
+
console.error(`[DEBUG] handleHttpRequest caught:`, e?.message, "\nStack:", e?.stack?.split?.("\n")?.slice(0, 8)?.join("\n"));
|
|
536
|
+
post({
|
|
537
|
+
type: "http-response",
|
|
538
|
+
requestId: msg.requestId,
|
|
539
|
+
statusCode: 500,
|
|
540
|
+
statusMessage: "Internal Server Error",
|
|
541
|
+
headers: { "Content-Type": "text/plain" },
|
|
542
|
+
body: e?.message || "Internal Server Error",
|
|
543
|
+
} as any);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// --- WebSocket upgrade handling ---
|
|
548
|
+
|
|
549
|
+
// Active WS connections: uid → socket (TcpSocket in the worker)
|
|
550
|
+
const _wsConnections = new Map<string, any>();
|
|
551
|
+
|
|
552
|
+
async function handleWsUpgrade(msg: {
|
|
553
|
+
uid: string;
|
|
554
|
+
port: number;
|
|
555
|
+
path: string;
|
|
556
|
+
headers: Record<string, string>;
|
|
557
|
+
}): Promise<void> {
|
|
558
|
+
try {
|
|
559
|
+
const httpMod = await import("../polyfills/http");
|
|
560
|
+
const { encodeFrame, decodeFrame } = httpMod;
|
|
561
|
+
const { Buffer } = await import("../polyfills/buffer");
|
|
562
|
+
|
|
563
|
+
const server = httpMod.getServer(msg.port);
|
|
564
|
+
if (!server) {
|
|
565
|
+
post({ type: "ws-frame", uid: msg.uid, kind: "error", message: `No server on port ${msg.port}` } as any);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const { socket } = server.dispatchUpgrade(msg.path || "/", msg.headers);
|
|
570
|
+
|
|
571
|
+
let outboundBuf = new Uint8Array(0);
|
|
572
|
+
let handshakeDone = false;
|
|
573
|
+
|
|
574
|
+
// Intercept socket.write to decode WS frames and relay to main thread
|
|
575
|
+
socket.write = ((
|
|
576
|
+
chunk: Uint8Array | string,
|
|
577
|
+
encOrCb?: string | ((err?: Error | null) => void),
|
|
578
|
+
cb?: (err?: Error | null) => void,
|
|
579
|
+
): boolean => {
|
|
580
|
+
const raw = typeof chunk === "string" ? Buffer.from(chunk) : new Uint8Array(chunk);
|
|
581
|
+
const fn = typeof encOrCb === "function" ? encOrCb : cb;
|
|
582
|
+
|
|
583
|
+
if (!handshakeDone) {
|
|
584
|
+
const text = new TextDecoder().decode(raw);
|
|
585
|
+
if (text.startsWith("HTTP/1.1 101")) {
|
|
586
|
+
handshakeDone = true;
|
|
587
|
+
post({ type: "ws-frame", uid: msg.uid, kind: "open" } as any);
|
|
588
|
+
if (fn) queueMicrotask(() => fn(null));
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const merged = new Uint8Array(outboundBuf.length + raw.length);
|
|
594
|
+
merged.set(outboundBuf, 0);
|
|
595
|
+
merged.set(raw, outboundBuf.length);
|
|
596
|
+
outboundBuf = merged;
|
|
597
|
+
|
|
598
|
+
while (outboundBuf.length >= 2) {
|
|
599
|
+
const frame = decodeFrame(outboundBuf);
|
|
600
|
+
if (!frame) break;
|
|
601
|
+
outboundBuf = outboundBuf.slice(frame.consumed);
|
|
602
|
+
|
|
603
|
+
switch (frame.op) {
|
|
604
|
+
case 0x01: { // TEXT
|
|
605
|
+
const text = new TextDecoder().decode(frame.data);
|
|
606
|
+
post({ type: "ws-frame", uid: msg.uid, kind: "text", data: text } as any);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case 0x02: // BINARY
|
|
610
|
+
post({ type: "ws-frame", uid: msg.uid, kind: "binary", bytes: Array.from(frame.data) } as any);
|
|
611
|
+
break;
|
|
612
|
+
case 0x08: { // CLOSE
|
|
613
|
+
const code = frame.data.length >= 2 ? (frame.data[0] << 8) | frame.data[1] : 1000;
|
|
614
|
+
post({ type: "ws-frame", uid: msg.uid, kind: "close", code } as any);
|
|
615
|
+
_wsConnections.delete(msg.uid);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
case 0x09: // PING — send pong back
|
|
619
|
+
socket._feedData(Buffer.from(encodeFrame(0x0A, frame.data, true)));
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (fn) queueMicrotask(() => fn(null));
|
|
625
|
+
return true;
|
|
626
|
+
}) as any;
|
|
627
|
+
|
|
628
|
+
_wsConnections.set(msg.uid, socket);
|
|
629
|
+
} catch (e: any) {
|
|
630
|
+
post({ type: "ws-frame", uid: msg.uid, kind: "error", message: e?.message || "WS upgrade failed" } as any);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function handleWsData(msg: { uid: string; frame: number[] }): void {
|
|
635
|
+
const socket = _wsConnections.get(msg.uid);
|
|
636
|
+
if (!socket) return;
|
|
637
|
+
try {
|
|
638
|
+
const { Buffer } = require("../polyfills/buffer");
|
|
639
|
+
socket._feedData(Buffer.from(new Uint8Array(msg.frame)));
|
|
640
|
+
} catch { /* ignore */ }
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function handleWsClose(msg: { uid: string; code: number }): void {
|
|
644
|
+
const socket = _wsConnections.get(msg.uid);
|
|
645
|
+
if (!socket) return;
|
|
646
|
+
try {
|
|
647
|
+
const codeBuf = new Uint8Array(2);
|
|
648
|
+
codeBuf[0] = (msg.code >> 8) & 0xff;
|
|
649
|
+
codeBuf[1] = msg.code & 0xff;
|
|
650
|
+
const { encodeFrame } = require("../polyfills/http");
|
|
651
|
+
const { Buffer } = require("../polyfills/buffer");
|
|
652
|
+
socket._feedData(Buffer.from(encodeFrame(0x08, codeBuf, true)));
|
|
653
|
+
} catch { /* ignore */ }
|
|
654
|
+
try { socket.destroy(); } catch { /* ignore */ }
|
|
655
|
+
_wsConnections.delete(msg.uid);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// --- Child process spawning ---
|
|
659
|
+
|
|
660
|
+
// Forks a child process via main thread. Returns IPC handles immediately;
|
|
661
|
+
// output and exit arrive via callbacks.
|
|
662
|
+
export function forkChild(
|
|
663
|
+
modulePath: string,
|
|
664
|
+
args: string[],
|
|
665
|
+
opts: {
|
|
666
|
+
cwd: string;
|
|
667
|
+
env: Record<string, string>;
|
|
668
|
+
onStdout?: (data: string) => void;
|
|
669
|
+
onStderr?: (data: string) => void;
|
|
670
|
+
onIPC?: (data: unknown) => void;
|
|
671
|
+
onExit?: (exitCode: number) => void;
|
|
672
|
+
},
|
|
673
|
+
): { sendIPC: (data: unknown) => void; disconnect: () => void; requestId: number } {
|
|
674
|
+
const requestId = _nextRequestId++;
|
|
675
|
+
|
|
676
|
+
if (opts.onIPC) {
|
|
677
|
+
_ipcCallbacks.set(requestId, opts.onIPC);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
_spawnCallbacks.set(requestId, (result: any) => {
|
|
681
|
+
if (result.error) {
|
|
682
|
+
_ipcCallbacks.delete(requestId);
|
|
683
|
+
opts.onStderr?.(`Fork error: ${result.error}\n`);
|
|
684
|
+
opts.onExit?.(1);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
_childOutputCallbacks.set(requestId, (stream: string, data: string) => {
|
|
689
|
+
if (stream === "stdout") {
|
|
690
|
+
opts.onStdout?.(data);
|
|
691
|
+
} else {
|
|
692
|
+
opts.onStderr?.(data);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
_childExitCallbacks.set(requestId, (exitCode: number) => {
|
|
697
|
+
_ipcCallbacks.delete(requestId);
|
|
698
|
+
opts.onExit?.(exitCode);
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
post({
|
|
703
|
+
type: "fork-request",
|
|
704
|
+
requestId,
|
|
705
|
+
modulePath,
|
|
706
|
+
args,
|
|
707
|
+
cwd: opts.cwd,
|
|
708
|
+
env: opts.env,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
requestId,
|
|
713
|
+
sendIPC: (data: unknown) => {
|
|
714
|
+
post({
|
|
715
|
+
type: "ipc-message",
|
|
716
|
+
targetRequestId: requestId,
|
|
717
|
+
data,
|
|
718
|
+
});
|
|
719
|
+
},
|
|
720
|
+
disconnect: () => {
|
|
721
|
+
_ipcCallbacks.delete(requestId);
|
|
722
|
+
_childOutputCallbacks.delete(requestId);
|
|
723
|
+
_childExitCallbacks.delete(requestId);
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Same as forkChild but for worker_threads — posts "workerthread-request" with workerData/threadId
|
|
729
|
+
function workerThreadFork(
|
|
730
|
+
modulePath: string,
|
|
731
|
+
opts: {
|
|
732
|
+
workerData: unknown;
|
|
733
|
+
threadId: number;
|
|
734
|
+
isEval?: boolean;
|
|
735
|
+
cwd: string;
|
|
736
|
+
env: Record<string, string>;
|
|
737
|
+
onMessage: (data: unknown) => void;
|
|
738
|
+
onError: (err: Error) => void;
|
|
739
|
+
onExit: (code: number) => void;
|
|
740
|
+
onStdout?: (data: string) => void;
|
|
741
|
+
onStderr?: (data: string) => void;
|
|
742
|
+
},
|
|
743
|
+
): { postMessage: (data: unknown) => void; terminate: () => void; requestId: number } {
|
|
744
|
+
const requestId = _nextRequestId++;
|
|
745
|
+
|
|
746
|
+
_ipcCallbacks.set(requestId, (data: unknown) => {
|
|
747
|
+
opts.onMessage(data);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
_spawnCallbacks.set(requestId, (result: any) => {
|
|
751
|
+
if (result.error) {
|
|
752
|
+
_ipcCallbacks.delete(requestId);
|
|
753
|
+
opts.onError(new Error(result.error));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
_childOutputCallbacks.set(requestId, (stream: string, data: string) => {
|
|
758
|
+
if (stream === "stdout") {
|
|
759
|
+
opts.onStdout?.(data);
|
|
760
|
+
} else {
|
|
761
|
+
opts.onStderr?.(data);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
_childExitCallbacks.set(requestId, (exitCode: number) => {
|
|
766
|
+
_ipcCallbacks.delete(requestId);
|
|
767
|
+
opts.onExit(exitCode);
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
post({
|
|
772
|
+
type: "workerthread-request",
|
|
773
|
+
requestId,
|
|
774
|
+
modulePath,
|
|
775
|
+
isEval: opts.isEval,
|
|
776
|
+
args: [],
|
|
777
|
+
cwd: opts.cwd,
|
|
778
|
+
env: opts.env,
|
|
779
|
+
workerData: opts.workerData,
|
|
780
|
+
threadId: opts.threadId,
|
|
781
|
+
} as any);
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
requestId,
|
|
785
|
+
postMessage: (data: unknown) => {
|
|
786
|
+
post({
|
|
787
|
+
type: "ipc-message",
|
|
788
|
+
targetRequestId: requestId,
|
|
789
|
+
data,
|
|
790
|
+
});
|
|
791
|
+
},
|
|
792
|
+
terminate: () => {
|
|
793
|
+
_ipcCallbacks.delete(requestId);
|
|
794
|
+
_childOutputCallbacks.delete(requestId);
|
|
795
|
+
_childExitCallbacks.delete(requestId);
|
|
796
|
+
},
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Spawns a child process via main thread. Resolves when child exits.
|
|
801
|
+
// onStdout/onStderr fire in real-time as child-output messages arrive.
|
|
802
|
+
export function spawnChild(
|
|
803
|
+
command: string,
|
|
804
|
+
args: string[],
|
|
805
|
+
opts?: {
|
|
806
|
+
cwd?: string;
|
|
807
|
+
env?: Record<string, string>;
|
|
808
|
+
stdio?: "pipe" | "inherit";
|
|
809
|
+
onStdout?: (data: string) => void;
|
|
810
|
+
onStderr?: (data: string) => void;
|
|
811
|
+
},
|
|
812
|
+
): Promise<{ pid: number; exitCode: number; stdout: string; stderr: string }> {
|
|
813
|
+
return new Promise((resolve, reject) => {
|
|
814
|
+
const requestId = _nextRequestId++;
|
|
815
|
+
let stdout = "";
|
|
816
|
+
let stderr = "";
|
|
817
|
+
|
|
818
|
+
_spawnCallbacks.set(requestId, (result: any) => {
|
|
819
|
+
if (result.error) {
|
|
820
|
+
reject(new Error(result.error));
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
_childOutputCallbacks.set(requestId, (stream: string, data: string) => {
|
|
825
|
+
if (stream === "stdout") {
|
|
826
|
+
stdout += data;
|
|
827
|
+
opts?.onStdout?.(data);
|
|
828
|
+
} else {
|
|
829
|
+
stderr += data;
|
|
830
|
+
opts?.onStderr?.(data);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
_childExitCallbacks.set(requestId, (exitCode: number, fullStdout: string, fullStderr: string) => {
|
|
835
|
+
resolve({
|
|
836
|
+
pid: result.pid,
|
|
837
|
+
exitCode,
|
|
838
|
+
stdout: fullStdout || stdout,
|
|
839
|
+
stderr: fullStderr || stderr,
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
post({
|
|
845
|
+
type: "spawn-request",
|
|
846
|
+
requestId,
|
|
847
|
+
command,
|
|
848
|
+
args,
|
|
849
|
+
cwd: opts?.cwd || _cwd,
|
|
850
|
+
env: opts?.env || _env,
|
|
851
|
+
stdio: opts?.stdio || "pipe",
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
}
|