2021-04-11 01:09:09 +03:00
|
|
|
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
2021-03-07 05:19:12 +03:00
|
|
|
import { invokeTauriCommand } from './helpers/tauri'
|
2021-03-31 08:19:03 +03:00
|
|
|
import { transformCallback } from './tauri'
|
2021-02-12 08:42:40 +03:00
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/**
|
|
|
|
* Access the system shell.
|
|
|
|
* Allows you to spawn child processes and manage files and URLs using their default application.
|
2021-05-18 04:33:09 +03:00
|
|
|
*
|
2021-05-17 23:56:14 +03:00
|
|
|
* This package is also accessible with `window.__TAURI__.shell` when `tauri.conf.json > build > withGlobalTauri` is set to true.
|
2021-05-18 04:33:09 +03:00
|
|
|
*
|
|
|
|
* The APIs must be allowlisted on `tauri.conf.json`:
|
|
|
|
* ```json
|
|
|
|
* {
|
|
|
|
* "tauri": {
|
|
|
|
* "allowlist": {
|
|
|
|
* "shell": {
|
|
|
|
* "all": true, // enable all shell APIs
|
|
|
|
* "execute": true, // enable process spawn APIs
|
|
|
|
* "open": true // enable opening files/URLs using the default program
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
* It is recommended to allowlist only the APIs you use for optimal bundle size and security.
|
2021-07-21 07:23:16 +03:00
|
|
|
* @module
|
2021-05-05 20:36:40 +03:00
|
|
|
*/
|
|
|
|
|
2021-04-29 00:25:44 +03:00
|
|
|
interface SpawnOptions {
|
|
|
|
/** Current working directory. */
|
|
|
|
cwd?: string
|
|
|
|
/** Environment variables. set to `null` to clear the process env. */
|
|
|
|
env?: { [name: string]: string }
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/** @ignore */
|
2021-04-29 00:25:44 +03:00
|
|
|
interface InternalSpawnOptions extends SpawnOptions {
|
|
|
|
sidecar?: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ChildProcess {
|
2021-05-05 20:36:40 +03:00
|
|
|
/** Exit code of the process. `null` if the process was terminated by a signal on Unix. */
|
2021-04-29 00:25:44 +03:00
|
|
|
code: number | null
|
2021-05-05 20:36:40 +03:00
|
|
|
/** If the process was terminated by a signal, represents that signal. */
|
2021-04-29 00:25:44 +03:00
|
|
|
signal: number | null
|
2021-05-05 20:36:40 +03:00
|
|
|
/** The data that the process wrote to `stdout`. */
|
2021-04-29 00:25:44 +03:00
|
|
|
stdout: string
|
2021-05-05 20:36:40 +03:00
|
|
|
/** The data that the process wrote to `stderr`. */
|
2021-04-29 00:25:44 +03:00
|
|
|
stderr: string
|
|
|
|
}
|
|
|
|
|
2021-02-12 08:42:40 +03:00
|
|
|
/**
|
2021-04-13 04:44:50 +03:00
|
|
|
* Spawns a process.
|
2021-02-12 08:42:40 +03:00
|
|
|
*
|
2021-05-05 20:36:40 +03:00
|
|
|
* @ignore
|
|
|
|
* @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 onEvent Event handler.
|
|
|
|
* @param args Program arguments.
|
|
|
|
* @param options Configuration for the process spawn.
|
2021-04-13 04:44:50 +03:00
|
|
|
* @returns A promise resolving to the process id.
|
2021-02-12 08:42:40 +03:00
|
|
|
*/
|
2021-03-13 03:02:36 +03:00
|
|
|
async function execute(
|
2021-03-31 08:19:03 +03:00
|
|
|
onEvent: (event: CommandEvent) => void,
|
2021-04-29 00:25:44 +03:00
|
|
|
program: string,
|
|
|
|
args?: string | string[],
|
|
|
|
options?: InternalSpawnOptions
|
2021-03-31 08:19:03 +03:00
|
|
|
): Promise<number> {
|
2021-02-12 08:42:40 +03:00
|
|
|
if (typeof args === 'object') {
|
|
|
|
Object.freeze(args)
|
|
|
|
}
|
|
|
|
|
2021-03-31 08:19:03 +03:00
|
|
|
return invokeTauriCommand<number>({
|
2021-02-16 07:23:15 +03:00
|
|
|
__tauriModule: 'Shell',
|
2021-02-12 08:42:40 +03:00
|
|
|
message: {
|
|
|
|
cmd: 'execute',
|
2021-03-31 08:19:03 +03:00
|
|
|
program,
|
2021-04-29 00:25:44 +03:00
|
|
|
args: typeof args === 'string' ? [args] : args,
|
|
|
|
options,
|
|
|
|
onEventFn: transformCallback(onEvent)
|
2021-02-12 08:42:40 +03:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 16:30:24 +03:00
|
|
|
class EventEmitter<E extends string> {
|
2021-05-05 20:36:40 +03:00
|
|
|
/** @ignore */
|
2021-04-05 21:37:06 +03:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
2021-05-05 20:36:40 +03:00
|
|
|
private eventListeners: {
|
|
|
|
[key: string]: Array<(arg: any) => void>
|
|
|
|
} = Object.create(null)
|
2021-03-31 08:19:03 +03:00
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/** @ignore */
|
2021-03-31 08:19:03 +03:00
|
|
|
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]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/** @ignore */
|
2021-03-31 08:19:03 +03:00
|
|
|
_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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/**
|
|
|
|
* Listen to an event from the child process.
|
|
|
|
*
|
|
|
|
* @param event The event name.
|
|
|
|
* @param handler The event handler.
|
|
|
|
*
|
|
|
|
* @return The `this` instance for chained calls.
|
|
|
|
*/
|
2021-03-31 08:19:03 +03:00
|
|
|
on(event: E, handler: (arg: any) => void): EventEmitter<E> {
|
2021-10-13 16:30:24 +03:00
|
|
|
this.addEventListener(event, handler)
|
2021-03-31 08:19:03 +03:00
|
|
|
return this
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Child {
|
2021-05-05 20:36:40 +03:00
|
|
|
/** The child process `pid`. */
|
2021-03-31 08:19:03 +03:00
|
|
|
pid: number
|
|
|
|
|
|
|
|
constructor(pid: number) {
|
|
|
|
this.pid = pid
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/**
|
|
|
|
* Writes `data` to the `stdin`.
|
|
|
|
*
|
|
|
|
* @param data The message to write, either a string or a byte array.
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* const command = new Command('node')
|
|
|
|
* const child = await command.spawn()
|
|
|
|
* await child.write('message')
|
|
|
|
* await child.write([0, 1, 2, 3, 4, 5])
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @return A promise indicating the success or failure of the operation.
|
|
|
|
*/
|
2021-03-31 08:19:03 +03:00
|
|
|
async write(data: string | number[]): Promise<void> {
|
|
|
|
return invokeTauriCommand({
|
|
|
|
__tauriModule: 'Shell',
|
|
|
|
message: {
|
|
|
|
cmd: 'stdinWrite',
|
|
|
|
pid: this.pid,
|
|
|
|
buffer: data
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/**
|
|
|
|
* Kills the child process.
|
|
|
|
*
|
|
|
|
* @return A promise indicating the success or failure of the operation.
|
|
|
|
*/
|
2021-03-31 08:19:03 +03:00
|
|
|
async kill(): Promise<void> {
|
|
|
|
return invokeTauriCommand({
|
|
|
|
__tauriModule: 'Shell',
|
|
|
|
message: {
|
|
|
|
cmd: 'killChild',
|
|
|
|
pid: this.pid
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/**
|
|
|
|
* The entry point for spawning child processes.
|
|
|
|
* It emits the `close` and `error` events.
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* const command = new Command('node')
|
|
|
|
* command.on('close', data => {
|
|
|
|
* console.log(`command finished with code ${data.code} and signal ${data.signal}`)
|
|
|
|
* })
|
|
|
|
* command.on('error', error => console.error(`command error: "${error}"`))
|
|
|
|
* command.stdout.on('data', line => console.log(`command stdout: "${line}"`))
|
|
|
|
* command.stderr.on('data', line => console.log(`command stderr: "${line}"`))
|
|
|
|
*
|
|
|
|
* const child = await command.spawn()
|
|
|
|
* console.log('pid:', child.pid)
|
|
|
|
* ```
|
|
|
|
*/
|
2021-03-31 08:19:03 +03:00
|
|
|
class Command extends EventEmitter<'close' | 'error'> {
|
2021-05-05 20:36:40 +03:00
|
|
|
/** @ignore Program to execute. */
|
|
|
|
private readonly program: string
|
|
|
|
/** @ignore Program arguments */
|
|
|
|
private readonly args: string[]
|
|
|
|
/** @ignore Spawn options. */
|
|
|
|
private readonly options: InternalSpawnOptions
|
|
|
|
/** Event emitter for the `stdout`. Emits the `data` event. */
|
|
|
|
readonly stdout = new EventEmitter<'data'>()
|
|
|
|
/** Event emitter for the `stderr`. Emits the `data` event. */
|
|
|
|
readonly stderr = new EventEmitter<'data'>()
|
2021-03-31 08:19:03 +03:00
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/**
|
|
|
|
* Creates a new `Command` instance.
|
|
|
|
*
|
|
|
|
* @param program The program to execute.
|
|
|
|
* @param args Program arguments.
|
|
|
|
* @param options Spawn options.
|
|
|
|
*/
|
2021-04-29 00:25:44 +03:00
|
|
|
constructor(
|
|
|
|
program: string,
|
|
|
|
args: string | string[] = [],
|
|
|
|
options?: SpawnOptions
|
|
|
|
) {
|
2021-03-31 08:19:03 +03:00
|
|
|
super()
|
|
|
|
this.program = program
|
|
|
|
this.args = typeof args === 'string' ? [args] : args
|
2021-04-29 00:25:44 +03:00
|
|
|
this.options = options ?? {}
|
2021-03-31 08:19:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-05-05 20:36:40 +03:00
|
|
|
* Creates a command to execute the given sidecar program.
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* const command = Command.sidecar('my-sidecar')
|
|
|
|
* const output = await command.execute()
|
|
|
|
* ```
|
2021-03-31 08:19:03 +03:00
|
|
|
*
|
2021-05-05 20:36:40 +03:00
|
|
|
* @param program The program to execute.
|
|
|
|
* @param args Program arguments.
|
|
|
|
* @param options Spawn options.
|
2021-04-13 04:44:50 +03:00
|
|
|
* @returns
|
2021-03-31 08:19:03 +03:00
|
|
|
*/
|
2021-05-05 20:36:40 +03:00
|
|
|
static sidecar(
|
|
|
|
program: string,
|
|
|
|
args: string | string[] = [],
|
|
|
|
options?: SpawnOptions
|
|
|
|
): Command {
|
|
|
|
const instance = new Command(program, args, options)
|
2021-04-29 00:25:44 +03:00
|
|
|
instance.options.sidecar = true
|
2021-03-31 08:19:03 +03:00
|
|
|
return instance
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/**
|
|
|
|
* Executes the command as a child process, returning a handle to it.
|
|
|
|
*
|
|
|
|
* @return A promise resolving to the child process handle.
|
|
|
|
*/
|
2021-03-31 08:19:03 +03:00
|
|
|
async spawn(): Promise<Child> {
|
|
|
|
return execute(
|
|
|
|
(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
|
|
|
|
}
|
|
|
|
},
|
2021-04-29 00:25:44 +03:00
|
|
|
this.program,
|
|
|
|
this.args,
|
|
|
|
this.options
|
2021-03-31 08:19:03 +03:00
|
|
|
).then((pid) => new Child(pid))
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/**
|
|
|
|
* Executes the command as a child process, waiting for it to finish and collecting all of its output.
|
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* const output = await new Command('echo', 'message').execute()
|
|
|
|
* assert(output.code === 0)
|
|
|
|
* assert(output.signal === null)
|
|
|
|
* assert(output.stdout === 'message')
|
|
|
|
* assert(output.stderr === '')
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @return A promise resolving to the child process output.
|
|
|
|
*/
|
2021-03-31 08:19:03 +03:00
|
|
|
async execute(): Promise<ChildProcess> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.on('error', reject)
|
|
|
|
const stdout: string[] = []
|
|
|
|
const stderr: string[] = []
|
2021-10-13 16:30:24 +03:00
|
|
|
this.stdout.on('data', (line: string) => {
|
2021-03-31 08:19:03 +03:00
|
|
|
stdout.push(line)
|
|
|
|
})
|
2021-10-13 16:30:24 +03:00
|
|
|
this.stderr.on('data', (line: string) => {
|
2021-03-31 08:19:03 +03:00
|
|
|
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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/**
|
|
|
|
* Describes the event message received from the command.
|
|
|
|
*/
|
2021-03-31 08:19:03 +03:00
|
|
|
interface Event<T, V> {
|
|
|
|
event: T
|
|
|
|
payload: V
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/**
|
|
|
|
* Payload for the `Terminated` command event.
|
|
|
|
*/
|
2021-03-31 08:19:03 +03:00
|
|
|
interface TerminatedPayload {
|
2021-05-05 20:36:40 +03:00
|
|
|
/** Exit code of the process. `null` if the process was terminated by a signal on Unix. */
|
2021-03-31 08:19:03 +03:00
|
|
|
code: number | null
|
2021-05-05 20:36:40 +03:00
|
|
|
/** If the process was terminated by a signal, represents that signal. */
|
2021-03-31 08:19:03 +03:00
|
|
|
signal: number | null
|
|
|
|
}
|
|
|
|
|
2021-05-05 20:36:40 +03:00
|
|
|
/** Events emitted by the child process. */
|
2021-03-31 08:19:03 +03:00
|
|
|
type CommandEvent =
|
|
|
|
| Event<'Stdout', string>
|
|
|
|
| Event<'Stderr', string>
|
|
|
|
| Event<'Terminated', TerminatedPayload>
|
|
|
|
| Event<'Error', string>
|
|
|
|
|
2021-02-12 08:42:40 +03:00
|
|
|
/**
|
2021-04-13 04:44:50 +03:00
|
|
|
* Opens a path or URL with the system's default app,
|
|
|
|
* or the one specified with `openWith`.
|
2021-05-05 20:36:40 +03:00
|
|
|
* @example
|
|
|
|
* ```typescript
|
|
|
|
* // opens the given URL on the default browser:
|
|
|
|
* await open('https://github.com/tauri-apps/tauri')
|
|
|
|
* // opens the given URL using `firefox`:
|
|
|
|
* await open('https://github.com/tauri-apps/tauri', 'firefox')
|
|
|
|
* // opens a file using the default program:
|
|
|
|
* await open('/path/to/file')
|
|
|
|
* ```
|
2021-02-12 08:42:40 +03:00
|
|
|
*
|
2021-05-05 20:36:40 +03:00
|
|
|
* @param path The path or URL to open.
|
|
|
|
* @param openWith The app to open the file or URL with. Defaults to the system default application for the specified path type.
|
2021-04-13 04:44:50 +03:00
|
|
|
* @returns
|
2021-02-12 08:42:40 +03:00
|
|
|
*/
|
2021-03-13 03:02:36 +03:00
|
|
|
async function open(path: string, openWith?: string): Promise<void> {
|
2021-03-07 05:19:12 +03:00
|
|
|
return invokeTauriCommand({
|
2021-02-16 07:23:15 +03:00
|
|
|
__tauriModule: 'Shell',
|
2021-02-12 08:42:40 +03:00
|
|
|
message: {
|
|
|
|
cmd: 'open',
|
2021-03-10 07:20:54 +03:00
|
|
|
path,
|
|
|
|
with: openWith
|
2021-02-12 08:42:40 +03:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-03-31 08:19:03 +03:00
|
|
|
export { Command, Child, open }
|
2021-04-29 00:25:44 +03:00
|
|
|
export type { ChildProcess, SpawnOptions }
|