From 5e224cd5e6376385b842f63f5ff2b3b3d002b5d5 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 17 Sep 2026 09:51:23 -0500 Subject: [PATCH] Drain command output before emitting the exit receipt Bound the drain wait when a detached descendant retains a pipe. Co-Authored-By: Paperclip --- docker/exe-dev-runner/lease.mjs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docker/exe-dev-runner/lease.mjs b/docker/exe-dev-runner/lease.mjs index 70ad852ebed..9a05e7bb04b 100644 --- a/docker/exe-dev-runner/lease.mjs +++ b/docker/exe-dev-runner/lease.mjs @@ -36,6 +36,8 @@ if (mode === 'serve') { const server = net.createServer((socket) => { let child; let timer; + let drainTimer; + let exitResult; let done = false; let timedOut = false; const terminate = () => { @@ -62,18 +64,25 @@ if (mode === 'serve') { stdio: ['pipe', 'pipe', 'pipe'], }); child.stdin.on('error', () => {}); - child.stdout.on('data', (data) => send(socket, { type: 'stdout', data: data.toString('base64') })); - child.stderr.on('data', (data) => send(socket, { type: 'stderr', data: data.toString('base64') })); + child.stdout.on('data', (data) => { if (!done) send(socket, { type: 'stdout', data: data.toString('base64') }); }); + child.stderr.on('data', (data) => { if (!done) send(socket, { type: 'stderr', data: data.toString('base64') }); }); const finish = (code, signal, error) => { if (done) return; done = true; clearTimeout(timer); + clearTimeout(drainTimer); send(socket, { type: 'exit', code, signal, timedOut, error }); socket.end(); }; child.on('error', (error) => finish(null, null, error.message)); - // Use exit, not close: a deliberately detached daemon can retain an fd. - child.on('exit', (code, signal) => finish(code, signal)); + // exit can precede the last pipe data. Drain normal commands before the + // receipt, but bound the wait when a background descendant inherits an fd. + child.on('close', (code, signal) => finish(code, signal)); + child.on('exit', (code, signal) => { + exitResult = [code, signal]; + clearTimeout(timer); + drainTimer = setTimeout(() => finish(...exitResult), 1000); + }); if (message.timeoutMs > 0) timer = setTimeout(() => { timedOut = true; terminate(); }, message.timeoutMs); }); });