2016-09-21 06:27:11 -08:00
|
|
|
const {EventEmitter} = require('events');
|
2016-10-02 08:54:27 -08:00
|
|
|
const {StringDecoder} = require('string_decoder');
|
2016-09-21 06:27:11 -08:00
|
|
|
|
2016-07-01 15:50:28 -08:00
|
|
|
const defaultShell = require('default-shell');
|
2016-09-21 06:27:11 -08:00
|
|
|
|
|
|
|
|
const {getDecoratedEnv} = require('./plugins');
|
|
|
|
|
const {productName, version} = require('./package');
|
2016-08-01 23:49:25 -08:00
|
|
|
const config = require('./config');
|
2016-06-30 22:01:04 -08:00
|
|
|
|
2017-09-10 05:35:10 -08:00
|
|
|
const createNodePtyError = () =>
|
|
|
|
|
new Error(
|
|
|
|
|
'`node-pty` failed to load. Typically this means that it was built incorrectly. Please check the `readme.md` to more info.'
|
|
|
|
|
);
|
2016-11-11 08:18:04 -09:00
|
|
|
|
2016-07-13 13:06:05 -08:00
|
|
|
let spawn;
|
|
|
|
|
try {
|
2017-01-09 17:37:46 -09:00
|
|
|
spawn = require('node-pty').spawn;
|
2016-07-13 13:06:05 -08:00
|
|
|
} catch (err) {
|
2017-01-15 06:22:07 -09:00
|
|
|
throw createNodePtyError();
|
2016-07-13 13:06:05 -08:00
|
|
|
}
|
|
|
|
|
|
2016-08-01 23:49:25 -08:00
|
|
|
const envFromConfig = config.getConfig().env || {};
|
|
|
|
|
|
2018-12-28 14:13:00 -09:00
|
|
|
// Max duration to batch session data before sending it to the renderer process.
|
|
|
|
|
const BATCH_DURATION_MS = 16;
|
|
|
|
|
|
|
|
|
|
// Max size of a session data batch. Note that this value can be exceeded by ~4k
|
|
|
|
|
// (chunk sizes seem to be 4k at the most)
|
|
|
|
|
const BATCH_MAX_SIZE = 200 * 1024;
|
|
|
|
|
|
|
|
|
|
// Data coming from the pty is sent to the renderer process for further
|
|
|
|
|
// vt parsing and rendering. This class batches data to minimize the number of
|
|
|
|
|
// IPC calls. It also reduces GC pressure and CPU cost: each chunk is prefixed
|
|
|
|
|
// with the window ID which is then stripped on the renderer process and this
|
|
|
|
|
// overhead is reduced with batching.
|
|
|
|
|
class DataBatcher extends EventEmitter {
|
|
|
|
|
constructor(uid) {
|
|
|
|
|
super();
|
|
|
|
|
this.uid = uid;
|
|
|
|
|
this.decoder = new StringDecoder('utf8');
|
|
|
|
|
|
|
|
|
|
this.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
|
this.data = this.uid;
|
|
|
|
|
this.timeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
write(chunk) {
|
|
|
|
|
if (this.data.length + chunk.length >= BATCH_MAX_SIZE) {
|
|
|
|
|
// We've reached the max batch size. Flush it and start another one
|
|
|
|
|
if (this.timeout) {
|
|
|
|
|
clearTimeout(this.timeout);
|
|
|
|
|
this.timeout = null;
|
|
|
|
|
}
|
|
|
|
|
this.flush();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.data += this.decoder.write(chunk);
|
|
|
|
|
|
|
|
|
|
if (!this.timeout) {
|
|
|
|
|
this.timeout = setTimeout(() => this.flush(), BATCH_DURATION_MS);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
flush() {
|
|
|
|
|
// Reset before emitting to allow for potential reentrancy
|
|
|
|
|
const data = this.data;
|
|
|
|
|
this.reset();
|
|
|
|
|
|
|
|
|
|
this.emit('flush', data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-06-30 22:01:04 -08:00
|
|
|
module.exports = class Session extends EventEmitter {
|
2019-02-10 12:31:52 -09:00
|
|
|
constructor(options) {
|
2016-06-30 22:01:04 -08:00
|
|
|
super();
|
2019-02-10 12:31:52 -09:00
|
|
|
this.pty = null;
|
|
|
|
|
this.batcher = null;
|
|
|
|
|
this.shell = null;
|
|
|
|
|
this.ended = false;
|
|
|
|
|
this.init(options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init({uid, rows, cols: columns, cwd, shell, shellArgs}) {
|
|
|
|
|
const osLocale = require('os-locale');
|
2017-09-10 05:35:10 -08:00
|
|
|
const baseEnv = Object.assign(
|
|
|
|
|
{},
|
|
|
|
|
process.env,
|
|
|
|
|
{
|
2018-04-27 05:41:59 -08:00
|
|
|
LANG: osLocale.sync() + '.UTF-8',
|
2017-09-10 05:35:10 -08:00
|
|
|
TERM: 'xterm-256color',
|
2017-11-22 04:29:07 -09:00
|
|
|
COLORTERM: 'truecolor',
|
2017-09-10 05:35:10 -08:00
|
|
|
TERM_PROGRAM: productName,
|
|
|
|
|
TERM_PROGRAM_VERSION: version
|
|
|
|
|
},
|
|
|
|
|
envFromConfig
|
|
|
|
|
);
|
2016-07-24 10:03:24 -08:00
|
|
|
|
2017-04-28 12:57:17 -08:00
|
|
|
// Electron has a default value for process.env.GOOGLE_API_KEY
|
|
|
|
|
// We don't want to leak this to the shell
|
|
|
|
|
// See https://github.com/zeit/hyper/issues/696
|
|
|
|
|
if (baseEnv.GOOGLE_API_KEY && process.env.GOOGLE_API_KEY === baseEnv.GOOGLE_API_KEY) {
|
|
|
|
|
delete baseEnv.GOOGLE_API_KEY;
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-13 11:30:17 -08:00
|
|
|
const defaultShellArgs = ['--login'];
|
|
|
|
|
|
2016-11-11 08:18:04 -09:00
|
|
|
try {
|
|
|
|
|
this.pty = spawn(shell || defaultShell, shellArgs || defaultShellArgs, {
|
2016-12-07 05:28:41 -09:00
|
|
|
cols: columns,
|
2016-11-11 08:18:04 -09:00
|
|
|
rows,
|
|
|
|
|
cwd,
|
|
|
|
|
env: getDecoratedEnv(baseEnv)
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (/is not a function/.test(err.message)) {
|
2017-01-15 06:22:07 -09:00
|
|
|
throw createNodePtyError();
|
2016-11-11 08:18:04 -09:00
|
|
|
} else {
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-06-30 22:01:04 -08:00
|
|
|
|
2018-12-28 14:13:00 -09:00
|
|
|
this.batcher = new DataBatcher(uid);
|
|
|
|
|
this.pty.on('data', chunk => {
|
2016-08-19 12:19:04 -08:00
|
|
|
if (this.ended) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-12-28 14:13:00 -09:00
|
|
|
this.batcher.write(chunk);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.batcher.on('flush', data => {
|
|
|
|
|
this.emit('data', data);
|
2016-06-30 22:01:04 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.pty.on('exit', () => {
|
|
|
|
|
if (!this.ended) {
|
|
|
|
|
this.ended = true;
|
|
|
|
|
this.emit('exit');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2016-07-23 16:57:47 -08:00
|
|
|
this.shell = shell || defaultShell;
|
2016-06-30 22:01:04 -08:00
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
exit() {
|
2016-06-30 22:01:04 -08:00
|
|
|
this.destroy();
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
write(data) {
|
2019-02-10 12:31:52 -09:00
|
|
|
if (this.pty) {
|
|
|
|
|
this.pty.write(data);
|
|
|
|
|
} else {
|
|
|
|
|
//eslint-disable-next-line no-console
|
|
|
|
|
console.warn('Warning: Attempted to write to a session with no pty');
|
|
|
|
|
}
|
2016-06-30 22:01:04 -08:00
|
|
|
}
|
|
|
|
|
|
2016-11-11 08:18:04 -09:00
|
|
|
resize({cols, rows}) {
|
2019-02-10 12:31:52 -09:00
|
|
|
if (this.pty) {
|
|
|
|
|
try {
|
|
|
|
|
this.pty.resize(cols, rows);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
//eslint-disable-next-line no-console
|
|
|
|
|
console.error(err.stack);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2017-09-10 05:35:10 -08:00
|
|
|
//eslint-disable-next-line no-console
|
2019-02-10 12:31:52 -09:00
|
|
|
console.warn('Warning: Attempted to resize a session with no pty');
|
2016-06-30 22:01:04 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 06:27:11 -08:00
|
|
|
destroy() {
|
2019-02-10 12:31:52 -09:00
|
|
|
if (this.pty) {
|
|
|
|
|
try {
|
|
|
|
|
this.pty.kill();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
//eslint-disable-next-line no-console
|
|
|
|
|
console.error('exit error', err.stack);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2017-09-10 05:35:10 -08:00
|
|
|
//eslint-disable-next-line no-console
|
2019-02-10 12:31:52 -09:00
|
|
|
console.warn('Warning: Attempted to destroy a session with no pty');
|
2016-07-03 12:35:45 -08:00
|
|
|
}
|
2016-06-30 22:01:04 -08:00
|
|
|
this.emit('exit');
|
|
|
|
|
this.ended = true;
|
|
|
|
|
}
|
|
|
|
|
};
|