| import type { IDisposable, IPty } from 'node-pty'; |
| |
| import type { PtyStack } from './pty-stack.js'; |
| |
| export interface PtyProcessExit { |
| exitCode: number; |
| signal?: number; |
| } |
| |
| export interface PtyProcessDriverOptions { |
| stack: PtyStack; |
| file: string; |
| args: string[]; |
| cwd: string; |
| env: NodeJS.ProcessEnv; |
| cols: number; |
| rows: number; |
| onData: (data: string) => void; |
| onExit: (exit: PtyProcessExit) => void; |
| onInvariantFailure: (error: Error) => void; |
| } |
| |
| export class PtyProcessDriver { |
| private readonly pty: IPty; |
| private readonly subscriptions: IDisposable[]; |
| private exited = false; |
| private disposed = false; |
| |
| constructor(options: PtyProcessDriverOptions) { |
| const env = { ...options.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }; |
| const pty = options.stack.spawn(options.file, options.args, { |
| cwd: options.cwd, |
| env, |
| name: 'xterm-256color', |
| cols: options.cols, |
| rows: options.rows, |
| encoding: 'utf8', |
| handleFlowControl: false, |
| }); |
| this.pty = pty; |
| const subscriptions: IDisposable[] = []; |
| try { |
| subscriptions.push( |
| pty.onData((data) => { |
| if (this.disposed) return; |
| if (this.exited) { |
| options.onInvariantFailure(new Error('node-pty emitted data after its exit fence')); |
| return; |
| } |
| options.onData(data); |
| }), |
| ); |
| subscriptions.push( |
| pty.onExit((exit) => { |
| if (this.disposed || this.exited) return; |
| this.exited = true; |
| options.onExit(exit); |
| }), |
| ); |
| } catch (error) { |
| for (const subscription of subscriptions) { |
| try { |
| subscription.dispose(); |
| } catch { |
| /* startup cleanup continues */ |
| } |
| } |
| try { |
| killPty(pty, 'SIGKILL'); |
| } catch { |
| /* startup cleanup continues */ |
| } |
| throw error; |
| } |
| this.subscriptions = subscriptions; |
| } |
| |
| get pid(): number { |
| // ConPTY publishes its inner PID after construction; never cache the initial 0. |
| return this.pty.pid; |
| } |
| |
| write(data: string): void { |
| this.pty.write(data); |
| } |
| |
| resize(cols: number, rows: number): void { |
| this.pty.resize(cols, rows); |
| } |
| |
| kill(signal: 'SIGTERM' | 'SIGKILL'): void { |
| killPty(this.pty, signal); |
| } |
| |
| dispose(): void { |
| if (this.disposed) return; |
| this.disposed = true; |
| for (const subscription of this.subscriptions) { |
| try { |
| subscription.dispose(); |
| } catch { |
| // Subscription cleanup is best-effort and must remain idempotent. |
| } |
| } |
| } |
| } |
| |
| function killPty(pty: IPty, signal: 'SIGTERM' | 'SIGKILL'): void { |
| if (process.platform === 'win32') { |
| pty.kill(); |
| return; |
| } |
| pty.kill(signal); |
| } |