| |
| |
| |
| |
| |
| import * as net from 'net'; |
| import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from './terminal'; |
| import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from './interfaces'; |
| import { ArgvOrCommandLine } from './types'; |
| import { assign } from './utils'; |
|
|
| let pty: IUnixNative; |
| try { |
| pty = require('../build/Release/pty.node'); |
| } catch (outerError) { |
| try { |
| pty = require('../build/Debug/pty.node'); |
| } catch (innerError) { |
| console.error('innerError', innerError); |
| |
| throw outerError; |
| } |
| } |
|
|
| const DEFAULT_FILE = 'sh'; |
| const DEFAULT_NAME = 'xterm'; |
| const DESTROY_SOCKET_TIMEOUT_MS = 200; |
|
|
| export class UnixTerminal extends Terminal { |
| protected _fd: number; |
| protected _pty: string; |
|
|
| protected _file: string; |
| protected _name: string; |
|
|
| protected _readable: boolean; |
| protected _writable: boolean; |
|
|
| private _boundClose: boolean; |
| private _emittedClose: boolean; |
| private _master: net.Socket; |
| private _slave: net.Socket; |
|
|
| public get master(): net.Socket { return this._master; } |
| public get slave(): net.Socket { return this._slave; } |
|
|
| constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions) { |
| super(opt); |
|
|
| if (typeof args === 'string') { |
| throw new Error('args as a string is not supported on unix.'); |
| } |
|
|
| |
| args = args || []; |
| file = file || DEFAULT_FILE; |
| opt = opt || {}; |
| opt.env = opt.env || process.env; |
|
|
| this._cols = opt.cols || DEFAULT_COLS; |
| this._rows = opt.rows || DEFAULT_ROWS; |
| const uid = opt.uid || -1; |
| const gid = opt.gid || -1; |
| const env = assign({}, opt.env); |
|
|
| if (opt.env === process.env) { |
| this._sanitizeEnv(env); |
| } |
|
|
| const cwd = opt.cwd || process.cwd(); |
| const name = opt.name || env.TERM || DEFAULT_NAME; |
| env.TERM = name; |
| const parsedEnv = this._parseEnv(env); |
|
|
| const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding); |
|
|
| const onexit = (code: number, signal: number) => { |
| |
| |
| if (!this._emittedClose) { |
| if (this._boundClose) { |
| return; |
| } |
| this._boundClose = true; |
| |
| |
| |
| let timeout = setTimeout(() => { |
| timeout = null; |
| |
| this._socket.destroy(); |
| }, DESTROY_SOCKET_TIMEOUT_MS); |
| this.once('close', () => { |
| if (timeout !== null) { |
| clearTimeout(timeout); |
| } |
| this.emit('exit', code, signal); |
| }); |
| return; |
| } |
| this.emit('exit', code, signal); |
| }; |
|
|
| |
| const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), onexit); |
|
|
| this._socket = new PipeSocket(term.fd); |
| if (encoding !== null) { |
| this._socket.setEncoding(encoding); |
| } |
|
|
| |
| this._socket.on('error', (err: any) => { |
| |
| if (err.code) { |
| if (~err.code.indexOf('EAGAIN')) { |
| return; |
| } |
| } |
|
|
| |
| this._close(); |
| |
| if (!this._emittedClose) { |
| this._emittedClose = true; |
| this.emit('close'); |
| } |
|
|
| |
| |
| |
| |
| if (err.code) { |
| if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) { |
| return; |
| } |
| } |
|
|
| |
| if (this.listeners('error').length < 2) { |
| throw err; |
| } |
| }); |
|
|
| this._pid = term.pid; |
| this._fd = term.fd; |
| this._pty = term.pty; |
|
|
| this._file = file; |
| this._name = name; |
|
|
| this._readable = true; |
| this._writable = true; |
|
|
| this._socket.on('close', () => { |
| if (this._emittedClose) { |
| return; |
| } |
| this._emittedClose = true; |
| this._close(); |
| this.emit('close'); |
| }); |
|
|
| this._forwardEvents(); |
| } |
|
|
| protected _write(data: string): void { |
| this._socket.write(data); |
| } |
|
|
| |
| |
| |
|
|
| public static open(opt: IPtyOpenOptions): UnixTerminal { |
| const self: UnixTerminal = Object.create(UnixTerminal.prototype); |
| opt = opt || {}; |
|
|
| if (arguments.length > 1) { |
| opt = { |
| cols: arguments[1], |
| rows: arguments[2] |
| }; |
| } |
|
|
| const cols = opt.cols || DEFAULT_COLS; |
| const rows = opt.rows || DEFAULT_ROWS; |
| const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding); |
|
|
| |
| const term: IUnixOpenProcess = pty.open(cols, rows); |
|
|
| self._master = new PipeSocket(<number>term.master); |
| if (encoding !== null) { |
| self._master.setEncoding(encoding); |
| } |
| self._master.resume(); |
|
|
| self._slave = new PipeSocket(term.slave); |
| if (encoding !== null) { |
| self._slave.setEncoding(encoding); |
| } |
| self._slave.resume(); |
|
|
| self._socket = self._master; |
| self._pid = null; |
| self._fd = term.master; |
| self._pty = term.pty; |
|
|
| self._file = process.argv[0] || 'node'; |
| self._name = process.env.TERM || ''; |
|
|
| self._readable = true; |
| self._writable = true; |
|
|
| self._socket.on('error', err => { |
| self._close(); |
| if (self.listeners('error').length < 2) { |
| throw err; |
| } |
| }); |
|
|
| self._socket.on('close', () => { |
| self._close(); |
| }); |
|
|
| return self; |
| } |
|
|
| public destroy(): void { |
| this._close(); |
|
|
| |
| |
| this._socket.once('close', () => { |
| this.kill('SIGHUP'); |
| }); |
|
|
| this._socket.destroy(); |
| } |
|
|
| public kill(signal?: string): void { |
| try { |
| process.kill(this.pid, signal || 'SIGHUP'); |
| } catch (e) { } |
| } |
|
|
| |
| |
| |
| public get process(): string { |
| return pty.process(this._fd, this._pty) || this._file; |
| } |
|
|
| |
| |
| |
|
|
| public resize(cols: number, rows: number): void { |
| if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) { |
| throw new Error('resizing must be done using positive cols and rows'); |
| } |
| pty.resize(this._fd, cols, rows); |
| this._cols = cols; |
| this._rows = rows; |
| } |
|
|
| private _sanitizeEnv(env: IProcessEnv): void { |
| |
| delete env['TMUX']; |
| delete env['TMUX_PANE']; |
|
|
| |
| |
| delete env['STY']; |
| delete env['WINDOW']; |
|
|
| |
| delete env['WINDOWID']; |
| delete env['TERMCAP']; |
| delete env['COLUMNS']; |
| delete env['LINES']; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| class PipeSocket extends net.Socket { |
| constructor(fd: number) { |
| const { Pipe, constants } = (<any>process).binding('pipe_wrap'); |
| |
| const handle = new Pipe(constants.SOCKET); |
| handle.open(fd); |
| super(<any>{ handle }); |
| } |
| } |
|
|