PageRenderTime 29ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/programs/server/shell-server.js

https://gitlab.com/Cebo/projekt-um
JavaScript | 290 lines | 206 code | 42 blank | 42 comment | 18 complexity | 4b46d232dcba00049cb8ef4b3dcdf01c MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0, MIT, CC-BY-SA-3.0, 0BSD
  1. var assert = require("assert");
  2. var path = require("path");
  3. var stream = require("stream");
  4. var fs = require("fs");
  5. var net = require("net");
  6. var tty = require("tty");
  7. var vm = require("vm");
  8. var Fiber = require("fibers");
  9. var _ = require("underscore");
  10. var INFO_FILE_MODE = 0600; // Only the owner can read or write.
  11. var EXITING_MESSAGE =
  12. // Exported so that ./client.js can know what to expect.
  13. exports.EXITING_MESSAGE = "Shell exiting...";
  14. // Invoked by the server process to listen for incoming connections from
  15. // shell clients. Each connection gets its own REPL instance.
  16. exports.listen = function listen(shellDir) {
  17. new Server(shellDir).listen();
  18. };
  19. // Disabling the shell causes all attached clients to disconnect and exit.
  20. exports.disable = function disable(shellDir) {
  21. try {
  22. // Replace info.json with a file that says the shell server is
  23. // disabled, so that any connected shell clients will fail to
  24. // reconnect after the server process closes their sockets.
  25. fs.writeFileSync(
  26. getInfoFile(shellDir),
  27. JSON.stringify({
  28. status: "disabled",
  29. reason: "Shell server has shut down."
  30. }) + "\n",
  31. { mode: INFO_FILE_MODE }
  32. );
  33. } catch (ignored) {}
  34. };
  35. function Server(shellDir) {
  36. var self = this;
  37. assert.ok(self instanceof Server);
  38. self.shellDir = shellDir;
  39. self.key = Math.random().toString(36).slice(2);
  40. self.server = net.createServer(function(socket) {
  41. self.onConnection(socket);
  42. }).on("error", function(err) {
  43. console.error(err.stack);
  44. });
  45. }
  46. var Sp = Server.prototype;
  47. Sp.listen = function listen() {
  48. var self = this;
  49. var infoFile = getInfoFile(self.shellDir);
  50. fs.unlink(infoFile, function() {
  51. self.server.listen(0, "127.0.0.1", function() {
  52. fs.writeFileSync(infoFile, JSON.stringify({
  53. status: "enabled",
  54. port: self.server.address().port,
  55. key: self.key
  56. }) + "\n", {
  57. mode: INFO_FILE_MODE
  58. });
  59. });
  60. });
  61. };
  62. Sp.onConnection = function onConnection(socket) {
  63. var self = this;
  64. var dataSoFar = "";
  65. // Make sure this function doesn't try to write anything to the socket
  66. // after it has been closed.
  67. socket.on("close", function() {
  68. socket = null;
  69. });
  70. // If communication is not established within 1000ms of the first
  71. // connection, forcibly close the socket.
  72. var timeout = setTimeout(function() {
  73. if (socket) {
  74. socket.removeAllListeners("data");
  75. socket.end(EXITING_MESSAGE + "\n");
  76. }
  77. }, 1000);
  78. // Let connecting clients configure certain REPL options by sending a
  79. // JSON object over the socket. For example, only the client knows
  80. // whether it's running a TTY or an Emacs subshell or some other kind of
  81. // terminal, so the client must decide the value of options.terminal.
  82. socket.on("data", function onData(buffer) {
  83. // Just in case the options JSON comes in fragments.
  84. dataSoFar += buffer.toString("utf8");
  85. try {
  86. var options = JSON.parse(dataSoFar);
  87. } finally {
  88. if (! _.isObject(options)) {
  89. return; // Silence any parsing exceptions.
  90. }
  91. }
  92. if (socket) {
  93. socket.removeListener("data", onData);
  94. }
  95. if (options.key !== self.key) {
  96. if (socket) {
  97. socket.end(EXITING_MESSAGE + "\n");
  98. }
  99. return;
  100. }
  101. delete options.key;
  102. clearTimeout(timeout);
  103. // Immutable options.
  104. _.extend(options, {
  105. input: socket,
  106. output: socket,
  107. eval: evalCommand
  108. });
  109. // Overridable options.
  110. _.defaults(options, {
  111. prompt: "> ",
  112. terminal: true,
  113. useColors: true,
  114. useGlobal: true,
  115. ignoreUndefined: true,
  116. });
  117. self.startREPL(options);
  118. });
  119. };
  120. Sp.startREPL = function startREPL(options) {
  121. var self = this;
  122. if (! options.output.columns) {
  123. // The REPL's tab completion logic assumes process.stdout is a TTY,
  124. // and while that isn't technically true here, we can get tab
  125. // completion to behave correctly if we fake the .columns property.
  126. options.output.columns = getTerminalWidth();
  127. }
  128. // Make sure this function doesn't try to write anything to the output
  129. // stream after it has been closed.
  130. options.output.on("close", function() {
  131. options.output = null;
  132. });
  133. var repl = self.repl = require("repl").start(options);
  134. // History persists across shell sessions!
  135. self.initializeHistory();
  136. Object.defineProperty(repl.context, "_", {
  137. // Force the global _ variable to remain bound to underscore.
  138. get: function () { return _; },
  139. // Expose the last REPL result as __ instead of _.
  140. set: function(lastResult) {
  141. repl.context.__ = lastResult;
  142. },
  143. enumerable: true,
  144. // Allow this property to be (re)defined more than once (e.g. each
  145. // time the server restarts).
  146. configurable: true
  147. });
  148. // Use the same `require` function and `module` object visible to the
  149. // shell.js module.
  150. repl.context.require = require;
  151. repl.context.module = module;
  152. repl.context.repl = repl;
  153. // Some improvements to the existing help messages.
  154. repl.commands[".break"].help =
  155. "Terminate current command input and display new prompt";
  156. repl.commands[".exit"].help = "Disconnect from server and leave shell";
  157. repl.commands[".help"].help = "Show this help information";
  158. // When the REPL exits, signal the attached client to exit by sending it
  159. // the special EXITING_MESSAGE.
  160. repl.on("exit", function() {
  161. if (options.output) {
  162. options.output.write(EXITING_MESSAGE + "\n");
  163. options.output.end();
  164. }
  165. });
  166. // When the server process exits, end the output stream but do not
  167. // signal the attached client to exit.
  168. process.on("exit", function() {
  169. if (options.output) {
  170. options.output.end();
  171. }
  172. });
  173. // This Meteor-specific shell command rebuilds the application as if a
  174. // change was made to server code.
  175. repl.defineCommand("reload", {
  176. help: "Restart the server and the shell",
  177. action: function() {
  178. process.exit(0);
  179. }
  180. });
  181. };
  182. function getInfoFile(shellDir) {
  183. return path.join(shellDir, "info.json");
  184. }
  185. exports.getInfoFile = getInfoFile;
  186. function getHistoryFile(shellDir) {
  187. return path.join(shellDir, "history");
  188. }
  189. function getTerminalWidth() {
  190. try {
  191. // Inspired by https://github.com/TooTallNate/ttys/blob/master/index.js
  192. var fd = fs.openSync("/dev/tty", "r");
  193. assert.ok(tty.isatty(fd));
  194. var ws = new tty.WriteStream(fd);
  195. ws.end();
  196. return ws.columns;
  197. } catch (fancyApproachWasTooFancy) {
  198. return 80;
  199. }
  200. }
  201. // Shell commands need to be executed in fibers in case they call into
  202. // code that yields.
  203. function evalCommand(command, context, filename, callback) {
  204. Fiber(function() {
  205. try {
  206. var result = vm.runInThisContext(command, filename);
  207. } catch (error) {
  208. if (process.domain) {
  209. process.domain.emit("error", error);
  210. process.domain.exit();
  211. } else {
  212. callback(error);
  213. }
  214. return;
  215. }
  216. callback(null, result);
  217. }).run();
  218. }
  219. // This function allows a persistent history of shell commands to be saved
  220. // to and loaded from .meteor/local/shell-history.
  221. Sp.initializeHistory = function initializeHistory() {
  222. var self = this;
  223. var rli = self.repl.rli;
  224. var historyFile = getHistoryFile(self.shellDir);
  225. var historyFd = fs.openSync(historyFile, "a+");
  226. var historyLines = fs.readFileSync(historyFile, "utf8").split("\n");
  227. var seenLines = Object.create(null);
  228. if (! rli.history) {
  229. rli.history = [];
  230. rli.historyIndex = -1;
  231. }
  232. while (rli.history && historyLines.length > 0) {
  233. var line = historyLines.pop();
  234. if (line && /\S/.test(line) && ! seenLines[line]) {
  235. rli.history.push(line);
  236. seenLines[line] = true;
  237. }
  238. }
  239. rli.addListener("line", function(line) {
  240. if (historyFd >= 0 && /\S/.test(line)) {
  241. fs.writeSync(historyFd, line + "\n");
  242. }
  243. });
  244. self.repl.on("exit", function() {
  245. fs.closeSync(historyFd);
  246. historyFd = -1;
  247. });
  248. };