shell.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. /**
  5. * Access the system shell.
  6. * Allows you to spawn child processes and manage files and URLs using their default application.
  7. *
  8. * This package is also accessible with `window.__TAURI__.shell` when [`build.withGlobalTauri`](https://tauri.app/v1/api/config/#buildconfig.withglobaltauri) in `tauri.conf.json` is set to `true`.
  9. *
  10. * The APIs must be added to [`tauri.allowlist.shell`](https://tauri.app/v1/api/config/#allowlistconfig.shell) in `tauri.conf.json`:
  11. * ```json
  12. * {
  13. * "tauri": {
  14. * "allowlist": {
  15. * "shell": {
  16. * "all": true, // enable all shell APIs
  17. * "execute": true, // enable process spawn APIs
  18. * "sidecar": true, // enable spawning sidecars
  19. * "open": true // enable opening files/URLs using the default program
  20. * }
  21. * }
  22. * }
  23. * }
  24. * ```
  25. * It is recommended to allowlist only the APIs you use for optimal bundle size and security.
  26. *
  27. * ## Security
  28. *
  29. * This API has a scope configuration that forces you to restrict the programs and arguments that can be used.
  30. *
  31. * ### Restricting access to the {@link open | `open`} API
  32. *
  33. * On the allowlist, `open: true` means that the {@link open} API can be used with any URL,
  34. * as the argument is validated with the `^https?://` regex.
  35. * You can change that regex by changing the boolean value to a string, e.g. `open: ^https://github.com/`.
  36. *
  37. * ### Restricting access to the {@link Command | `Command`} APIs
  38. *
  39. * The `shell` allowlist object has a `scope` field that defines an array of CLIs that can be used.
  40. * Each CLI is a configuration object `{ name: string, cmd: string, sidecar?: bool, args?: boolean | Arg[] }`.
  41. *
  42. * - `name`: the unique identifier of the command, passed to the {@link Command.constructor | Command constructor}.
  43. * If it's a sidecar, this must be the value defined on `tauri.conf.json > tauri > bundle > externalBin`.
  44. * - `cmd`: the program that is executed on this configuration. If it's a sidecar, this value is ignored.
  45. * - `sidecar`: whether the object configures a sidecar or a system program.
  46. * - `args`: the arguments that can be passed to the program. By default no arguments are allowed.
  47. * - `true` means that any argument list is allowed.
  48. * - `false` means that no arguments are allowed.
  49. * - otherwise an array can be configured. Each item is either a string representing the fixed argument value
  50. * or a `{ validator: string }` that defines a regex validating the argument value.
  51. *
  52. * #### Example scope configuration
  53. *
  54. * CLI: `git commit -m "the commit message"`
  55. *
  56. * Configuration:
  57. * ```json
  58. * {
  59. * "scope": {
  60. * "name": "run-git-commit",
  61. * "cmd": "git",
  62. * "args": ["commit", "-m", { "validator": "\\S+" }]
  63. * }
  64. * }
  65. * ```
  66. * Usage:
  67. * ```typescript
  68. * import { Command } from '@tauri-apps/api/shell'
  69. * new Command('run-git-commit', ['commit', '-m', 'the commit message'])
  70. * ```
  71. *
  72. * Trying to execute any API with a program not configured on the scope results in a promise rejection due to denied access.
  73. *
  74. * @module
  75. */
  76. import { invokeTauriCommand } from './helpers/tauri'
  77. import { transformCallback } from './tauri'
  78. interface SpawnOptions {
  79. /** Current working directory. */
  80. cwd?: string
  81. /** Environment variables. set to `null` to clear the process env. */
  82. env?: { [name: string]: string }
  83. }
  84. /** @ignore */
  85. interface InternalSpawnOptions extends SpawnOptions {
  86. sidecar?: boolean
  87. }
  88. interface ChildProcess {
  89. /** Exit code of the process. `null` if the process was terminated by a signal on Unix. */
  90. code: number | null
  91. /** If the process was terminated by a signal, represents that signal. */
  92. signal: number | null
  93. /** The data that the process wrote to `stdout`. */
  94. stdout: string
  95. /** The data that the process wrote to `stderr`. */
  96. stderr: string
  97. }
  98. /**
  99. * Spawns a process.
  100. *
  101. * @ignore
  102. * @param program The name of the scoped command.
  103. * @param onEvent Event handler.
  104. * @param args Program arguments.
  105. * @param options Configuration for the process spawn.
  106. * @returns A promise resolving to the process id.
  107. */
  108. async function execute(
  109. onEvent: (event: CommandEvent) => void,
  110. program: string,
  111. args: string | string[] = [],
  112. options?: InternalSpawnOptions
  113. ): Promise<number> {
  114. if (typeof args === 'object') {
  115. Object.freeze(args)
  116. }
  117. return invokeTauriCommand<number>({
  118. __tauriModule: 'Shell',
  119. message: {
  120. cmd: 'execute',
  121. program,
  122. args,
  123. options,
  124. onEventFn: transformCallback(onEvent)
  125. }
  126. })
  127. }
  128. class EventEmitter<E extends string> {
  129. /** @ignore */
  130. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
  131. private eventListeners: {
  132. [key: string]: Array<(arg: any) => void>
  133. } = Object.create(null)
  134. /** @ignore */
  135. private addEventListener(event: string, handler: (arg: any) => void): void {
  136. if (event in this.eventListeners) {
  137. // eslint-disable-next-line security/detect-object-injection
  138. this.eventListeners[event].push(handler)
  139. } else {
  140. // eslint-disable-next-line security/detect-object-injection
  141. this.eventListeners[event] = [handler]
  142. }
  143. }
  144. /** @ignore */
  145. _emit(event: E, payload: any): void {
  146. if (event in this.eventListeners) {
  147. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  148. const listeners = this.eventListeners[event as any]
  149. for (const listener of listeners) {
  150. listener(payload)
  151. }
  152. }
  153. }
  154. /**
  155. * Listen to an event from the child process.
  156. *
  157. * @param event The event name.
  158. * @param handler The event handler.
  159. *
  160. * @return The `this` instance for chained calls.
  161. */
  162. on(event: E, handler: (arg: any) => void): EventEmitter<E> {
  163. this.addEventListener(event, handler)
  164. return this
  165. }
  166. }
  167. class Child {
  168. /** The child process `pid`. */
  169. pid: number
  170. constructor(pid: number) {
  171. this.pid = pid
  172. }
  173. /**
  174. * Writes `data` to the `stdin`.
  175. *
  176. * @param data The message to write, either a string or a byte array.
  177. * @example
  178. * ```typescript
  179. * import { Command } from '@tauri-apps/api/shell';
  180. * const command = new Command('node');
  181. * const child = await command.spawn();
  182. * await child.write('message');
  183. * await child.write([0, 1, 2, 3, 4, 5]);
  184. * ```
  185. *
  186. * @return A promise indicating the success or failure of the operation.
  187. */
  188. async write(data: string | Uint8Array): Promise<void> {
  189. return invokeTauriCommand({
  190. __tauriModule: 'Shell',
  191. message: {
  192. cmd: 'stdinWrite',
  193. pid: this.pid,
  194. // correctly serialize Uint8Arrays
  195. buffer: typeof data === 'string' ? data : Array.from(data)
  196. }
  197. })
  198. }
  199. /**
  200. * Kills the child process.
  201. *
  202. * @return A promise indicating the success or failure of the operation.
  203. */
  204. async kill(): Promise<void> {
  205. return invokeTauriCommand({
  206. __tauriModule: 'Shell',
  207. message: {
  208. cmd: 'killChild',
  209. pid: this.pid
  210. }
  211. })
  212. }
  213. }
  214. /**
  215. * The entry point for spawning child processes.
  216. * It emits the `close` and `error` events.
  217. * @example
  218. * ```typescript
  219. * import { Command } from '@tauri-apps/api/shell';
  220. * const command = new Command('node');
  221. * command.on('close', data => {
  222. * console.log(`command finished with code ${data.code} and signal ${data.signal}`)
  223. * });
  224. * command.on('error', error => console.error(`command error: "${error}"`));
  225. * command.stdout.on('data', line => console.log(`command stdout: "${line}"`));
  226. * command.stderr.on('data', line => console.log(`command stderr: "${line}"`));
  227. *
  228. * const child = await command.spawn();
  229. * console.log('pid:', child.pid);
  230. * ```
  231. */
  232. class Command extends EventEmitter<'close' | 'error'> {
  233. /** @ignore Program to execute. */
  234. private readonly program: string
  235. /** @ignore Program arguments */
  236. private readonly args: string[]
  237. /** @ignore Spawn options. */
  238. private readonly options: InternalSpawnOptions
  239. /** Event emitter for the `stdout`. Emits the `data` event. */
  240. readonly stdout = new EventEmitter<'data'>()
  241. /** Event emitter for the `stderr`. Emits the `data` event. */
  242. readonly stderr = new EventEmitter<'data'>()
  243. /**
  244. * Creates a new `Command` instance.
  245. *
  246. * @param program The program name to execute.
  247. * It must be configured on `tauri.conf.json > tauri > allowlist > shell > scope`.
  248. * @param args Program arguments.
  249. * @param options Spawn options.
  250. */
  251. constructor(
  252. program: string,
  253. args: string | string[] = [],
  254. options?: SpawnOptions
  255. ) {
  256. super()
  257. this.program = program
  258. this.args = typeof args === 'string' ? [args] : args
  259. this.options = options ?? {}
  260. }
  261. /**
  262. * Creates a command to execute the given sidecar program.
  263. * @example
  264. * ```typescript
  265. * import { Command } from '@tauri-apps/api/shell';
  266. * const command = Command.sidecar('my-sidecar');
  267. * const output = await command.execute();
  268. * ```
  269. *
  270. * @param program The program to execute.
  271. * It must be configured on `tauri.conf.json > tauri > allowlist > shell > scope`.
  272. * @param args Program arguments.
  273. * @param options Spawn options.
  274. * @returns
  275. */
  276. static sidecar(
  277. program: string,
  278. args: string | string[] = [],
  279. options?: SpawnOptions
  280. ): Command {
  281. const instance = new Command(program, args, options)
  282. instance.options.sidecar = true
  283. return instance
  284. }
  285. /**
  286. * Executes the command as a child process, returning a handle to it.
  287. *
  288. * @return A promise resolving to the child process handle.
  289. */
  290. async spawn(): Promise<Child> {
  291. return execute(
  292. (event) => {
  293. switch (event.event) {
  294. case 'Error':
  295. this._emit('error', event.payload)
  296. break
  297. case 'Terminated':
  298. this._emit('close', event.payload)
  299. break
  300. case 'Stdout':
  301. this.stdout._emit('data', event.payload)
  302. break
  303. case 'Stderr':
  304. this.stderr._emit('data', event.payload)
  305. break
  306. }
  307. },
  308. this.program,
  309. this.args,
  310. this.options
  311. ).then((pid) => new Child(pid))
  312. }
  313. /**
  314. * Executes the command as a child process, waiting for it to finish and collecting all of its output.
  315. * @example
  316. * ```typescript
  317. * import { Command } from '@tauri-apps/api/shell';
  318. * const output = await new Command('echo', 'message').execute();
  319. * assert(output.code === 0);
  320. * assert(output.signal === null);
  321. * assert(output.stdout === 'message');
  322. * assert(output.stderr === '');
  323. * ```
  324. *
  325. * @return A promise resolving to the child process output.
  326. */
  327. async execute(): Promise<ChildProcess> {
  328. return new Promise((resolve, reject) => {
  329. this.on('error', reject)
  330. const stdout: string[] = []
  331. const stderr: string[] = []
  332. this.stdout.on('data', (line: string) => {
  333. stdout.push(line)
  334. })
  335. this.stderr.on('data', (line: string) => {
  336. stderr.push(line)
  337. })
  338. this.on('close', (payload: TerminatedPayload) => {
  339. resolve({
  340. code: payload.code,
  341. signal: payload.signal,
  342. stdout: stdout.join('\n'),
  343. stderr: stderr.join('\n')
  344. })
  345. })
  346. this.spawn().catch(reject)
  347. })
  348. }
  349. }
  350. /**
  351. * Describes the event message received from the command.
  352. */
  353. interface Event<T, V> {
  354. event: T
  355. payload: V
  356. }
  357. /**
  358. * Payload for the `Terminated` command event.
  359. */
  360. interface TerminatedPayload {
  361. /** Exit code of the process. `null` if the process was terminated by a signal on Unix. */
  362. code: number | null
  363. /** If the process was terminated by a signal, represents that signal. */
  364. signal: number | null
  365. }
  366. /** Events emitted by the child process. */
  367. type CommandEvent =
  368. | Event<'Stdout', string>
  369. | Event<'Stderr', string>
  370. | Event<'Terminated', TerminatedPayload>
  371. | Event<'Error', string>
  372. /**
  373. * Opens a path or URL with the system's default app,
  374. * or the one specified with `openWith`.
  375. *
  376. * The `openWith` value must be one of `firefox`, `google chrome`, `chromium` `safari`,
  377. * `open`, `start`, `xdg-open`, `gio`, `gnome-open`, `kde-open` or `wslview`.
  378. *
  379. * @example
  380. * ```typescript
  381. * import { open } from '@tauri-apps/api/shell';
  382. * // opens the given URL on the default browser:
  383. * await open('https://github.com/tauri-apps/tauri');
  384. * // opens the given URL using `firefox`:
  385. * await open('https://github.com/tauri-apps/tauri', 'firefox');
  386. * // opens a file using the default program:
  387. * await open('/path/to/file');
  388. * ```
  389. *
  390. * @param path The path or URL to open.
  391. * This value is matched against the string regex defined on `tauri.conf.json > tauri > allowlist > shell > open`,
  392. * which defaults to `^https?://`.
  393. * @param openWith The app to open the file or URL with.
  394. * Defaults to the system default application for the specified path type.
  395. * @returns
  396. */
  397. async function open(path: string, openWith?: string): Promise<void> {
  398. return invokeTauriCommand({
  399. __tauriModule: 'Shell',
  400. message: {
  401. cmd: 'open',
  402. path,
  403. with: openWith
  404. }
  405. })
  406. }
  407. export { Command, Child, EventEmitter, open }
  408. export type { ChildProcess, SpawnOptions }