Survive an upload command that rejects the tar

A dying consumer destroys the tar pack and its pending entry sinks,
each of which emits the EPIPE as an unhandled streamx error and
crashed the process before the child's exit code was reported. Keep
the pack and its sinks quiet; the command's exit code is the report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Christopher Monsanto
2026-08-17 15:55:29 -04:00
parent 989ffd54ce
commit c56428fd9d
2 changed files with 10 additions and 7 deletions

View File

@@ -210,9 +210,12 @@ common(program.command('deploy [names...]'))
console.log(`${name}: ${matched.size} files | ${entry.cmd}`);
const upload = spawn(entry.cmd, {shell: true, stdio: ['pipe', 'inherit', 'inherit']});
// If the command dies early we report its exit code; don't
// also crash on the resulting EPIPE.
// also crash on the resulting EPIPE, which reaches both
// stdin and (via streamx's destroy propagation) the pack.
upload.stdin!.on('error', () => {});
aq.pack(dst => matched.has(dst)).pipe(upload.stdin!);
const pack = aq.pack(dst => matched.has(dst));
pack.on('error', () => {});
pack.pipe(upload.stdin!);
if (await waitExit(upload) !== 0) {
return 1;
}

View File

@@ -168,11 +168,11 @@ export class ActionQueue {
if (entry.type !== 'Op' || (filter !== undefined && !filter(entry.dst)))
continue;
const op = entry.op;
if (op.type === 'Copy'){
t.entry({name: entry.dst}, fs.readFileSync(op.src));
} else if (op.type === 'Write') {
t.entry({name: entry.dst}, op.data);
}
const data = op.type === 'Copy' ? fs.readFileSync(op.src) : op.data;
// A dying consumer destroys the pack and every pending entry
// sink, and each sink emits the error; the consumer is the one
// reporting the failure, so keep the sinks quiet.
t.entry({name: entry.dst}, data).on('error', () => {});
}
// Without this the archive has no end-of-archive marker, and strict
// readers (Python tarfile in stream mode) die on the truncation.