--- /dev/null
+/**
+ * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
+ * Copyright (c) 2016, Daniel Imms (MIT License).
+ * Copyright (c) 2018, Microsoft Corporation (MIT License).
+ */
+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);
+ // Re-throw the exception from the Release require if the Debug require fails as well
+ 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.');
+ }
+
+ // Initialize arguments
+ 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) => {
+ // XXX Sometimes a data event is emitted after exit. Wait til socket is
+ // destroyed.
+ if (!this._emittedClose) {
+ if (this._boundClose) {
+ return;
+ }
+ this._boundClose = true;
+ // From macOS High Sierra 10.13.2 sometimes the socket never gets
+ // closed. A timeout is applied here to avoid the terminal never being
+ // destroyed when this occurs.
+ let timeout = setTimeout(() => {
+ timeout = null;
+ // Destroying the socket now will cause the close event to fire
+ 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);
+ };
+
+ // fork
+ 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);
+ }
+
+ // setup
+ this._socket.on('error', (err: any) => {
+ // NOTE: fs.ReadStream gets EAGAIN twice at first:
+ if (err.code) {
+ if (~err.code.indexOf('EAGAIN')) {
+ return;
+ }
+ }
+
+ // close
+ this._close();
+ // EIO on exit from fs.ReadStream:
+ if (!this._emittedClose) {
+ this._emittedClose = true;
+ this.emit('close');
+ }
+
+ // EIO, happens when someone closes our child process: the only process in
+ // the terminal.
+ // node < 0.6.14: errno 5
+ // node >= 0.6.14: read EIO
+ if (err.code) {
+ if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) {
+ return;
+ }
+ }
+
+ // throw anything else
+ 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);
+ }
+
+ /**
+ * openpty
+ */
+
+ 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);
+
+ // open
+ 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();
+
+ // Need to close the read stream so node stops reading a dead file
+ // descriptor. Then we can safely SIGHUP the shell.
+ this._socket.once('close', () => {
+ this.kill('SIGHUP');
+ });
+
+ this._socket.destroy();
+ }
+
+ public kill(signal?: string): void {
+ try {
+ process.kill(this.pid, signal || 'SIGHUP');
+ } catch (e) { /* swallow */ }
+ }
+
+ /**
+ * Gets the name of the process.
+ */
+ public get process(): string {
+ return pty.process(this._fd, this._pty) || this._file;
+ }
+
+ /**
+ * TTY
+ */
+
+ 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 {
+ // Make sure we didn't start our server from inside tmux.
+ delete env['TMUX'];
+ delete env['TMUX_PANE'];
+
+ // Make sure we didn't start our server from inside screen.
+ // http://web.mit.edu/gnu/doc/html/screen_20.html
+ delete env['STY'];
+ delete env['WINDOW'];
+
+ // Delete some variables that might confuse our terminal.
+ delete env['WINDOWID'];
+ delete env['TERMCAP'];
+ delete env['COLUMNS'];
+ delete env['LINES'];
+ }
+}
+
+/**
+ * Wraps net.Socket to force the handle type "PIPE" by temporarily overwriting
+ * tty_wrap.guessHandleType.
+ * See: https://github.com/chjj/pty.js/issues/103
+ */
+class PipeSocket extends net.Socket {
+ constructor(fd: number) {
+ const { Pipe, constants } = (<any>process).binding('pipe_wrap'); // tslint:disable-line
+ // @types/node has fd as string? https://github.com/DefinitelyTyped/DefinitelyTyped/pull/18275
+ const handle = new Pipe(constants.SOCKET);
+ handle.open(fd);
+ super(<any>{ handle });
+ }
+}