mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Convert tools/shell-client.js to TypeScript (#10619)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
217
tools/shell-client.ts
Normal file
217
tools/shell-client.ts
Normal file
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user