From 2db8d07791e4d618a6edc8fc3d0d1774dbcab73f Mon Sep 17 00:00:00 2001 From: afrokick Date: Tue, 16 Jul 2019 18:10:17 +0300 Subject: [PATCH] Convert tools/shell-client.js to TypeScript (#10619) --- .eslintignore | 2 +- tools/cli/commands.js | 2 +- tools/shell-client.js | 216 ----------------------------------------- tools/shell-client.ts | 217 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 219 insertions(+), 218 deletions(-) delete mode 100644 tools/shell-client.js create mode 100644 tools/shell-client.ts diff --git a/.eslintignore b/.eslintignore index 42f463beff..c4d6cb792b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -39,7 +39,7 @@ tools/runners/run-log.js tools/fs/safe-pathwatcher.js tools/selftest.js tools/service-connection.js -tools/shell-client.js +tools/shell-client.ts tools/stats.js tools/test-utils.js tools/upgraders.js diff --git a/tools/cli/commands.js b/tools/cli/commands.js index a312133a93..7e4f0e7fe6 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -482,7 +482,7 @@ main.registerCommand({ // Convert to OS path here because shell/server.js doesn't know how to // convert paths, since it exists in the app and in the tool. - require('../shell-client.js').connect( + require('../shell-client').connect( files.convertToOSPath(projectContext.getMeteorShellDirectory()) ); diff --git a/tools/shell-client.js b/tools/shell-client.js deleted file mode 100644 index 1caec8d097..0000000000 --- a/tools/shell-client.js +++ /dev/null @@ -1,216 +0,0 @@ -var assert = require("assert"); -var fs = require("fs"); -var path = require("path"); -var net = require("net"); -var chalk = require("chalk"); -var EOL = require("os").EOL; -import { isEmacs } from "./utils/utils.js"; - -// These two values (EXITING_MESSAGE and getInfoFile) must match the -// values used by the shell-server package. -var EXITING_MESSAGE = "Shell exiting..."; -function getInfoFile(shellDir) { - return path.join(shellDir, "info.json"); -} - -// Invoked by the process running `meteor shell` to attempt to connect to -// the server via the socket file. -exports.connect = function connect(shellDir) { - new Client(shellDir).connect(); -}; - -function Client(shellDir) { - var self = this; - assert.ok(self instanceof Client); - - self.shellDir = shellDir; - self.exitOnClose = false; - self.firstTimeConnecting = true; - self.connected = false; - self.reconnectCount = 0; -} - -var Cp = Client.prototype; - -Cp.reconnect = function reconnect(delay) { - var self = this; - - // Display the "Server unavailable" warning only on the third attempt - // to reconnect, so it doesn't get shown for successful reconnects. - if (++self.reconnectCount === 3) { - console.error(chalk.yellow( - "Server unavailable (waiting to reconnect)" - )); - } - - if (!self.reconnectTimer) { - self.reconnectTimer = setTimeout(function() { - delete self.reconnectTimer; - self.connect(); - }, delay || 100); - } -}; - -Cp.connect = function connect() { - var self = this; - var infoFile = getInfoFile(self.shellDir); - - fs.readFile(infoFile, "utf8", function(err, json) { - if (err) { - return self.reconnect(); - } - - try { - var info = JSON.parse(json); - } catch (err) { - return self.reconnect(); - } - - if (info.status !== "enabled") { - if (self.firstTimeConnecting) { - return self.reconnect(); - } - - if (info.reason) { - console.error(info.reason); - } - - console.error(EXITING_MESSAGE); - process.exit(0); - } - - self.setUpSocket( - net.connect(info.port, "127.0.0.1"), - info.key - ); - }); -}; - -Cp.setUpSocketForSingleUse = function (sock, key) { - sock.on("connect", function () { - const inputBuffers = []; - 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 = []; - sock.on("data", buffer => outputBuffers.push(buffer)); - sock.on("close", function () { - var 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); - } - }); -}; - -Cp.setUpSocket = function setUpSocket(sock, key) { - const self = this; - - if (! process.stdin.isTTY) { - return self.setUpSocketForSingleUse(sock, key); - } - - // Put STDIN into "flowing mode": - // http://nodejs.org/api/stream.html#stream_compatibility_with_older_node_versions - process.stdin.resume(); - - function onConnect() { - self.firstTimeConnecting = false; - self.reconnectCount = 0; - self.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); - } - } - - function onClose() { - tearDown(); - - // If we received the special EXITING_MESSAGE just before the socket - // closed, then exit the shell instead of reconnecting. - if (self.exitOnClose) { - process.exit(0); - } else { - self.reconnect(); - } - } - - function onError(err) { - tearDown(); - self.reconnect(); - } - - function tearDown() { - self.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); - - require("./utils/eachline").eachline(sock, function (line) { - self.exitOnClose = line.indexOf(EXITING_MESSAGE) >= 0; - }); - - sock.on("connect", onConnect); - sock.on("close", onClose); - sock.on("error", onError); -}; - -function shellBanner() { - var 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)); -} diff --git a/tools/shell-client.ts b/tools/shell-client.ts new file mode 100644 index 0000000000..7e46812bb8 --- /dev/null +++ b/tools/shell-client.ts @@ -0,0 +1,217 @@ +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 line; + }); + + 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)); +}