shell.cjs 16 KB

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