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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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));
}
}
}
|