gritty
Version:
Web terminal emulator
205 lines (152 loc) • 4.68 kB
JavaScript
;
const DIR_ROOT = __dirname + '/..';
const path = require('path');
const log = require('debug')('gritty');
const Router = require('router');
const currify = require('currify');
const wraptile = require('wraptile');
const pty = require('node-pty');
const stringArgv = require('string-to-argv');
const terminalFn = currify(_terminalFn);
const connectionWraped = wraptile(connection);
const CMD = process.platform === 'win32' ? 'cmd.exe' : 'bash';
const isDev = process.env.NODE_ENV === 'development';
const getDist = () => {
if (isDev)
return '/dist-dev';
return '/dist';
};
const choose = (a, b, options) => {
if (typeof a === 'boolean')
return a;
if (typeof b === 'boolean')
return b;
return options.default;
};
module.exports = (options = {}) => {
const router = Router();
const {
prefix = '/gritty',
} = options;
router.route(prefix + '/*')
.get(terminalFn(options))
.get(staticFn);
return router;
};
function _terminalFn(options, req, res, next) {
const {
prefix = '/gritty',
} = options;
req.url = req.url.replace(prefix, '');
if (/^\/gritty\.js(\.map)?$/.test(req.url))
req.url = getDist() + req.url;
next();
}
function staticFn(req, res) {
const file = path.normalize(DIR_ROOT + req.url);
res.sendFile(file);
}
function createTerminal({command, env, cwd, cols, rows}) {
cols = cols || 80;
rows = rows || 24;
const [cmd, ...args] = stringArgv(command);
const term = pty.spawn(cmd, args, {
name: 'xterm-color',
cols,
rows,
cwd,
env: {
...process.env,
...env,
},
});
log(`Created terminal with PID: ${term.pid}`);
return term;
}
module.exports.listen = (socket, options = {}) => {
check(socket, options);
const {
prefix,
auth,
} = options;
socket
.of(prefix || '/gritty')
.on('connection', (socket) => {
const connect = connectionWraped(options, socket);
if (!auth)
return connection(options, socket);
const reject = () => socket.emit('reject');
socket.on('auth', auth(connect, reject));
});
};
function check(socket, options) {
if (!socket)
throw Error('socket could not be empty!');
const {auth} = options;
if (auth && typeof auth !== 'function')
throw Error('options.auth should be a function!');
}
function connection(options, socket) {
socket.emit('accept');
let term;
socket.on('terminal', onTerminal);
const onResize = (size) => {
size = size || {};
const {
cols = 80,
rows = 25,
} = size;
term.resize(cols, rows);
log(`Resized terminal ${term.pid} to ${cols} cols and ${rows} rows.`);
};
const onData = (msg) => {
term.write(msg);
};
function onTerminal(params) {
params = params || {};
const env = {
...params.env,
...socket.request.env,
};
const command = params.command || options.command || CMD;
const autoRestart = choose(params.autoRestart, options.autoRestart, {
default: true,
});
const {
rows,
cols,
cwd,
} = params;
term = createTerminal({
command,
cwd,
env,
rows,
cols,
});
const onExit = (code) => {
socket.emit('exit', code);
onDisconnect();
if (!autoRestart)
return;
onTerminal();
};
const onDisconnect = () => {
term.removeListener('exit', onExit);
term.kill();
log(`Closed terminal ${term.pid}`);
socket.removeListener('resize', onResize);
socket.removeListener('data', onData);
socket.removeListener('terminal', onTerminal);
socket.removeListener('disconnect', onDisconnect);
};
term.on('data', (data) => {
socket.emit('data', data);
});
term.on('exit', onExit);
log('Connected to terminal ' + term.pid);
socket.on('data', onData);
socket.on('resize', onResize);
socket.on('disconnect', onDisconnect);
}
}