fix(spawn): add EBADF fallback using temp files (fixes #3556)
This commit is contained in:
parent
109ac1c549
commit
0cc9ed8b65
@ -1,5 +1,10 @@
|
|||||||
import type { ChildProcess, SpawnOptions } from "node:child_process";
|
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 = {
|
export type SpawnFallback = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -49,12 +54,112 @@ function shouldRetry(err: unknown, codes: string[]): boolean {
|
|||||||
return code.length > 0 && codes.includes(code);
|
return code.length > 0 && codes.includes(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EBADF workaround: capture output via temp files, use stdio: ignore for spawn
|
||||||
|
function createFakeChildFromSync(argv: string[], options: SpawnOptions): ChildProcess {
|
||||||
|
const child = new EventEmitter() as unknown as ChildProcess;
|
||||||
|
const stdout = new PassThrough();
|
||||||
|
const stderr = new PassThrough();
|
||||||
|
|
||||||
|
child.stdout = stdout;
|
||||||
|
child.stderr = stderr;
|
||||||
|
child.stdin = new PassThrough();
|
||||||
|
child.pid = process.pid;
|
||||||
|
child.killed = false;
|
||||||
|
child.kill = () => {
|
||||||
|
child.killed = true;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 {}
|
||||||
|
child.emit("error", result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.pid = result.pid || process.pid;
|
||||||
|
|
||||||
|
// Read output from temp files
|
||||||
|
try {
|
||||||
|
const stdoutData = fs.readFileSync(stdoutFile, "utf8");
|
||||||
|
if (stdoutData) stdout.write(stdoutData);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stderrData = fs.readFileSync(stderrFile, "utf8");
|
||||||
|
if (stderrData) stderr.write(stderrData);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Clean up temp files
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(stdoutFile);
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(stderrFile);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
stdout.end();
|
||||||
|
stderr.end();
|
||||||
|
child.emit("close", result.status, result.signal);
|
||||||
|
} catch (err) {
|
||||||
|
// Clean up temp files
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(stdoutFile);
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(stderrFile);
|
||||||
|
} catch {}
|
||||||
|
child.emit("error", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit spawn immediately
|
||||||
|
process.nextTick(() => {
|
||||||
|
child.emit("spawn");
|
||||||
|
});
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
async function spawnAndWaitForSpawn(
|
async function spawnAndWaitForSpawn(
|
||||||
spawnImpl: typeof spawn,
|
spawnImpl: typeof spawn,
|
||||||
argv: string[],
|
argv: string[],
|
||||||
options: SpawnOptions,
|
options: SpawnOptions,
|
||||||
|
useSyncFallback = false,
|
||||||
): Promise<ChildProcess> {
|
): 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) => {
|
return await new Promise((resolve, reject) => {
|
||||||
let settled = false;
|
let settled = false;
|
||||||
@ -95,19 +200,27 @@ export async function spawnWithFallback(
|
|||||||
const retryCodes = params.retryCodes ?? DEFAULT_RETRY_CODES;
|
const retryCodes = params.retryCodes ?? DEFAULT_RETRY_CODES;
|
||||||
const baseOptions = { ...params.options };
|
const baseOptions = { ...params.options };
|
||||||
const fallbacks = params.fallbacks ?? [];
|
const fallbacks = params.fallbacks ?? [];
|
||||||
const attempts: Array<{ label?: string; options: SpawnOptions }> = [
|
const attempts: Array<{ label?: string; options: SpawnOptions; useSync?: boolean }> = [
|
||||||
{ options: baseOptions },
|
{ options: baseOptions, useSync: false },
|
||||||
...fallbacks.map((fallback) => ({
|
...fallbacks.map((fallback) => ({
|
||||||
label: fallback.label,
|
label: fallback.label,
|
||||||
options: { ...baseOptions, ...fallback.options },
|
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;
|
let lastError: unknown;
|
||||||
for (let index = 0; index < attempts.length; index += 1) {
|
for (let index = 0; index < attempts.length; index += 1) {
|
||||||
const attempt = attempts[index];
|
const attempt = attempts[index];
|
||||||
try {
|
try {
|
||||||
const child = await spawnAndWaitForSpawn(spawnImpl, params.argv, attempt.options);
|
const child = await spawnAndWaitForSpawn(
|
||||||
|
spawnImpl,
|
||||||
|
params.argv,
|
||||||
|
attempt.options,
|
||||||
|
attempt.useSync,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
child,
|
child,
|
||||||
usedFallback: index > 0,
|
usedFallback: index > 0,
|
||||||
@ -115,11 +228,16 @@ export async function spawnWithFallback(
|
|||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lastError = err;
|
lastError = err;
|
||||||
const nextFallback = fallbacks[index];
|
const nextAttempt = attempts[index + 1];
|
||||||
if (!nextFallback || !shouldRetry(err, retryCodes)) {
|
if (!nextAttempt || !shouldRetry(err, retryCodes)) {
|
||||||
throw err;
|
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