mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-12 22:47:08 +02:00
* feat(shell): add process group spawn option (fix #1332) Add a `processGroup` boolean option to the shell plugin's spawn command. When enabled, the child process is spawned in its own process group (POSIX) or job object (Windows) using the `process-wrap` crate, so that killing the child also kills the entire process tree. This fixes the issue where programs like PyInstaller wrappers spawn a child process that gets orphaned when Tauri kills the parent. * test(shell): add PyInstaller simulation tests for process group kill Add end-to-end tests that reproduce the exact scenario from issue #1332: a wrapper process (like PyInstaller's bootloader) spawns a grandchild. - Without process_group: killing the wrapper orphans the grandchild - With process_group: killing the wrapper also kills the grandchild * refactor(shell): drop redundant cfg gates, trim process-wrap features Address review feedback on #3351: - Remove `#[cfg(any(unix, windows))]` gates on `ChildKind::ProcessGroup`, the kill/pid match arms, and the spawn block, plus the now-dead `#[cfg(not(any(unix, windows)))]` fallback. That cfg only excluded wasm, which this plugin never builds for. - Set `default-features = false` on process-wrap and enable only the features actually used: std, process-group (unix), creation-flags and job-object (windows). Drops kill-on-drop, process-session, and tracing. * fix(shell): enable process-wrap tracing feature, fix CI formatting and changefile - Add `tracing` feature to process-wrap: with default-features = false its Windows job_object.rs calls `debug\!` unconditionally while the import is gated behind `#[cfg(feature = "tracing")]`, failing the Windows build with "cannot find macro `debug`". - Reformat process/mod.rs assertions to satisfy `cargo fmt --check`. - Reindent the process-wrap features array to 2 spaces for taplo. - Use covector package keys (`shell` / `shell-js`) in the change file so the check-change-files and covector status jobs pass. * refactor(shell): unify child handling on process-wrap, drop SharedChild Collapse the ChildKind enum and the per-platform GroupChild types into a single process-wrap-backed child. The command always spawns through StdCommandWrap; the process_group flag is now just an optional wrapper rather than a switch into a separate child type. This drops the shared_child dependency and resolves two review notes: the Unix path no longer hand-rolls killpg, and kill() now goes through process-wrap's kill (start_kill + wait), which reaps the entire process group instead of only signalling it. A background thread polls try_wait to surface the exit status, since process-wrap's wrappers only expose &mut self wait methods (the reason SharedChild was used on Unix in the first place). * chore(example): add process group toggle to shell view * fix(shell): pass typed creation flags to process-wrap on Windows `process_wrap::std::CreationFlags` is a newtype over the `windows` crate's `PROCESS_CREATION_FLAGS`, not a raw `u32`, so the Windows build failed to compile. Depend on `windows` (already in the tree via `tauri`, pinned to the same 0.61 that `process-wrap` links against) and use its `CREATE_NO_WINDOW` constant, which drops the hand-rolled magic number. The wrap is still required: `JobObject::pre_spawn` calls `creation_flags` and would otherwise clobber the `CREATE_NO_WINDOW` set at construction. * perf(shell): block on child exit instead of polling try_wait The switch from `SharedChild` to `process-wrap` moved the child behind a mutex, since `StdChildWrapper`'s wait methods take `&mut self` while both the wait thread and `CommandChild::kill()` need access. A blocking wait would hold the lock for the child's whole lifetime and starve `kill()`, so the wait thread polled `try_wait` every 10ms instead. Block on the raw process outside the lock using a wait that does not reap (`waitid` with `WNOWAIT` on unix, `WaitForSingleObject` on windows), then take the lock once to let process-wrap collect the exit status. This is the same technique shared_child uses internally, applied on top of process-wrap's kill/reap semantics. No new dependencies or Cargo features are required. `kill()` reaps the child while holding the lock, so the raw wait can lose that race and return ECHILD. That means the exit status is already cached in the wrapper, so it is treated as success and the status is collected via `try_wait`. Without this guard, a stress test of 300 spawn+kill cycles produced 36 Error events in place of Terminated; with it, none in 900 cycles. The `try_wait` loop stays as a defensive fallback, though in practice the first call succeeds immediately. Mean latency for `Command::output()` on macOS (debug, 30 iterations of `echo`) drops from 13.22ms to ~1.8ms, and 10 idle 5s children cost ~11ms CPU total instead of ~75ms with the poll loop. * fix(shell): keep CREATE_NO_WINDOW on process-group children (windows) process-wrap's CreationFlags/JobObject coordination is broken during spawn (watchexec/process-wrap#35): JobObject::pre_spawn cannot see the CreationFlags wrapper, so it overwrote our flags with just CREATE_SUSPENDED and process-group children flashed a console window. Register CreationFlags(CREATE_SUSPENDED | CREATE_NO_WINDOW) after JobObject so its pre_spawn runs last and wins. CREATE_SUSPENDED is kept so the child cannot spawn grandchildren before job assignment; the same upstream bug keeps wrap_child's resume check from seeing it, so the child is still resumed. * test(shell): run the process-tree kill tests on windows Add a powershell variant of the pyinstaller simulation script and lift the unix-only gates on the process-group tests, so the windows job in test-rust exercises the job-object spawn/kill path instead of skipping it. Both pyinstaller tests now also assert that a Terminated event is still delivered after kill(). * Update plugins/shell/src/process/mod.rs Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com> * Update .changes/shell-process-group.md Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com> * Update plugins/shell/src/process/mod.rs Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com> * Delete .changes/shell-wait-no-poll.md * refactor(shell): replace process-wrap with shared_child and an inlined job object Use std process groups on unix and a job object on windows, with shared_child handling wait and kill on all platforms. * fix(shell): kill the process group even after its leader exited CommandChild::kill returned early once the direct child had been reaped, so killpg/TerminateJobObject never ran and anything the child left running in its process group or job object survived, including on app exit. Adds a launcher-style test (the child starts a grandchild and exits) that runs on Windows, Linux and macOS. * fix(shell): restore Debug on CommandChild Dropping the derive was a breaking change for downstream types that derive Debug and hold a CommandChild. * fix(shell): don't block the main thread in the kill command CommandChild::kill now waits for the child to exit, and the sync kill command ran it on the main thread while still holding the children lock. Make the command async and release the lock before killing. --------- Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com> Co-authored-by: Lucas Nogueira <lucas@tauri.app>
939 lines
29 KiB
TypeScript
939 lines
29 KiB
TypeScript
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
/**
|
|
* Access the system shell.
|
|
* Allows you to spawn child processes and manage files and URLs using their default application.
|
|
*
|
|
* ## Security
|
|
*
|
|
* This API has a scope configuration that forces you to restrict the programs and arguments that can be used.
|
|
*
|
|
* ### Restricting access to the {@link open | `open`} API
|
|
*
|
|
* On the configuration object, `open: true` means that the {@link open} API can be used with any URL,
|
|
* as the argument is validated with the `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+` regex.
|
|
* You can change that regex by changing the boolean value to a string, e.g. `open: ^https://github.com/`.
|
|
*
|
|
* ### Restricting access to the {@link Command | `Command`} APIs
|
|
*
|
|
* The plugin permissions object has a `scope` field that defines an array of CLIs that can be used.
|
|
* Each CLI is a configuration object `{ name: string, cmd: string, sidecar?: bool, args?: boolean | Arg[] }`.
|
|
*
|
|
* - `name`: the unique identifier of the command, passed to the {@link Command.create | Command.create function}.
|
|
* If it's a sidecar, this must be the value defined on `tauri.conf.json > bundle > externalBin`.
|
|
* - `cmd`: the program that is executed on this configuration. If it's a sidecar, this value is ignored.
|
|
* - `sidecar`: whether the object configures a sidecar or a system program.
|
|
* - `args`: the arguments that can be passed to the program. By default no arguments are allowed.
|
|
* - `true` means that any argument list is allowed.
|
|
* - `false` means that no arguments are allowed.
|
|
* - otherwise an array can be configured. Each item is either a string representing the fixed argument value
|
|
* or a `{ validator: string }` that defines a regex validating the argument value.
|
|
*
|
|
* #### Example scope configuration
|
|
*
|
|
* CLI: `git commit -m "the commit message"`
|
|
*
|
|
* Capability:
|
|
* ```json
|
|
* {
|
|
* "permissions": [
|
|
* {
|
|
* "identifier": "shell:allow-execute",
|
|
* "allow": [
|
|
* {
|
|
* "name": "run-git-commit",
|
|
* "cmd": "git",
|
|
* "args": ["commit", "-m", { "validator": "\\S+" }]
|
|
* }
|
|
* ]
|
|
* }
|
|
* ]
|
|
* }
|
|
* ```
|
|
* Usage:
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell'
|
|
* Command.create('run-git-commit', ['commit', '-m', 'the commit message'])
|
|
* ```
|
|
*
|
|
* Trying to execute any API with a program not configured on the scope results in a promise rejection due to denied access.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { invoke, Channel } from '@tauri-apps/api/core'
|
|
|
|
/**
|
|
* Options that configure how a child process is spawned.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
interface SpawnOptions {
|
|
/** Current working directory. */
|
|
cwd?: string
|
|
/** Environment variables. set to `null` to clear the process env. */
|
|
env?: Record<string, string>
|
|
/**
|
|
* Character encoding for stdout/stderr
|
|
*
|
|
* @since 2.0.0
|
|
* */
|
|
encoding?: string
|
|
/**
|
|
* When enabled, spawns the child process in its own process group (POSIX)
|
|
* or job object (Windows). This allows killing the entire process tree
|
|
* when calling `kill()` on the child process.
|
|
*
|
|
* Useful for programs that spawn child processes, such as PyInstaller wrappers.
|
|
*
|
|
* Defaults to `false`.
|
|
*/
|
|
processGroup?: boolean
|
|
}
|
|
|
|
/** @ignore */
|
|
interface InternalSpawnOptions extends SpawnOptions {
|
|
sidecar?: boolean
|
|
}
|
|
|
|
/**
|
|
* The output collected from a child process that ran to completion.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
interface ChildProcess<O extends IOPayload> {
|
|
/** Exit code of the process. `null` if the process was terminated by a signal on Unix. */
|
|
code: number | null
|
|
/** If the process was terminated by a signal, represents that signal. */
|
|
signal: number | null
|
|
/** The data that the process wrote to `stdout`. */
|
|
stdout: O
|
|
/** The data that the process wrote to `stderr`. */
|
|
stderr: O
|
|
}
|
|
|
|
/**
|
|
* A minimal event emitter modeled after Node.js' `EventEmitter`, used by
|
|
* {@link Command} and by its `stdout` and `stderr` streams.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
class EventEmitter<E extends Record<string, any>> {
|
|
/** @ignore */
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
|
private eventListeners: Record<keyof E, Array<(arg: any) => void>> =
|
|
Object.create(null)
|
|
|
|
/**
|
|
* Alias for `emitter.on(eventName, listener)`.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* command.addListener('error', (error) => console.error(error));
|
|
* ```
|
|
*
|
|
* @param eventName The name of the event to listen to.
|
|
* @param listener The callback invoked with the event payload.
|
|
*
|
|
* @returns A reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
addListener<N extends keyof E>(
|
|
eventName: N,
|
|
listener: (arg: E[typeof eventName]) => void
|
|
): this {
|
|
return this.on(eventName, listener)
|
|
}
|
|
|
|
/**
|
|
* Alias for `emitter.off(eventName, listener)`.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* const listener = (error: string) => console.error(error);
|
|
* command.addListener('error', listener);
|
|
* command.removeListener('error', listener);
|
|
* ```
|
|
*
|
|
* @param eventName The name of the event to stop listening to.
|
|
* @param listener The exact callback that was registered before.
|
|
*
|
|
* @returns A reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
removeListener<N extends keyof E>(
|
|
eventName: N,
|
|
listener: (arg: E[typeof eventName]) => void
|
|
): this {
|
|
return this.off(eventName, listener)
|
|
}
|
|
|
|
/**
|
|
* Adds the `listener` function to the end of the listeners array for the
|
|
* event named `eventName`. No checks are made to see if the `listener` has
|
|
* already been added. Multiple calls passing the same combination of `eventName`and `listener` will result in the `listener` being added, and called, multiple
|
|
* times.
|
|
*
|
|
* Returns a reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* command.on('close', (data) => {
|
|
* console.log(`command finished with code ${data.code}`);
|
|
* });
|
|
* ```
|
|
*
|
|
* @param eventName The name of the event to listen to.
|
|
* @param listener The callback invoked with the event payload.
|
|
*
|
|
* @returns A reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
on<N extends keyof E>(
|
|
eventName: N,
|
|
listener: (arg: E[typeof eventName]) => void
|
|
): this {
|
|
if (eventName in this.eventListeners) {
|
|
// eslint-disable-next-line security/detect-object-injection
|
|
this.eventListeners[eventName].push(listener)
|
|
} else {
|
|
// eslint-disable-next-line security/detect-object-injection
|
|
this.eventListeners[eventName] = [listener]
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a **one-time**`listener` function for the event named `eventName`. The
|
|
* next time `eventName` is triggered, this listener is removed and then invoked.
|
|
*
|
|
* Returns a reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* command.once('close', (data) => {
|
|
* console.log(`command finished with code ${data.code}`);
|
|
* });
|
|
* ```
|
|
*
|
|
* @param eventName The name of the event to listen to once.
|
|
* @param listener The callback invoked with the event payload.
|
|
*
|
|
* @returns A reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
once<N extends keyof E>(
|
|
eventName: N,
|
|
listener: (arg: E[typeof eventName]) => void
|
|
): this {
|
|
const wrapper = (arg: E[typeof eventName]): void => {
|
|
this.removeListener(eventName, wrapper)
|
|
listener(arg)
|
|
}
|
|
return this.addListener(eventName, wrapper)
|
|
}
|
|
|
|
/**
|
|
* Removes the all specified listener from the listener array for the event eventName
|
|
* Returns a reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* const listener = (error: string) => console.error(error);
|
|
* command.on('error', listener);
|
|
* command.off('error', listener);
|
|
* ```
|
|
*
|
|
* @param eventName The name of the event to stop listening to.
|
|
* @param listener The exact callback that was registered before.
|
|
*
|
|
* @returns A reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
off<N extends keyof E>(
|
|
eventName: N,
|
|
listener: (arg: E[typeof eventName]) => void
|
|
): this {
|
|
if (eventName in this.eventListeners) {
|
|
// eslint-disable-next-line security/detect-object-injection
|
|
this.eventListeners[eventName] = this.eventListeners[eventName].filter(
|
|
(l) => l !== listener
|
|
)
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Removes all listeners, or those of the specified eventName.
|
|
*
|
|
* Returns a reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* command.on('error', (error) => console.error(error));
|
|
* command.removeAllListeners('error');
|
|
* ```
|
|
*
|
|
* @param event The name of the event to remove the listeners of.
|
|
* When omitted, the listeners of every event are removed.
|
|
*
|
|
* @returns A reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
removeAllListeners<N extends keyof E>(event?: N): this {
|
|
if (event) {
|
|
// eslint-disable-next-line security/detect-object-injection
|
|
delete this.eventListeners[event]
|
|
} else {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
this.eventListeners = Object.create(null)
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Synchronously calls each of the listeners registered for the event named
|
|
* `eventName`, in the order they were registered, passing the supplied arguments
|
|
* to each.
|
|
*
|
|
* @ignore
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { EventEmitter } from '@tauri-apps/plugin-shell';
|
|
* const emitter = new EventEmitter<{ data: string }>();
|
|
* emitter.on('data', (line) => console.log(line));
|
|
* emitter.emit('data', 'hello');
|
|
* ```
|
|
*
|
|
* @param eventName The name of the event to emit.
|
|
* @param arg The payload passed to every registered listener.
|
|
*
|
|
* @returns `true` if the event had listeners, `false` otherwise.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
emit<N extends keyof E>(eventName: N, arg: E[typeof eventName]): boolean {
|
|
if (eventName in this.eventListeners) {
|
|
// eslint-disable-next-line security/detect-object-injection
|
|
const listeners = this.eventListeners[eventName]
|
|
for (const listener of listeners) listener(arg)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Returns the number of listeners listening to the event named `eventName`.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* command.on('close', () => {});
|
|
* console.log(command.listenerCount('close')); // 1
|
|
* ```
|
|
*
|
|
* @param eventName The name of the event to count the listeners of.
|
|
*
|
|
* @returns The number of listeners registered for the given event.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
listenerCount<N extends keyof E>(eventName: N): number {
|
|
if (eventName in this.eventListeners)
|
|
// eslint-disable-next-line security/detect-object-injection
|
|
return this.eventListeners[eventName].length
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
* Adds the `listener` function to the _beginning_ of the listeners array for the
|
|
* event named `eventName`. No checks are made to see if the `listener` has
|
|
* already been added. Multiple calls passing the same combination of `eventName`and `listener` will result in the `listener` being added, and called, multiple
|
|
* times.
|
|
*
|
|
* Returns a reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* command.prependListener('error', (error) => console.error(error));
|
|
* ```
|
|
*
|
|
* @param eventName The name of the event to listen to.
|
|
* @param listener The callback invoked with the event payload.
|
|
*
|
|
* @returns A reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
prependListener<N extends keyof E>(
|
|
eventName: N,
|
|
listener: (arg: E[typeof eventName]) => void
|
|
): this {
|
|
if (eventName in this.eventListeners) {
|
|
// eslint-disable-next-line security/detect-object-injection
|
|
this.eventListeners[eventName].unshift(listener)
|
|
} else {
|
|
// eslint-disable-next-line security/detect-object-injection
|
|
this.eventListeners[eventName] = [listener]
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a **one-time**`listener` function for the event named `eventName` to the_beginning_ of the listeners array. The next time `eventName` is triggered, this
|
|
* listener is removed, and then invoked.
|
|
*
|
|
* Returns a reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* command.prependOnceListener('error', (error) => console.error(error));
|
|
* ```
|
|
*
|
|
* @param eventName The name of the event to listen to once.
|
|
* @param listener The callback invoked with the event payload.
|
|
*
|
|
* @returns A reference to the `EventEmitter`, so that calls can be chained.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
prependOnceListener<N extends keyof E>(
|
|
eventName: N,
|
|
listener: (arg: E[typeof eventName]) => void
|
|
): this {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const wrapper = (arg: any): void => {
|
|
this.removeListener(eventName, wrapper)
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
listener(arg)
|
|
}
|
|
return this.prependListener(eventName, wrapper)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A handle to a child process spawned with {@link Command.spawn},
|
|
* which can be used to write to its `stdin` or to kill it.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
class Child {
|
|
/** The child process `pid`. */
|
|
pid: number
|
|
|
|
/**
|
|
* Creates a handle to the child process with the given process id.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* // a `Child` is usually obtained by spawning a command:
|
|
* const child = await Command.create('node').spawn();
|
|
* console.log(child.pid);
|
|
* ```
|
|
*
|
|
* @param pid The process id of the child process.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
constructor(pid: number) {
|
|
this.pid = pid
|
|
}
|
|
|
|
/**
|
|
* Writes `data` to the `stdin`.
|
|
*
|
|
* @param data The message to write, either a string or a byte array.
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* const child = await command.spawn();
|
|
* await child.write('message');
|
|
* await child.write([0, 1, 2, 3, 4, 5]);
|
|
* ```
|
|
*
|
|
* @returns A promise indicating the success or failure of the operation.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
async write(data: IOPayload | number[]): Promise<void> {
|
|
await invoke('plugin:shell|stdin_write', {
|
|
pid: this.pid,
|
|
buffer: data
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Kills the child process.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* const child = await command.spawn();
|
|
* await child.kill();
|
|
* ```
|
|
*
|
|
* @returns A promise indicating the success or failure of the operation.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
async kill(): Promise<void> {
|
|
await invoke('plugin:shell|kill', {
|
|
cmd: 'killChild',
|
|
pid: this.pid
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The events emitted by a {@link Command} instance.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
interface CommandEvents {
|
|
/** Emitted when the child process terminated, carrying its exit code and signal. */
|
|
close: TerminatedPayload
|
|
/** Emitted when the child process could not be spawned or failed unexpectedly, carrying the error message. */
|
|
error: string
|
|
}
|
|
|
|
/**
|
|
* The events emitted by the `stdout` and `stderr` streams of a {@link Command}.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
interface OutputEvents<O extends IOPayload> {
|
|
/** Emitted for each line the process wrote to the stream, or for each raw chunk when the `raw` encoding is used. */
|
|
data: O
|
|
}
|
|
|
|
/**
|
|
* The entry point for spawning child processes.
|
|
* It emits the `close` and `error` events.
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('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);
|
|
* ```
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
*/
|
|
class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
|
|
/** @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<OutputEvents<O>>()
|
|
/** Event emitter for the `stderr`. Emits the `data` event. */
|
|
readonly stderr = new EventEmitter<OutputEvents<O>>()
|
|
|
|
/**
|
|
* @ignore
|
|
* Creates a new `Command` instance.
|
|
*
|
|
* @param program The program name to execute.
|
|
* It must be configured in your project's capabilities.
|
|
* @param args Program arguments.
|
|
* @param options Spawn options.
|
|
*/
|
|
private constructor(
|
|
program: string,
|
|
args: string | string[] = [],
|
|
options?: SpawnOptions
|
|
) {
|
|
super()
|
|
this.program = program
|
|
this.args = typeof args === 'string' ? [args] : args
|
|
this.options = options ?? {}
|
|
}
|
|
|
|
/**
|
|
* Creates a command to execute the given program, decoding its output as text.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('my-app', ['run', 'tauri']);
|
|
* const output = await command.execute();
|
|
* ```
|
|
*
|
|
* @param program The program to execute.
|
|
* It must be configured in your project's capabilities.
|
|
* @param args The arguments to pass to the program. Defaults to no arguments.
|
|
*
|
|
* @returns The command instance, ready to be spawned or executed.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
static create(program: string, args?: string | string[]): Command<string>
|
|
/**
|
|
* Creates a command to execute the given program, keeping its output as raw bytes.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('my-app', ['run', 'tauri'], { encoding: 'raw' });
|
|
* const output = await command.execute();
|
|
* console.log(output.stdout); // a Uint8Array
|
|
* ```
|
|
*
|
|
* @param program The program to execute.
|
|
* It must be configured in your project's capabilities.
|
|
* @param args The arguments to pass to the program. Defaults to no arguments.
|
|
* @param options Spawn options using the `raw` encoding, which makes the process
|
|
* output be delivered as `Uint8Array` instead of `string`.
|
|
*
|
|
* @returns The command instance, ready to be spawned or executed.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
static create(
|
|
program: string,
|
|
args?: string | string[],
|
|
options?: SpawnOptions & { encoding: 'raw' }
|
|
): Command<Uint8Array>
|
|
/**
|
|
* Creates a command to execute the given program with the given spawn options.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('my-app', ['run', 'tauri'], { cwd: '/path/to/project' });
|
|
* const output = await command.execute();
|
|
* ```
|
|
*
|
|
* @param program The program to execute.
|
|
* It must be configured in your project's capabilities.
|
|
* @param args The arguments to pass to the program. Defaults to no arguments.
|
|
* @param options Spawn options such as the working directory, the environment
|
|
* variables and the character encoding of the process output.
|
|
*
|
|
* @returns The command instance, ready to be spawned or executed.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
static create(
|
|
program: string,
|
|
args?: string | string[],
|
|
options?: SpawnOptions
|
|
): Command<string>
|
|
|
|
/**
|
|
* Creates a command to execute the given program.
|
|
*
|
|
* @param program The program to execute.
|
|
* It must be configured in your project's capabilities.
|
|
* @param args The arguments to pass to the program. Defaults to no arguments.
|
|
* @param options Spawn options such as the working directory, the environment
|
|
* variables and the character encoding of the process output.
|
|
*
|
|
* @returns The command instance, ready to be spawned or executed.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
static create<O extends IOPayload>(
|
|
program: string,
|
|
args: string | string[] = [],
|
|
options?: SpawnOptions
|
|
): Command<O> {
|
|
return new Command(program, args, options)
|
|
}
|
|
|
|
/**
|
|
* Creates a command to execute the given sidecar program, decoding its output as text.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.sidecar('my-sidecar');
|
|
* const output = await command.execute();
|
|
* ```
|
|
*
|
|
* @param program The sidecar program to execute.
|
|
* It must be configured in your project's capabilities
|
|
* and defined on `tauri.conf.json > bundle > externalBin`.
|
|
* @param args The arguments to pass to the program. Defaults to no arguments.
|
|
*
|
|
* @returns The command instance, ready to be spawned or executed.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
static sidecar(program: string, args?: string | string[]): Command<string>
|
|
/**
|
|
* Creates a command to execute the given sidecar program, keeping its output as raw bytes.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.sidecar('my-sidecar', [], { encoding: 'raw' });
|
|
* const output = await command.execute();
|
|
* console.log(output.stdout); // a Uint8Array
|
|
* ```
|
|
*
|
|
* @param program The sidecar program to execute.
|
|
* It must be configured in your project's capabilities
|
|
* and defined on `tauri.conf.json > bundle > externalBin`.
|
|
* @param args The arguments to pass to the program. Defaults to no arguments.
|
|
* @param options Spawn options using the `raw` encoding, which makes the process
|
|
* output be delivered as `Uint8Array` instead of `string`.
|
|
*
|
|
* @returns The command instance, ready to be spawned or executed.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
static sidecar(
|
|
program: string,
|
|
args?: string | string[],
|
|
options?: SpawnOptions & { encoding: 'raw' }
|
|
): Command<Uint8Array>
|
|
/**
|
|
* Creates a command to execute the given sidecar program with the given spawn options.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.sidecar('my-sidecar', [], { cwd: '/path/to/project' });
|
|
* const output = await command.execute();
|
|
* ```
|
|
*
|
|
* @param program The sidecar program to execute.
|
|
* It must be configured in your project's capabilities
|
|
* and defined on `tauri.conf.json > bundle > externalBin`.
|
|
* @param args The arguments to pass to the program. Defaults to no arguments.
|
|
* @param options Spawn options such as the working directory, the environment
|
|
* variables and the character encoding of the process output.
|
|
*
|
|
* @returns The command instance, ready to be spawned or executed.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
static sidecar(
|
|
program: string,
|
|
args?: string | string[],
|
|
options?: SpawnOptions
|
|
): Command<string>
|
|
|
|
/**
|
|
* Creates a command to execute the given sidecar program.
|
|
*
|
|
* @param program The sidecar program to execute.
|
|
* It must be configured in your project's capabilities
|
|
* and defined on `tauri.conf.json > bundle > externalBin`.
|
|
* @param args The arguments to pass to the program. Defaults to no arguments.
|
|
* @param options Spawn options such as the working directory, the environment
|
|
* variables and the character encoding of the process output.
|
|
*
|
|
* @returns The command instance, ready to be spawned or executed.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
static sidecar<O extends IOPayload>(
|
|
program: string,
|
|
args: string | string[] = [],
|
|
options?: SpawnOptions
|
|
): Command<O> {
|
|
const instance = new Command<O>(program, args, options)
|
|
instance.options.sidecar = true
|
|
return instance
|
|
}
|
|
|
|
/**
|
|
* Executes the command as a child process, returning a handle to it.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const command = Command.create('node');
|
|
* command.stdout.on('data', (line) => console.log(line));
|
|
* const child = await command.spawn();
|
|
* console.log('pid:', child.pid);
|
|
* ```
|
|
*
|
|
* @returns A promise resolving to the child process handle.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
async spawn(): Promise<Child> {
|
|
const program = this.program
|
|
const args = this.args
|
|
const options = this.options
|
|
|
|
if (typeof args === 'object') {
|
|
Object.freeze(args)
|
|
}
|
|
|
|
const onEvent = new Channel<CommandEvent<O>>()
|
|
onEvent.onmessage = (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
|
|
}
|
|
}
|
|
|
|
return await invoke<number>('plugin:shell|spawn', {
|
|
program,
|
|
args,
|
|
options,
|
|
onEvent
|
|
}).then((pid) => new Child(pid))
|
|
}
|
|
|
|
/**
|
|
* Executes the command as a child process, waiting for it to finish and collecting all of its output.
|
|
* @example
|
|
* ```typescript
|
|
* import { Command } from '@tauri-apps/plugin-shell';
|
|
* const output = await Command.create('echo', 'message').execute();
|
|
* assert(output.code === 0);
|
|
* assert(output.signal === null);
|
|
* assert(output.stdout === 'message');
|
|
* assert(output.stderr === '');
|
|
* ```
|
|
*
|
|
* @returns A promise resolving to the child process output.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
async execute(): Promise<ChildProcess<O>> {
|
|
const program = this.program
|
|
const args = this.args
|
|
const options = this.options
|
|
|
|
if (typeof args === 'object') {
|
|
Object.freeze(args)
|
|
}
|
|
|
|
return await invoke<ChildProcess<O>>('plugin:shell|execute', {
|
|
program,
|
|
args,
|
|
options
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Describes the event message received from the command.
|
|
*/
|
|
interface Event<T, V> {
|
|
event: T
|
|
payload: V
|
|
}
|
|
|
|
/**
|
|
* Payload for the `Terminated` command event.
|
|
*/
|
|
interface TerminatedPayload {
|
|
/** Exit code of the process. `null` if the process was terminated by a signal on Unix. */
|
|
code: number | null
|
|
/** If the process was terminated by a signal, represents that signal. */
|
|
signal: number | null
|
|
}
|
|
|
|
/**
|
|
* The type of the data a child process writes to `stdout` and `stderr`:
|
|
* a `string`, or a `Uint8Array` when the `raw` encoding is configured.
|
|
*/
|
|
type IOPayload = string | Uint8Array
|
|
|
|
/** Events emitted by the child process. */
|
|
type CommandEvent<O extends IOPayload> =
|
|
| Event<'Stdout', O>
|
|
| Event<'Stderr', O>
|
|
| Event<'Terminated', TerminatedPayload>
|
|
| Event<'Error', string>
|
|
|
|
/**
|
|
* Opens a path or URL with the system's default app,
|
|
* or the one specified with `openWith`.
|
|
*
|
|
* The `openWith` value must be one of `firefox`, `google chrome`, `chromium` `safari`,
|
|
* `open`, `start`, `xdg-open`, `gio`, `gnome-open`, `kde-open` or `wslview`.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { open } from '@tauri-apps/plugin-shell';
|
|
* // 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');
|
|
* ```
|
|
*
|
|
* @param path The path or URL to open.
|
|
* This value is matched against the string regex defined on `tauri.conf.json > plugins > shell > open`,
|
|
* which defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
|
|
* @param openWith The app to open the file or URL with.
|
|
* Defaults to the system default application for the specified path type.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
async function open(path: string, openWith?: string): Promise<void> {
|
|
await invoke('plugin:shell|open', {
|
|
path,
|
|
with: openWith
|
|
})
|
|
}
|
|
|
|
export { Command, Child, EventEmitter, open }
|
|
export type {
|
|
IOPayload,
|
|
CommandEvents,
|
|
TerminatedPayload,
|
|
OutputEvents,
|
|
ChildProcess,
|
|
SpawnOptions
|
|
}
|