Files
meteor/tools/shell-client.ts
2020-04-17 13:43:19 -04:00

218 lines
5.6 KiB
TypeScript

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));
}