aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSam Wilkins <samwilkins333@gmail.com>2020-01-03 23:28:34 -0800
committerSam Wilkins <samwilkins333@gmail.com>2020-01-03 23:28:34 -0800
commitb31d54b285236dc92f7d287af6a441878f429a34 (patch)
tree4f2289276b33eb37f5c75b0221cdc046d2967fcd /src
parent5111eb546d9bcd6070ddbe8076f3389a37cd7081 (diff)
session restructuring and schema enforced json configuration
Diffstat (limited to 'src')
-rw-r--r--src/server/ActionUtilities.ts26
-rw-r--r--src/server/Session/session.ts (renamed from src/server/session.ts)56
-rw-r--r--src/server/Session/session_config_schema.ts25
-rw-r--r--src/server/index.ts2
-rw-r--r--src/server/repl.ts (renamed from src/server/session_manager/input_manager.ts)2
-rw-r--r--src/server/session_manager/config.ts33
-rw-r--r--src/server/session_manager/email.ts26
-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.ts206
-rw-r--r--src/server/session_manager/session_manager_cluster.ts36
12 files changed, 94 insertions, 321 deletions
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts
index 053576a92..950fba093 100644
--- a/src/server/ActionUtilities.ts
+++ b/src/server/ActionUtilities.ts
@@ -4,6 +4,8 @@ import { exec } from 'child_process';
import * as path from 'path';
import * as rimraf from "rimraf";
import { yellow, Color } from 'colors';
+import * as nodemailer from "nodemailer";
+import { MailOptions } from "nodemailer/lib/json-transport";
const projectRoot = path.resolve(__dirname, "../../");
export function pathFromRoot(relative?: string) {
@@ -105,3 +107,27 @@ export async function Prune(rootDirectory: string): Promise<boolean> {
}
export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => unlink(mediaPath, error => resolve(error === null)));
+
+export namespace Email {
+
+ const smtpTransport = nodemailer.createTransport({
+ service: 'Gmail',
+ auth: {
+ user: 'brownptcdash@gmail.com',
+ pass: 'browngfx1'
+ }
+ });
+
+ export async function dispatch(recipient: string, subject: string, content: string): Promise<boolean> {
+ const mailOptions = {
+ to: recipient,
+ from: 'brownptcdash@gmail.com',
+ subject,
+ text: `Hello ${recipient.split("@")[0]},\n\n${content}`
+ } as MailOptions;
+ return new Promise<boolean>(resolve => {
+ smtpTransport.sendMail(mailOptions, (dispatchError: Error | null) => resolve(dispatchError === null));
+ });
+ }
+
+} \ No newline at end of file
diff --git a/src/server/session.ts b/src/server/Session/session.ts
index ec51b6e18..6d8ce53c0 100644
--- a/src/server/session.ts
+++ b/src/server/Session/session.ts
@@ -1,25 +1,51 @@
-import { yellow, red, cyan, magenta, green } from "colors";
+import { red, cyan, green, yellow, magenta } from "colors";
import { isMaster, on, fork, setupMaster, Worker } from "cluster";
-import InputManager from "./session_manager/input_manager";
import { execSync } from "child_process";
-import { Email } from "./session_manager/email";
import { get } from "request-promise";
-import { WebSocket } from "./Websocket/Websocket";
-import { Utils } from "../Utils";
-import { MessageStore } from "./Message";
+import { WebSocket } from "../Websocket/Websocket";
+import { Utils } from "../../Utils";
+import { MessageStore } from "../Message";
+import { Email } from "../ActionUtilities";
+import Repl from "../repl";
+import { readFileSync } from "fs";
+import { validate, ValidationError } from "jsonschema";
+import { configurationSchema } from "./session_config_schema";
const onWindows = process.platform === "win32";
-const heartbeat = `http://localhost:1050/serverHeartbeat`;
-const admin = ["samuel_wilkins@brown.edu"];
export namespace Session {
+ const { masterIdentifier, workerIdentifier, recipients, signature, heartbeat, silentChildren } = loadConfiguration();
export let key: string;
- export const signature = "Best,\nServer Session Manager";
let activeWorker: Worker;
let listening = false;
- const masterIdentifier = `${yellow("__master__")}:`;
- const workerIdentifier = `${magenta("__worker__")}:`;
+
+ function loadConfiguration() {
+ try {
+ const raw = readFileSync('./session.config.json', 'utf8');
+ const configuration = JSON.parse(raw);
+ const options = {
+ throwError: true,
+ allowUnknownAttributes: false
+ };
+ validate(configuration, configurationSchema, options);
+ configuration.masterIdentifier = `${yellow(configuration.masterIdentifier)}:`;
+ configuration.workerIdentifier = `${magenta(configuration.workerIdentifier)}:`;
+ return configuration;
+ } catch (error) {
+ console.log(red("\nSession configuration failed."));
+ if (error instanceof ValidationError) {
+ console.log("The given session.config.json configuration file is invalid.");
+ console.log(`${error.instance}: ${error.stack}`);
+ } else if (error.code === "ENOENT" && error.path === "./session.config.json") {
+ console.log("Please include a session.config.json configuration file in your project root.");
+ } else {
+ console.log(error.stack);
+ }
+ console.log();
+ process.exit(0);
+ }
+ }
function log(message?: any, ...optionalParams: any[]) {
const identifier = isMaster ? masterIdentifier : workerIdentifier;
@@ -30,7 +56,7 @@ export namespace Session {
key = Utils.GenerateGuid();
const timestamp = new Date().toUTCString();
const content = `The key for this session (started @ ${timestamp}) is ${key}.\n\n${signature}`;
- return Promise.all(admin.map(recipient => Email.dispatch(recipient, "Server Termination Key", content)));
+ return Promise.all(recipients.map((recipient: string) => Email.dispatch(recipient, "Server Termination Key", content)));
}
function tryKillActiveWorker() {
@@ -64,7 +90,7 @@ export namespace Session {
return;
}
listening = false;
- await Promise.all(admin.map(recipient => Email.dispatch(recipient, "Dash Web Server Crash", crashReport(error))));
+ await Promise.all(recipients.map((recipient: string) => Email.dispatch(recipient, "Dash Web Server Crash", crashReport(error))));
const { _socket } = WebSocket;
if (_socket) {
Utils.Emit(_socket, MessageStore.ConnectionTerminated, "Manual");
@@ -95,7 +121,7 @@ export namespace Session {
}
}
});
- setupMaster({ silent: true });
+ setupMaster({ silent: silentChildren });
const spawn = () => {
tryKillActiveWorker();
activeWorker = fork();
@@ -107,7 +133,7 @@ export namespace Session {
log(cyan(prompt));
spawn();
});
- const { registerCommand } = new InputManager({ identifier: masterIdentifier });
+ const { registerCommand } = new Repl({ identifier: masterIdentifier });
registerCommand("exit", [], () => execSync(onWindows ? "taskkill /f /im node.exe" : "killall -9 node"));
registerCommand("pull", [], () => execSync("git pull", { stdio: ["ignore", "inherit", "inherit"] }));
registerCommand("restart", [], () => {
diff --git a/src/server/Session/session_config_schema.ts b/src/server/Session/session_config_schema.ts
new file mode 100644
index 000000000..25d95c243
--- /dev/null
+++ b/src/server/Session/session_config_schema.ts
@@ -0,0 +1,25 @@
+import { Schema } from "jsonschema";
+
+export const configurationSchema: Schema = {
+ id: "/Configuration",
+ type: "object",
+ properties: {
+ recipients: {
+ type: "array",
+ items: {
+ type: "string",
+ pattern: /[^\@]+\@[^\@]+/g
+ },
+ minLength: 1
+ },
+ heartbeat: {
+ type: "string",
+ pattern: /http\:\/\/localhost:\d+\/[a-zA-Z]+/g
+ },
+ signature: { type: "string" },
+ masterIdentifier: { type: "string", minLength: 1 },
+ workerIdentifier: { type: "string", minLength: 1 },
+ silentChildren: { type: "boolean" }
+ },
+ required: ["heartbeat", "recipients", "signature", "masterIdentifier", "workerIdentifier", "silentChildren"]
+}; \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 8706c2d84..88bab7dea 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 { Logger } from "./ProcessFactory";
import { yellow } from "colors";
-import { Session } from "./session";
+import { Session } from "./Session/session";
import { Utils } from "../Utils";
export const publicDirectory = path.resolve(__dirname, "public");
diff --git a/src/server/session_manager/input_manager.ts b/src/server/repl.ts
index 133b7144a..ec525582b 100644
--- a/src/server/session_manager/input_manager.ts
+++ b/src/server/repl.ts
@@ -13,7 +13,7 @@ export interface Registration {
action: Action;
}
-export default class InputManager {
+export default class Repl {
private identifier: string;
private onInvalid: ((culprit?: string) => string) | string;
private isCaseSensitive: boolean;
diff --git a/src/server/session_manager/config.ts b/src/server/session_manager/config.ts
deleted file mode 100644
index ebbd999c6..000000000
--- a/src/server/session_manager/config.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-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/email.ts b/src/server/session_manager/email.ts
deleted file mode 100644
index a638644db..000000000
--- a/src/server/session_manager/email.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import * as nodemailer from "nodemailer";
-import { MailOptions } from "nodemailer/lib/json-transport";
-
-export namespace Email {
-
- const smtpTransport = nodemailer.createTransport({
- service: 'Gmail',
- auth: {
- user: 'brownptcdash@gmail.com',
- pass: 'browngfx1'
- }
- });
-
- export async function dispatch(recipient: string, subject: string, content: string): Promise<boolean> {
- const mailOptions = {
- to: recipient,
- from: 'brownptcdash@gmail.com',
- subject,
- text: `Hello ${recipient.split("@")[0]},\n\n${content}`
- } as MailOptions;
- return new Promise<boolean>(resolve => {
- smtpTransport.sendMail(mailOptions, (dispatchError: Error | null) => resolve(dispatchError === null));
- });
- }
-
-} \ 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
deleted file mode 100644
index 557e3d7c3..000000000
--- a/src/server/session_manager/logs/current_daemon_pid.log
+++ /dev/null
@@ -1 +0,0 @@
-26860
diff --git a/src/server/session_manager/logs/current_server_pid.log b/src/server/session_manager/logs/current_server_pid.log
deleted file mode 100644
index 85fdb7ae0..000000000
--- a/src/server/session_manager/logs/current_server_pid.log
+++ /dev/null
@@ -1 +0,0 @@
-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
deleted file mode 100644
index 75c23b35a..000000000
--- a/src/server/session_manager/logs/current_session_manager_pid.log
+++ /dev/null
@@ -1 +0,0 @@
-54643
diff --git a/src/server/session_manager/session_manager.ts b/src/server/session_manager/session_manager.ts
deleted file mode 100644
index 97c2ab214..000000000
--- a/src/server/session_manager/session_manager.ts
+++ /dev/null
@@ -1,206 +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 InputManager from "./input_manager";
-import { identifier, logPath, crashPath, onWindows, pid, ports, heartbeat, recipient, latency, SessionState } from "./config";
-const killport = require("kill-port");
-import * as io from "socket.io";
-
-process.on('SIGINT', endPrevious);
-let state: SessionState = SessionState.STARTING;
-const is = (...reference: SessionState[]) => reference.includes(state);
-const set = (reference: SessionState) => state = reference;
-
-const endpoint = io();
-endpoint.on("connection", socket => {
-
-});
-endpoint.listen(process.env.PORT);
-
-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();
- 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
diff --git a/src/server/session_manager/session_manager_cluster.ts b/src/server/session_manager/session_manager_cluster.ts
deleted file mode 100644
index 546465c03..000000000
--- a/src/server/session_manager/session_manager_cluster.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { isMaster, fork, on } from "cluster";
-import { cpus } from "os";
-import { createServer } from "http";
-
-const capacity = cpus().length;
-
-let thrown = false;
-
-if (isMaster) {
- console.log(capacity);
- for (let i = 0; i < capacity; i++) {
- fork();
- }
- on("exit", (worker, code, signal) => {
- console.log(`worker ${worker.process.pid} died with code ${code} and signal ${signal}`);
- fork();
- });
-} else {
- const port = 1234;
- createServer().listen(port, () => {
- console.log('process id local', process.pid);
- console.log(`http server started at port ${port}`);
- if (!thrown) {
- thrown = true;
- setTimeout(() => {
- throw new Error("Hey I'm a fake error!");
- }, 1000);
- }
- });
-}
-
-process.on('uncaughtException', function (err) {
- console.error((new Date).toUTCString() + ' uncaughtException:', err.message);
- console.error(err.stack);
- process.exit(1);
-}); \ No newline at end of file