DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on • Updated on

Electron Adventures: Episode 16: Streaming Terminal Output

Let's address the biggest limitation of our terminal app - it currently waits for command to finish before it displays the output.

We'll start with codebase from episode 15 and add a streaming feature.

Promises and callback

Node APIs don't use promises. We were able to wrap child_process.exec in a promise, because we could just wait for it to finish, and then deliver results all at once:

let runCommand = (command) => {
  return new Promise((resolve, reject) => {
    child_process.exec(command, (error, stdout, stderr) => {
      resolve({stdout, stderr, error})
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately we have to undo this. Promises are very convenient, but their whole point is that they deliver their result (or error) all at once, and then they're done.

runCommand in preload.js

And once more we change the way we run command. First we used child_process.execSync, then child_process.exec, and now we'll change to child_process.sync.

let runCommand = ({command, onout, onerr, ondone}) => {
  const proc = child_process.spawn(
    command,
    [],
    {
      shell: true,
      stdio: ["ignore", "pipe", "pipe"],
    },
  )
  proc.stdout.on("data", (data) => onout(data.toString()))
  proc.stderr.on("data", (data) => onerr(data.toString()))
  proc.on("close", (code) => ondone(code))
}

contextBridge.exposeInMainWorld(
  "api", { runCommand }
)
Enter fullscreen mode Exit fullscreen mode

This does the following:

  • connects stdin to /dev/null, so command we run won't be waiting for input that cannot ever come - and yes, obviously we'll address that in a future episode
  • connects stdout and stderr to our callbacks onout and onerr; data is received as binary, so we need to convert it to UTF8 string
  • calls back ondone when command finishes; exit code is 0 to 255, where 0 means success, and every other value means various errors in a way that's completely inconsistent between commands
  • we use shell: true to run command through a shell, so we can use all the shell things like pipes, redirection, and so on - this also simplified error handling, as we don't need to deal with command missing etc.

Use new interface

We don't need to do a single change anywhere in the UI code. We just change onsubmit handler to use new interface:

  async function onsubmit(command) {
    let entry = {command, stdout: "", stderr: "", error: null, running: true}
    history.push(entry)
    history = history
    let onout = (data) => {
      entry.stdout += data
      history = history
    }
    let onerr = (data) => {
      entry.stderr += data
      history = history
    }
    let ondone = (code) => {
      entry.running = false
      entry.error = (code !== 0)
      history = history
    }
    window.api.runCommand({command,onout,onerr,ondone})
  }
Enter fullscreen mode Exit fullscreen mode

As before, instead of convoluted functional style updating just the right part of history array, we'll modify the right part directly and then tell Svelte it changed with history = history.

Result

And here's the result:

Episode 16 screenshot

In the next episode, we'll add some ways to interact with the spawned commands.

As usual, all the code for the episode is here.

Discussion (0)