From b19d804dc13d51357ffbe9bfc1e9844601bb01e9 Mon Sep 17 00:00:00 2001 From: gabriel engvall Date: Fri, 5 Jun 2026 21:22:45 +0200 Subject: [PATCH] fix(build): ignore stdin on remote ssh so concurrent calls cannot deadlock cf runs ssh with stdin inherited from the interactive terminal. Building many recipes at once deadlocks the prefetch stage: concurrent prefetch ssh calls (mkdir and ISO existence check in captureRemote) plus a long build stream all share fd 0 and block each other, the classic parallel-ssh-eats-stdin footgun. A stalled prefetch never reaches its wget, so it holds a queue slot forever while the download itself takes seconds. Use stdin ignore (ssh -n) for the non-interactive captureRemote and the build stream; nothing there reads stdin. Verified on a 16-recipe build: prefetch runs 3 concurrent downloads with live progress instead of stalling at 0. --- src/build/remote.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/build/remote.ts b/src/build/remote.ts index 368b826..6aec392 100644 --- a/src/build/remote.ts +++ b/src/build/remote.ts @@ -25,8 +25,12 @@ export const captureRemote = async ( cmd: string ): Promise => { try { + // stdin: 'ignore' (≈ ssh -n), never 'inherit'. Concurrent ssh calls that + // inherit the shared interactive stdin fight over fd 0 and block — the + // classic "parallel ssh eats stdin" deadlock that stalls prefetch + // (mkdir/file-check) for later recipes while an earlier build streams. const { stdout } = await execa('ssh', [target, cmd], { - stdin: 'inherit', + stdin: 'ignore', stderr: 'inherit', }) return stdout @@ -129,7 +133,9 @@ export const streaming = async ( return } const proc = execa(cmd, args, { - stdin: 'inherit', + // ignore (not inherit): a long packer stream must not hold the shared + // stdin and starve concurrent prefetch ssh calls. See captureRemote. + stdin: 'ignore', stdout: 'pipe', stderr: 'pipe', })