aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/server/ActionUtilities.ts24
-rw-r--r--src/server/ApiManagers/SessionManager.ts22
-rw-r--r--src/server/DashSession.ts53
-rw-r--r--src/server/Session/session.ts79
4 files changed, 137 insertions, 41 deletions
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts
index f0bfbc525..60f66c878 100644
--- a/src/server/ActionUtilities.ts
+++ b/src/server/ActionUtilities.ts
@@ -119,16 +119,24 @@ export namespace Email {
}
});
+ export interface DispatchOptions<T extends string | string[]> {
+ to: T;
+ subject: string;
+ content: string;
+ attachments?: Mail.Attachment | Mail.Attachment[];
+ }
+
export interface DispatchFailure {
recipient: string;
error: Error;
}
- export async function dispatchAll(recipients: string[], subject: string, content: string) {
+ export async function dispatchAll({ to, subject, content, attachments }: DispatchOptions<string[]>) {
const failures: DispatchFailure[] = [];
- await Promise.all(recipients.map(async (recipient: string) => {
+ await Promise.all(to.map(async recipient => {
let error: Error | null;
- if ((error = await Email.dispatch(recipient, subject, content)) !== null) {
+ const resolved = attachments ? "length" in attachments ? attachments : [attachments] : undefined;
+ if ((error = await Email.dispatch({ to: recipient, subject, content, attachments: resolved })) !== null) {
failures.push({
recipient,
error
@@ -138,17 +146,15 @@ export namespace Email {
return failures.length ? failures : undefined;
}
- export async function dispatch(recipient: string, subject: string, content: string, attachments?: Mail.Attachment[]): Promise<Error | null> {
+ export async function dispatch({ to, subject, content, attachments }: DispatchOptions<string>): Promise<Error | null> {
const mailOptions = {
- to: recipient,
+ to,
from: 'brownptcdash@gmail.com',
subject,
- text: `Hello ${recipient.split("@")[0]},\n\n${content}`,
+ text: `Hello ${to.split("@")[0]},\n\n${content}`,
attachments
} as MailOptions;
- return new Promise<Error | null>(resolve => {
- smtpTransport.sendMail(mailOptions, resolve);
- });
+ return new Promise<Error | null>(resolve => smtpTransport.sendMail(mailOptions, resolve));
}
} \ No newline at end of file
diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts
index 0290b578c..6782643bc 100644
--- a/src/server/ApiManagers/SessionManager.ts
+++ b/src/server/ApiManagers/SessionManager.ts
@@ -19,7 +19,7 @@ export default class SessionManager extends ApiManager {
if (password !== process.env.session_key) {
return _permission_denied(res, permissionError);
}
- handler(core);
+ return handler(core);
};
}
@@ -28,11 +28,15 @@ export default class SessionManager extends ApiManager {
register({
method: Method.GET,
subscription: this.secureSubscriber("debug", "mode", "recipient"),
- secureHandler: this.authorizedAction(({ req, res }) => {
+ secureHandler: this.authorizedAction(async ({ req, res }) => {
const { mode, recipient } = req.params;
if (["passive", "active"].includes(mode)) {
- sessionAgent.serverWorker.sendMonitorAction("debug", { mode, recipient });
- res.send(`Your request was successful: the server is ${mode === "active" ? "creating and compressing a new" : "retrieving and compressing the most recent"} back up. It will be sent to ${recipient}.`);
+ const response = await sessionAgent.serverWorker.sendMonitorAction("debug", { mode, recipient }, true);
+ if (response instanceof Error) {
+ res.send(response);
+ } else {
+ res.send(`Your request was successful: the server ${mode === "active" ? "created and compressed a new" : "retrieved and compressed the most recent"} back up. It was sent to ${recipient}.`);
+ }
} else {
res.send(`Your request failed. '${mode}' is not a valid mode: please choose either 'active' or 'passive'`);
}
@@ -42,9 +46,13 @@ export default class SessionManager extends ApiManager {
register({
method: Method.GET,
subscription: this.secureSubscriber("backup"),
- secureHandler: this.authorizedAction(({ res }) => {
- sessionAgent.serverWorker.sendMonitorAction("backup");
- res.send(`Your request was successful: the server is creating a new back up.`);
+ secureHandler: this.authorizedAction(async ({ res }) => {
+ const response = await sessionAgent.serverWorker.sendMonitorAction("backup");
+ if (response instanceof Error) {
+ res.send(response);
+ } else {
+ res.send("Your request was successful: the server successfully created a new back up.");
+ }
})
});
diff --git a/src/server/DashSession.ts b/src/server/DashSession.ts
index 56610874e..b8686a5e9 100644
--- a/src/server/DashSession.ts
+++ b/src/server/DashSession.ts
@@ -20,6 +20,13 @@ 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");
+ 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 = Session.Monitor.Create(this.notifiers);
@@ -43,7 +50,11 @@ export class DashSessionAgent extends Session.AppliedSessionAgent {
// 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);
+ 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;
@@ -59,7 +70,11 @@ export class DashSessionAgent extends Session.AppliedSessionAgent {
"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);
+ 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;
@@ -95,23 +110,30 @@ export class DashSessionAgent extends Session.AppliedSessionAgent {
private backup = async () => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop });
- private async dispatchZippedDebugBackup(mode: string, recipient: string) {
+ 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");
}
- 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);
@@ -120,15 +142,20 @@ export class DashSessionAgent extends Session.AppliedSessionAgent {
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)}`);
+
+ // 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..."));
diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts
index d46e6b6e7..7b194598b 100644
--- a/src/server/Session/session.ts
+++ b/src/server/Session/session.ts
@@ -133,6 +133,56 @@ export namespace Session {
export type ExitHandler = (reason: Error | boolean) => void | Promise<void>;
+ namespace IPC {
+
+ export const suffix = isMaster ? Utils.GenerateGuid() : process.env.ipc_suffix;
+ const ipc_id = `ipc_id_${suffix}`;
+ const response_expected = `response_expected_${suffix}`;
+ const is_response = `is_response_${suffix}`;
+
+ export async function dispatchMessage(target: NodeJS.EventEmitter & { send?: Function }, message: any, expectResponse = false): Promise<Error | undefined> {
+ if (!target.send) {
+ return new Error("Cannot dispatch when send is undefined.");
+ }
+ message[response_expected] = expectResponse;
+ if (expectResponse) {
+ return new Promise(resolve => {
+ const messageId = Utils.GenerateGuid();
+ message[ipc_id] = messageId;
+ const responseHandler: (args: any) => void = response => {
+ const { error } = response;
+ if (response[is_response] && response[ipc_id] === messageId) {
+ target.removeListener("message", responseHandler);
+ resolve(error);
+ }
+ };
+ target.addListener("message", responseHandler);
+ target.send!(message);
+ });
+ } else {
+ target.send(message);
+ }
+ }
+
+ export function addMessagesHandler(target: NodeJS.EventEmitter & { send?: Function }, handler: (message: any) => void | Promise<void>): void {
+ target.addListener("message", async incoming => {
+ let error: Error | undefined;
+ try {
+ await handler(incoming);
+ } catch (e) {
+ error = e;
+ }
+ if (incoming[response_expected] && target.send) {
+ const response: any = { error };
+ response[ipc_id] = incoming[ipc_id];
+ response[is_response] = true;
+ target.send(response);
+ }
+ });
+ }
+
+ }
+
export namespace Monitor {
export interface NotifierHooks {
@@ -166,7 +216,7 @@ export namespace Session {
public static Create(notifiers?: Monitor.NotifierHooks) {
if (isWorker) {
- process.send?.({
+ IPC.dispatchMessage(process, {
action: {
message: "kill",
args: {
@@ -418,15 +468,15 @@ export namespace Session {
repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0));
repl.registerCommand("restart", [/clean|force/], args => this.killActiveWorker(args[0] === "clean"));
repl.registerCommand("set", [letters, "port", number, boolean], args => this.setPort(args[0], Number(args[2]), args[3] === "true"));
- repl.registerCommand("set", [/polling/, number, boolean], args => {
- const newPollingIntervalSeconds = Math.floor(Number(args[2]));
+ repl.registerCommand("set", [/polling/, number, boolean], async args => {
+ const newPollingIntervalSeconds = Math.floor(Number(args[1]));
if (newPollingIntervalSeconds < 0) {
this.mainLog(red("the polling interval must be a non-negative integer"));
} else {
if (newPollingIntervalSeconds !== this.config.polling.intervalSeconds) {
this.config.polling.intervalSeconds = newPollingIntervalSeconds;
- if (args[3] === "true") {
- this.activeWorker?.send({ newPollingIntervalSeconds });
+ if (args[2] === "true") {
+ return IPC.dispatchMessage(this.activeWorker!, { newPollingIntervalSeconds }, true);
}
}
}
@@ -442,7 +492,7 @@ export namespace Session {
private killActiveWorker = (graceful = true, isSessionEnd = false): void => {
if (this.activeWorker && !this.activeWorker.isDead()) {
if (graceful) {
- this.activeWorker.send({ manualExit: { isSessionEnd } });
+ IPC.dispatchMessage(this.activeWorker, { manualExit: { isSessionEnd } });
} else {
this.activeWorker.process.kill();
}
@@ -487,11 +537,12 @@ export namespace Session {
serverPort: ports.server,
socketPort: ports.socket,
pollingIntervalSeconds: intervalSeconds,
- session_key: this.key
+ session_key: this.key,
+ ipc_suffix: IPC.suffix
});
this.mainLog(cyan(`spawned new server worker with process id ${this.activeWorker.process.pid}`));
// an IPC message handler that executes actions on the master thread when prompted by the active worker
- this.activeWorker.on("message", async ({ lifecycle, action }) => {
+ IPC.addMessagesHandler(this.activeWorker, async ({ lifecycle, action }) => {
if (action) {
const { message, args } = action as Monitor.Action;
console.log(this.timestamp(), `${this.config.identifiers.worker.text} action requested (${cyan(message)})`);
@@ -547,7 +598,7 @@ export namespace Session {
console.error(red("cannot create a worker on the monitor process."));
process.exit(1);
} else if (++ServerWorker.count > 1) {
- process.send?.({
+ IPC.dispatchMessage(process, {
action: {
message: "kill", args: {
reason: "cannot create more than one worker on a given worker process.",
@@ -579,7 +630,7 @@ export namespace Session {
* A convenience wrapper to tell the session monitor (parent process)
* to carry out the action with the specified message and arguments.
*/
- public sendMonitorAction = (message: string, args?: any) => process.send!({ action: { message, args } });
+ public sendMonitorAction = (message: string, args?: any, expectResponse = false) => IPC.dispatchMessage(process, { action: { message, args } }, expectResponse);
private constructor(work: Function) {
this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(" ")}]`)}`));
@@ -601,8 +652,11 @@ export namespace Session {
*/
private configureProcess = () => {
// updates the local values of variables to the those sent from master
- process.on("message", async ({ newPollingIntervalSeconds, manualExit }) => {
+ IPC.addMessagesHandler(process, async ({ newPollingIntervalSeconds, manualExit }) => {
if (newPollingIntervalSeconds !== undefined) {
+ await new Promise<void>(resolve => {
+ setTimeout(resolve, 1000 * 10);
+ });
this.pollingIntervalSeconds = newPollingIntervalSeconds;
}
if (manualExit !== undefined) {
@@ -629,7 +683,7 @@ export namespace Session {
/**
* Notify master thread (which will log update in the console) of initialization via IPC.
*/
- public lifecycleNotification = (event: string) => process.send?.({ lifecycle: event });
+ public lifecycleNotification = (event: string) => IPC.dispatchMessage(process, { lifecycle: event });
/**
* Called whenever the process has a reason to terminate, either through an uncaught exception
@@ -643,6 +697,7 @@ export namespace Session {
// notify master thread (which will log update in the console) of crash event via IPC
this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`));
this.lifecycleNotification(red(error.message));
+ console.log("GAH!", error);
process.exit(1);
}