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/server/Session/session.ts | |
parent | 5111eb546d9bcd6070ddbe8076f3389a37cd7081 (diff) |
session restructuring and schema enforced json configuration
Diffstat (limited to 'src/server/Session/session.ts')
-rw-r--r-- | src/server/Session/session.ts | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts new file mode 100644 index 000000000..6d8ce53c0 --- /dev/null +++ b/src/server/Session/session.ts @@ -0,0 +1,168 @@ +import { red, cyan, green, yellow, magenta } from "colors"; +import { isMaster, on, fork, setupMaster, Worker } from "cluster"; +import { execSync } from "child_process"; +import { get } from "request-promise"; +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"; + +export namespace Session { + + const { masterIdentifier, workerIdentifier, recipients, signature, heartbeat, silentChildren } = loadConfiguration(); + export let key: string; + let activeWorker: Worker; + let listening = false; + + 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; + console.log(identifier, message, ...optionalParams); + } + + export async function distributeKey() { + 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(recipients.map((recipient: string) => Email.dispatch(recipient, "Server Termination Key", content))); + } + + function tryKillActiveWorker() { + if (activeWorker && !activeWorker.isDead()) { + activeWorker.process.kill(); + return true; + } + return false; + } + + function logLifecycleEvent(lifecycle: string) { + process.send?.({ lifecycle }); + } + + function messageHandler({ lifecycle, action }: any) { + if (action) { + console.log(`${workerIdentifier} action requested (${action})`); + switch (action) { + case "kill": + log(red("An authorized user has ended the server from the /kill route")); + tryKillActiveWorker(); + process.exit(0); + } + } else if (lifecycle) { + console.log(`${workerIdentifier} lifecycle phase (${lifecycle})`); + } + } + + async function activeExit(error: Error) { + if (!listening) { + return; + } + listening = false; + 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"); + } + logLifecycleEvent(red(`Crash event detected @ ${new Date().toUTCString()}`)); + logLifecycleEvent(red(error.message)); + process.exit(1); + } + + function crashReport({ name, message, stack }: Error) { + return [ + "You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:", + `name:\n${name}`, + `message:\n${message}`, + `stack:\n${stack}`, + "The server is already restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress.", + signature + ].join("\n\n"); + } + + export async function initialize(work: Function) { + if (isMaster) { + process.on("uncaughtException", error => { + if (error.message !== "Channel closed") { + log(red(error.message)); + if (error.stack) { + log(`\n${red(error.stack)}`); + } + } + }); + setupMaster({ silent: silentChildren }); + const spawn = () => { + tryKillActiveWorker(); + activeWorker = fork(); + activeWorker.on("message", messageHandler); + }; + spawn(); + on("exit", ({ process: { pid } }, code, signal) => { + const prompt = `Server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`; + log(cyan(prompt)); + spawn(); + }); + 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", [], () => { + listening = false; + tryKillActiveWorker(); + }); + } else { + logLifecycleEvent(green("initializing...")); + process.on('uncaughtException', activeExit); + const checkHeartbeat = async () => { + await new Promise<void>(resolve => { + setTimeout(async () => { + try { + await get(heartbeat); + if (!listening) { + logLifecycleEvent(green("listening...")); + } + listening = true; + resolve(); + } catch (error) { + await activeExit(error); + } + }, 1000 * 15); + }); + checkHeartbeat(); + }; + work(); + checkHeartbeat(); + } + } + +}
\ No newline at end of file |