ethstats-server/web-app/.meteor/local/build/programs/server/shell-server.js
2015-08-14 19:22:53 +02:00

291 lines
7.8 KiB
JavaScript

var assert = require("assert");
var path = require("path");
var stream = require("stream");
var fs = require("fs");
var net = require("net");
var tty = require("tty");
var vm = require("vm");
var Fiber = require("fibers");
var _ = require("underscore");
var INFO_FILE_MODE = 0600; // Only the owner can read or write.
var EXITING_MESSAGE =
// Exported so that ./client.js can know what to expect.
exports.EXITING_MESSAGE = "Shell exiting...";
// Invoked by the server process to listen for incoming connections from
// shell clients. Each connection gets its own REPL instance.
exports.listen = function listen(shellDir) {
new Server(shellDir).listen();
};
// Disabling the shell causes all attached clients to disconnect and exit.
exports.disable = function disable(shellDir) {
try {
// Replace info.json with a file that says the shell server is
// disabled, so that any connected shell clients will fail to
// reconnect after the server process closes their sockets.
fs.writeFileSync(
getInfoFile(shellDir),
JSON.stringify({
status: "disabled",
reason: "Shell server has shut down."
}) + "\n",
{ mode: INFO_FILE_MODE }
);
} catch (ignored) {}
};
function Server(shellDir) {
var self = this;
assert.ok(self instanceof Server);
self.shellDir = shellDir;
self.key = Math.random().toString(36).slice(2);
self.server = net.createServer(function(socket) {
self.onConnection(socket);
}).on("error", function(err) {
console.error(err.stack);
});
}
var Sp = Server.prototype;
Sp.listen = function listen() {
var self = this;
var infoFile = getInfoFile(self.shellDir);
fs.unlink(infoFile, function() {
self.server.listen(0, "127.0.0.1", function() {
fs.writeFileSync(infoFile, JSON.stringify({
status: "enabled",
port: self.server.address().port,
key: self.key
}) + "\n", {
mode: INFO_FILE_MODE
});
});
});
};
Sp.onConnection = function onConnection(socket) {
var self = this;
var dataSoFar = "";
// Make sure this function doesn't try to write anything to the socket
// after it has been closed.
socket.on("close", function() {
socket = null;
});
// If communication is not established within 1000ms of the first
// connection, forcibly close the socket.
var timeout = setTimeout(function() {
if (socket) {
socket.removeAllListeners("data");
socket.end(EXITING_MESSAGE + "\n");
}
}, 1000);
// Let connecting clients configure certain REPL options by sending a
// JSON object over the socket. For example, only the client knows
// whether it's running a TTY or an Emacs subshell or some other kind of
// terminal, so the client must decide the value of options.terminal.
socket.on("data", function onData(buffer) {
// Just in case the options JSON comes in fragments.
dataSoFar += buffer.toString("utf8");
try {
var options = JSON.parse(dataSoFar);
} finally {
if (! _.isObject(options)) {
return; // Silence any parsing exceptions.
}
}
if (socket) {
socket.removeListener("data", onData);
}
if (options.key !== self.key) {
if (socket) {
socket.end(EXITING_MESSAGE + "\n");
}
return;
}
delete options.key;
clearTimeout(timeout);
// Immutable options.
_.extend(options, {
input: socket,
output: socket,
eval: evalCommand
});
// Overridable options.
_.defaults(options, {
prompt: "> ",
terminal: true,
useColors: true,
useGlobal: true,
ignoreUndefined: true,
});
self.startREPL(options);
});
};
Sp.startREPL = function startREPL(options) {
var self = this;
if (! options.output.columns) {
// The REPL's tab completion logic assumes process.stdout is a TTY,
// and while that isn't technically true here, we can get tab
// completion to behave correctly if we fake the .columns property.
options.output.columns = getTerminalWidth();
}
// Make sure this function doesn't try to write anything to the output
// stream after it has been closed.
options.output.on("close", function() {
options.output = null;
});
var repl = self.repl = require("repl").start(options);
// History persists across shell sessions!
self.initializeHistory();
Object.defineProperty(repl.context, "_", {
// Force the global _ variable to remain bound to underscore.
get: function () { return _; },
// Expose the last REPL result as __ instead of _.
set: function(lastResult) {
repl.context.__ = lastResult;
},
enumerable: true,
// Allow this property to be (re)defined more than once (e.g. each
// time the server restarts).
configurable: true
});
// Use the same `require` function and `module` object visible to the
// shell.js module.
repl.context.require = require;
repl.context.module = module;
repl.context.repl = repl;
// Some improvements to the existing help messages.
repl.commands[".break"].help =
"Terminate current command input and display new prompt";
repl.commands[".exit"].help = "Disconnect from server and leave shell";
repl.commands[".help"].help = "Show this help information";
// When the REPL exits, signal the attached client to exit by sending it
// the special EXITING_MESSAGE.
repl.on("exit", function() {
if (options.output) {
options.output.write(EXITING_MESSAGE + "\n");
options.output.end();
}
});
// When the server process exits, end the output stream but do not
// signal the attached client to exit.
process.on("exit", function() {
if (options.output) {
options.output.end();
}
});
// This Meteor-specific shell command rebuilds the application as if a
// change was made to server code.
repl.defineCommand("reload", {
help: "Restart the server and the shell",
action: function() {
process.exit(0);
}
});
};
function getInfoFile(shellDir) {
return path.join(shellDir, "info.json");
}
exports.getInfoFile = getInfoFile;
function getHistoryFile(shellDir) {
return path.join(shellDir, "history");
}
function getTerminalWidth() {
try {
// Inspired by https://github.com/TooTallNate/ttys/blob/master/index.js
var fd = fs.openSync("/dev/tty", "r");
assert.ok(tty.isatty(fd));
var ws = new tty.WriteStream(fd);
ws.end();
return ws.columns;
} catch (fancyApproachWasTooFancy) {
return 80;
}
}
// Shell commands need to be executed in fibers in case they call into
// code that yields.
function evalCommand(command, context, filename, callback) {
Fiber(function() {
try {
var result = vm.runInThisContext(command, filename);
} catch (error) {
if (process.domain) {
process.domain.emit("error", error);
process.domain.exit();
} else {
callback(error);
}
return;
}
callback(null, result);
}).run();
}
// This function allows a persistent history of shell commands to be saved
// to and loaded from .meteor/local/shell-history.
Sp.initializeHistory = function initializeHistory() {
var self = this;
var rli = self.repl.rli;
var historyFile = getHistoryFile(self.shellDir);
var historyFd = fs.openSync(historyFile, "a+");
var historyLines = fs.readFileSync(historyFile, "utf8").split("\n");
var seenLines = Object.create(null);
if (! rli.history) {
rli.history = [];
rli.historyIndex = -1;
}
while (rli.history && historyLines.length > 0) {
var line = historyLines.pop();
if (line && /\S/.test(line) && ! seenLines[line]) {
rli.history.push(line);
seenLines[line] = true;
}
}
rli.addListener("line", function(line) {
if (historyFd >= 0 && /\S/.test(line)) {
fs.writeSync(historyFd, line + "\n");
}
});
self.repl.on("exit", function() {
fs.closeSync(historyFd);
historyFd = -1;
});
};