aboutsummaryrefslogtreecommitdiff
path: root/src/server/ChildProcessUtilities/daemon/session.ts
diff options
context:
space:
mode:
authorSam Wilkins <samwilkins333@gmail.com>2019-12-13 14:25:45 -0500
committerSam Wilkins <samwilkins333@gmail.com>2019-12-13 14:25:45 -0500
commit529c9dbe42ab8f732bd8a244246eb1b94c3b5053 (patch)
tree13110af32830b25fca4eed583c6f23cee7ecd0ee /src/server/ChildProcessUtilities/daemon/session.ts
parenta2d2737fa4ec1a57f5288ed72eb44a2be4588ee8 (diff)
parent9a90830162cf176b58729385588accf3b589454d (diff)
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web
Diffstat (limited to 'src/server/ChildProcessUtilities/daemon/session.ts')
-rw-r--r--src/server/ChildProcessUtilities/daemon/session.ts190
1 files changed, 190 insertions, 0 deletions
diff --git a/src/server/ChildProcessUtilities/daemon/session.ts b/src/server/ChildProcessUtilities/daemon/session.ts
new file mode 100644
index 000000000..fb2b3551e
--- /dev/null
+++ b/src/server/ChildProcessUtilities/daemon/session.ts
@@ -0,0 +1,190 @@
+import * as request from "request-promise";
+import { log_execution, pathFromRoot } from "../../ActionUtilities";
+import { red, yellow, cyan, green, Color } from "colors";
+import * as nodemailer from "nodemailer";
+import { MailOptions } from "nodemailer/lib/json-transport";
+import { writeFileSync, existsSync, mkdirSync } from "fs";
+import { resolve } from 'path';
+import { ChildProcess, exec, execSync } from "child_process";
+import { createInterface } from "readline";
+const killport = require("kill-port");
+
+process.on('SIGINT', endPrevious);
+const identifier = yellow("__session_manager__:");
+
+let manualRestartActive = false;
+createInterface(process.stdin, process.stdout).on('line', async line => {
+ const prompt = line.trim().toLowerCase();
+ switch (prompt) {
+ case "restart":
+ manualRestartActive = true;
+ identifiedLog(cyan("Initializing manual restart..."));
+ await endPrevious();
+ break;
+ case "exit":
+ identifiedLog(cyan("Initializing session end"));
+ await endPrevious();
+ identifiedLog("Cleanup complete. Exiting session...\n");
+ execSync(killAllCommand());
+ break;
+ default:
+ identifiedLog(red("commands: { exit, restart }"));
+ return;
+ }
+});
+
+const logPath = resolve(__dirname, "./logs");
+const crashPath = resolve(logPath, "./crashes");
+if (!existsSync(logPath)) {
+ mkdirSync(logPath);
+}
+if (!existsSync(crashPath)) {
+ mkdirSync(crashPath);
+}
+
+const crashLogPath = resolve(crashPath, `./session_crashes_${new Date().toISOString()}.log`);
+function addLogEntry(message: string, color: Color) {
+ const formatted = color(`${message} ${timestamp()}.`);
+ identifiedLog(formatted);
+ // appendFileSync(crashLogPath, `${formatted}\n`);
+}
+
+function identifiedLog(message?: any, ...optionalParams: any[]) {
+ console.log(identifier, message, ...optionalParams);
+}
+
+if (!["win32", "darwin"].includes(process.platform)) {
+ identifiedLog(red("Invalid operating system: this script is supported only on Mac and Windows."));
+ process.exit(1);
+}
+
+const latency = 10;
+const ports = [1050, 4321];
+const onWindows = process.platform === "win32";
+const LOCATION = "http://localhost";
+const heartbeat = `${LOCATION}:1050/serverHeartbeat`;
+const recipient = "samuel_wilkins@brown.edu";
+const { pid } = process;
+let restarting = false;
+let count = 0;
+
+function startServerCommand() {
+ if (onWindows) {
+ return '"C:\\Program Files\\Git\\git-bash.exe" -c "npm run start-release"';
+ }
+ return `osascript -e 'tell app "Terminal"\ndo script "cd ${pathFromRoot()} && npm run start-release"\nend tell'`;
+}
+
+function killAllCommand() {
+ if (onWindows) {
+ return "taskkill /f /im node.exe";
+ }
+ return "killall -9 node";
+}
+
+identifiedLog("Initializing session...");
+
+writeLocalPidLog("session_manager", pid);
+
+function writeLocalPidLog(filename: string, contents: any) {
+ const path = `./logs/current_${filename}_pid.log`;
+ identifiedLog(cyan(`${contents} written to ${path}`));
+ writeFileSync(resolve(__dirname, path), `${contents}\n`);
+}
+
+function timestamp() {
+ return `@ ${new Date().toISOString()}`;
+}
+
+async function endPrevious() {
+ identifiedLog(yellow("Cleaning up previous connections..."));
+ current_backup?.kill("SIGTERM");
+ await Promise.all(ports.map(port => {
+ const task = killport(port, 'tcp');
+ return task.catch((error: any) => identifiedLog(red(error)));
+ }));
+ identifiedLog(yellow("Done. Any failures will be printed in red immediately above."));
+}
+
+let current_backup: ChildProcess | undefined = undefined;
+
+async function checkHeartbeat() {
+ let error: any;
+ try {
+ count && !restarting && process.stdout.write(`${identifier} 👂 `);
+ await request.get(heartbeat);
+ count && !restarting && console.log('⇠ 💚');
+ if (restarting || manualRestartActive) {
+ addLogEntry(count++ ? "Backup server successfully restarted" : "Server successfully started", green);
+ restarting = false;
+ }
+ } catch (e) {
+ count && !restarting && console.log("⇠ 💔");
+ error = e;
+ } finally {
+ if (error) {
+ if (!restarting || manualRestartActive) {
+ restarting = true;
+ if (count && !manualRestartActive) {
+ console.log();
+ addLogEntry("Detected a server crash", red);
+ identifiedLog(red(error.message));
+ await endPrevious();
+ await log_execution({
+ startMessage: identifier + " Sending crash notification email",
+ endMessage: ({ error, result }) => {
+ const success = error === null && result === true;
+ return identifier + ` ${(success ? `Notification successfully sent to` : `Failed to notify`)} ${recipient} ${timestamp()}`;
+ },
+ action: async () => notify(error || "Hmm, no error to report..."),
+ color: cyan
+ });
+ identifiedLog(green("Initiating server restart..."));
+ }
+ manualRestartActive = false;
+ current_backup = exec(startServerCommand(), err => identifiedLog(err?.message || count ? "Previous server process exited." : "Spawned initial server."));
+ writeLocalPidLog("server", `${(current_backup?.pid ?? -2) + 1} created ${timestamp()}`);
+ }
+ }
+ setTimeout(checkHeartbeat, 1000 * latency);
+ }
+}
+
+async function startListening() {
+ identifiedLog(yellow(`After initialization, will poll server heartbeat repeatedly...\n`));
+ if (!LOCATION) {
+ identifiedLog(red("No location specified for session manager. Please include as a command line environment variable or in a .env file."));
+ process.exit(0);
+ }
+ await checkHeartbeat();
+}
+
+function emailText(error: any) {
+ return [
+ `Hey ${recipient.split("@")[0]},`,
+ "You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:",
+ `Location: ${LOCATION}\nError: ${error}`,
+ "The server should already be restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress."
+ ].join("\n\n");
+}
+
+async function notify(error: any) {
+ const smtpTransport = nodemailer.createTransport({
+ service: 'Gmail',
+ auth: {
+ user: 'brownptcdash@gmail.com',
+ pass: 'browngfx1'
+ }
+ });
+ const mailOptions = {
+ to: recipient,
+ from: 'brownptcdash@gmail.com',
+ subject: 'Dash Server Crash',
+ text: emailText(error)
+ } as MailOptions;
+ return new Promise<boolean>(resolve => {
+ smtpTransport.sendMail(mailOptions, (dispatchError: Error | null) => resolve(dispatchError === null));
+ });
+}
+
+startListening(); \ No newline at end of file