Electron Adventures: Episode 16: Streaming Terminal Output
Tags: electron, javascript, svelte
Published: 2021-08-08 23:02:39

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:

```javascript
let runCommand = (command) => {
  return new Promise((resolve, reject) => {
    child_process.exec(command, (error, stdout, stderr) => {
      resolve({stdout, stderr, error})
    })
  })
}
```

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`.

```javascript
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 }
)
```

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:

```javascript
  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})
  }
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cqksdyy387dc0c43mubf.png)

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](https://github.com/taw/electron-adventures/tree/master/episode-16-streaming-terminal-output).