2019-11-28 05:17:01 -09:00
|
|
|
import {EventEmitter} from 'events';
|
2023-07-25 09:30:19 -08:00
|
|
|
import {dirname} from 'path';
|
2019-11-28 05:17:01 -09:00
|
|
|
import {StringDecoder} from 'string_decoder';
|
2023-07-25 09:30:19 -08:00
|
|
|
|
2019-11-28 05:17:01 -09:00
|
|
|
import defaultShell from 'default-shell';
|
2023-06-26 01:29:50 -08:00
|
|
|
import type {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty';
|
2022-02-06 22:11:56 -09:00
|
|
|
import osLocale from 'os-locale';
|
2023-07-25 09:30:19 -08:00
|
|
|
import shellEnv from 'shell-env';
|
|
|
|
|
|
|
|
|
|
import * as config from './config';
|
|
|
|
|
import {cliScriptPath} from './config/paths';
|
|
|
|
|
import {productName, version} from './package.json';
|
|
|
|
|
import {getDecoratedEnv} from './plugins';
|
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
|
|
|
|
2020-01-02 05:44:11 -09:00
|
|
|
let spawn: typeof npSpawn;
|
2016-07-13 13:06:05 -08:00
|
|
|
try {
|
2020-07-13 05:05:34 -08:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
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
|
|
|
}
|
|
|
|
|
|
2019-08-01 18:12:46 -08:00
|
|
|
const useConpty = config.getConfig().useConpty;
|
2016-08-01 23:49:25 -08:00
|
|
|
|
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 {
|
2020-01-02 05:44:11 -09:00
|
|
|
uid: string;
|
|
|
|
|
decoder: StringDecoder;
|
|
|
|
|
data!: string;
|
|
|
|
|
timeout!: NodeJS.Timeout | null;
|
|
|
|
|
constructor(uid: string) {
|
2018-12-28 14:13:00 -09:00
|
|
|
super();
|
|
|
|
|
this.uid = uid;
|
|
|
|
|
this.decoder = new StringDecoder('utf8');
|
|
|
|
|
|
|
|
|
|
this.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
|
this.data = this.uid;
|
|
|
|
|
this.timeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-26 03:17:43 -08:00
|
|
|
write(chunk: Buffer | string) {
|
2018-12-28 14:13:00 -09:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-26 03:17:43 -08:00
|
|
|
this.data += typeof chunk === 'string' ? chunk : this.decoder.write(chunk);
|
2018-12-28 14:13:00 -09:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-02 05:44:11 -09:00
|
|
|
interface SessionOptions {
|
|
|
|
|
uid: string;
|
2023-06-26 06:13:05 -08:00
|
|
|
rows?: number;
|
|
|
|
|
cols?: number;
|
|
|
|
|
cwd?: string;
|
|
|
|
|
shell?: string;
|
|
|
|
|
shellArgs?: string[];
|
2023-06-28 21:04:33 -08:00
|
|
|
profile: string;
|
2020-01-02 05:44:11 -09:00
|
|
|
}
|
2019-11-28 05:17:01 -09:00
|
|
|
export default class Session extends EventEmitter {
|
2020-01-02 05:44:11 -09:00
|
|
|
pty: IPty | null;
|
|
|
|
|
batcher: DataBatcher | null;
|
|
|
|
|
shell: string | null;
|
|
|
|
|
ended: boolean;
|
2021-02-08 06:35:23 -09:00
|
|
|
initTimestamp: number;
|
2023-06-28 21:04:33 -08:00
|
|
|
profile!: string;
|
2020-01-02 05:44:11 -09:00
|
|
|
constructor(options: SessionOptions) {
|
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;
|
2021-02-08 06:35:23 -09:00
|
|
|
this.initTimestamp = new Date().getTime();
|
2019-02-10 12:31:52 -09:00
|
|
|
this.init(options);
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-28 21:04:33 -08:00
|
|
|
init({uid, rows, cols, cwd, shell: _shell, shellArgs: _shellArgs, profile}: SessionOptions) {
|
|
|
|
|
this.profile = profile;
|
|
|
|
|
const envFromConfig = config.getProfileConfig(profile).env || {};
|
2023-06-26 06:13:05 -08:00
|
|
|
const defaultShellArgs = ['--login'];
|
|
|
|
|
|
|
|
|
|
const shell = _shell || defaultShell;
|
|
|
|
|
const shellArgs = _shellArgs || defaultShellArgs;
|
|
|
|
|
|
2022-02-06 21:57:20 -09:00
|
|
|
const cleanEnv =
|
|
|
|
|
process.env['APPIMAGE'] && process.env['APPDIR'] ? shellEnv.sync(_shell || defaultShell) : process.env;
|
2023-06-26 06:13:05 -08:00
|
|
|
const baseEnv: Record<string, string> = {
|
|
|
|
|
...cleanEnv,
|
|
|
|
|
LANG: `${osLocale.sync().replace(/-/, '_')}.UTF-8`,
|
|
|
|
|
TERM: 'xterm-256color',
|
|
|
|
|
COLORTERM: 'truecolor',
|
|
|
|
|
TERM_PROGRAM: productName,
|
|
|
|
|
TERM_PROGRAM_VERSION: version,
|
|
|
|
|
...envFromConfig
|
|
|
|
|
};
|
2021-02-19 03:41:41 -09:00
|
|
|
// path to AppImage mount point is added to PATH environment variable automatically
|
|
|
|
|
// which conflicts with the cli
|
|
|
|
|
if (baseEnv['APPIMAGE'] && baseEnv['APPDIR']) {
|
|
|
|
|
baseEnv['PATH'] = [dirname(cliScriptPath)]
|
|
|
|
|
.concat((baseEnv['PATH'] || '').split(':').filter((val) => !val.startsWith(baseEnv['APPDIR'])))
|
|
|
|
|
.join(':');
|
|
|
|
|
}
|
|
|
|
|
|
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
|
2020-07-13 05:05:34 -08:00
|
|
|
// See https://github.com/vercel/hyper/issues/696
|
2017-04-28 12:57:17 -08:00
|
|
|
if (baseEnv.GOOGLE_API_KEY && process.env.GOOGLE_API_KEY === baseEnv.GOOGLE_API_KEY) {
|
|
|
|
|
delete baseEnv.GOOGLE_API_KEY;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-02 05:44:11 -09:00
|
|
|
const options: IWindowsPtyForkOptions = {
|
2023-06-26 06:13:05 -08:00
|
|
|
cols,
|
2019-08-01 18:12:46 -08:00
|
|
|
rows,
|
|
|
|
|
cwd,
|
|
|
|
|
env: getDecoratedEnv(baseEnv)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// if config do not set the useConpty, it will be judged by the node-pty
|
|
|
|
|
if (typeof useConpty === 'boolean') {
|
2019-10-22 12:08:15 -08:00
|
|
|
options.useConpty = useConpty;
|
2019-08-01 18:12:46 -08:00
|
|
|
}
|
|
|
|
|
|
2016-11-11 08:18:04 -09:00
|
|
|
try {
|
2021-02-08 06:35:23 -09:00
|
|
|
this.pty = spawn(shell, shellArgs, options);
|
2021-08-30 05:37:13 -08:00
|
|
|
} catch (_err) {
|
|
|
|
|
const err = _err as {message: string};
|
2016-11-11 08:18:04 -09:00
|
|
|
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);
|
2020-03-25 02:15:08 -08:00
|
|
|
this.pty.onData((chunk) => {
|
2016-08-19 12:19:04 -08:00
|
|
|
if (this.ended) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-06-26 03:17:43 -08:00
|
|
|
this.batcher?.write(chunk);
|
2018-12-28 14:13:00 -09:00
|
|
|
});
|
|
|
|
|
|
2020-06-19 04:51:34 -08:00
|
|
|
this.batcher.on('flush', (data: string) => {
|
2018-12-28 14:13:00 -09:00
|
|
|
this.emit('data', data);
|
2016-06-30 22:01:04 -08:00
|
|
|
});
|
|
|
|
|
|
2021-02-08 06:35:23 -09:00
|
|
|
this.pty.onExit((e) => {
|
2016-06-30 22:01:04 -08:00
|
|
|
if (!this.ended) {
|
2021-02-08 06:35:23 -09:00
|
|
|
// fall back to default shell config if the shell exits within 1 sec with non zero exit code
|
|
|
|
|
// this will inform users in case there are errors in the config instead of instant exit
|
|
|
|
|
const runDuration = new Date().getTime() - this.initTimestamp;
|
|
|
|
|
if (e.exitCode > 0 && runDuration < 1000) {
|
|
|
|
|
const defaultShellConfig = {shell: defaultShell, shellArgs: defaultShellArgs};
|
|
|
|
|
const msg = `
|
|
|
|
|
shell exited in ${runDuration} ms with exit code ${e.exitCode}
|
|
|
|
|
please check the shell config: ${JSON.stringify({shell, shellArgs}, undefined, 2)}
|
|
|
|
|
fallback to default shell config: ${JSON.stringify(defaultShellConfig, undefined, 2)}
|
|
|
|
|
`;
|
|
|
|
|
console.warn(msg);
|
2023-06-26 03:17:43 -08:00
|
|
|
this.batcher?.write(msg.replace(/\n/g, '\r\n'));
|
2023-06-28 21:04:33 -08:00
|
|
|
this.init({uid, rows, cols, cwd, ...defaultShellConfig, profile});
|
2021-02-08 06:35:23 -09:00
|
|
|
} else {
|
|
|
|
|
this.ended = true;
|
|
|
|
|
this.emit('exit');
|
|
|
|
|
}
|
2016-06-30 22:01:04 -08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2021-02-08 06:35:23 -09:00
|
|
|
this.shell = shell;
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-02 05:44:11 -09:00
|
|
|
write(data: string) {
|
2019-02-10 12:31:52 -09:00
|
|
|
if (this.pty) {
|
|
|
|
|
this.pty.write(data);
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('Warning: Attempted to write to a session with no pty');
|
|
|
|
|
}
|
2016-06-30 22:01:04 -08:00
|
|
|
}
|
|
|
|
|
|
2020-01-02 05:44:11 -09:00
|
|
|
resize({cols, rows}: {cols: number; rows: number}) {
|
2019-02-10 12:31:52 -09:00
|
|
|
if (this.pty) {
|
|
|
|
|
try {
|
|
|
|
|
this.pty.resize(cols, rows);
|
2021-08-30 05:37:13 -08:00
|
|
|
} catch (_err) {
|
|
|
|
|
const err = _err as {stack: any};
|
2019-02-10 12:31:52 -09:00
|
|
|
console.error(err.stack);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
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();
|
2021-08-30 05:37:13 -08:00
|
|
|
} catch (_err) {
|
|
|
|
|
const err = _err as {stack: any};
|
2019-02-10 12:31:52 -09:00
|
|
|
console.error('exit error', err.stack);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
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;
|
|
|
|
|
}
|
2019-11-28 05:17:01 -09:00
|
|
|
}
|