Keep a process stream open when JavaScript pipes into it

pipe() called from untyped JavaScript on a native stream went through
scr_stream_pipe directly, so a helper that piped into the converted
process.stdout or process.stderr ended it at the source's end, and later
writes through the program's NodeJS.WritableStream slots failed. The
stream view's pipe now calls scr_stream_pipe_general, which keeps end off
for those Writables, as Node's pipe() does for process.stdout and
process.stderr.

Adds corpus 6469.
This commit is contained in:
Chris Tate
2026-10-20 11:20:28 -05:00
parent 19e2da69a3
commit 1818faef10
5 changed files with 54 additions and 3 deletions
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6469-node-stdio-writable-js-pipe/pipe.js",
"<repo>/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts"
],
"diags": []
}
+6 -3
View File
@@ -6451,8 +6451,10 @@ static ScrDyn *scr_stream_view_call(ScrClosure *cb, ScrDyn *const *args, size_t
end = option->kind == SCR_DYN_UNDEF || scr_dyn_truthy(option);
scr_dyn_release(option);
}
/* scr_stream_pipe_general: a JavaScript caller can pipe into the
* converted process.stdout / process.stderr, which a pipe never ends. */
ScrStream *returned = !strcmp(method, "pipe")
? scr_stream_pipe(stream, destination, end) : scr_stream_unpipe(stream, destination);
? scr_stream_pipe_general(stream, destination, end) : scr_stream_unpipe(stream, destination);
scr_stream_release(returned);
if (!strcmp(method, "pipe") && !scr_exc_pending()) result = scr_dyn_retain(first);
}
@@ -6696,8 +6698,9 @@ ScrStream *scr_stream_stdio_writable(double fd) {
}
/* readable.pipe() into a Writable slot under the general representation
* (the readable.pipeGeneral libCall): Node never ends process.stdout or
* process.stderr from a pipe, so their converted Writable keeps `end` off. */
* (the readable.pipeGeneral libCall), and pipe() called from JavaScript on
* a native stream: Node never ends process.stdout or process.stderr from a
* pipe, so their converted Writable keeps `end` off. */
ScrStream *scr_stream_pipe_general(ScrStream *src, ScrStream *dst, bool end) {
if (dst == scr_stream_stdio_writables[0] || dst == scr_stream_stdio_writables[1]) end = false;
return scr_stream_pipe(src, dst, end);
@@ -0,0 +1,36 @@
// process.stdout and process.stderr reach an untyped JavaScript helper
// through NodeJS.WritableStream slots, and the helper pipes into them. As
// in Node, the pipe leaves the process stream open, so later writes
// through the same slots still work.
import { Readable, Writable } from "node:stream";
import { pipeAll } from "./pipe.js";
class Sink extends Writable {
readonly parts: string[] = [];
_write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
this.parts.push(chunk.toString("utf8"));
callback();
}
}
function into(destination: NodeJS.WritableStream, parts: string[]): Promise<void> {
return new Promise((resolve) => {
pipeAll(Readable.from(parts), destination, () => resolve());
});
}
function write(destination: NodeJS.WritableStream, text: string): boolean {
return destination.write(text);
}
async function main(): Promise<void> {
const sink = new Sink();
await into(sink, ["to ", "a sink"]);
console.log("sink", JSON.stringify(sink.parts), sink.writableEnded);
await into(process.stdout, ["piped ", "by javascript\n"]);
console.log("write after pipe", write(process.stdout, "still open\n"));
await into(process.stderr, ["piped to stderr\n"]);
write(process.stderr, "stderr still open\n");
}
void main();
@@ -0,0 +1,4 @@
export function pipeAll(source, destination, done) {
source.on("end", () => done());
source.pipe(destination);
}
@@ -0,0 +1 @@
{ "plain": 3000, "san": 3500 }