Reputation: 4208
I want to combine stdout and stderr of a child process into a single, intermediate stream which I can then consume and do processing on.
Is this possible with the standard API's?
(I realize there are subtleties around interleaving the two streams, and I would need them to be only interleaved on line endings for this to work. i.e. writing a line to stdout or stderr should be an atomic operation).
Upvotes: 7
Views: 351
Reputation: 328
This should work as expected:
// This custom stream merges stdout and stderr.
// Instead of concatenating the two streams, it keeps their interleaving.
const stdoutStderrInterleavedStream = (
stdout: ReadableStreamDefaultReader<Uint8Array>,
stderr: ReadableStreamDefaultReader<Uint8Array>
) =>
new ReadableStream({
start(controller) {
// These sentinel values are used to know if both stream have been stopped.
let stdoutDone = false
let stderrDone = false
type SentinelFunctions = {
getDone: () => boolean
setDone: () => void
}
// stdout will check if stderr is done and vice-versa
const sentinelStdout: SentinelFunctions = {
getDone: () => stderrDone,
setDone: () => (stdoutDone = true),
}
const sentinelStderr: SentinelFunctions = {
getDone: () => stdoutDone,
setDone: () => (stderrDone = true),
}
const onData =
({ getDone, setDone }: SentinelFunctions) =>
({ done, value }: ReadableStreamDefaultReadResult<Uint8Array>) => {
if (done) {
setDone()
if (getDone()) {
controller.close()
}
return
}
controller.enqueue(value)
push()
}
const push = () => {
stdout.read().then(onData(sentinelStdout))
stderr.read().then(onData(sentinelStderr))
}
push()
},
})
Example usage:
const stream = stdoutStderrInterleavedStream(
myDenoProcess.stdout.getReader(),
myDenoProcess.stderr.getReader()
)
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TextLineStream())
const output = await Array.fromAsync(stream)
console.log(output)
Upvotes: 0