Skip to content

Commit

Permalink
refactor(core): rewrite shell execute API, closes #1229 (#1408)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Mar 31, 2021
1 parent b5cecbe commit 3713066
Show file tree
Hide file tree
Showing 16 changed files with 567 additions and 206 deletions.
6 changes: 6 additions & 0 deletions .changes/command-refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"tauri": minor
"api": minor
---

The shell process spawning API was rewritten and now includes stream access.
174 changes: 168 additions & 6 deletions api/src/shell.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,192 @@
import { invokeTauriCommand } from './helpers/tauri'
import { transformCallback } from './tauri'

/**
* spawns a process
*
* @param command the name of the cmd to execute e.g. 'mkdir' or 'node'
* @param program the name of the program to execute e.g. 'mkdir' or 'node'
* @param sidecar whether the program is a sidecar or a system program
* @param [args] command args
* @return promise resolving to the stdout text
*/
async function execute(
command: string,
program: string,
sidecar: boolean,
onEvent: (event: CommandEvent) => void,
args?: string | string[]
): Promise<string> {
): Promise<number> {
if (typeof args === 'object') {
Object.freeze(args)
}

return invokeTauriCommand<string>({
return invokeTauriCommand<number>({
__tauriModule: 'Shell',
message: {
cmd: 'execute',
command,
program,
sidecar,
onEventFn: transformCallback(onEvent),
args: typeof args === 'string' ? [args] : args
}
})
}

interface ChildProcess {
code: number | null
signal: number | null
stdout: string
stderr: string
}

class EventEmitter<E> {
eventListeners: { [key: string]: Array<(arg: any) => void> } = {}

private addEventListener(event: string, handler: (arg: any) => void): void {
if (event in this.eventListeners) {
// eslint-disable-next-line security/detect-object-injection
this.eventListeners[event].push(handler)
} else {
// eslint-disable-next-line security/detect-object-injection
this.eventListeners[event] = [handler]
}
}

_emit(event: E, payload: any): void {
if (event in this.eventListeners) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const listeners = this.eventListeners[event as any]
for (const listener of listeners) {
listener(payload)
}
}
}

on(event: E, handler: (arg: any) => void): EventEmitter<E> {
this.addEventListener(event as any, handler)
return this
}
}

class Child {
pid: number

constructor(pid: number) {
this.pid = pid
}

async write(data: string | number[]): Promise<void> {
return invokeTauriCommand({
__tauriModule: 'Shell',
message: {
cmd: 'stdinWrite',
pid: this.pid,
buffer: data
}
})
}

async kill(): Promise<void> {
return invokeTauriCommand({
__tauriModule: 'Shell',
message: {
cmd: 'killChild',
pid: this.pid
}
})
}
}

class Command extends EventEmitter<'close' | 'error'> {
program: string
args: string[]
sidecar = false
stdout = new EventEmitter<'data'>()
stderr = new EventEmitter<'data'>()
pid: number | null = null

constructor(program: string, args: string | string[] = []) {
super()
this.program = program
this.args = typeof args === 'string' ? [args] : args
}

/**
* Creates a command to execute the given sidecar binary
*
* @param {string} program Binary name
*
* @return {Command}
*/
static sidecar(program: string, args: string | string[] = []): Command {
const instance = new Command(program, args)
instance.sidecar = true
return instance
}

async spawn(): Promise<Child> {
return execute(
this.program,
this.sidecar,
(event) => {
switch (event.event) {
case 'Error':
this._emit('error', event.payload)
break
case 'Terminated':
this._emit('close', event.payload)
break
case 'Stdout':
this.stdout._emit('data', event.payload)
break
case 'Stderr':
this.stderr._emit('data', event.payload)
break
}
},
this.args
).then((pid) => new Child(pid))
}

async execute(): Promise<ChildProcess> {
return new Promise((resolve, reject) => {
this.on('error', reject)
const stdout: string[] = []
const stderr: string[] = []
this.stdout.on('data', (line) => {
stdout.push(line)
})
this.stderr.on('data', (line) => {
stderr.push(line)
})
this.on('close', (payload: TerminatedPayload) => {
resolve({
code: payload.code,
signal: payload.signal,
stdout: stdout.join('\n'),
stderr: stderr.join('\n')
})
})
this.spawn().catch(reject)
})
}
}

interface Event<T, V> {
event: T
payload: V
}

interface TerminatedPayload {
code: number | null
signal: number | null
}

type CommandEvent =
| Event<'Stdout', string>
| Event<'Stderr', string>
| Event<'Terminated', TerminatedPayload>
| Event<'Error', string>

/**
* opens a path or URL with the system's default app,
* or the one specified with `openWith`
Expand All @@ -43,4 +205,4 @@ async function open(path: string, openWith?: string): Promise<void> {
})
}

export { execute, open }
export { Command, Child, open }
4 changes: 2 additions & 2 deletions examples/api/public/build/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/api/public/build/bundle.js.map

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions examples/api/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { onMount } from "svelte";
import { open } from "@tauri-apps/api/shell";
import Welcome from "./components/Welcome.svelte";
import Cli from "./components/Cli.svelte";
import Communication from "./components/Communication.svelte";
import Dialog from "./components/Dialog.svelte";
Expand All @@ -10,7 +11,7 @@
import Notifications from "./components/Notifications.svelte";
import Window from "./components/Window.svelte";
import Shortcuts from "./components/Shortcuts.svelte";
import Welcome from "./components/Welcome.svelte";
import Shell from "./components/Shell.svelte";
const views = [
{
Expand Down Expand Up @@ -49,6 +50,10 @@
label: "Shortcuts",
component: Shortcuts,
},
{
label: "Shell",
component: Shell,
}
];
let selected = views[0];
Expand Down Expand Up @@ -97,7 +102,7 @@
<svelte:component this={selected.component} {onMessage} />
</div>
</div>
<div id="response">
<div id="response" style="white-space: pre-line">
<p class="flex row just-around">
<strong>Tauri Console</strong>
<a class="nv" on:click={()=> {
Expand Down
52 changes: 52 additions & 0 deletions examples/api/src/components/Shell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script>
import { Command } from "@tauri-apps/api/shell"
const windows = navigator.userAgent.includes('Windows')
let cmd = windows ? 'cmd' : 'sh'
let args = windows ? ['/C'] : ['-c']
export let onMessage;
let script = 'echo "hello world"'
let stdin = ''
let child
function spawn() {
child = null
const command = new Command(cmd, [...args, script])
command.on('close', data => {
onMessage(`command finished with code ${data.code} and signal ${data.signal}`)
child = null
})
command.on('error', error => onMessage(`command error: "${error}"`))
command.stdout.on('data', line => onMessage(`command stdout: "${line}"`))
command.stderr.on('data', line => onMessage(`command stderr: "${line}"`))
command.spawn()
.then(c => {
child = c
})
.catch(onMessage)
}
function kill() {
child.kill().then(() => onMessage('killed child process')).error(onMessage)
}
function writeToStdin() {
child.write(stdin).catch(onMessage)
}
</script>

<div>
<div>
<input bind:value={script}>
<button class="button" on:click={spawn}>Run</button>
<button class="button" on:click={kill}>Kill</button>
{#if child}
<input placeholder="write to stdin" bind:value={stdin}>
<button class="button" on:click={writeToStdin}>Write</button>
{/if}
</div>
</div>
3 changes: 3 additions & 0 deletions tauri-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ notify-rust = { version = "4.3.0", optional = true }
once_cell = "1.7.2"
tauri-hotkey = { git = "https://github.com/tauri-apps/tauri-hotkey-rs", branch = "dev", optional = true }
open = "1.6.0"
tokio = { version = "1.3", features = ["rt", "rt-multi-thread", "sync"] }
shared_child = "0.3"
os_pipe = "0.9"

[dev-dependencies]
quickcheck = "1.0.3"
Expand Down
Loading

0 comments on commit 3713066

Please sign in to comment.