mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-08-18 04:53:35 +00:00
fix: prevent timer leaks and event-loop stalls in SSH timeout handling (#2200)
- Unref the SIGKILL timer in killWithTimeout() so it doesn't keep the event loop alive for 5 extra seconds after a timed-out process exits - Wrap all setTimeout/clearTimeout pairs in try/finally across 6 cloud providers (12 call sites) to guarantee cleanup on exceptions - Add missing 60s timeout guard to runSpriteSilent() which could hang indefinitely on unresponsive sprite processes Agent: code-health Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2fea3de685
commit
701e3af56e
7 changed files with 129 additions and 91 deletions
|
|
@ -1047,10 +1047,13 @@ export async function runServer(cmd: string, timeoutSecs?: number): Promise<void
|
|||
);
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
|
||||
try {
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1075,17 +1078,20 @@ export async function runServerCapture(cmd: string, timeoutSecs?: number): Promi
|
|||
);
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
|
||||
const [stdout] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server_capture failed (exit ${exitCode})`);
|
||||
try {
|
||||
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
|
||||
const [stdout] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server_capture failed (exit ${exitCode})`);
|
||||
}
|
||||
return stdout.trim();
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
export async function uploadFile(localPath: string, remotePath: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -389,14 +389,15 @@ export async function runServer(cmd: string, timeoutSecs?: number): Promise<void
|
|||
}
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
|
||||
// Brief sleep to let gateway release connection slot
|
||||
await sleep(1000);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
|
||||
try {
|
||||
const exitCode = await proc.exited;
|
||||
// Brief sleep to let gateway release connection slot
|
||||
await sleep(1000);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -426,20 +427,21 @@ export async function runServerCapture(cmd: string, timeoutSecs?: number): Promi
|
|||
}
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
|
||||
const [stdout] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server_capture failed (exit ${exitCode})`);
|
||||
try {
|
||||
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
|
||||
const [stdout] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
await sleep(1000);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server_capture failed (exit ${exitCode})`);
|
||||
}
|
||||
return stdout.trim();
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -984,11 +984,13 @@ export async function runServer(cmd: string, timeoutSecs?: number, ip?: string):
|
|||
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
|
||||
try {
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1016,18 +1018,20 @@ export async function runServerCapture(cmd: string, timeoutSecs?: number, ip?: s
|
|||
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
|
||||
const [stdout] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server_capture failed (exit ${exitCode})`);
|
||||
try {
|
||||
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
|
||||
const [stdout] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server_capture failed (exit ${exitCode})`);
|
||||
}
|
||||
return stdout.trim();
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
export async function uploadFile(localPath: string, remotePath: string, ip?: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -848,10 +848,13 @@ export async function runServer(cmd: string, timeoutSecs?: number): Promise<void
|
|||
);
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
|
||||
try {
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -879,17 +882,20 @@ export async function runServerCapture(cmd: string, timeoutSecs?: number): Promi
|
|||
);
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
|
||||
const [stdout] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server_capture failed (exit ${exitCode})`);
|
||||
try {
|
||||
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
|
||||
const [stdout] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server_capture failed (exit ${exitCode})`);
|
||||
}
|
||||
return stdout.trim();
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
export async function uploadFile(localPath: string, remotePath: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -511,11 +511,13 @@ export async function runServer(cmd: string, timeoutSecs?: number, ip?: string):
|
|||
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
|
||||
try {
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server failed (exit ${exitCode}): ${cmd}`);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -543,18 +545,20 @@ export async function runServerCapture(cmd: string, timeoutSecs?: number, ip?: s
|
|||
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
|
||||
const [stdout] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server_capture failed (exit ${exitCode})`);
|
||||
try {
|
||||
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
|
||||
const [stdout] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`run_server_capture failed (exit ${exitCode})`);
|
||||
}
|
||||
return stdout.trim();
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
export async function uploadFile(localPath: string, remotePath: string, ip?: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export function killWithTimeout(
|
|||
} catch {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
const sigkillTimer = setTimeout(() => {
|
||||
try {
|
||||
if (!proc.killed) {
|
||||
proc.kill(9);
|
||||
|
|
@ -129,6 +129,9 @@ export function killWithTimeout(
|
|||
/* already dead */
|
||||
}
|
||||
}, gracePeriodMs);
|
||||
// Don't let this timer keep the event loop alive — the process may already
|
||||
// be dead from SIGTERM, so there's no reason to block exit for 5 seconds.
|
||||
sigkillTimer.unref();
|
||||
}
|
||||
|
||||
// ─── TCP Pre-Check ───────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -461,10 +461,13 @@ export async function runSprite(cmd: string, timeoutSecs?: number): Promise<void
|
|||
);
|
||||
const timeout = (timeoutSecs || 300) * 1000;
|
||||
const timer = setTimeout(() => killWithTimeout(proc), timeout);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`sprite exec failed (exit ${exitCode}): ${cmd.slice(0, 80)}`);
|
||||
try {
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`sprite exec failed (exit ${exitCode}): ${cmd.slice(0, 80)}`);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -492,9 +495,15 @@ async function runSpriteSilent(cmd: string): Promise<void> {
|
|||
],
|
||||
},
|
||||
);
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`sprite exec (silent) failed (exit ${exitCode})`);
|
||||
// 60s timeout — silent commands should not hang indefinitely
|
||||
const timer = setTimeout(() => killWithTimeout(proc), 60_000);
|
||||
try {
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`sprite exec (silent) failed (exit ${exitCode})`);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -629,8 +638,12 @@ export async function destroyServer(name?: string): Promise<void> {
|
|||
const stderrText = new Response(proc.stderr).text();
|
||||
// 60s timeout — sprite destroy should not hang indefinitely
|
||||
const timer = setTimeout(() => killWithTimeout(proc), 60_000);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timer);
|
||||
let exitCode: number;
|
||||
try {
|
||||
exitCode = await proc.exited;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
logError(`Failed to destroy sprite '${target}'`);
|
||||
logError(`Delete it manually: sprite destroy ${target}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue