deno

Stream stdout from subprocess


I'm running a Docker build as a subprocess in Deno and would like the stdout streamed to the parent stdout (Deno.stdout) so it's outputted straight away.

How can I achieve this?

Currently I have the following but it doesn't output anything until the subprocess has finished.

const p = Deno.run({
  cmd: ['docker', 'build', '.'],
  stdout: 'piped'
});

const stdout = await p.output();
await Deno.stdout.write(stdout);

Solution

  • You're not far off; you just need to start the process of piping the output before you await the process. There's some other optimizations you can make, like using the standard library's copy to pipe the subprocess' output to the main process' stdout without copying stuff in memory for example. Note, you'll need to convert the process's stdout from a web standard ReadableStream to a Deno Reader.

    import { copy, readerFromStreamReader } from "jsr:@std/io";
    
    const cat = new Deno.Command("docker", {
      args: ["build", "--no-cache", ".", "-t", "foobar:latest"],
      cwd: "/path/to/your/project",
      stdout: "piped",
      stderr: "piped",
    }).spawn();
    
    copy(readerFromStreamReader(cat.stdout.getReader()), Deno.stdout);
    copy(readerFromStreamReader(cat.stderr.getReader()), Deno.stderr);
    
    await cat.status;
    console.log("Done!");
    

    If you want to prefix each line with the name of the process it came from (useful when you have multiple subprocesses running), you can make a simple function that iterates over the subprocess's stdout chunks and a text decoder and encoder to do that.

    import { copy, readerFromStreamReader, writeAll } from "jsr:@std/io";
    
    async function pipeThrough(
      prefix: string,
      reader: ReadableStream,
      writer: Deno.Writer,
    ) {
      const decoder = new TextDecoder();
      const encoder = new TextEncoder();
      for await (const chunk of reader) {
        const lines = decoder.decode(chunk).split("\n");
        for (const line of lines) {
          await writeAll(writer, encoder.encode(`[${prefix}] ${line}\n`));
        }
      }
    }
    
    const cat = new Deno.Command("docker", {
      args: ["build", "--no-cache", ".", "-t", "foobar:latest"],
      cwd: "/path/to/your/project",
      stdout: "piped",
      stderr: "piped",
    }).spawn();
    
    pipeThrough("docker", cat.stdout, Deno.stdout);
    pipeThrough("docker", cat.stderr, Deno.stderr);
    
    await cat.status;
    console.log("Done!");