diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/DocServer.ts | 22 | ||||
-rw-r--r-- | src/client/cognitive_services/CognitiveServices.ts | 9 | ||||
-rw-r--r-- | src/new_fields/InkField.ts | 1 | ||||
-rw-r--r-- | src/server/DashSession.ts | 7 | ||||
-rw-r--r-- | src/server/Session/session.ts | 64 |
5 files changed, 64 insertions, 39 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index ed7fbd7ba..12fed3e46 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -64,6 +64,24 @@ export namespace DocServer { } } + const instructions = "This page will automatically refresh after this alert is closed. Expect to reconnect after about 30 seconds."; + function alertUser(connectionTerminationReason: string) { + switch (connectionTerminationReason) { + case "crash": + alert(`Dash has temporarily crashed. Administrators have been notified and the server is restarting itself. ${instructions}`); + break; + case "temporary": + alert(`An administrator has chosen to restart the server. ${instructions}`); + break; + case "exit": + alert("An administrator has chosen to kill the server. Do not expect to reconnect until administrators start the server."); + break; + default: + console.log(`Received an unknown ConnectionTerminated message: ${connectionTerminationReason}`); + } + window.location.reload(); + } + export function init(protocol: string, hostname: string, port: number, identifier: string) { _cache = {}; GUID = identifier; @@ -82,9 +100,7 @@ export namespace DocServer { Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate); Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete); Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete); - Utils.AddServerHandler(_socket, MessageStore.ConnectionTerminated, () => { - alert("Your connection to the server has been terminated."); - }); + Utils.AddServerHandler(_socket, MessageStore.ConnectionTerminated, alertUser); } function errorFunc(): never { diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index 02eff3b25..57296c961 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -137,7 +137,7 @@ export namespace CognitiveServices { let id = 0; const strokes: AzureStrokeData[] = inkData.map(points => ({ id: id++, - points: points.map(({ x, y }) => `${x},${y}`).join(","), + points: points.map(({ X: x, Y: y }) => `${x},${y}`).join(","), language: "en-US" })); return JSON.stringify({ @@ -153,7 +153,7 @@ export namespace CognitiveServices { const serverAddress = "https://api.cognitive.microsoft.com"; const endpoint = serverAddress + "/inkrecognizer/v1.0-preview/recognize"; - const promisified = (resolve: any, reject: any) => { + return new Promise<string>((resolve, reject) => { xhttp.onreadystatechange = function () { if (this.readyState === 4) { const result = xhttp.responseText; @@ -171,11 +171,8 @@ export namespace CognitiveServices { xhttp.setRequestHeader('Ocp-Apim-Subscription-Key', apiKey); xhttp.setRequestHeader('Content-Type', 'application/json'); xhttp.send(body); - }; - - return new Promise<any>(promisified); + }); }, - }; export namespace Appliers { diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts index 83d631958..e2aa7ee16 100644 --- a/src/new_fields/InkField.ts +++ b/src/new_fields/InkField.ts @@ -2,7 +2,6 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { serializable, custom, createSimpleSchema, list, object, map } from "serializr"; import { ObjectField } from "./ObjectField"; import { Copy, ToScriptString } from "./FieldSymbols"; -import { DeepCopy } from "../Utils"; export enum InkTool { None, diff --git a/src/server/DashSession.ts b/src/server/DashSession.ts index a0e00adda..1c70ae1dc 100644 --- a/src/server/DashSession.ts +++ b/src/server/DashSession.ts @@ -63,9 +63,12 @@ export class DashSessionAgent extends Session.AppliedSessionAgent { protected async launchServerWorker() { const worker = Session.ServerWorker.Create(launchServer); // server initialization delegated to worker - worker.addExitHandler(() => { + worker.addExitHandler(reason => { const { _socket } = WebSocket; - _socket && Utils.Emit(_socket, MessageStore.ConnectionTerminated, "Manual"); + if (_socket) { + const message = typeof reason === "boolean" ? (reason ? "exit" : "temporary") : "crash"; + Utils.Emit(_socket, MessageStore.ConnectionTerminated, message); + } }); return worker; } diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts index 6967ece52..d46e6b6e7 100644 --- a/src/server/Session/session.ts +++ b/src/server/Session/session.ts @@ -131,7 +131,7 @@ export namespace Session { } }; - export type ExitHandler = (reason: Error | null) => void | Promise<void>; + export type ExitHandler = (reason: Error | boolean) => void | Promise<void>; export namespace Monitor { @@ -193,9 +193,9 @@ export namespace Session { */ public killSession = async (reason: string, graceful = true, errorCode = 0) => { this.mainLog(cyan(`exiting session ${graceful ? "clean" : "immediate"}ly`)); - this.mainLog(`reason: ${(red(reason))}`); - await this.executeExitHandlers(null); - this.killActiveWorker(graceful); + this.mainLog(`session exit reason: ${(red(reason))}`); + await this.executeExitHandlers(true); + this.killActiveWorker(graceful, true); process.exit(errorCode); } @@ -341,18 +341,25 @@ export namespace Session { * 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. + * @returns the composition of all of the assigned objects, much like Object.assign(), but with more + * granularity in the overwriting of nested objects */ - 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] = {}); + private preciseAssign = (target: any, ...sources: any[]): any => { + for (const source of sources) { + this.preciseAssignHelper(target, source); + } + return target; + } + + private preciseAssignHelper = (target: any, source: any) => { + Array.from(new Set([...Object.keys(target), ...Object.keys(source)])).map(property => { + let targetValue: any, sourceValue: any; + if (sourceValue = source[property]) { + if (typeof sourceValue === "object" && typeof (targetValue = target[property]) === "object") { + this.preciseAssignHelper(targetValue, sourceValue); } else { - collector[property] = specifiedValue; + target[property] = sourceValue; } - } else { - collector[property] = defaultObject[property]; } }); } @@ -372,9 +379,7 @@ export namespace Session { }; // ensure all necessary and no excess information is specified by the configuration file validate(config, configurationSchema, options); - const results: any = {}; - this.assign(defaultConfig, config, results); - config = results; + config = this.preciseAssign({}, defaultConfig, config); } catch (error) { if (error instanceof ValidationError) { console.log(red("\nSession configuration failed.")); @@ -384,7 +389,7 @@ 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."); - config = { ...defaultConfig }; + config = this.preciseAssign({}, defaultConfig); } else { console.log(red("\nSession configuration failed.")); console.log("The following unknown error occurred during configuration."); @@ -429,15 +434,15 @@ export namespace Session { return repl; } - private executeExitHandlers = async (reason: Error | null) => Promise.all(this.exitHandlers.map(handler => handler(reason))); + private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason))); /** * Attempts to kill the active worker gracefully, unless otherwise specified. */ - private killActiveWorker = (graceful = true): void => { + private killActiveWorker = (graceful = true, isSessionEnd = false): void => { if (this.activeWorker && !this.activeWorker.isDead()) { if (graceful) { - this.activeWorker.send({ manualExit: true }); + this.activeWorker.send({ manualExit: { isSessionEnd } }); } else { this.activeWorker.process.kill(); } @@ -577,7 +582,7 @@ export namespace Session { public sendMonitorAction = (message: string, args?: any) => process.send!({ action: { message, args } }); private constructor(work: Function) { - this.lifecycleNotification(green(`initializing process... (${white(`${process.execPath} ${process.execArgv.join(" ")}`)})`)); + this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(" ")}]`)}`)); const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env; this.serverPort = Number(serverPort); @@ -601,21 +606,25 @@ export namespace Session { this.pollingIntervalSeconds = newPollingIntervalSeconds; } if (manualExit !== undefined) { - await this.executeExitHandlers(null); + const { isSessionEnd } = manualExit; + await this.executeExitHandlers(isSessionEnd); process.exit(0); } }); // one reason to exit, as the process might be in an inconsistent state after such an exception process.on('uncaughtException', this.proactiveUnplannedExit); - process.on('unhandledRejection', this.proactiveUnplannedExit); + process.on('unhandledRejection', reason => { + const appropriateError = reason instanceof Error ? reason : new Error(`unhandled rejection: ${reason}`); + this.proactiveUnplannedExit(appropriateError); + }); } /** * Execute the list of functions registered to be called * whenever the process exits. */ - private executeExitHandlers = async (reason: Error | null) => Promise.all(this.exitHandlers.map(handler => handler(reason))); + private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason))); /** * Notify master thread (which will log update in the console) of initialization via IPC. @@ -626,14 +635,14 @@ export namespace Session { * Called whenever the process has a reason to terminate, either through an uncaught exception * in the process (potentially inconsistent state) or the server cannot be reached. */ - private proactiveUnplannedExit = async (error: any): Promise<void> => { + private proactiveUnplannedExit = async (error: Error): Promise<void> => { this.shouldServerBeResponsive = false; // communicates via IPC to the master thread that it should dispatch a crash notification email this.sendMonitorAction("notify_crash", { error }); await this.executeExitHandlers(error); // 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 || error)); + this.lifecycleNotification(red(error.message)); process.exit(1); } @@ -651,7 +660,6 @@ export namespace Session { this.lifecycleNotification(green(`listening on ${this.serverPort}...`)); } this.shouldServerBeResponsive = true; - resolve(); } catch (error) { // if we expect the server to be unavailable, i.e. during compilation, // the listening variable is false, activeExit will return early and the child @@ -663,6 +671,8 @@ export namespace Session { this.lifecycleNotification(yellow(`the server has encountered ${this.pollingFailureCount} of ${this.pollingFailureTolerance} tolerable failures`)); } } + } finally { + resolve(); } }, 1000 * this.pollingIntervalSeconds); }); |