aboutsummaryrefslogtreecommitdiff
path: root/src/server/DashSessionAgent.ts
diff options
context:
space:
mode:
authorSam Wilkins <samwilkins333@gmail.com>2020-01-10 06:35:30 -0500
committerSam Wilkins <samwilkins333@gmail.com>2020-01-10 06:35:30 -0500
commita1760ecbb780dc17a7675bd60fb50aa1103fa961 (patch)
tree8cdaae6f81ea00d27183d03f9373fde26ae3ec7d /src/server/DashSessionAgent.ts
parentcf3e869023d3027ae42c828ba3670b77d838ac50 (diff)
added hierarchical structure to session
Diffstat (limited to 'src/server/DashSessionAgent.ts')
-rw-r--r--src/server/DashSessionAgent.ts168
1 files changed, 168 insertions, 0 deletions
diff --git a/src/server/DashSessionAgent.ts b/src/server/DashSessionAgent.ts
new file mode 100644
index 000000000..3073e69c3
--- /dev/null
+++ b/src/server/DashSessionAgent.ts
@@ -0,0 +1,168 @@
+import { Email, pathFromRoot } from "./ActionUtilities";
+import { red, yellow, green, cyan } from "colors";
+import { get } from "request-promise";
+import { Utils } from "../Utils";
+import { WebSocket } from "./Websocket/Websocket";
+import { MessageStore } from "./Message";
+import { launchServer, onWindows } from ".";
+import { existsSync, mkdirSync, readdirSync, statSync, createWriteStream, readFileSync } from "fs";
+import * as Archiver from "archiver";
+import { resolve } from "path";
+import { AppliedSessionAgent, ExitHandler } from "./session/agents/applied_session_agent";
+import { Monitor } from "./session/agents/monitor";
+import { ServerWorker } from "./session/agents/server_worker";
+
+/**
+ * If we're the monitor (master) thread, we should launch the monitor logic for the session.
+ * Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus
+ * our job should be to run the server.
+ */
+export class DashSessionAgent extends AppliedSessionAgent {
+
+ private readonly notificationRecipients = ["samuel_wilkins@brown.edu"];
+ private readonly signature = "-Dash Server Session Manager";
+ private readonly releaseDesktop = pathFromRoot("../../Desktop");
+ private _instructions: string | undefined;
+ private get instructions() {
+ if (!this._instructions) {
+ this._instructions = readFileSync(resolve(__dirname, "./remote_debug_instructions.txt"), { encoding: "utf8" });
+ }
+ return this._instructions;
+ }
+
+ protected async launchMonitor() {
+ const monitor = Monitor.Create(this.notifiers);
+ monitor.addReplCommand("pull", [], () => monitor.exec("git pull"));
+ monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand);
+ monitor.addReplCommand("backup", [], this.backup);
+ monitor.addReplCommand("debug", [/active|passive/, /\S+\@\S+/], async ([mode, recipient]) => this.dispatchZippedDebugBackup(mode, recipient));
+ monitor.addServerMessageListener("backup", this.backup);
+ monitor.addServerMessageListener("debug", ({ args: { mode, recipient } }) => this.dispatchZippedDebugBackup(mode, recipient));
+ return monitor;
+ }
+
+ protected async launchServerWorker() {
+ const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker
+ worker.addExitHandler(this.notifyClient);
+ return worker;
+ }
+
+ private readonly notifiers: Monitor.NotifierHooks = {
+ key: async key => {
+ // this sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
+ // to kill the server via the /kill/:key route
+ const content = `The key for this session (started @ ${new Date().toUTCString()}) is ${key}.\n\n${this.signature}`;
+ const failures = await Email.dispatchAll({
+ to: this.notificationRecipients,
+ subject: "Dash Release Session Admin Authentication Key",
+ content
+ });
+ if (failures) {
+ failures.map(({ recipient, error: { message } }) => this.sessionMonitor.mainLog(red(`dispatch failure @ ${recipient} (${yellow(message)})`)));
+ return false;
+ }
+ return true;
+ },
+ crash: async ({ name, message, stack }) => {
+ const body = [
+ "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.",
+ ].join("\n\n");
+ const content = `${body}\n\n${this.signature}`;
+ const failures = await Email.dispatchAll({
+ to: this.notificationRecipients,
+ subject: "Dash Web Server Crash",
+ content
+ });
+ if (failures) {
+ failures.map(({ recipient, error: { message } }) => this.sessionMonitor.mainLog(red(`dispatch failure @ ${recipient} (${yellow(message)})`)));
+ return false;
+ }
+ return true;
+ }
+ };
+
+ private executeSolrCommand = async (args: string[]) => {
+ const { exec, mainLog } = this.sessionMonitor;
+ const action = args[0];
+ if (action === "index") {
+ exec("npx ts-node ./updateSearch.ts", { cwd: pathFromRoot("./src/server") });
+ } else {
+ const command = `${onWindows ? "solr.cmd" : "solr"} ${args[0] === "start" ? "start" : "stop -p 8983"}`;
+ await exec(command, { cwd: "./solr-8.3.1/bin" });
+ try {
+ await get("http://localhost:8983");
+ mainLog(green("successfully connected to 8983 after running solr initialization"));
+ } catch {
+ mainLog(red("unable to connect at 8983 after running solr initialization"));
+ }
+ }
+ }
+
+ private notifyClient: ExitHandler = reason => {
+ const { _socket } = WebSocket;
+ if (_socket) {
+ const message = typeof reason === "boolean" ? (reason ? "exit" : "temporary") : "crash";
+ Utils.Emit(_socket, MessageStore.ConnectionTerminated, message);
+ }
+ }
+
+ private backup = async () => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop });
+
+ private async dispatchZippedDebugBackup(mode: string, to: string) {
+ const { mainLog } = this.sessionMonitor;
+ try {
+ // if desired, complete an immediate backup to send
+ if (mode === "active") {
+ await this.backup();
+ mainLog("backup complete");
+ }
+
+ // ensure the directory for compressed backups exists
+ const backupsDirectory = `${this.releaseDesktop}/backups`;
+ const compressedDirectory = `${this.releaseDesktop}/compressed`;
+ if (!existsSync(compressedDirectory)) {
+ mkdirSync(compressedDirectory);
+ }
+
+ // sort all backups by their modified time, and choose the most recent one
+ const target = readdirSync(backupsDirectory).map(filename => ({
+ modifiedTime: statSync(`${backupsDirectory}/${filename}`).mtimeMs,
+ filename
+ })).sort((a, b) => b.modifiedTime - a.modifiedTime)[0].filename;
+ mainLog(`targeting ${target}...`);
+
+ // create a zip file and to it, write the contents of the backup directory
+ const zipName = `${target}.zip`;
+ const zipPath = `${compressedDirectory}/${zipName}`;
+ const output = createWriteStream(zipPath);
+ const zip = Archiver('zip');
+ zip.pipe(output);
+ zip.directory(`${backupsDirectory}/${target}/Dash`, false);
+ await zip.finalize();
+ mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`);
+
+ // dispatch the email to the recipient, containing the finalized zip file
+ const error = await Email.dispatch({
+ to,
+ subject: `Remote debug: compressed backup of ${target}...`,
+ content: this.instructions // prepare the body of the email with instructions on restoring the local database
+ .replace(/__zipname__/, zipName)
+ .replace(/__target__/, target)
+ .replace(/__signature__/, this.signature),
+ attachments: [{ filename: zipName, path: zipPath }]
+ });
+
+ // indicate success or failure
+ mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`);
+ error && mainLog(red(error.message));
+ } catch (error) {
+ mainLog(red("unable to dispatch zipped backup..."));
+ mainLog(red(error.message));
+ }
+ }
+
+} \ No newline at end of file