import * as fs from "fs"; import * as path from "path"; import * as net from "net"; import { isEmacs } from "./utils/utils"; import { eachline } from "./utils/eachline"; const chalk = require("chalk"); const EOL = require("os").EOL; // These two values (EXITING_MESSAGE and getInfoFile) must match the // values used by the shell-server package. const EXITING_MESSAGE = "Shell exiting..."; function getInfoFile(shellDir: string): string { return path.join(shellDir, "info.json"); } // Invoked by the process running `meteor shell` to attempt to connect to // the server via the socket file. export function connect(shellDir: string) { new Client(shellDir).connect(); } class Client { public connected = false; private exitOnClose = false; private firstTimeConnecting = true; private reconnectCount = 0; private reconnectTimer?: NodeJS.Timeout; constructor(public shellDir: string) {} reconnect(delay: number = 100) { // Display the "Server unavailable" warning only on the third attempt // to reconnect, so it doesn't get shown for successful reconnects. if (++this.reconnectCount === 3) { console.error(chalk.yellow( "Server unavailable (waiting to reconnect)" )); } if (!this.reconnectTimer) { this.reconnectTimer = setTimeout(() => { delete this.reconnectTimer; this.connect(); }, delay); } }; connect() { const infoFile = getInfoFile(this.shellDir); fs.readFile(infoFile, "utf8", (err, json) => { if (err) { return this.reconnect(); } let info; try { info = JSON.parse(json); } catch (err) { return this.reconnect(); } if (info.status !== "enabled") { if (this.firstTimeConnecting) { return this.reconnect(); } if (info.reason) { console.error(info.reason); } console.error(EXITING_MESSAGE); process.exit(0); } this.setUpSocket( net.connect(info.port, "127.0.0.1"), info.key ); }); }; setUpSocketForSingleUse(sock: net.Socket, key: string) { sock.on("connect", function () { const inputBuffers: Buffer[] = []; process.stdin.on("data", buffer => inputBuffers.push(buffer)); process.stdin.on("end", () => { sock.write(JSON.stringify({ evaluateAndExit: { // Make sure the entire command is written as a string within a // JSON object, so that the server can easily tell when it has // received the whole command. command: Buffer.concat(inputBuffers).toString("utf8") }, terminal: false, key: key }) + "\n"); }); }); const outputBuffers: Buffer[] = []; sock.on("data", buffer => outputBuffers.push(buffer)); sock.on("close", function () { const output = JSON.parse(Buffer.concat(outputBuffers)); if (output.error) { console.error(output.error); process.exit(output.code); } else { process.stdout.write(JSON.stringify(output.result) + "\n"); process.exit(0); } }); }; setUpSocket(sock: net.Socket, key: string) { if (!process.stdin.isTTY) { return this.setUpSocketForSingleUse(sock, key); } // Put STDIN into "flowing mode": // http://nodejs.org/api/stream.html#stream_compatibility_with_older_node_versions process.stdin.resume(); const onConnect = () => { this.firstTimeConnecting = false; this.reconnectCount = 0; this.connected = true; // Sending a JSON-stringified options object (even just an empty // object) over the socket is required to start the REPL session. sock.write(JSON.stringify({ columns: process.stdout.columns, terminal: !isEmacs(), key: key }) + "\n"); process.stderr.write(shellBanner()); process.stdin.pipe(sock); if (process.stdin.setRawMode) { // https://github.com/joyent/node/issues/8204 process.stdin.setRawMode(true); } } const onClose = () => { tearDown(); // If we received the special EXITING_MESSAGE just before the socket // closed, then exit the shell instead of reconnecting. if (this.exitOnClose) { process.exit(0); } else { this.reconnect(); } } const onError = () => { tearDown(); this.reconnect(); } const tearDown = () => { this.connected = false; if (process.stdin.setRawMode) { // https://github.com/joyent/node/issues/8204 process.stdin.setRawMode(false); } process.stdin.unpipe(sock); sock.unpipe(process.stdout); sock.removeListener("connect", onConnect); sock.removeListener("close", onClose); sock.removeListener("error", onError); sock.end(); } sock.pipe(process.stdout); eachline(sock, (line: string) => { this.exitOnClose = line.indexOf(EXITING_MESSAGE) >= 0; return undefined as unknown as string; }); sock.on("connect", onConnect); sock.on("close", onClose); sock.on("error", onError); }; } function shellBanner(): string { const bannerLines = [ "", "Welcome to the server-side interactive shell!" ]; if (!isEmacs()) { // Tab completion sadly does not work in Emacs. bannerLines.push( "", "Tab completion is enabled for global variables." ); } bannerLines.push( "", "Type .reload to restart the server and the shell.", "Type .exit to disconnect from the server and leave the shell.", "Type .help for additional help.", EOL ); return chalk.green(bannerLines.join(EOL)); }