diff options
author | Sam Wilkins <samwilkins333@gmail.com> | 2020-01-03 23:28:34 -0800 |
---|---|---|
committer | Sam Wilkins <samwilkins333@gmail.com> | 2020-01-03 23:28:34 -0800 |
commit | b31d54b285236dc92f7d287af6a441878f429a34 (patch) | |
tree | 4f2289276b33eb37f5c75b0221cdc046d2967fcd /src | |
parent | 5111eb546d9bcd6070ddbe8076f3389a37cd7081 (diff) |
session restructuring and schema enforced json configuration
Diffstat (limited to 'src')
-rw-r--r-- | src/server/ActionUtilities.ts | 26 | ||||
-rw-r--r-- | src/server/Session/session.ts (renamed from src/server/session.ts) | 56 | ||||
-rw-r--r-- | src/server/Session/session_config_schema.ts | 25 | ||||
-rw-r--r-- | src/server/index.ts | 2 | ||||
-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.ts | 33 | ||||
-rw-r--r-- | src/server/session_manager/email.ts | 26 | ||||
-rw-r--r-- | src/server/session_manager/logs/current_daemon_pid.log | 1 | ||||
-rw-r--r-- | src/server/session_manager/logs/current_server_pid.log | 1 | ||||
-rw-r--r-- | src/server/session_manager/logs/current_session_manager_pid.log | 1 | ||||
-rw-r--r-- | src/server/session_manager/session_manager.ts | 206 | ||||
-rw-r--r-- | src/server/session_manager/session_manager_cluster.ts | 36 |
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 |