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})
})
})
}
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 }
)
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
andonerr
; 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})
}
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:
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.
Top comments (1)
You can’t send a function from the renderer to the main
It’s not working anymore