From e08598d32fdfdcbd12532facf05b7119bd780a19 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 8 Jan 2020 12:31:01 -0500 Subject: solr manager fixes, regex --- src/server/ApiManagers/SearchManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/server/ApiManagers') diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index c1c908088..cb30c9552 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -73,7 +73,7 @@ export namespace SolrManager { const args = status ? "start" : "stop -p 8983"; try { console.log(`Solr management: trying to ${args}`); - console.log(execSync(`./solr.cmd ${args}`, { cwd: "./solr-8.3.1/bin" })); + console.log(execSync(`${process.platform === "win32" ? "solr.cmd" : "solr"} ${args}`, { cwd: "./solr-8.3.1/bin" }).toString()); return true; } catch (e) { console.log(red(`Solr management error: unable to ${args}`)); -- cgit v1.2.3-70-g09d2 From 19a71cb2788b9c1c8d8ced4af285bf91033ba626 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 8 Jan 2020 12:47:47 -0500 Subject: solr manager async rather than sync exec --- src/server/ApiManagers/SearchManager.ts | 26 +++++++++++++------------- src/server/index.ts | 1 + 2 files changed, 14 insertions(+), 13 deletions(-) (limited to 'src/server/ApiManagers') diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index cb30c9552..316ba09ed 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -4,11 +4,10 @@ import { Search } from "../Search"; const findInFiles = require('find-in-files'); import * as path from 'path'; import { pathToDirectory, Directory } from "./UploadManager"; -import { command_line } from "../ActionUtilities"; -import request = require('request-promise'); -import { red } from "colors"; +import { red, cyan, yellow } from "colors"; import RouteSubscriber from "../RouteSubscriber"; -import { execSync } from "child_process"; +import { exec } from "child_process"; +import { onWindows } from ".."; export class SearchManager extends ApiManager { @@ -71,15 +70,16 @@ export namespace SolrManager { export async function SetRunning(status: boolean): Promise { const args = status ? "start" : "stop -p 8983"; - try { - console.log(`Solr management: trying to ${args}`); - console.log(execSync(`${process.platform === "win32" ? "solr.cmd" : "solr"} ${args}`, { cwd: "./solr-8.3.1/bin" }).toString()); - return true; - } catch (e) { - console.log(red(`Solr management error: unable to ${args}`)); - console.log(e); - return false; - } + console.log(`Solr management: trying to ${args}`); + exec(`${onWindows ? "solr.cmd" : "solr"} ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => { + if (error) { + console.log(red(error.message)); + console.log(red(`Solr management error: unable to ${args}`)); + } + console.log(cyan(stdout)); + console.log(yellow(stderr)); + }); + return true; } } \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 75e6a77d5..6b3dfd614 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -25,6 +25,7 @@ import { yellow, red } from "colors"; import { Session } from "./Session/session"; import { DashSessionAgent } from "./DashSession"; +export const onWindows = process.platform === "win32"; export let sessionAgent: Session.AppliedSessionAgent; export const publicDirectory = path.resolve(__dirname, "public"); export const filesDirectory = path.resolve(publicDirectory, "files"); -- cgit v1.2.3-70-g09d2 From d8361df45515c9724dcf0400a2d9484118b4cd71 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 8 Jan 2020 22:04:58 -0500 Subject: configuration assignment improvements, exec log and more granularity for identifiers --- session.config.json | 8 +- .../solr/dash/data/tlog/tlog.0000000000000000014 | Bin 0 -> 56466 bytes src/server/ApiManagers/SearchManager.ts | 16 +- src/server/DashSession.ts | 13 +- src/server/Session/session.ts | 201 ++++++++++++++------- src/server/Session/session_config_schema.ts | 72 +++++--- 6 files changed, 208 insertions(+), 102 deletions(-) create mode 100644 solr-8.3.1/server/solr/dash/data/tlog/tlog.0000000000000000014 (limited to 'src/server/ApiManagers') diff --git a/session.config.json b/session.config.json index 57ca9e3cc..f613dd904 100644 --- a/session.config.json +++ b/session.config.json @@ -4,7 +4,9 @@ "server": 1050, "socket": 4321 }, - "pollingRoute": "/serverHeartbeat", - "pollingIntervalSeconds": 15, - "pollingFailureTolerance": 0 + "polling": { + "route": "/serverHeartbeat", + "intervalSeconds": 15, + "failureTolerance": 0 + } } \ No newline at end of file diff --git a/solr-8.3.1/server/solr/dash/data/tlog/tlog.0000000000000000014 b/solr-8.3.1/server/solr/dash/data/tlog/tlog.0000000000000000014 new file mode 100644 index 000000000..e39ac337f Binary files /dev/null and b/solr-8.3.1/server/solr/dash/data/tlog/tlog.0000000000000000014 differ diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index 316ba09ed..4ce12f9f3 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -8,6 +8,7 @@ import { red, cyan, yellow } from "colors"; import RouteSubscriber from "../RouteSubscriber"; import { exec } from "child_process"; import { onWindows } from ".."; +import { get } from "request-promise"; export class SearchManager extends ApiManager { @@ -68,18 +69,25 @@ export class SearchManager extends ApiManager { export namespace SolrManager { + const command = onWindows ? "solr.cmd" : "solr"; + export async function SetRunning(status: boolean): Promise { const args = status ? "start" : "stop -p 8983"; - console.log(`Solr management: trying to ${args}`); - exec(`${onWindows ? "solr.cmd" : "solr"} ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => { + console.log(`solr management: trying to ${args}`); + exec(`${command} ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => { if (error) { + console.log(red(`solr management error: unable to ${args} server`)); console.log(red(error.message)); - console.log(red(`Solr management error: unable to ${args}`)); } console.log(cyan(stdout)); console.log(yellow(stderr)); }); - return true; + try { + await get("http://localhost:8983"); + return true; + } catch { + return false; + } } } \ No newline at end of file diff --git a/src/server/DashSession.ts b/src/server/DashSession.ts index 83ce7caaf..47a63c64f 100644 --- a/src/server/DashSession.ts +++ b/src/server/DashSession.ts @@ -1,8 +1,8 @@ import { Session } from "./Session/session"; import { Email } from "./ActionUtilities"; -import { red, yellow } from "colors"; +import { red, yellow, cyan } from "colors"; import { SolrManager } from "./ApiManagers/SearchManager"; -import { execSync } from "child_process"; +import { exec } from "child_process"; import { Utils } from "../Utils"; import { WebSocket } from "./Websocket/Websocket"; import { MessageStore } from "./Message"; @@ -48,7 +48,14 @@ export class DashSessionAgent extends Session.AppliedSessionAgent { return true; } }); - monitor.addReplCommand("pull", [], () => execSync("git pull", { stdio: ["ignore", "inherit", "inherit"] })); + monitor.addReplCommand("pull", [], () => exec("git pull", (error, stdout, stderr) => { + if (error) { + monitor.log(red("unable to pull from version control")); + monitor.log(red(error.message)); + } + stdout.split("\n").forEach(line => line.length && monitor.execLog(cyan(line))); + stderr.split("\n").forEach(line => line.length && monitor.execLog(yellow(line))); + })); monitor.addReplCommand("solr", [/start|stop/], args => SolrManager.SetRunning(args[0] === "start")); return monitor; } diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts index 06a076ae4..9a222b2eb 100644 --- a/src/server/Session/session.ts +++ b/src/server/Session/session.ts @@ -1,4 +1,4 @@ -import { red, cyan, green, yellow, magenta, blue, white } from "colors"; +import { red, cyan, green, yellow, magenta, blue, white, Color, grey, gray, black } from "colors"; import { on, fork, setupMaster, Worker, isMaster, isWorker } from "cluster"; import { get } from "request-promise"; import { Utils } from "../../Utils"; @@ -20,6 +20,20 @@ import { configurationSchema } from "./session_config_schema"; */ export namespace Session { + type ColorLabel = "yellow" | "red" | "cyan" | "green" | "blue" | "magenta" | "grey" | "gray" | "white" | "black"; + const colorMapping: Map = new Map([ + ["yellow", yellow], + ["red", red], + ["cyan", cyan], + ["green", green], + ["blue", blue], + ["magenta", magenta], + ["grey", grey], + ["gray", gray], + ["white", white], + ["black", black] + ]); + export abstract class AppliedSessionAgent { // the following two methods allow the developer to create a custom @@ -70,25 +84,50 @@ export namespace Session { } + interface Identifier { + text: string; + color: ColorLabel; + } + + interface Identifiers { + master: Identifier; + worker: Identifier; + exec: Identifier; + } + interface Configuration { showServerOutput: boolean; - masterIdentifier: string; - workerIdentifier: string; + identifiers: Identifiers; ports: { [description: string]: number }; - pollingRoute: string; - pollingIntervalSeconds: number; - pollingFailureTolerance: number; - [key: string]: any; + polling: { + route: string; + intervalSeconds: number; + failureTolerance: number; + }; } - const defaultConfiguration: Configuration = { + const defaultConfig: Configuration = { showServerOutput: false, - masterIdentifier: yellow("__monitor__:"), - workerIdentifier: magenta("__server__:"), + identifiers: { + master: { + text: "__monitor__", + color: "yellow" + }, + worker: { + text: "__server__", + color: "magenta" + }, + exec: { + text: "__exec__", + color: "green" + } + }, ports: { server: 3000 }, - pollingRoute: "/", - pollingIntervalSeconds: 30, - pollingFailureTolerance: 0 + polling: { + route: "/", + intervalSeconds: 30, + failureTolerance: 0 + } }; export type ExitHandler = (reason: Error | null) => void | Promise; @@ -118,7 +157,7 @@ export namespace Session { private static count = 0; private exitHandlers: ExitHandler[] = []; private readonly notifiers: Monitor.NotifierHooks | undefined; - private readonly configuration: Configuration; + private readonly config: Configuration; private onMessage: { [message: string]: Monitor.ServerMessageHandler[] | undefined } = {}; private activeWorker: Worker | undefined; private key: string | undefined; @@ -209,10 +248,11 @@ export namespace Session { console.log(this.timestamp(), cyan("initializing session...")); - this.configuration = this.loadAndValidateConfiguration(); + this.config = this.loadAndValidateConfiguration(); + this.initializeSessionKey(); // determines whether or not we see the compilation / initialization / runtime output of each child server process - setupMaster({ silent: !this.configuration.showServerOutput }); + setupMaster({ silent: !this.config.showServerOutput }); // handle exceptions in the master thread - there shouldn't be many of these // the IPC (inter process communication) channel closed exception can't seem @@ -238,7 +278,6 @@ export namespace Session { this.spawn(); } - /** * Generates a blue UTC string associated with the time * of invocation. @@ -249,7 +288,14 @@ export namespace Session { * A formatted, identified and timestamped log in color */ public log = (...optionalParams: any[]) => { - console.log(this.timestamp(), this.configuration.masterIdentifier, ...optionalParams); + console.log(this.timestamp(), this.config.identifiers.master.text, ...optionalParams); + } + + /** + * A formatted, identified and timestamped log in color for non- + */ + public execLog = (...optionalParams: any[]) => { + console.log(this.timestamp(), this.config.identifiers.exec.text, ...optionalParams); } /** @@ -269,30 +315,24 @@ export namespace Session { } /** - * Builds the repl that allows the following commands to be typed into stdin of the master thread. + * At any arbitrary layer of nesting within the configuration objects, any single value that + * is not specified by the configuration is given the default counterpart. If, within an object, + * one peer is given by configuration and two are not, the one is preserved while the two are given + * the default value. */ - private initializeRepl = (): Repl => { - const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.configuration.masterIdentifier}` }); - const boolean = /true|false/; - const number = /\d+/; - const letters = /[a-zA-Z]+/; - repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0)); - repl.registerCommand("restart", [/clean|force/], args => this.tryKillActiveWorker(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])); - if (newPollingIntervalSeconds < 0) { - this.log(red("the polling interval must be a non-negative integer")); - } else { - if (newPollingIntervalSeconds !== this.configuration.pollingIntervalSeconds) { - this.configuration.pollingIntervalSeconds = newPollingIntervalSeconds; - if (args[3] === "true") { - this.activeWorker?.send({ newPollingIntervalSeconds }); - } + private assign = (defaultObject: any, specifiedObject: any, collector: any) => { + Array.from(new Set([...Object.keys(defaultObject), ...Object.keys(specifiedObject)])).map(property => { + let defaultValue: any, specifiedValue: any; + if (specifiedValue = specifiedObject[property]) { + if (typeof specifiedValue === "object" && typeof (defaultValue = defaultObject[property]) === "object") { + this.assign(defaultValue, specifiedValue, collector[property] = {}); + } else { + collector[property] = specifiedValue; } + } else { + collector[property] = defaultObject[property]; } }); - return repl; } /** @@ -300,34 +340,19 @@ export namespace Session { * and pass down any variables the pertinent to the child processes as environment variables. */ private loadAndValidateConfiguration = (): Configuration => { + let config: Configuration; try { console.log(this.timestamp(), cyan("validating configuration...")); - const configuration: Configuration = JSON.parse(readFileSync('./session.config.json', 'utf8')); + config = JSON.parse(readFileSync('./session.config.json', 'utf8')); const options = { throwError: true, allowUnknownAttributes: false }; // ensure all necessary and no excess information is specified by the configuration file - validate(configuration, configurationSchema, options); - let formatMaster = true; - let formatWorker = true; - Object.keys(defaultConfiguration).forEach(property => { - if (!configuration[property]) { - if (property === "masterIdentifier") { - formatMaster = false; - } else if (property === "workerIdentifier") { - formatWorker = false; - } - configuration[property] = defaultConfiguration[property]; - } - }); - if (formatMaster) { - configuration.masterIdentifier = yellow(configuration.masterIdentifier + ":"); - } - if (formatWorker) { - configuration.workerIdentifier = magenta(configuration.workerIdentifier + ":"); - } - return configuration; + validate(config, configurationSchema, options); + const results: any = {}; + this.assign(defaultConfig, config, results); + config = results; } catch (error) { if (error instanceof ValidationError) { console.log(red("\nSession configuration failed.")); @@ -337,16 +362,50 @@ export namespace Session { } else if (error.code === "ENOENT" && error.path === "./session.config.json") { console.log(cyan("Loading default session parameters...")); console.log("Consider including a session.config.json configuration file in your project root for customization."); - return defaultConfiguration; + config = { ...defaultConfig }; } else { console.log(red("\nSession configuration failed.")); console.log("The following unknown error occurred during configuration."); console.log(error.stack); process.exit(0); } + } finally { + const { identifiers } = config!; + Object.keys(identifiers).forEach(key => { + const resolved = key as keyof Identifiers; + const { text, color } = identifiers[resolved]; + identifiers[resolved].text = (colorMapping.get(color) || white)(`${text}:`); + }); + return config!; } } + /** + * Builds the repl that allows the following commands to be typed into stdin of the master thread. + */ + private initializeRepl = (): Repl => { + const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.config.identifiers.master.text}` }); + const boolean = /true|false/; + const number = /\d+/; + const letters = /[a-zA-Z]+/; + repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0)); + repl.registerCommand("restart", [/clean|force/], args => this.tryKillActiveWorker(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])); + if (newPollingIntervalSeconds < 0) { + this.log(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 }); + } + } + } + }); + return repl; + } private executeExitHandlers = async (reason: Error | null) => Promise.all(this.exitHandlers.map(handler => handler(reason))); @@ -374,7 +433,7 @@ export namespace Session { */ private setPort = (port: "server" | "socket" | string, value: number, immediateRestart: boolean): void => { if (value > 1023 && value < 65536) { - this.configuration.ports[port] = value; + this.config.ports[port] = value; if (immediateRestart) { this.tryKillActiveWorker(); } @@ -389,18 +448,20 @@ export namespace Session { */ private spawn = (): void => { const { - pollingRoute, - pollingFailureTolerance, - pollingIntervalSeconds, + polling: { + route, + failureTolerance, + intervalSeconds + }, ports - } = this.configuration; + } = this.config; this.tryKillActiveWorker(); this.activeWorker = fork({ - pollingRoute, - pollingFailureTolerance, + pollingRoute: route, + pollingFailureTolerance: failureTolerance, serverPort: ports.server, socketPort: ports.socket, - pollingIntervalSeconds, + pollingIntervalSeconds: intervalSeconds, session_key: this.key }); this.log(cyan(`spawned new server worker with process id ${this.activeWorker.process.pid}`)); @@ -408,7 +469,7 @@ export namespace Session { this.activeWorker.on("message", async ({ lifecycle, action }) => { if (action) { const { message, args } = action as Monitor.Action; - console.log(this.timestamp(), `${this.configuration.workerIdentifier} action requested (${cyan(message)})`); + console.log(this.timestamp(), `${this.config.identifiers.worker.text} action requested (${cyan(message)})`); switch (message) { case "kill": const { reason, graceful, errorCode } = args; @@ -432,7 +493,7 @@ export namespace Session { handlers.forEach(handler => handler({ message, args })); } } else if (lifecycle) { - console.log(this.timestamp(), `${this.configuration.workerIdentifier} lifecycle phase (${lifecycle})`); + console.log(this.timestamp(), `${this.config.identifiers.worker.text} lifecycle phase (${lifecycle})`); } }); } diff --git a/src/server/Session/session_config_schema.ts b/src/server/Session/session_config_schema.ts index 5a85a45e3..e32cf8c6a 100644 --- a/src/server/Session/session_config_schema.ts +++ b/src/server/Session/session_config_schema.ts @@ -1,39 +1,67 @@ import { Schema } from "jsonschema"; +const colorPattern = /black|red|green|yellow|blue|magenta|cyan|white|gray|grey/; + +const identifierProperties: Schema = { + type: "object", + properties: { + text: { + type: "string", + minLength: 1 + }, + color: { + type: "string", + pattern: colorPattern + } + } +}; + +const portProperties: Schema = { + type: "number", + minimum: 1024, + maximum: 65535 +}; + export const configurationSchema: Schema = { id: "/configuration", type: "object", properties: { + showServerOutput: { type: "boolean" }, ports: { type: "object", properties: { - server: { type: "number", minimum: 1024, maximum: 65535 }, - socket: { type: "number", minimum: 1024, maximum: 65535 } + server: portProperties, + socket: portProperties }, required: ["server"], additionalProperties: true }, - pollingRoute: { - type: "string", - pattern: /\/[a-zA-Z]*/g - }, - masterIdentifier: { - type: "string", - minLength: 1 - }, - workerIdentifier: { - type: "string", - minLength: 1 + identifiers: { + type: "object", + properties: { + master: identifierProperties, + worker: identifierProperties, + exec: identifierProperties + } }, - showServerOutput: { type: "boolean" }, - pollingIntervalSeconds: { - type: "number", - minimum: 1, - maximum: 86400 + polling: { + type: "object", + additionalProperties: false, + properties: { + intervalSeconds: { + type: "number", + minimum: 1, + maximum: 86400 + }, + route: { + type: "string", + pattern: /\/[a-zA-Z]*/g + }, + failureTolerance: { + type: "number", + minimum: 0, + } + } }, - pollingFailureTolerance: { - type: "number", - minimum: 0, - } } }; \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 47f405aae0b4cf13ed24f4c5a699ea63877573ea Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Thu, 9 Jan 2020 22:34:14 -0500 Subject: remote authorized session actions --- src/server/ApiManagers/SessionManager.ts | 57 ++++++++++++++++++++++ src/server/DashSession.ts | 83 +++++++++++++++++--------------- src/server/RouteManager.ts | 3 +- src/server/index.ts | 17 +------ 4 files changed, 105 insertions(+), 55 deletions(-) create mode 100644 src/server/ApiManagers/SessionManager.ts (limited to 'src/server/ApiManagers') diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts new file mode 100644 index 000000000..eb17ff567 --- /dev/null +++ b/src/server/ApiManagers/SessionManager.ts @@ -0,0 +1,57 @@ +import ApiManager, { Registration } from "./ApiManager"; +import { Method, _permission_denied, AuthorizedCore, SecureHandler } from "../RouteManager"; +import RouteSubscriber from "../RouteSubscriber"; +import { sessionAgent } from ".."; + +const permissionError = "You are not authorized!"; + +export default class SessionManager extends ApiManager { + + private secureSubscriber = (root: string, ...params: string[]) => new RouteSubscriber(root).add("password", ...params); + + private authorizedAction = (handler: SecureHandler) => { + return (core: AuthorizedCore) => { + const { req, res, isRelease } = core; + const { password } = req.params; + if (!isRelease) { + return res.send("This can be run only on the release server."); + } + if (password !== process.env.session_key) { + return _permission_denied(res, permissionError); + } + handler(core); + res.redirect("/home"); + }; + } + + protected initialize(register: Registration): void { + + register({ + method: Method.GET, + subscription: this.secureSubscriber("debug", "mode", "recipient"), + secureHandler: this.authorizedAction(({ req }) => { + const { mode, recipient } = req.params; + if (["passive", "active"].includes(mode)) { + sessionAgent.serverWorker.sendMonitorAction("debug", { mode, recipient }); + } + }) + }); + + register({ + method: Method.GET, + subscription: this.secureSubscriber("backup"), + secureHandler: this.authorizedAction(() => sessionAgent.serverWorker.sendMonitorAction("backup")) + }); + + register({ + method: Method.GET, + subscription: this.secureSubscriber("kill"), + secureHandler: this.authorizedAction(({ res }) => { + res.send(""); + sessionAgent.killSession("an authorized user has manually ended the server session via the /kill route", true); + }) + }); + + } + +} \ No newline at end of file diff --git a/src/server/DashSession.ts b/src/server/DashSession.ts index e03487a61..c893f4b64 100644 --- a/src/server/DashSession.ts +++ b/src/server/DashSession.ts @@ -19,7 +19,7 @@ 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({ @@ -67,44 +67,10 @@ export class DashSessionAgent extends Session.AppliedSessionAgent { } } }); - const releaseDesktop = pathFromRoot("../../Desktop"); - const backup = () => monitor.exec("backup.bat", { cwd: releaseDesktop }); - monitor.addReplCommand("backup", [], backup); - monitor.addReplCommand("debug", [/active|passive/, /\S+\@\S+/], async args => { - const [mode, recipient] = args; - if (mode === "active") { - await backup(); - } - monitor.mainLog("backup complete"); - const backupsDirectory = `${releaseDesktop}/backups`; - const compressedDirectory = `${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; - monitor.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(); - monitor.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 - } - ]); - monitor.mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(recipient)}`); - error && monitor.mainLog(red(error.message)); - }); + 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; } @@ -120,4 +86,43 @@ export class DashSessionAgent extends Session.AppliedSessionAgent { return worker; } + public async backup() { + return this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop }); + } + + public async dispatchZippedDebugBackup(mode: string, recipient: string) { + if (mode === "active") { + await this.backup(); + } + this.sessionMonitor.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; + this.sessionMonitor.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(); + this.sessionMonitor.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 + } + ]); + this.sessionMonitor.mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(recipient)}`); + error && this.sessionMonitor.mainLog(red(error.message)); + } + } \ No newline at end of file diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts index 25259bd88..a8ad81bf7 100644 --- a/src/server/RouteManager.ts +++ b/src/server/RouteManager.ts @@ -14,7 +14,8 @@ export interface CoreArguments { isRelease: boolean; } -export type SecureHandler = (core: CoreArguments & { user: DashUserModel }) => any | Promise; +export type AuthorizedCore = CoreArguments & { user: DashUserModel }; +export type SecureHandler = (core: AuthorizedCore) => any | Promise; export type PublicHandler = (core: CoreArguments) => any | Promise; export type ErrorHandler = (core: CoreArguments & { error: any }) => any | Promise; diff --git a/src/server/index.ts b/src/server/index.ts index 6b3dfd614..d73e48d8a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -24,6 +24,7 @@ import { Logger } from "./ProcessFactory"; import { yellow, red } from "colors"; import { Session } from "./Session/session"; import { DashSessionAgent } from "./DashSession"; +import SessionManager from "./ApiManagers/SessionManager"; export const onWindows = process.platform === "win32"; export let sessionAgent: Session.AppliedSessionAgent; @@ -58,6 +59,7 @@ async function preliminaryFunctions() { */ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: RouteManager) { const managers = [ + new SessionManager(), new UserManager(), new UploadManager(), new DownloadManager(), @@ -88,21 +90,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: secureHandler: ({ res }) => res.send(true) }); - addSupervisedRoute({ - method: Method.GET, - subscription: new RouteSubscriber("kill").add("key"), - secureHandler: ({ req, res }) => { - if (req.params.key === process.env.session_key) { - res.send(""); - setTimeout(() => { - sessionAgent.killSession("an authorized user has manually ended the server session via the /kill route", false); - }, 5000); - } else { - res.redirect("/home"); - } - } - }); - const serve: PublicHandler = ({ req, res }) => { const detector = new mobileDetect(req.headers['user-agent'] || ""); const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html'; -- cgit v1.2.3-70-g09d2 From 0e7f623ac6241f1a2c5c67aed00e47513aaf063a Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Thu, 9 Jan 2020 23:24:46 -0500 Subject: more informative error messages for session routes --- src/server/ApiManagers/SessionManager.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'src/server/ApiManagers') diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts index eb17ff567..66aea91f2 100644 --- a/src/server/ApiManagers/SessionManager.ts +++ b/src/server/ApiManagers/SessionManager.ts @@ -20,7 +20,6 @@ export default class SessionManager extends ApiManager { return _permission_denied(res, permissionError); } handler(core); - res.redirect("/home"); }; } @@ -29,10 +28,13 @@ export default class SessionManager extends ApiManager { register({ method: Method.GET, subscription: this.secureSubscriber("debug", "mode", "recipient"), - secureHandler: this.authorizedAction(({ req }) => { + secureHandler: this.authorizedAction(({ 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}.`); + } else { + res.send(`Your request failed. '${mode}' is not a valid mode: please choose either 'active' or 'passive'`); } }) }); @@ -40,7 +42,10 @@ export default class SessionManager extends ApiManager { register({ method: Method.GET, subscription: this.secureSubscriber("backup"), - secureHandler: this.authorizedAction(() => sessionAgent.serverWorker.sendMonitorAction("backup")) + secureHandler: this.authorizedAction(({ res }) => { + sessionAgent.serverWorker.sendMonitorAction("backup"); + res.send(`Your request was successful: the server is creating a new back up.`); + }) }); register({ -- cgit v1.2.3-70-g09d2 From a8aa0facfaa23298398c15aa906bc6d69c538564 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Thu, 9 Jan 2020 23:32:40 -0500 Subject: final session --- src/server/ApiManagers/SessionManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/server/ApiManagers') diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts index 66aea91f2..0290b578c 100644 --- a/src/server/ApiManagers/SessionManager.ts +++ b/src/server/ApiManagers/SessionManager.ts @@ -52,8 +52,8 @@ export default class SessionManager extends ApiManager { method: Method.GET, subscription: this.secureSubscriber("kill"), secureHandler: this.authorizedAction(({ res }) => { - res.send(""); - sessionAgent.killSession("an authorized user has manually ended the server session via the /kill route", true); + res.send("Your request was successful: the server and its session have been killed."); + sessionAgent.killSession("an authorized user has manually ended the server session via the /kill route"); }) }); -- cgit v1.2.3-70-g09d2