UNPKG

gpii-universal

Version:

Cross platform, core components of the GPII personalization infrastructure.

902 lines (833 loc) 88 kB
/*! * Lifecycle Manager * * Copyright 2012 Antranig Basman * * Licensed under the New BSD license. You may not use this file except in * compliance with this License. * * The research leading to these results has received funding from the European Union's * Seventh Framework Programme (FP7/2007-2013) * under grant agreement no. 289016. * * You may obtain a copy of the License at * https://github.com/GPII/universal/blob/master/LICENSE.txt */ "use strict"; var fluid = fluid || require("infusion"), gpii = fluid.registerNamespace("gpii"), $ = fluid.registerNamespace("jQuery"); (function () { fluid.defaults("gpii.lifecycleManager", { gradeNames: ["fluid.modelComponent", "fluid.contextAware"], retryOptions: { // Options governing how often to recheck whether settings are set (and in future, to check for running processes) rewriteEvery: 0, // This feature is now disabled except in integration tests, causing settings handler instability via GPII-2522 numRetries: 12, // Make 12 attempts over a period of 12 seconds to discover whether settings are set retryInterval: 1000 }, debounceTimeMs: 1500, // The debounce time in milliseconds for /proximityTriggered endpoint to reject the following request with the same GPII key components: { // Note: The subcomponent "privateMatchMaker" is distributed from flowManager so it doesn't disturb lifecycle manager unit tests. // TODO: privateMatchMaker will eventually be factored into the UserLogonRequest where key-in matchmaker requests are handled. // privateMatchMaker: { // type: "gpii.lifecycleManager.privateMatchMaker" // }, userSession: { type: "gpii.lifecycleManager.userSession" }, restoreSession: { type: "gpii.lifecycleManager.restoreSession", options: { model: { gpiiKey: "restore" } } }, variableResolver: { type: "gpii.lifecycleManager.variableResolver" }, nameResolver: { type: "gpii.lifecycleManager.nameResolver" }, userErrors: { type: "gpii.userErrors" } }, dynamicComponents: { userLogonRequests: { type: "{arguments}.0", createOnEvent: "onUserLogonRequestReceived", options: { gpiiKey: "{arguments}.1", requestPromise: "{arguments}.2" } } }, members: { /* action queue for high-level lifecycle manager tasks (start, stop or update). * The entries in the action queue are of the format { func: <functionToCall>, arg: <argument> } where * <functionToCall> is a single-argument function that returns a promise. The promise should be resolved when * the function is complete (including side-effects). * A more detailed description: https://github.com/GPII/universal/tree/master/documentation/LifecycleMananger.md */ actionQueue: [], userLogonRequestQueue: [] }, model: { // Holds the GPII key from the last environmental login lastEnvironmentalLoginGpiiKey: undefined, currentUserLogon: { // Holds information about the last non-noUser login/logout action // for /proximityTriggered endpoint to verify its debounce rule. type: undefined, // "login"/"logout" gpiiKey: undefined, // string with GPII key timeStamp: 0 }, logonChange: { // Holds information about the most recent login/logout action so its progress may be tracked in a modelised way type: undefined, // "login"/"logout" inProgress: false, // boolean gpiiKey: undefined, // string with GPII key timeStamp: 0 } }, events: { onSessionStart: null, // fired with [gradeName, gpiiKey] - notify listeners onSessionSnapshotUpdate: null, // fired with [{lifecycleManager}, {session}, originalSettings] onSessionStop: null, // fired with [{lifecycleManager}, {session}] onAutoSaveRequired: null, // fired with [gpiiKey, {session}.model.preferences] onClearActionQueue: null, onUserLogonRequestReceived: null, // fired with [gradeName, reqeustId, gpiiKey, requestPromise] onQueueEmpty: null, onClearUserLogonRequestQueue: null }, listeners: { "onSessionSnapshotUpdate.log": "gpii.lifecycleManager.logSnapshotUpdate", "onCreate.createActionFunctions": "gpii.lifecycleManager.createActionQueueFunctions", "onClearActionQueue.clearQueue": "gpii.lifecycleManager.clearQueue({that}.actionQueue, 'action')", "onClearUserLogonRequestQueue.clearQueue": "gpii.lifecycleManager.clearQueue({that}.userLogonRequestQueue, 'user logon request')" }, actionQueueFunctions: { expander: { // manually expanded via the createActionQueueFunctions function type: "fluid.noexpand", value: { stop: "{that}.processStop", start: "{that}.processStart", read: "{that}.processRead", update: "{that}.processUpdate" } } }, invokers: { // Handle login, logout and proximityTriggered requests performLogin: { funcName: "gpii.lifecycleManager.performLogin", args: ["{that}", "{arguments}.0", "{arguments}.1"] // gpiiKey, loginInfo }, performLogout: { funcName: "gpii.lifecycleManager.performLogout", args: ["{that}", "{arguments}.0"] // gpiiKey }, performProximityTriggered: { funcName: "gpii.lifecycleManager.performProximityTriggered", args: ["{that}", "{arguments}.0"] // gpiiKey }, replayEnvironmentalLogin: { funcName: "gpii.lifecycleManager.replayEnvironmentalLogin", args: ["{that}", "{that}.model.lastEnvironmentalLoginGpiiKey"] }, isBounce: { funcName: "gpii.lifecycleManager.isBounce", args: ["{that}.model.logonChange", "{that}.model.currentUserLogon", "{that}.options.debounceTimeMs", "{arguments}.0"] // gpiiKey }, errorResponse: { funcName: "gpii.lifecycleManager.errorResponse", args: ["{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3"] // requestPromise or requestComponent, message, statusCode, ignoreUserErrors }, // get the current active GPII key getActiveSessionGpiiKey: { funcName: "gpii.lifecycleManager.getActiveSessionGpiiKey", args: ["{that}.userSession"] }, addLifecycleInstructionsToPayload: { funcName: "gpii.lifecycleManager.addLifecycleInstructionsToPayload", args: ["{arguments}.0"] // fullPayload }, updateActivePrefsSetName: { funcName: "gpii.lifecycleManager.updateActivePrefsSetName", args: ["{that}", "{arguments}.0", "{arguments}.1"] // fullPayload, newPrefsSetName }, prefsSetNameChanged: { funcName: "gpii.lifecycleManager.prefsSetNameChanged", args: ["{that}", "{arguments}.0"] // newPrefsSetName }, applyPreferences: { funcName: "gpii.lifecycleManager.applyPreferences", args: ["{that}", "{arguments}.0", "{arguments}.1"] // preferences, prefsAppliedLocalEvent }, readPreferences: { funcName: "gpii.lifecycleManager.readPreferences", args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // preferences, preferenceReadSuccessEvent, preferenceReadFailEvent }, // Return lifecycleManager.userSession by default. If a GPII key is provided, return the session of the given key. getSession: { funcName: "gpii.lifecycleManager.getSession", args: ["{that}.userSession", "{that}.restoreSession", "{arguments}.0"] // GPII key }, // Handle the action queue addToActionQueue: { funcName: "gpii.lifecycleManager.addToActionQueue", args: ["{that}", "{that}.actionQueue", "{arguments}.0"] }, processActionQueue: { funcName: "gpii.lifecycleManager.processActionQueue", args: ["{that}", "{that}.actionQueue"] }, // Handle the user logon request queue addToUserLogonRequestQueue: { funcName: "gpii.lifecycleManager.addToUserLogonRequestQueue", args: ["{that}", "{that}.userLogonRequestQueue", "{arguments}.0"] // queueItem }, processUserLogonRequestQueue: { funcName: "gpii.lifecycleManager.processUserLogonRequestQueue", args: ["{that}", "{that}.userLogonRequestQueue"] }, endProcessOneUserLogonRequest: { funcName: "gpii.lifecycleManager.endProcessOneUserLogonRequest", args: ["{lifecycleManager}"] // queueItem }, // start: manually created function which should be called on user login (i.e. configuring the system) // stop: manually created function which should be called on user logout (i.e. restoring the system) // read: manually created function which should be called on reading settings of an already configured system // update: manually created function which should be called on update (i.e. changes in the setting of an already configured system) processStart: { // should not be used directly, use the manually created 'start' invoker instead funcName: "gpii.lifecycleManager.processStart", args: [ "{that}", "{that}.userSession", "{arguments}.0"] // Use IoC declaration "{that}.userSession" to force the instantiation of "userSession" subcomponent }, processStop: { // should not be used directly, use the manually created 'stop' invoker instead funcName: "gpii.lifecycleManager.processStop", args: [ "{that}", "{arguments}.0"] }, processRead: { // should not be used directly, use the manually created 'read' invoker instead funcName: "gpii.lifecycleManager.processRead", args: ["{that}", "{arguments}.0"] // solutions }, processUpdate: { // should not be used directly, use the manually created 'update' invoker instead funcName: "gpii.lifecycleManager.processUpdate", args: [ "{that}", "{arguments}.0"] // finalPayload }, applySolution: { funcName: "gpii.lifecycleManager.applySolution", args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3", "{arguments}.4"] // solutionId, solutionRecord, session, lifecycleBlocksKeys, rootAction }, executeActions: { funcName: "gpii.lifecycleManager.executeActions", args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3", "{arguments}.4"] // solutionId, settingsHandlers, actions, session, rootAction }, invokeSettingsHandlerGet: { funcName: "gpii.lifecycleManager.invokeSettingsHandlerGet", args: ["{that}", "{arguments}.0", "{arguments}.1"] // solutionId, settingsHandlers }, invokeSettingsHandlerSet: { funcName: "gpii.lifecycleManager.invokeSettingsHandlerSet", args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // solutionId, settingsHandlers, handlerType }, restoreSnapshot: { funcName: "gpii.lifecycleManager.restoreSnapshot", args: ["{that}", "{arguments}.0"] // originalSettings }, getSolutionRunningState: { funcName: "gpii.lifecycleManager.getSolutionRunningState", args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // solutionId, solution, session } } }); fluid.defaults("gpii.test.lifecycleManager.integration", { // Test this only in integration testing scenarios retryOptions: { rewriteEvery: 3 } }); fluid.contextAware.makeAdaptation({ distributionName: "gpii.test.lifecycleManager.integration.adaptation", targetName: "gpii.lifecycleManager", adaptationName: "integrationTest", checkName: "integrationTest", record: { contextValue: "{gpii.contexts.test.integration}", gradeNames: "gpii.test.lifecycleManager.integration" } }); /** * Function to check whether a proximity trigger should bounce. * Debounce rule: any RFID actions is ignored for <myGpiiKey> if a login/logout for <myGpiiKey> is in progress * OR if the last login/logout process for <myGpiiKey> finished less than 1.5 seconds ago * If no user is logged in and debounce doesn't apply, log in <myGpiiKey> * If <myGpiiKey> is logged in and debounce doesn't apply, log out <myGpiiKey> * If another user is already logged in or in the process of logging in or out, log that user out and log in <myGpiiKey> * * @param {Component} currentLogonChange - The value of lifecycleManager.model.logonChange. * @param {Component} currentUserLogon - The value of lifecycleManager.model.currentUserLogon. * @param {Integer} debounceTimeMs - The time in milliseconds for which to debounce after last proximityTrigger was fired. * @param {String} gpiiKey - The GPII key of the user for whom the proximityTrigger was fired. * @return {Boolean} Returns true if the proximityTrigger should be ignored (i.e. is considered a bounce). This is done according * to the following rule: any RFID actions is ignored for <myGpiiKey> if a login/logout for <myGpiiKey> is in progress * OR if the last login/logout process for <myGpiiKey> finished less than {debounceTimeMs} milliseconds ago */ gpii.lifecycleManager.isBounce = function (currentLogonChange, currentUserLogon, debounceTimeMs, gpiiKey) { // if login/logout is in progress for <gpiiKey> we consider it a bounce var now = Date.now(); if (currentLogonChange.gpiiKey === "noUser" && currentLogonChange.type === "login" && !currentLogonChange.inProgress) { return currentUserLogon.gpiiKey === gpiiKey && now - currentUserLogon.timeStamp < debounceTimeMs; } else if (gpiiKey === currentLogonChange.gpiiKey) { return currentLogonChange.inProgress || now - currentLogonChange.timeStamp < debounceTimeMs; } else { return false; } }; /** * Function to perform proximityTriggered keyin and keyout. * * @param {Component} that - An instance of gpii.lifecycleManager. * @param {String} gpiiKey - The GPII key of the user for whom the keyin or keyout was performed. * @return {Promise} The promise whose resolved value contains the logon success message and rejected value contains * the fail message. */ gpii.lifecycleManager.performProximityTriggered = function (that, gpiiKey) { fluid.log("proximity triggered by GPII key: ", gpiiKey); var promiseTogo = fluid.promise(); // find currently logged in user: var activeGpiiKey = that.getActiveSessionGpiiKey(); if (that.isBounce(gpiiKey)) { fluid.log("Proximity trigger ignored due to bounce rules"); that.errorResponse(promiseTogo, "Proximity trigger ignored due to bounce rules. Please wait current logon change is complete", 429); return promiseTogo; } // if the login user is "reset", trigger the reset process via login with "reset" key if (gpiiKey === "reset") { promiseTogo = that.addToUserLogonRequestQueue({ gpiiKey: "reset", logonState: "login" }); return promiseTogo; } // if user is already logged in, log them out. If it's a different user who is logged in // log that user out before logging in with the new user if (activeGpiiKey) { var previousUser = activeGpiiKey; if (previousUser !== gpiiKey) { that.addToUserLogonRequestQueue({ gpiiKey: previousUser, logonState: "logout" }); promiseTogo = that.addToUserLogonRequestQueue({ gpiiKey: gpiiKey, logonState: "login" }); } else { promiseTogo = that.addToUserLogonRequestQueue({ gpiiKey: previousUser, logonState: "logout" }); } } return promiseTogo; }; /** * Shared function to perform keyin and keyout. * * @param {Component} that - An instance of gpii.lifecycleManager. * @param {String} gpiiKey - The GPII key of the user for whom the keyin or keyout was performed. * @param {Object} loginInfo - [optional] Extra login information * @param {Boolean} loginInfo.isEnvironmentalLogin - Whether this login is an environmental login. For example, * logins via USB or NFC are not environmental login while the windows auto logins are environmental login. When * this argument is not provided, this login is considered as a non-environmental login. * @return {Promise} The promise whose resolved value contains the logon success message and rejected value contains * the fail message. */ gpii.lifecycleManager.performLogin = function (that, gpiiKey, loginInfo) { // If the active key is "noUser", log it out before logging in another key var activeGpiiKey = that.getActiveSessionGpiiKey(); var lastLogonRequest = that.userLogonRequestQueue[that.userLogonRequestQueue.length - 1]; loginInfo = loginInfo || {}; // Add the logout of noUser to the user logon request queue when: // 1. There is a current active key; // 2. The last logon request in the queue is to key in this current active key. This case sometimes occurs // when a key in request is still in progress or is waiting in the logon request queue to be processed. if (activeGpiiKey || lastLogonRequest && lastLogonRequest.gpiiKey === activeGpiiKey && lastLogonRequest.logonState === "login") { that.addToUserLogonRequestQueue({ gpiiKey: activeGpiiKey, logonState: "logout" }); } if (loginInfo.isEnvironmentalLogin) { that.applier.change("lastEnvironmentalLoginGpiiKey", gpiiKey); } // Login the requested key var logonPromise = that.addToUserLogonRequestQueue({ gpiiKey: gpiiKey, logonState: "login" }); return logonPromise; }; /** * Shared function to perform keyout. * * @param {Component} that - An instance of gpii.lifecycleManager. * @param {String} gpiiKey - The GPII key of the user for whom the keyin or keyout was performed. * @return {Promise} The promise whose resolved value contains the logon success message and rejected value contains * the fail message. */ gpii.lifecycleManager.performLogout = function (that, gpiiKey) { var logonPromise = that.addToUserLogonRequestQueue({ gpiiKey: gpiiKey, logonState: "logout" }); return logonPromise; }; /** * To re-apply the last environmental login. * This function is used by gpii-app to easily trigger a re-login of the last environmental login. * The replay is only performed when: * 1. The active GPII key is "noUser", in which case for some reason (e.g. the "reset to standard" function has * been activated) the user has become logged out. * 2. The active GPII key is the environmental login key. * * @param {Component} that - An instance of gpii.lifecycleManager. * @param {String} lastEnvironmentalLoginGpiiKey - The GPII key of the last environmental login. * @return {Promise} The promise whose resolved value contains the login success message and rejected value contains * the fail message. */ gpii.lifecycleManager.replayEnvironmentalLogin = function (that, lastEnvironmentalLoginGpiiKey) { var promiseTogo = fluid.promise(); var activeGpiiKey = that.getActiveSessionGpiiKey(); if (activeGpiiKey === "noUser" || activeGpiiKey === lastEnvironmentalLoginGpiiKey) { promiseTogo = that.performLogin(lastEnvironmentalLoginGpiiKey, true); } else { promiseTogo.reject({ isError: true, message: "Replay environmental login ignored because the current active GPII key is not \"noUser\" or the last GPII key used for the environmental login" }); } return promiseTogo; }; /** * This function is used in 2 cases: * 1. Report an error at the early stage of a logon request processing before a dynamic user logon request component * is created. At this moment, the report is onto a request promise by calling requestPromise.reject(). An typical * example is when a proximityTriggered request is bounced. See performProximityTriggered(); * 2. Report an error after a dynamic user logon request component ("gpii.lifecycleManager.loginRequest" or * "gpii.lifecycleManager.logoutRequest") has been constructed and the error occurs when the request component is processing * the logon. This error will be reported to the request component via request.events.onError.fire(). The example of this * type spreads out on the handler grade gpii.lifecycleManager.userLogonHandling. * * @param {Promise|Component} togo - When `togo` is a requst promise, its reject() is invoked with the constructed message. * When `togo` is an instance of "gpii.lifecycleManager.loginRequest" or "gpii.lifecycleManager.logoutRequest", the error is * reported by firing onError event with the constructed message. * @param {String} message - The error message to be included in the constructed message. * @param {Number} statusCode - The status code to be included in the constructed message. * @param {Boolean} ignoreUserErrors - The flag that indicate whether this error should be reported to users via userErrors. */ gpii.lifecycleManager.errorResponse = function (togo, message, statusCode, ignoreUserErrors) { var error = { statusCode: statusCode || 500, message: message || "Unknown error occurred on logon change action", ignoreUserErrors: ignoreUserErrors || false }; if (fluid.isPromise(togo)) { togo.reject(error); } else { togo.events.onError.fire(error); } }; /** * Manually constructs the functions in the actionQueueFunctions options. * This is done manually to avoid resolving the "{that}" part of the function to be called (e.g. "{that}.processStart") * @param {Component} that - An instance of gpii.lifecycleManager. */ gpii.lifecycleManager.createActionQueueFunctions = function (that) { fluid.each(that.options.actionQueueFunctions, function (queueFunc, invokerName) { that[invokerName] = function (arg) { return that.addToActionQueue({ func: queueFunc, invokerName: invokerName, arg: arg }); }; }); }; /** * Add lifecycle instructions to the payload. * @param {Object} fullPayload - The payload to add lifecycle instructions. * @return {Object} - The payload that has lifecycle instructions added. */ gpii.lifecycleManager.addLifecycleInstructionsToPayload = function (fullPayload) { fullPayload.activeConfiguration = { inferredConfiguration: fluid.extend(true, {}, fullPayload.matchMakerOutput.inferredConfiguration[fullPayload.activePrefsSetName]) }; var lifecycleInstructions = gpii.transformer.configurationToSettings(fullPayload.activeConfiguration.inferredConfiguration, fullPayload.solutionsRegistryEntries); fluid.set(fullPayload, "activeConfiguration.lifecycleInstructions", lifecycleInstructions); return fullPayload; }; /** * Transform preferences to final payload and apply it to the local computer. * @param {Component} that - An instance of gpii.lifecycleManager. * @param {Object} preferences - The preferences to be transformed and applied. * @param {String|Array} prefsAppliedLocalEvent - Fired once the preferences are applied to the local computer. */ gpii.lifecycleManager.applyPreferences = function (that, preferences, prefsAppliedLocalEvent) { fluid.log("Lifecycle Manager: Received updated preferences ", preferences); // Grab current session var userSession = that.getSession(); // tweak payload to be in preferences set format and find out if the changed preferences contain new ones var parsedPrefs = {}; var hasNewPrefs = false; var previousPrefs = fluid.get(userSession.model.preferences, ["contexts", userSession.model.activePrefsSetName, "preferences"]); var oldPrefsPaths = gpii.transformer.computeDemandedSettingsPaths(previousPrefs); fluid.each(preferences, function (spec, pref) { // Filter out preferences in the pspChannel request that have no value change but were initially recorded // in "pspChannel.model.settingControls" for reporting default schema values if (spec.value !== undefined) { var prefPath = fluid.pathUtil.parseEL(pref); fluid.set(parsedPrefs, prefPath, spec.value); if (oldPrefsPaths.indexOf(prefPath[0]) === -1) { hasNewPrefs = true; } } }); // GPII-3378: 2 routes for applying incoming preferences based on whether they contain new preferences that are not // part of userSession.model.preferences: // 1. When there are new preferences, preform a round of local matchmaking and use its result to apply preferences; // 2. When there aren't new preferences, use the previous matchmaking result from userSession.model to apply preferences. gpii.lifecycleManager[hasNewPrefs ? "applyWithNewPrefs" : "applyExistingPrefs"](that, userSession, parsedPrefs, prefsAppliedLocalEvent); }; // This function is called when preferences to be applied contain new preferences that are not in session.model.preferences. // It performs a round of local matchmaking and uses the matchmaking result to apply all preferences including new ones. // This is because when GPII runs in the untrusted config, for the security reason, the matchamking results fetched from // the cloud for the previous preferences only contains the filtered results on solution registry and inferred configurations // (etc) that are relevant to the previous preferences. This result is not enough for applying new preferences. gpii.lifecycleManager.applyWithNewPrefs = function (that, session, parsedPrefs, prefsAppliedLocalEvent) { var newPrefsSet = {}; fluid.set(newPrefsSet, ["contexts", session.model.activePrefsSetName, "preferences"], parsedPrefs); var updatedPrefSet = fluid.extend({}, session.model.preferences, newPrefsSet); var activeGpiiKey = that.getActiveSessionGpiiKey(); // Perform a local matchmaking to get matched solutions for existing and new preferences. var matchMakingPromise = that.privateMatchMaker.doMatch(activeGpiiKey, updatedPrefSet); matchMakingPromise.then(function (newPayload) { var fullPayload = fluid.extend(true, {}, session.model); fluid.set(fullPayload, ["preferences"], updatedPrefSet); fluid.set(fullPayload, ["matchMakerOutput", "inferredConfiguration"], newPayload.matchMakerOutput.inferredConfiguration); fluid.set(fullPayload, ["solutionsRegistryEntries"], newPayload.solutionsRegistryEntries); fluid.set(fullPayload, ["currentPreferences"], gpii.lifecycleManager.getPreferencesForSet(fullPayload.preferences, fullPayload.activePrefsSetName)); that.addLifecycleInstructionsToPayload(fullPayload); session.applier.change("", fullPayload, "ADD", "userUpdate"); that.update(fullPayload).then(prefsAppliedLocalEvent.fire); }); }; // Being the counterpart of applyWithNewPrefs(), this function is called when preferences to be applied only contain // existing preferences that are in session.model.preferences. It uses the existing matchmaking result stored in // session.model to apply these preferences. gpii.lifecycleManager.applyExistingPrefs = function (that, session, parsedPrefs, prefsAppliedLocalEvent) { // Only the user updated preferences will be saved to the cloud, which is identified by firing the model change request // with source === "userUpdate". Note that changes requests on model.preferences can also be triggered by initial // key-in or the change of the preferences set. The preferences change from these actions should not be saved to the cloud. session.applier.change(["preferences", "contexts", session.model.activePrefsSetName, "preferences"], parsedPrefs, "ADD", "userUpdate"); // get new inferred configuration to be applied: var newInferred = gpii.matchMakerFramework.utils.updateInferredConfiguration(session.model.preferences, session.model.matchMakerOutput.inferredConfiguration, session.model.solutionsRegistryEntries); // full payload from session: var fullPayload = fluid.extend(true, {}, session.model); fluid.set(fullPayload, ["matchMakerOutput", "inferredConfiguration"], newInferred); that.addLifecycleInstructionsToPayload(fullPayload); session.applier.change("", { activeConfiguration: fullPayload.activeConfiguration, currentPreferences: gpii.lifecycleManager.getPreferencesForSet(fullPayload.preferences, fullPayload.activePrefsSetName) }); that.update(fullPayload).then(prefsAppliedLocalEvent.fire); }; /** * Read corresponding setting values of the requested preferences. The read result is applied as * a model change to userSession.model.currentPreferences. * @param {Component} that - An instance of gpii.lifecycleManager. * @param {Object} preferences - The preferences to read settings for. An example is: * "http://registry\\.gpii\\.net/common/magnification": { * value: 3 * } * @param {Event} preferenceReadSuccessEvent - The event fired when the read succeeds. * @param {Event} preferenceReadFailEvent - The event fired when the read fails. */ gpii.lifecycleManager.readPreferences = function (that, preferences, preferenceReadSuccessEvent, preferenceReadFailEvent) { fluid.log("Lifecycle Manager: Received preferences to read ", preferences); var session = that.getSession(); // Mock values for running preferences thru matchMaker var parsedPreferences = {}; var activePrefsSetName = "gpii-default"; fluid.each(preferences, function (spec, pref) { var prefPath = fluid.pathUtil.parseEL(pref); fluid.set(parsedPreferences, prefPath, spec.value); }); var onePrefsSet = {}; fluid.set(onePrefsSet, ["contexts", activePrefsSetName, "preferences"], parsedPreferences); // Perform a local matchmaking to get matched solutions for preferences. var matchMakingPromise = that.privateMatchMaker.doMatch("readSetting", onePrefsSet); matchMakingPromise.then(function (payload) { fluid.set(payload, ["activePrefsSetName"], activePrefsSetName); fluid.set(payload, ["preferences"], onePrefsSet); that.addLifecycleInstructionsToPayload(payload); // compile a list of solution registry entries for all matched solutoins var matchMakerSolutionRegistryEntries = fluid.get(payload, ["activeConfiguration", "lifecycleInstructions"]); var settingsPromise = that.read(matchMakerSolutionRegistryEntries); settingsPromise.then(function (allSettingValues) { // As lifecycleManager.read() returns values for all settings that matched solutions support, // only filter out setting values that are in the matchmaker result. var matchMakerSettings = gpii.lifecycleManager.getMatchMakerSettings(allSettingValues, matchMakerSolutionRegistryEntries); // Transform settings to preferences var transformed = gpii.lifecycleManager.transformSettingsToPrefs(matchMakerSettings, matchMakerSolutionRegistryEntries); // Filter matched preferences var matchedPrefs = gpii.lifecycleManager.filterMatchedPrefs(transformed, parsedPreferences); // Use cases discovered that will result in "matchedPrefs" being an empty object include: // 1. Settings handler that have get() defined but not implemented - https://issues.gpii.net/browse/GPII-4217 // 2. "inverseCapabilitiesTransformations" blocks for one or more settings are not defined. In this case, // although setting values are read successfully, when these values are transformed into preference values via // gpii.lifecycleManager.transformSettingsToPrefs(), an empty object will return - https://issues.gpii.net/browse/GPII-4216 if ($.isEmptyObject(matchedPrefs)) { preferenceReadFailEvent.fire(); } else { session.applier.change(["currentPreferences"], fluid.extend(true, fluid.copy(session.model.currentPreferences), matchedPrefs)); preferenceReadSuccessEvent.fire(); } }, preferenceReadFailEvent.fire); }); }; /* * Get preferences block for a certain context. * @param {Object} prefsSets - An object containing one or more preferences sets. * @param {String} prefsSetName - A name of a preferences set. * @return {Object} The actual preferences for the active context. */ gpii.lifecycleManager.getPreferencesForSet = function (prefsSets, prefsSetName) { var prefs = fluid.get(prefsSets, ["contexts", prefsSetName, "preferences"]); return prefs || {}; }; /** * Return the user's GPII key that is associated with the active session. * @param {Component} userSession - An instance of gpii.lifecycleManager.userSession. * @return {String} The GPII key of the active session. */ gpii.lifecycleManager.getActiveSessionGpiiKey = function (userSession) { return userSession.model.gpiiKey; }; /** * Return the active session. The logic is: * 1. If a GPII key is not provided, return the instance of gpii.lifecycleManager.userSession; * 2. If a GPII key is provided: * 2.1 The given key is "restore", return the instance of gpii.lifecycleManager.restoreSession; * 2.2 The given key is NOT "restore", checks if gpii.lifecycleManager.userSession is handling the given GPII key. * If yes, return gpii.lifecycleManager.userSession. Otherwise, return undefined. * @param {Component} userSession - An instance of gpii.lifecycleManager.userSession. * @param {Component} restoreSession - An instance of gpii.lifecycleManager.restoreSession. * @param {String} [gpiiKey] - [optional] A GPII key. If provided, return the matched session for the given GPII * key. If no matched session is found, return undefined. * @return {Component} Could be an instance of gpii.lifecycleManager.userSession or an instance of * gpii.lifecycleManager.restoreSession or undefined. */ gpii.lifecycleManager.getSession = function (userSession, restoreSession, gpiiKey) { return gpiiKey ? userSession.model.gpiiKey === gpiiKey ? userSession : gpiiKey === "restore" ? restoreSession : undefined : userSession; }; /* Transforms the handlerSpec (handler part of the transformer's response payload) to a form * accepted by a settingsHandler - we use a 1-element array holding the payload for a single solution * per handler */ gpii.lifecycleManager.specToSettingsHandler = function (solutionId, handlerSpec) { var returnObj = {}, settings = {}; if (handlerSpec.supportedSettings === undefined) { // if supportedSettings directive is not present, pass all settings: settings = handlerSpec.settings; } else { // we cant simply use fluid.filterKeys because that wont handle the cases where // there are 'undefined' values for the keys in handlerSpec.settings // TODO: Kaspar believes that the reason for filtering happening here rather than in the MatchMaker is // that the transformation of common terms into application specific settings doesn't occur until the // transformation stage - so we don't have the full list of app-specific settings to filter until now. for (var settingName in handlerSpec.supportedSettings) { if (handlerSpec.settings && settingName in handlerSpec.settings) { settings[settingName] = handlerSpec.settings[settingName]; } } } returnObj[solutionId] = [{ settings: settings, options: handlerSpec.options }]; return returnObj; }; // Transform the response from the handler SET to a format that we can persist in models before passing to handler SET on restore // - "oldValue" becomes {type: "ADD", value: <oldValue>} // - `undefined` value becomes {type: "DELETE"} gpii.lifecycleManager.responseToSnapshot = function (solutionId, handlerResponse) { var unValued = gpii.settingsHandlers.setResponseToSnapshot(handlerResponse); var armoured = gpii.settingsHandlers.settingsPayloadToChanges(unValued); // Note - we deal in these 1-element arrays just for simplicity in the LifecycleManager. A more efficient // implementation might send settings for multiple solutions to the same settingsHandler in a single request. // Note that in the session's snapshots, this level of array containment has been removed. return fluid.get(armoured, [solutionId, 0]); }; gpii.lifecycleManager.invokeSettingsHandlerGet = function (that, solutionId, handlerSpec) { // first prepare the payload for the settingsHandler in question - a more efficient // implementation might bulk together payloads destined for the same handler var settingsHandlerPayload = gpii.lifecycleManager.specToSettingsHandler(solutionId, handlerSpec); var resolvedName = that.nameResolver.resolveName(handlerSpec.type, "settingsHandler"); return gpii.settingsHandlers.dispatchSettingsHandlerGet(resolvedName, settingsHandlerPayload); }; /** * @param {Component} that - The gpii.lifecycleManager * @param {String} solutionId - The id of the solution for which the settings handler should be invoked * @param {Object} handlerSpec - A single settings handler specification. * @param {String} handlerType - The name of the handler block (i.e. "settings" or "launchers") * @return {Promise} - A promise that will be resolved with a settings snapshot. * Payload example: * http://wiki.gpii.net/index.php/Settings_Handler_Payload_Examples * Transformer output: * http://wiki.gpii.net/index.php/Transformer_Payload_Examples */ gpii.lifecycleManager.invokeSettingsHandlerSet = function (that, solutionId, handlerSpec, handlerType) { // first prepare the payload for the settingsHandler in question - a more efficient // implementation might bulk together payloads destined for the same handler var settingsHandlerPayload = gpii.lifecycleManager.specToSettingsHandler(solutionId, handlerSpec); var resolvedName = that.nameResolver.resolveName(handlerSpec.type, "settingsHandler"); var setSettingsPromise = gpii.settingsHandlers.dispatchSettingsHandlerSet(resolvedName, settingsHandlerPayload, that.options.retryOptions); var togo = fluid.promise(); var setSettingsFail = function (error) { togo.reject(error); var userMessageKey = handlerType === "launchHandlers" ? handlerSpec.settings.running ? "StartApplicationFail" : "StopApplicationFail" : "WriteSettingFail"; that.userErrors.events.userError.fire({ isError: true, messageKey: userMessageKey, originalError: error }); }; setSettingsPromise.then(function (handlerResponse) { // update the settings section of our snapshot to contain the new information var settingsSnapshot = gpii.lifecycleManager.responseToSnapshot(solutionId, handlerResponse); // Settings handlers may or may not return options (currently it seems they all do) - gain resistance to this by restoring the // original "options" supplied to them. fluid.each(handlerSpec, function (entry, key) { if (key !== "settings") { settingsSnapshot[key] = fluid.copy(entry); } }); togo.resolve(settingsSnapshot); }, setSettingsFail); return togo; }; gpii.lifecycleManager.invokeAction = function (action, nameResolver) { var resolvedName = nameResolver.resolveName(action.type, "action"); return fluid.invokeGradedFunction(resolvedName, action); }; /* Compensate for the effect of simpleminded merging when applied to a snapshot where an "DELETE" is merged on top of an "ADD" */ gpii.lifecycleManager.cleanDeletes = function (value) { if (value.type === "DELETE") { delete value.value; } return value; }; /* Remove all the settings blocks from a solutions registry entry */ gpii.lifecycleManager.removeSettingsBlocks = function (solutionEntry) { fluid.each(["settingsHandlers", "launchHandlers"], function (handlersBlock) { solutionEntry[handlersBlock] = fluid.transform(solutionEntry[handlersBlock], function (handler) { return fluid.filterKeys(handler, "settings", true); }); }); return solutionEntry; }; /* Applies snapshotted settings from a single settingsHandler block attached to a single solution into the "originalSettings" * model snapshot area in the LifecycleManager's session. Tightly bound to executeSettingsAction, executes one-to-one with it * with almost identical argument list. */ gpii.lifecycleManager.recordSnapshotInSession = function (that, snapshot, solutionId, solutionRecord, session, handlerType, settingsHandlerBlockName, rootAction) { if (rootAction === "start" || rootAction === "update") { var toSnapshot = gpii.lifecycleManager.removeSettingsBlocks(fluid.copy(solutionRecord)); // do not remove the settingshandlers blocks, since we need them when restoring the system. // This is particularly relevant for launch handlers, where we will need to run the "get" directives on logout to decide // whether we an application is running or not, and consequently, whether to run the "update" or "stop" block toSnapshot[handlerType][settingsHandlerBlockName] = snapshot; // keep the settings that are already stored from the // original snapshot, but augment it with any settings from the new snapshot // that were not present in the original snapshot. // // This is relevant when doing an update for obvious reasons // (and tested) in LifecycleManagerTests.js "Updating with normal reference to settingsHandler block, and 'undefined' value stored in snapshot" // // It is also relevant for logins ("start" root action), in case a solution is already running. // This would trigger a call to its "update" block. If that in turn eg. looks like the following // [ "stop", "configure", "start" ] we would want the original state recorded during the "stop" // action to persist - even when the "start" block is later run var mergedSettings = fluid.extend(true, {}, toSnapshot, session.model.originalSettings[solutionId]); var cleanedSettings = gpii.lifecycleManager.transformSolutionSettings(mergedSettings, gpii.lifecycleManager.cleanDeletes); session.applier.change(["originalSettings", solutionId], cleanedSettings); } else if (rootAction === "restore" || rootAction === "stop") { // no-op - during a restore action we don't attempt to create a further snapshot } else { fluid.fail("Unrecognised rootAction " + rootAction); } }; /* In the case we are servicing a "restore" rootAction, wrap "dangerous" promises which perform system actions so * that they do not reject, to ensure that we continue to try to restore the system come what may - GPII-2160 */ gpii.lifecycleManager.wrapRestorePromise = function (promise, rootAction) { return rootAction === "restore" && fluid.isPromise(promise) ? gpii.rejectToLog(promise, " while restoring journal snapshot") : promise; }; /** * @param {Object} that - The lifecycle manager component. * @param {String} solutionId - The ID of the solution for which to execute the settings. * @param {Object} solutionRecord - The solution registry entry for the solution. * @param {Component} session - The current session component. This function will attach the * solution record to the 'currentSettings' of the session's model (if successful). * @param {String} handlerType - The name of the handler block (i.e. "settings" or "launchers"). * @param {String} settingsHandlerBlockName - Should be a reference to a settings block from the * settingsHandlers section. * @param {String} rootAction - The root action on the LifecycleManager which is being serviced: "start", "stop", * "update", "restore" or "isRunning". * @return {Function} A nullary function (a task), that once executed will set the settings returning a promise * that will be resolved once the settings are successfully set. */ gpii.lifecycleManager.executeSettingsAction = function (that, solutionId, solutionRecord, session, handlerType, settingsHandlerBlockName, rootAction) { var settingsHandlerBlock = solutionRecord[handlerType][settingsHandlerBlockName]; if (settingsHandlerBlock === undefined) { fluid.fail("Reference to non-existing settingsHandler block named " + settingsHandlerBlockName + " in solution " + solutionId); } return function () { var expanded = session.localResolver(settingsHandlerBlock); if (rootAction === "isRunning") { // only run get and return directly if where checking for running applications return that.invokeSettingsHandlerGet(solutionId, expanded); } else { var settingsPromise = that.invokeSettingsHandlerSet(solutionId, expanded, handlerType); settingsPromise.then(function (snapshot) { session.applier.change(["currentSettings", solutionId], solutionRecord); gpii.lifecycleManager.re