Merge 52ffd744f7 into 9025da2296
This commit is contained in:
commit
6c6325527f
@ -1,5 +1,10 @@
|
||||
import type { ChildProcess, SpawnOptions } from "node:child_process";
|
||||
import { spawn } from "node:child_process";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { PassThrough } from "node:stream";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
export type SpawnFallback = {
|
||||
label: string;
|
||||
@ -49,12 +54,122 @@ function shouldRetry(err: unknown, codes: string[]): boolean {
|
||||
return code.length > 0 && codes.includes(code);
|
||||
}
|
||||
|
||||
// Fake child process interface for EBADF workaround
|
||||
interface FakeChildProcess extends EventEmitter {
|
||||
stdout: PassThrough;
|
||||
stderr: PassThrough;
|
||||
stdin: PassThrough;
|
||||
pid: number;
|
||||
killed: boolean;
|
||||
kill: () => boolean;
|
||||
}
|
||||
|
||||
// EBADF workaround: capture output via temp files, use stdio: ignore for spawn
|
||||
function createFakeChildFromSync(argv: string[], options: SpawnOptions): ChildProcess {
|
||||
const fakeChild: FakeChildProcess = Object.assign(new EventEmitter(), {
|
||||
stdout: new PassThrough(),
|
||||
stderr: new PassThrough(),
|
||||
stdin: new PassThrough(),
|
||||
pid: process.pid,
|
||||
killed: false,
|
||||
kill: () => {
|
||||
fakeChild.killed = true;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const child = fakeChild as unknown as ChildProcess;
|
||||
|
||||
// Extract command from argv (typically [shell, "-c", command])
|
||||
const command = argv.length >= 3 ? argv[2] : argv.join(" ");
|
||||
|
||||
// Create temp files for output capture
|
||||
const tmpDir = os.tmpdir();
|
||||
const id = `clawdbot-ebadf-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
const stdoutFile = path.join(tmpDir, `${id}.stdout`);
|
||||
const stderrFile = path.join(tmpDir, `${id}.stderr`);
|
||||
|
||||
// Wrap command to redirect output to files
|
||||
const wrappedCommand = `( ${command} ) > "${stdoutFile}" 2> "${stderrFile}"`;
|
||||
|
||||
setImmediate(() => {
|
||||
try {
|
||||
// spawnSync with stdio: ignore - no pipes needed
|
||||
const result = spawnSync("/bin/sh", ["-c", wrappedCommand], {
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 300000,
|
||||
stdio: "ignore",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
// Clean up temp files
|
||||
try {
|
||||
fs.unlinkSync(stdoutFile);
|
||||
} catch {}
|
||||
try {
|
||||
fs.unlinkSync(stderrFile);
|
||||
} catch {}
|
||||
fakeChild.emit("error", result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
fakeChild.pid = result.pid || process.pid;
|
||||
|
||||
// Read output from temp files
|
||||
try {
|
||||
const stdoutData = fs.readFileSync(stdoutFile, "utf8");
|
||||
if (stdoutData) fakeChild.stdout.write(stdoutData);
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const stderrData = fs.readFileSync(stderrFile, "utf8");
|
||||
if (stderrData) fakeChild.stderr.write(stderrData);
|
||||
} catch {}
|
||||
|
||||
// Clean up temp files
|
||||
try {
|
||||
fs.unlinkSync(stdoutFile);
|
||||
} catch {}
|
||||
try {
|
||||
fs.unlinkSync(stderrFile);
|
||||
} catch {}
|
||||
|
||||
fakeChild.stdout.end();
|
||||
fakeChild.stderr.end();
|
||||
fakeChild.emit("close", result.status, result.signal);
|
||||
} catch (err) {
|
||||
// Clean up temp files
|
||||
try {
|
||||
fs.unlinkSync(stdoutFile);
|
||||
} catch {}
|
||||
try {
|
||||
fs.unlinkSync(stderrFile);
|
||||
} catch {}
|
||||
fakeChild.emit("error", err);
|
||||
}
|
||||
});
|
||||
|
||||
// Emit spawn immediately
|
||||
process.nextTick(() => {
|
||||
fakeChild.emit("spawn");
|
||||
});
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
async function spawnAndWaitForSpawn(
|
||||
spawnImpl: typeof spawn,
|
||||
argv: string[],
|
||||
options: SpawnOptions,
|
||||
useSyncFallback = false,
|
||||
): Promise<ChildProcess> {
|
||||
const child = spawnImpl(argv[0], argv.slice(1), options);
|
||||
let child: ChildProcess;
|
||||
|
||||
if (useSyncFallback) {
|
||||
child = createFakeChildFromSync(argv, options);
|
||||
} else {
|
||||
child = spawnImpl(argv[0], argv.slice(1), options);
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
@ -95,19 +210,27 @@ export async function spawnWithFallback(
|
||||
const retryCodes = params.retryCodes ?? DEFAULT_RETRY_CODES;
|
||||
const baseOptions = { ...params.options };
|
||||
const fallbacks = params.fallbacks ?? [];
|
||||
const attempts: Array<{ label?: string; options: SpawnOptions }> = [
|
||||
{ options: baseOptions },
|
||||
const attempts: Array<{ label?: string; options: SpawnOptions; useSync?: boolean }> = [
|
||||
{ options: baseOptions, useSync: false },
|
||||
...fallbacks.map((fallback) => ({
|
||||
label: fallback.label,
|
||||
options: { ...baseOptions, ...fallback.options },
|
||||
useSync: false,
|
||||
})),
|
||||
// Final EBADF fallback: spawnSync with stdio:ignore + file capture
|
||||
{ label: "file-capture", options: baseOptions, useSync: true },
|
||||
];
|
||||
|
||||
let lastError: unknown;
|
||||
for (let index = 0; index < attempts.length; index += 1) {
|
||||
const attempt = attempts[index];
|
||||
try {
|
||||
const child = await spawnAndWaitForSpawn(spawnImpl, params.argv, attempt.options);
|
||||
const child = await spawnAndWaitForSpawn(
|
||||
spawnImpl,
|
||||
params.argv,
|
||||
attempt.options,
|
||||
attempt.useSync,
|
||||
);
|
||||
return {
|
||||
child,
|
||||
usedFallback: index > 0,
|
||||
@ -115,11 +238,16 @@ export async function spawnWithFallback(
|
||||
};
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
const nextFallback = fallbacks[index];
|
||||
if (!nextFallback || !shouldRetry(err, retryCodes)) {
|
||||
const nextAttempt = attempts[index + 1];
|
||||
if (!nextAttempt || !shouldRetry(err, retryCodes)) {
|
||||
throw err;
|
||||
}
|
||||
params.onFallback?.(err, nextFallback);
|
||||
if (nextAttempt.label) {
|
||||
params.onFallback?.(err, {
|
||||
label: nextAttempt.label,
|
||||
options: nextAttempt.options,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user