pty.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import * as psNode from 'ps-node'
  2. import { ipcRenderer } from 'electron'
  3. import { ChildProcess, PTYInterface, PTYProxy } from 'tabby-local'
  4. import { getWorkingDirectoryFromPID } from 'native-process-working-directory'
  5. /* eslint-disable block-scoped-var */
  6. try {
  7. var macOSNativeProcessList = require('macos-native-processlist') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
  8. } catch { }
  9. try {
  10. var windowsProcessTree = require('windows-process-tree') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
  11. } catch { }
  12. export class ElectronPTYInterface extends PTYInterface {
  13. async spawn (...options: any[]): Promise<PTYProxy> {
  14. const id = ipcRenderer.sendSync('pty:spawn', ...options)
  15. return new ElectronPTYProxy(id)
  16. }
  17. async restore (id: string): Promise<ElectronPTYProxy|null> {
  18. if (ipcRenderer.sendSync('pty:exists', id)) {
  19. return new ElectronPTYProxy(id)
  20. }
  21. return null
  22. }
  23. }
  24. // eslint-disable-next-line @typescript-eslint/no-extraneous-class
  25. export class ElectronPTYProxy extends PTYProxy {
  26. private subscriptions: Map<string, any> = new Map()
  27. private truePID: Promise<number>
  28. constructor (
  29. private id: string,
  30. ) {
  31. super()
  32. this.truePID = new Promise(async (resolve) => {
  33. let pid = await this.getPID()
  34. try {
  35. await new Promise(r => setTimeout(r, 2000))
  36. // Retrieve any possible single children now that shell has fully started
  37. let processes = await this.getChildProcessesInternal(pid)
  38. while (pid && processes.length === 1) {
  39. if (!processes[0].pid) {
  40. break
  41. }
  42. pid = processes[0].pid
  43. processes = await this.getChildProcessesInternal(pid)
  44. }
  45. } finally {
  46. resolve(pid)
  47. }
  48. })
  49. this.truePID = this.truePID.catch(() => this.getPID())
  50. }
  51. getID (): string {
  52. return this.id
  53. }
  54. getTruePID (): Promise<number> {
  55. return this.truePID
  56. }
  57. async getPID (): Promise<number> {
  58. return ipcRenderer.sendSync('pty:get-pid', this.id)
  59. }
  60. subscribe (event: string, handler: (..._: any[]) => void): void {
  61. const key = `pty:${this.id}:${event}`
  62. const newHandler = (_event, ...args) => handler(...args)
  63. this.subscriptions.set(key, newHandler)
  64. ipcRenderer.on(key, newHandler)
  65. }
  66. ackData (length: number): void {
  67. ipcRenderer.send('pty:ack-data', this.id, length)
  68. }
  69. unsubscribeAll (): void {
  70. for (const k of this.subscriptions.keys()) {
  71. ipcRenderer.off(k, this.subscriptions.get(k))
  72. }
  73. }
  74. async resize (columns: number, rows: number): Promise<void> {
  75. ipcRenderer.send('pty:resize', this.id, columns, rows)
  76. }
  77. async write (data: Buffer): Promise<void> {
  78. ipcRenderer.send('pty:write', this.id, data)
  79. }
  80. async kill (signal?: string): Promise<void> {
  81. ipcRenderer.send('pty:kill', this.id, signal)
  82. }
  83. async getChildProcesses (): Promise<ChildProcess[]> {
  84. return this.getChildProcessesInternal(await this.getTruePID())
  85. }
  86. async getChildProcessesInternal (truePID: number): Promise<ChildProcess[]> {
  87. if (process.platform === 'darwin') {
  88. const processes = await macOSNativeProcessList.getProcessList()
  89. return processes.filter(x => x.ppid === truePID).map(p => ({
  90. pid: p.pid,
  91. ppid: p.ppid,
  92. command: p.name,
  93. }))
  94. }
  95. if (process.platform === 'win32') {
  96. return new Promise<ChildProcess[]>(resolve => {
  97. windowsProcessTree.getProcessTree(truePID, tree => {
  98. resolve(tree ? tree.children.map(child => ({
  99. pid: child.pid,
  100. ppid: tree.pid,
  101. command: child.name,
  102. })) : [])
  103. })
  104. })
  105. }
  106. return new Promise<ChildProcess[]>((resolve, reject) => {
  107. psNode.lookup({ ppid: truePID }, (err, processes) => {
  108. if (err) {
  109. reject(err)
  110. return
  111. }
  112. resolve(processes as ChildProcess[])
  113. })
  114. })
  115. }
  116. async getWorkingDirectory (): Promise<string|null> {
  117. return getWorkingDirectoryFromPID(await this.getTruePID())
  118. }
  119. }