installed pty
[VSoRC/.git] / node_modules / node-pty / src / unixTerminal.ts
1 /**
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).
5  */
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';
11
12 let pty: IUnixNative;
13 try {
14   pty = require('../build/Release/pty.node');
15 } catch (outerError) {
16   try {
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
21     throw outerError;
22   }
23 }
24
25 const DEFAULT_FILE = 'sh';
26 const DEFAULT_NAME = 'xterm';
27 const DESTROY_SOCKET_TIMEOUT_MS = 200;
28
29 export class UnixTerminal extends Terminal {
30   protected _fd: number;
31   protected _pty: string;
32
33   protected _file: string;
34   protected _name: string;
35
36   protected _readable: boolean;
37   protected _writable: boolean;
38
39   private _boundClose: boolean;
40   private _emittedClose: boolean;
41   private _master: net.Socket;
42   private _slave: net.Socket;
43
44   public get master(): net.Socket { return this._master; }
45   public get slave(): net.Socket { return this._slave; }
46
47   constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions) {
48     super(opt);
49
50     if (typeof args === 'string') {
51       throw new Error('args as a string is not supported on unix.');
52     }
53
54     // Initialize arguments
55     args = args || [];
56     file = file || DEFAULT_FILE;
57     opt = opt || {};
58     opt.env = opt.env || process.env;
59
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);
65
66     if (opt.env === process.env) {
67       this._sanitizeEnv(env);
68     }
69
70     const cwd = opt.cwd || process.cwd();
71     const name = opt.name || env.TERM || DEFAULT_NAME;
72     env.TERM = name;
73     const parsedEnv = this._parseEnv(env);
74
75     const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
76
77     const onexit = (code: number, signal: number) => {
78       // XXX Sometimes a data event is emitted after exit. Wait til socket is
79       // destroyed.
80       if (!this._emittedClose) {
81         if (this._boundClose) {
82           return;
83         }
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(() => {
89           timeout = null;
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);
96           }
97           this.emit('exit', code, signal);
98         });
99         return;
100       }
101       this.emit('exit', code, signal);
102     };
103
104     // fork
105     const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), onexit);
106
107     this._socket = new PipeSocket(term.fd);
108     if (encoding !== null) {
109       this._socket.setEncoding(encoding);
110     }
111
112     // setup
113     this._socket.on('error', (err: any) => {
114       // NOTE: fs.ReadStream gets EAGAIN twice at first:
115       if (err.code) {
116         if (~err.code.indexOf('EAGAIN')) {
117           return;
118         }
119       }
120
121       // close
122       this._close();
123       // EIO on exit from fs.ReadStream:
124       if (!this._emittedClose) {
125         this._emittedClose = true;
126         this.emit('close');
127       }
128
129       // EIO, happens when someone closes our child process: the only process in
130       // the terminal.
131       // node < 0.6.14: errno 5
132       // node >= 0.6.14: read EIO
133       if (err.code) {
134         if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) {
135           return;
136         }
137       }
138
139       // throw anything else
140       if (this.listeners('error').length < 2) {
141         throw err;
142       }
143     });
144
145     this._pid = term.pid;
146     this._fd = term.fd;
147     this._pty = term.pty;
148
149     this._file = file;
150     this._name = name;
151
152     this._readable = true;
153     this._writable = true;
154
155     this._socket.on('close', () => {
156       if (this._emittedClose) {
157         return;
158       }
159       this._emittedClose = true;
160       this._close();
161       this.emit('close');
162     });
163
164     this._forwardEvents();
165   }
166
167   protected _write(data: string): void {
168     this._socket.write(data);
169   }
170
171   /**
172    * openpty
173    */
174
175   public static open(opt: IPtyOpenOptions): UnixTerminal {
176     const self: UnixTerminal = Object.create(UnixTerminal.prototype);
177     opt = opt || {};
178
179     if (arguments.length > 1) {
180       opt = {
181         cols: arguments[1],
182         rows: arguments[2]
183       };
184     }
185
186     const cols = opt.cols || DEFAULT_COLS;
187     const rows = opt.rows || DEFAULT_ROWS;
188     const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
189
190     // open
191     const term: IUnixOpenProcess = pty.open(cols, rows);
192
193     self._master = new PipeSocket(<number>term.master);
194     if (encoding !== null) {
195         self._master.setEncoding(encoding);
196     }
197     self._master.resume();
198
199     self._slave = new PipeSocket(term.slave);
200     if (encoding !== null) {
201         self._slave.setEncoding(encoding);
202     }
203     self._slave.resume();
204
205     self._socket = self._master;
206     self._pid = null;
207     self._fd = term.master;
208     self._pty = term.pty;
209
210     self._file = process.argv[0] || 'node';
211     self._name = process.env.TERM || '';
212
213     self._readable = true;
214     self._writable = true;
215
216     self._socket.on('error', err => {
217       self._close();
218       if (self.listeners('error').length < 2) {
219         throw err;
220       }
221     });
222
223     self._socket.on('close', () => {
224       self._close();
225     });
226
227     return self;
228   }
229
230   public destroy(): void {
231     this._close();
232
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', () => {
236       this.kill('SIGHUP');
237     });
238
239     this._socket.destroy();
240   }
241
242   public kill(signal?: string): void {
243     try {
244       process.kill(this.pid, signal || 'SIGHUP');
245     } catch (e) { /* swallow */ }
246   }
247
248   /**
249    * Gets the name of the process.
250    */
251   public get process(): string {
252     return pty.process(this._fd, this._pty) || this._file;
253   }
254
255   /**
256    * TTY
257    */
258
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');
262     }
263     pty.resize(this._fd, cols, rows);
264     this._cols = cols;
265     this._rows = rows;
266   }
267
268   private _sanitizeEnv(env: IProcessEnv): void {
269       // Make sure we didn't start our server from inside tmux.
270       delete env['TMUX'];
271       delete env['TMUX_PANE'];
272
273       // Make sure we didn't start our server from inside screen.
274       // http://web.mit.edu/gnu/doc/html/screen_20.html
275       delete env['STY'];
276       delete env['WINDOW'];
277
278       // Delete some variables that might confuse our terminal.
279       delete env['WINDOWID'];
280       delete env['TERMCAP'];
281       delete env['COLUMNS'];
282       delete env['LINES'];
283   }
284 }
285
286 /**
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
290  */
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);
296     handle.open(fd);
297     super(<any>{ handle });
298   }
299 }