This commit is contained in:
Sean McLellan 2026-01-30 00:39:04 -06:00 committed by GitHub
commit 6c6325527f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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,
});
}
}
}