gpii-universal
Version:
Cross platform, core components of the GPII personalization infrastructure.
902 lines (833 loc) • 88 kB
JavaScript
/*!
* 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