fix: add per-process timeout to SSH handshake probes in waitForSsh (#2299)

The Phase 2 SSH handshake loop in waitForSsh spawns SSH processes
without a per-process timeout. ConnectTimeout=10 only covers TCP
connect — if sshd accepts the connection but stalls during key
exchange or authentication, the process hangs indefinitely. This
causes the entire spawn command to freeze with no way to recover.

Add a 30s killWithTimeout guard to each probe, matching the pattern
already used in every cloud-specific runServer/uploadFile function.

-- refactor/code-health

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-07 15:40:48 -08:00 committed by GitHub
parent 099ad8940e
commit 90ae485c02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 25 additions and 16 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.15.9",
"version": "0.15.10",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -251,23 +251,32 @@ export async function waitForSsh(opts: WaitForSshOpts): Promise<void> {
],
},
);
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
const exitCode = await proc.exited;
// Per-process timeout: ConnectTimeout=10 only covers TCP connect, not
// the full SSH handshake. If sshd accepts the connection but stalls
// during key exchange or auth, the process hangs indefinitely. Kill it
// after 30s so the retry loop can continue.
const timer = setTimeout(() => killWithTimeout(proc), 30_000);
try {
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
const exitCode = await proc.exited;
if (exitCode === 0 && stdout.includes("ok")) {
logInfo("SSH is ready");
return;
}
if (exitCode === 0 && stdout.includes("ok")) {
logInfo("SSH is ready");
return;
}
// Show the actual SSH error reason dimly so users can debug
const reason = stderr.trim();
if (reason) {
logStep(`SSH handshake failed (${i}/${handshakeAttempts}): ${reason}`);
} else {
logStep(`SSH handshake failed (${i}/${handshakeAttempts})`);
// Show the actual SSH error reason dimly so users can debug
const reason = stderr.trim();
if (reason) {
logStep(`SSH handshake failed (${i}/${handshakeAttempts}): ${reason}`);
} else {
logStep(`SSH handshake failed (${i}/${handshakeAttempts})`);
}
} finally {
clearTimeout(timer);
}
} catch {
logStep(`SSH handshake error (${i}/${handshakeAttempts})`);