shell.js 16 KB

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