Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): rewrite shell execute API, closes #1229 #1408

Merged
merged 10 commits into from
Mar 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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