fix: drain piped stderr in runServerCapture/waitForCloudInit to prevent deadlock (#1915)

PR #1903 fixed a pipe buffer deadlock in awsCli() by draining both
stdout and stderr before awaiting proc.exited. The same pattern existed
in runServerCapture() across 4 cloud providers and waitForCloudInit()
across 3 providers. If SSH produces >64KB of stderr, the child blocks
writing to the full pipe while the parent blocks waiting for exit.

Fixes: hetzner, aws, digitalocean, gcp — 7 locations total.

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
This commit is contained in:
A 2026-02-25 02:24:02 -08:00 committed by GitHub
parent 5458cef9f1
commit 2e79d71bd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 35 additions and 6 deletions

View file

@ -1062,7 +1062,11 @@ export async function runServerCapture(cmd: string, timeoutSecs?: number): Promi
/* ignore */
}
}, timeout);
const stdout = await new Response(proc.stdout).text();
// 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) {

View file

@ -928,7 +928,11 @@ export async function waitForCloudInit(ip?: string, _maxAttempts = 60): Promise<
],
},
);
const stdout = await new Response(proc.stdout).text();
// 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(),
]);
if ((await proc.exited) === 0 && stdout.includes("done")) {
logInfo("Cloud-init complete");
return;
@ -1011,7 +1015,11 @@ export async function runServerCapture(cmd: string, timeoutSecs?: number, ip?: s
proc.kill();
} catch {}
}, timeout);
const stdout = await new Response(proc.stdout).text();
// 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;
try {
proc.stdin!.end();

View file

@ -792,6 +792,11 @@ export async function waitForCloudInit(maxAttempts = 60): Promise<void> {
],
},
);
// Drain both pipes before awaiting exit to prevent pipe buffer deadlock
await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
if ((await proc.exited) === 0) {
logInfo("Startup script completed");
return;
@ -868,7 +873,11 @@ export async function runServerCapture(cmd: string, timeoutSecs?: number): Promi
proc.kill();
} catch {}
}, timeout);
const stdout = await new Response(proc.stdout).text();
// 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) {

View file

@ -489,7 +489,11 @@ export async function waitForCloudInit(ip?: string, _maxAttempts = 60): Promise<
],
},
);
const stdout = await new Response(proc.stdout).text();
// 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 && stdout.includes("done")) {
logInfo("Cloud-init complete");
@ -576,7 +580,11 @@ export async function runServerCapture(cmd: string, timeoutSecs?: number, ip?: s
proc.kill();
} catch {}
}, timeout);
const stdout = await new Response(proc.stdout).text();
// 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;
try {
proc.stdin!.end();