shell.ts 5.1 KB

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