shell.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. import { invokeTauriCommand } from './helpers/tauri'
  5. import { transformCallback } from './tauri'
  6. interface SpawnOptions {
  7. /** Current working directory. */
  8. cwd?: string
  9. /** Environment variables. set to `null` to clear the process env. */
  10. env?: { [name: string]: string }
  11. }
  12. interface InternalSpawnOptions extends SpawnOptions {
  13. sidecar?: boolean
  14. }
  15. interface ChildProcess {
  16. code: number | null
  17. signal: number | null
  18. stdout: string
  19. stderr: string
  20. }
  21. /**
  22. * Spawns a process.
  23. *
  24. * @param program The name of the program to execute e.g. 'mkdir' or 'node'
  25. * @param sidecar Whether the program is a sidecar or a system program
  26. * @param onEvent
  27. * @param [args] Command args
  28. * @returns A promise resolving to the process id.
  29. */
  30. async function execute(
  31. onEvent: (event: CommandEvent) => void,
  32. program: string,
  33. args?: string | string[],
  34. options?: InternalSpawnOptions
  35. ): Promise<number> {
  36. if (typeof args === 'object') {
  37. Object.freeze(args)
  38. }
  39. return invokeTauriCommand<number>({
  40. __tauriModule: 'Shell',
  41. message: {
  42. cmd: 'execute',
  43. program,
  44. args: typeof args === 'string' ? [args] : args,
  45. options,
  46. onEventFn: transformCallback(onEvent)
  47. }
  48. })
  49. }
  50. class EventEmitter<E> {
  51. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
  52. eventListeners: { [key: string]: Array<(arg: any) => void> } = Object.create(
  53. null
  54. )
  55. private addEventListener(event: string, handler: (arg: any) => void): void {
  56. if (event in this.eventListeners) {
  57. // eslint-disable-next-line security/detect-object-injection
  58. this.eventListeners[event].push(handler)
  59. } else {
  60. // eslint-disable-next-line security/detect-object-injection
  61. this.eventListeners[event] = [handler]
  62. }
  63. }
  64. _emit(event: E, payload: any): void {
  65. if (event in this.eventListeners) {
  66. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  67. const listeners = this.eventListeners[event as any]
  68. for (const listener of listeners) {
  69. listener(payload)
  70. }
  71. }
  72. }
  73. on(event: E, handler: (arg: any) => void): EventEmitter<E> {
  74. this.addEventListener(event as any, handler)
  75. return this
  76. }
  77. }
  78. class Child {
  79. pid: number
  80. constructor(pid: number) {
  81. this.pid = pid
  82. }
  83. async write(data: string | number[]): Promise<void> {
  84. return invokeTauriCommand({
  85. __tauriModule: 'Shell',
  86. message: {
  87. cmd: 'stdinWrite',
  88. pid: this.pid,
  89. buffer: data
  90. }
  91. })
  92. }
  93. async kill(): Promise<void> {
  94. return invokeTauriCommand({
  95. __tauriModule: 'Shell',
  96. message: {
  97. cmd: 'killChild',
  98. pid: this.pid
  99. }
  100. })
  101. }
  102. }
  103. class Command extends EventEmitter<'close' | 'error'> {
  104. program: string
  105. args: string[]
  106. options: InternalSpawnOptions
  107. stdout = new EventEmitter<'data'>()
  108. stderr = new EventEmitter<'data'>()
  109. pid: number | null = null
  110. constructor(
  111. program: string,
  112. args: string | string[] = [],
  113. options?: SpawnOptions
  114. ) {
  115. super()
  116. this.program = program
  117. this.args = typeof args === 'string' ? [args] : args
  118. this.options = options ?? {}
  119. }
  120. /**
  121. * Creates a command to execute the given sidecar binary.
  122. *
  123. * @param program Binary name
  124. * @returns
  125. */
  126. static sidecar(program: string, args: string | string[] = []): Command {
  127. const instance = new Command(program, args)
  128. instance.options.sidecar = true
  129. return instance
  130. }
  131. async spawn(): Promise<Child> {
  132. return execute(
  133. (event) => {
  134. switch (event.event) {
  135. case 'Error':
  136. this._emit('error', event.payload)
  137. break
  138. case 'Terminated':
  139. this._emit('close', event.payload)
  140. break
  141. case 'Stdout':
  142. this.stdout._emit('data', event.payload)
  143. break
  144. case 'Stderr':
  145. this.stderr._emit('data', event.payload)
  146. break
  147. }
  148. },
  149. this.program,
  150. this.args,
  151. this.options
  152. ).then((pid) => new Child(pid))
  153. }
  154. async execute(): Promise<ChildProcess> {
  155. return new Promise((resolve, reject) => {
  156. this.on('error', reject)
  157. const stdout: string[] = []
  158. const stderr: string[] = []
  159. this.stdout.on('data', (line) => {
  160. stdout.push(line)
  161. })
  162. this.stderr.on('data', (line) => {
  163. stderr.push(line)
  164. })
  165. this.on('close', (payload: TerminatedPayload) => {
  166. resolve({
  167. code: payload.code,
  168. signal: payload.signal,
  169. stdout: stdout.join('\n'),
  170. stderr: stderr.join('\n')
  171. })
  172. })
  173. this.spawn().catch(reject)
  174. })
  175. }
  176. }
  177. interface Event<T, V> {
  178. event: T
  179. payload: V
  180. }
  181. interface TerminatedPayload {
  182. code: number | null
  183. signal: number | null
  184. }
  185. type CommandEvent =
  186. | Event<'Stdout', string>
  187. | Event<'Stderr', string>
  188. | Event<'Terminated', TerminatedPayload>
  189. | Event<'Error', string>
  190. /**
  191. * Opens a path or URL with the system's default app,
  192. * or the one specified with `openWith`.
  193. *
  194. * @param path the path or URL to open
  195. * @param [openWith] the app to open the file or URL with
  196. * @returns
  197. */
  198. async function open(path: string, openWith?: string): Promise<void> {
  199. return invokeTauriCommand({
  200. __tauriModule: 'Shell',
  201. message: {
  202. cmd: 'open',
  203. path,
  204. with: openWith
  205. }
  206. })
  207. }
  208. export { Command, Child, open }
  209. export type { ChildProcess, SpawnOptions }