mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Fixes the bug where the history would be parsed as one big blob on Windows, because it is always written with "\n" but parsed with "\r\n" on Windows and "\n" on Unixy platforms.
291 lines
7.8 KiB
JavaScript
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;
|
|
});
|
|
};
|