2 * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
3 * Copyright (c) 2016, Daniel Imms (MIT License).
4 * Copyright (c) 2018, Microsoft Corporation (MIT License).
6 import * as net from 'net';
7 import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from './terminal';
8 import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from './interfaces';
9 import { ArgvOrCommandLine } from './types';
10 import { assign } from './utils';
14 pty = require('../build/Release/pty.node');
15 } catch (outerError) {
17 pty = require('../build/Debug/pty.node');
18 } catch (innerError) {
19 console.error('innerError', innerError);
20 // Re-throw the exception from the Release require if the Debug require fails as well
25 const DEFAULT_FILE = 'sh';
26 const DEFAULT_NAME = 'xterm';
27 const DESTROY_SOCKET_TIMEOUT_MS = 200;
29 export class UnixTerminal extends Terminal {
30 protected _fd: number;
31 protected _pty: string;
33 protected _file: string;
34 protected _name: string;
36 protected _readable: boolean;
37 protected _writable: boolean;
39 private _boundClose: boolean;
40 private _emittedClose: boolean;
41 private _master: net.Socket;
42 private _slave: net.Socket;
44 public get master(): net.Socket { return this._master; }
45 public get slave(): net.Socket { return this._slave; }
47 constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions) {
50 if (typeof args === 'string') {
51 throw new Error('args as a string is not supported on unix.');
54 // Initialize arguments
56 file = file || DEFAULT_FILE;
58 opt.env = opt.env || process.env;
60 this._cols = opt.cols || DEFAULT_COLS;
61 this._rows = opt.rows || DEFAULT_ROWS;
62 const uid = opt.uid || -1;
63 const gid = opt.gid || -1;
64 const env = assign({}, opt.env);
66 if (opt.env === process.env) {
67 this._sanitizeEnv(env);
70 const cwd = opt.cwd || process.cwd();
71 const name = opt.name || env.TERM || DEFAULT_NAME;
73 const parsedEnv = this._parseEnv(env);
75 const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
77 const onexit = (code: number, signal: number) => {
78 // XXX Sometimes a data event is emitted after exit. Wait til socket is
80 if (!this._emittedClose) {
81 if (this._boundClose) {
84 this._boundClose = true;
85 // From macOS High Sierra 10.13.2 sometimes the socket never gets
86 // closed. A timeout is applied here to avoid the terminal never being
87 // destroyed when this occurs.
88 let timeout = setTimeout(() => {
90 // Destroying the socket now will cause the close event to fire
91 this._socket.destroy();
92 }, DESTROY_SOCKET_TIMEOUT_MS);
93 this.once('close', () => {
94 if (timeout !== null) {
95 clearTimeout(timeout);
97 this.emit('exit', code, signal);
101 this.emit('exit', code, signal);
105 const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), onexit);
107 this._socket = new PipeSocket(term.fd);
108 if (encoding !== null) {
109 this._socket.setEncoding(encoding);
113 this._socket.on('error', (err: any) => {
114 // NOTE: fs.ReadStream gets EAGAIN twice at first:
116 if (~err.code.indexOf('EAGAIN')) {
123 // EIO on exit from fs.ReadStream:
124 if (!this._emittedClose) {
125 this._emittedClose = true;
129 // EIO, happens when someone closes our child process: the only process in
131 // node < 0.6.14: errno 5
132 // node >= 0.6.14: read EIO
134 if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) {
139 // throw anything else
140 if (this.listeners('error').length < 2) {
145 this._pid = term.pid;
147 this._pty = term.pty;
152 this._readable = true;
153 this._writable = true;
155 this._socket.on('close', () => {
156 if (this._emittedClose) {
159 this._emittedClose = true;
164 this._forwardEvents();
167 protected _write(data: string): void {
168 this._socket.write(data);
175 public static open(opt: IPtyOpenOptions): UnixTerminal {
176 const self: UnixTerminal = Object.create(UnixTerminal.prototype);
179 if (arguments.length > 1) {
186 const cols = opt.cols || DEFAULT_COLS;
187 const rows = opt.rows || DEFAULT_ROWS;
188 const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
191 const term: IUnixOpenProcess = pty.open(cols, rows);
193 self._master = new PipeSocket(<number>term.master);
194 if (encoding !== null) {
195 self._master.setEncoding(encoding);
197 self._master.resume();
199 self._slave = new PipeSocket(term.slave);
200 if (encoding !== null) {
201 self._slave.setEncoding(encoding);
203 self._slave.resume();
205 self._socket = self._master;
207 self._fd = term.master;
208 self._pty = term.pty;
210 self._file = process.argv[0] || 'node';
211 self._name = process.env.TERM || '';
213 self._readable = true;
214 self._writable = true;
216 self._socket.on('error', err => {
218 if (self.listeners('error').length < 2) {
223 self._socket.on('close', () => {
230 public destroy(): void {
233 // Need to close the read stream so node stops reading a dead file
234 // descriptor. Then we can safely SIGHUP the shell.
235 this._socket.once('close', () => {
239 this._socket.destroy();
242 public kill(signal?: string): void {
244 process.kill(this.pid, signal || 'SIGHUP');
245 } catch (e) { /* swallow */ }
249 * Gets the name of the process.
251 public get process(): string {
252 return pty.process(this._fd, this._pty) || this._file;
259 public resize(cols: number, rows: number): void {
260 if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) {
261 throw new Error('resizing must be done using positive cols and rows');
263 pty.resize(this._fd, cols, rows);
268 private _sanitizeEnv(env: IProcessEnv): void {
269 // Make sure we didn't start our server from inside tmux.
271 delete env['TMUX_PANE'];
273 // Make sure we didn't start our server from inside screen.
274 // http://web.mit.edu/gnu/doc/html/screen_20.html
276 delete env['WINDOW'];
278 // Delete some variables that might confuse our terminal.
279 delete env['WINDOWID'];
280 delete env['TERMCAP'];
281 delete env['COLUMNS'];
287 * Wraps net.Socket to force the handle type "PIPE" by temporarily overwriting
288 * tty_wrap.guessHandleType.
289 * See: https://github.com/chjj/pty.js/issues/103
291 class PipeSocket extends net.Socket {
292 constructor(fd: number) {
293 const { Pipe, constants } = (<any>process).binding('pipe_wrap'); // tslint:disable-line
294 // @types/node has fd as string? https://github.com/DefinitelyTyped/DefinitelyTyped/pull/18275
295 const handle = new Pipe(constants.SOCKET);
297 super(<any>{ handle });