aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/server/ChildProcessUtilities/daemon/session.ts190
-rw-r--r--src/server/ProcessFactory.ts (renamed from src/server/ChildProcessUtilities/ProcessFactory.ts)25
-rw-r--r--src/server/index.ts2
-rw-r--r--src/server/session_manager/config.ts33
-rw-r--r--src/server/session_manager/input_manager.ts101
-rw-r--r--src/server/session_manager/logs/current_daemon_pid.log1
-rw-r--r--src/server/session_manager/logs/current_server_pid.log1
-rw-r--r--src/server/session_manager/logs/current_session_manager_pid.log1
-rw-r--r--src/server/session_manager/session_manager.ts199
9 files changed, 338 insertions, 215 deletions
diff --git a/src/server/ChildProcessUtilities/daemon/session.ts b/src/server/ChildProcessUtilities/daemon/session.ts
deleted file mode 100644
index fb2b3551e..000000000
--- a/src/server/ChildProcessUtilities/daemon/session.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-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
diff --git a/src/server/ChildProcessUtilities/ProcessFactory.ts b/src/server/ProcessFactory.ts
index 745b1479a..acb8b3a99 100644
--- a/src/server/ChildProcessUtilities/ProcessFactory.ts
+++ b/src/server/ProcessFactory.ts
@@ -1,10 +1,8 @@
import { existsSync, mkdirSync } from "fs";
-import { pathFromRoot, log_execution, fileDescriptorFromStream } from '../ActionUtilities';
-import { red, green } from "colors";
+import { pathFromRoot, fileDescriptorFromStream } from './ActionUtilities';
import rimraf = require("rimraf");
import { ChildProcess, spawn, StdioOptions } from "child_process";
import { Stream } from "stream";
-import { resolve } from "path";
export namespace ProcessFactory {
@@ -20,27 +18,6 @@ export namespace ProcessFactory {
return child;
}
- export namespace NamedAgents {
-
- export async function persistenceDaemon() {
- await log_execution({
- startMessage: "\ninitializing persistence daemon",
- endMessage: ({ result, error }) => {
- const success = error === null && result !== undefined;
- if (!success) {
- console.log(red("failed to initialize the persistance daemon"));
- console.log(error);
- process.exit(0);
- }
- return "failsafe daemon process successfully spawned";
- },
- action: () => createWorker('npx', ['ts-node', resolve(__dirname, "./daemon/persistence_daemon.ts")], ["ignore", "inherit", "inherit"]),
- color: green
- });
- console.log();
- }
- }
-
}
export namespace Logger {
diff --git a/src/server/index.ts b/src/server/index.ts
index bc481e579..2cc35ccec 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -23,7 +23,7 @@ import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager";
import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
import { yellow, red } from "colors";
import { disconnect } from "../server/Initialization";
-import { ProcessFactory, Logger } from "./ChildProcessUtilities/ProcessFactory";
+import { ProcessFactory, Logger } from "./ProcessFactory";
export const publicDirectory = path.resolve(__dirname, "public");
export const filesDirectory = path.resolve(publicDirectory, "files");
diff --git a/src/server/session_manager/config.ts b/src/server/session_manager/config.ts
new file mode 100644
index 000000000..ebbd999c6
--- /dev/null
+++ b/src/server/session_manager/config.ts
@@ -0,0 +1,33 @@
+import { resolve } from 'path';
+import { yellow } from "colors";
+
+export const latency = 10;
+export const ports = [1050, 4321];
+export const onWindows = process.platform === "win32";
+export const heartbeat = `http://localhost:1050/serverHeartbeat`;
+export const recipient = "samuel_wilkins@brown.edu";
+export const { pid, platform } = process;
+
+/**
+ * Logging
+ */
+export const identifier = yellow("__session_manager__:");
+
+/**
+ * Paths
+ */
+export const logPath = resolve(__dirname, "./logs");
+export const crashPath = resolve(logPath, "./crashes");
+
+/**
+ * State
+ */
+export enum SessionState {
+ STARTING = "STARTING",
+ INITIALIZED = "INITIALIZED",
+ LISTENING = "LISTENING",
+ AUTOMATICALLY_RESTARTING = "CRASH_RESTARTING",
+ MANUALLY_RESTARTING = "MANUALLY_RESTARTING",
+ EXITING = "EXITING",
+ UPDATING = "UPDATING"
+} \ No newline at end of file
diff --git a/src/server/session_manager/input_manager.ts b/src/server/session_manager/input_manager.ts
new file mode 100644
index 000000000..a95e6baae
--- /dev/null
+++ b/src/server/session_manager/input_manager.ts
@@ -0,0 +1,101 @@
+import { createInterface, Interface } from "readline";
+import { red } from "colors";
+
+export interface Configuration {
+ identifier: string;
+ onInvalid?: (culprit?: string) => string | string;
+ isCaseSensitive?: boolean;
+}
+
+export interface Registration {
+ argPattern: RegExp[];
+ action: (parsedArgs: IterableIterator<string>) => any | Promise<any>;
+}
+
+export default class InputManager {
+ private identifier: string;
+ private onInvalid: ((culprit?: string) => string) | string;
+ private isCaseSensitive: boolean;
+ private commandMap = new Map<string, Registration[]>();
+ public interface: Interface;
+ private busy = false;
+ private keys: string | undefined;
+
+ constructor({ identifier: prompt, onInvalid, isCaseSensitive }: Configuration) {
+ this.identifier = prompt;
+ this.onInvalid = onInvalid || this.usage;
+ this.isCaseSensitive = isCaseSensitive ?? true;
+ this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput);
+ }
+
+ private usage = () => {
+ const resolved = this.keys;
+ if (resolved) {
+ return resolved;
+ }
+ const members: string[] = [];
+ const keys = this.commandMap.keys();
+ let next: IteratorResult<string>;
+ while (!(next = keys.next()).done) {
+ members.push(next.value);
+ }
+ return `${this.identifier} commands: { ${members.sort().join(", ")} }`;
+ }
+
+ public registerCommand = (basename: string, argPattern: RegExp[], action: any | Promise<any>) => {
+ const existing = this.commandMap.get(basename);
+ const registration = { argPattern, action };
+ if (existing) {
+ existing.push(registration);
+ } else {
+ this.commandMap.set(basename, [registration]);
+ }
+ }
+
+ private invalid = (culprit?: string) => {
+ console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(culprit)));
+ this.busy = false;
+ }
+
+ private considerInput = async (line: string) => {
+ if (this.busy) {
+ console.log(red("Busy"));
+ return;
+ }
+ this.busy = true;
+ line = line.trim();
+ if (this.isCaseSensitive) {
+ line = line.toLowerCase();
+ }
+ const [command, ...args] = line.split(/\s+/g);
+ if (!command) {
+ return this.invalid();
+ }
+ const registered = this.commandMap.get(command);
+ if (registered) {
+ const { length } = args;
+ const candidates = registered.filter(({ argPattern: { length: count } }) => count === length);
+ for (const { argPattern, action } of candidates) {
+ const parsed: string[] = [];
+ let matched = false;
+ if (length) {
+ for (let i = 0; i < length; i++) {
+ let matches: RegExpExecArray | null;
+ if ((matches = argPattern[i].exec(args[i])) === null) {
+ break;
+ }
+ parsed.push(matches[0]);
+ }
+ matched = true;
+ }
+ if (!length || matched) {
+ await action(parsed[Symbol.iterator]());
+ this.busy = false;
+ return;
+ }
+ }
+ }
+ this.invalid(command);
+ }
+
+} \ No newline at end of file
diff --git a/src/server/session_manager/logs/current_daemon_pid.log b/src/server/session_manager/logs/current_daemon_pid.log
new file mode 100644
index 000000000..557e3d7c3
--- /dev/null
+++ b/src/server/session_manager/logs/current_daemon_pid.log
@@ -0,0 +1 @@
+26860
diff --git a/src/server/session_manager/logs/current_server_pid.log b/src/server/session_manager/logs/current_server_pid.log
new file mode 100644
index 000000000..85fdb7ae0
--- /dev/null
+++ b/src/server/session_manager/logs/current_server_pid.log
@@ -0,0 +1 @@
+54649 created @ 2019-12-14T08:04:42.391Z
diff --git a/src/server/session_manager/logs/current_session_manager_pid.log b/src/server/session_manager/logs/current_session_manager_pid.log
new file mode 100644
index 000000000..75c23b35a
--- /dev/null
+++ b/src/server/session_manager/logs/current_session_manager_pid.log
@@ -0,0 +1 @@
+54643
diff --git a/src/server/session_manager/session_manager.ts b/src/server/session_manager/session_manager.ts
new file mode 100644
index 000000000..d8b2f6e74
--- /dev/null
+++ b/src/server/session_manager/session_manager.ts
@@ -0,0 +1,199 @@
+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 InputManager from "./input_manager";
+import { identifier, logPath, crashPath, onWindows, pid, ports, heartbeat, recipient, latency, SessionState } from "./config";
+const killport = require("kill-port");
+
+process.on('SIGINT', endPrevious);
+let state: SessionState = SessionState.STARTING;
+const is = (...reference: SessionState[]) => reference.includes(state);
+const set = (reference: SessionState) => state = reference;
+
+const { registerCommand } = new InputManager({ identifier });
+
+registerCommand("restart", [], async () => {
+ set(SessionState.MANUALLY_RESTARTING);
+ identifiedLog(cyan("Initializing manual restart..."));
+ await endPrevious();
+});
+
+registerCommand("exit", [], exit);
+
+async function exit() {
+ set(SessionState.EXITING);
+ identifiedLog(cyan("Initializing session end"));
+ await endPrevious();
+ identifiedLog("Cleanup complete. Exiting session...\n");
+ execSync(killAllCommand());
+}
+
+registerCommand("update", [], async () => {
+ set(SessionState.UPDATING);
+ identifiedLog(cyan("Initializing server update from version control..."));
+ await endPrevious();
+ await new Promise<void>(resolve => {
+ exec(updateCommand(), error => {
+ if (error) {
+ identifiedLog(red(error.message));
+ }
+ resolve();
+ });
+ });
+ await exit();
+});
+
+registerCommand("state", [], () => identifiedLog(state));
+
+if (!existsSync(logPath)) {
+ mkdirSync(logPath);
+}
+if (!existsSync(crashPath)) {
+ mkdirSync(crashPath);
+}
+
+function addLogEntry(message: string, color: Color) {
+ const formatted = color(`${message} ${timestamp()}.`);
+ identifiedLog(formatted);
+ // appendFileSync(resolve(crashPath, `./session_crashes_${new Date().toISOString()}.log`), `${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 windowsPrepend = (command: string) => `"C:\\Program Files\\Git\\git-bash.exe" -c "${command}"`;
+const macPrepend = (command: string) => `osascript -e 'tell app "Terminal"\ndo script "cd ${pathFromRoot()} && ${command}"\nend tell'`;
+
+function updateCommand() {
+ const command = "git pull && npm install";
+ if (onWindows) {
+ return windowsPrepend(command);
+ }
+ return macPrepend(command);
+}
+
+function startServerCommand() {
+ const command = "npm run start-release";
+ if (onWindows) {
+ return windowsPrepend(command);
+ }
+ return macPrepend(command);
+}
+
+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("SIGKILL");
+ 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() {
+ const listening = is(SessionState.LISTENING);
+ let error: any;
+ try {
+ listening && process.stdout.write(`${identifier} 👂 `);
+ await request.get(heartbeat);
+ listening && console.log('⇠ 💚');
+ if (!listening) {
+ addLogEntry(is(SessionState.INITIALIZED) ? "Server successfully started" : "Backup server successfully restarted", green);
+ set(SessionState.LISTENING);
+ }
+ } catch (e) {
+ listening && console.log("⇠ 💔\n");
+ error = e;
+ } finally {
+ if (error && !is(SessionState.AUTOMATICALLY_RESTARTING, SessionState.INITIALIZED, SessionState.UPDATING)) {
+ if (is(SessionState.STARTING)) {
+ set(SessionState.INITIALIZED);
+ } else if (is(SessionState.MANUALLY_RESTARTING)) {
+ set(SessionState.AUTOMATICALLY_RESTARTING);
+ } else {
+ set(SessionState.AUTOMATICALLY_RESTARTING);
+ 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..."));
+ }
+ current_backup = exec(startServerCommand(), err => identifiedLog(err?.message || is(SessionState.INITIALIZED) ? "Spawned initial server." : "Previous server process exited."));
+ writeLocalPidLog("server", `${(current_backup?.pid ?? -2) + 1} created ${timestamp()}`);
+ }
+ setTimeout(checkHeartbeat, 1000 * latency);
+ }
+}
+
+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: ${heartbeat}\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));
+ });
+}
+
+identifiedLog(yellow(`After initialization, will poll server heartbeat repeatedly...\n`));
+checkHeartbeat(); \ No newline at end of file