aboutsummaryrefslogtreecommitdiff
path: root/src/server/DashSession.ts
blob: 56610874e5cd2c39c6f5d176488f240b6df84e57 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { Session } from "./Session/session";
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";

/**
 * 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 Session.AppliedSessionAgent {

    private readonly notificationRecipients = ["samuel_wilkins@brown.edu"];
    private readonly signature = "-Dash Server Session Manager";
    private readonly releaseDesktop = pathFromRoot("../../Desktop");

    protected async launchMonitor() {
        const monitor = Session.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 = Session.ServerWorker.Create(launchServer); // server initialization delegated to worker
        worker.addExitHandler(this.notifyClient);
        return worker;
    }

    private readonly notifiers: Session.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(this.notificationRecipients, "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(this.notificationRecipients, "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: Session.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, recipient: string) {
        const { mainLog } = this.sessionMonitor;
        try {
            if (mode === "active") {
                await this.backup();
            }
            mainLog("backup complete");
            const backupsDirectory = `${this.releaseDesktop}/backups`;
            const compressedDirectory = `${this.releaseDesktop}/compressed`;
            if (!existsSync(compressedDirectory)) {
                mkdirSync(compressedDirectory);
            }
            const target = readdirSync(backupsDirectory).map(filename => ({
                modifiedTime: statSync(`${backupsDirectory}/${filename}`).mtimeMs,
                filename
            })).sort((a, b) => b.modifiedTime - a.modifiedTime)[0].filename;
            mainLog(`targeting ${target}...`);
            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}`);
            let instructions = readFileSync(resolve(__dirname, "./remote_debug_instructions.txt"), { encoding: "utf8" });
            instructions = instructions.replace(/__zipname__/, zipName).replace(/__target__/, target).replace(/__signature__/, this.signature);
            const error = await Email.dispatch(recipient, `Compressed backup of ${target}...`, instructions, [
                {
                    filename: zipName,
                    path: zipPath
                }
            ]);
            mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(recipient)}`);
            error && mainLog(red(error.message));
        } catch (error) {
            mainLog(red("unable to dispatch zipped backup..."));
            mainLog(red(error.message));
        }
    }

}