import * as psNode from 'ps-node' import { ipcRenderer } from 'electron' import { ChildProcess, PTYInterface, PTYProxy } from 'tabby-local' import { getWorkingDirectoryFromPID } from 'native-process-working-directory' /* eslint-disable block-scoped-var */ try { var macOSNativeProcessList = require('macos-native-processlist') // eslint-disable-line @typescript-eslint/no-var-requires, no-var } catch { } try { var windowsProcessTree = require('windows-process-tree') // eslint-disable-line @typescript-eslint/no-var-requires, no-var } catch { } export class ElectronPTYInterface extends PTYInterface { async spawn (...options: any[]): Promise { const id = ipcRenderer.sendSync('pty:spawn', ...options) return new ElectronPTYProxy(id) } async restore (id: string): Promise { if (ipcRenderer.sendSync('pty:exists', id)) { return new ElectronPTYProxy(id) } return null } } // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class ElectronPTYProxy extends PTYProxy { private subscriptions: Map = new Map() private truePID: Promise constructor ( private id: string, ) { super() this.truePID = new Promise(async (resolve) => { let pid = await this.getPID() try { await new Promise(r => setTimeout(r, 2000)) // Retrieve any possible single children now that shell has fully started let processes = await this.getChildProcessesInternal(pid) while (pid && processes.length === 1) { if (!processes[0].pid) { break } pid = processes[0].pid processes = await this.getChildProcessesInternal(pid) } } finally { resolve(pid) } }) this.truePID = this.truePID.catch(() => this.getPID()) } getID (): string { return this.id } getTruePID (): Promise { return this.truePID } async getPID (): Promise { return ipcRenderer.sendSync('pty:get-pid', this.id) } subscribe (event: string, handler: (..._: any[]) => void): void { const key = `pty:${this.id}:${event}` const newHandler = (_event, ...args) => handler(...args) this.subscriptions.set(key, newHandler) ipcRenderer.on(key, newHandler) } ackData (length: number): void { ipcRenderer.send('pty:ack-data', this.id, length) } unsubscribeAll (): void { for (const k of this.subscriptions.keys()) { ipcRenderer.off(k, this.subscriptions.get(k)) } } async resize (columns: number, rows: number): Promise { ipcRenderer.send('pty:resize', this.id, columns, rows) } async write (data: Buffer): Promise { ipcRenderer.send('pty:write', this.id, data) } async kill (signal?: string): Promise { ipcRenderer.send('pty:kill', this.id, signal) } async getChildProcesses (): Promise { return this.getChildProcessesInternal(await this.getTruePID()) } async getChildProcessesInternal (truePID: number): Promise { if (process.platform === 'darwin') { const processes = await macOSNativeProcessList.getProcessList() return processes.filter(x => x.ppid === truePID).map(p => ({ pid: p.pid, ppid: p.ppid, command: p.name, })) } if (process.platform === 'win32') { return new Promise(resolve => { windowsProcessTree.getProcessTree(truePID, tree => { resolve(tree ? tree.children.map(child => ({ pid: child.pid, ppid: tree.pid, command: child.name, })) : []) }) }) } return new Promise((resolve, reject) => { psNode.lookup({ ppid: truePID }, (err, processes) => { if (err) { reject(err) return } resolve(processes as ChildProcess[]) }) }) } async getWorkingDirectory (): Promise { return getWorkingDirectoryFromPID(await this.getTruePID()) } }