All files / universal.klown/gpii/node_modules/lifecycleManager/src LifecycleManager.js

95.82% Statements 275/287
86.24% Branches 94/109
100% Functions 65/65
95.82% Lines 275/287

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876                                    3x 3x   3x   3x                                                                                                                                                                                                                                                                             3x             3x                           3x 292x 876x 498x                 3x 128x     128x 128x 128x       3x 484x           3x 262x     3x 863x 863x     863x 863x 111x   752x               3x 1377x 1377x   1377x   952x             425x 2901x 2471x       1377x         1377x           3x 1036x 1036x       1036x     3x     342x 342x 342x                   3x     1033x 1033x 1033x 1033x   1033x   1032x     1032x 4245x 3213x     1032x     1033x       3x 4x 4x         3x 2810x 2076x   2810x         3x 571x 1142x 1041x     571x             3x 1030x 571x         571x                       571x 571x 571x 459x                   3x 1031x                                   3x 1371x 1371x       1371x 1371x 1371x 340x   1031x   1031x 1030x 1030x     1031x                             3x 1023x 1023x 2046x 1823x     2046x 309x       1023x           3x 156x 156x                             3x 1x 1x 1x                               3x 13x 13x 12x   1x 1x 1x 1x   13x                       3x 1274x 1274x   328x 328x   946x   357x 396x   589x   50x 50x 10x 40x 29x     1274x                                           3x 1836x 1836x       1836x 1558x     1554x 689x 689x 865x 682x   682x   340x     682x 682x 682x 183x         183x 183x 183x               4x 4x 4x 4x 4x           1836x                                 3x 671x   1049x 1049x     671x                               3x 604x   604x 181x 109x   72x     423x 269x   154x     604x                   3x 213x 213x 296x     296x   296x 296x 296x 296x 296x   296x   296x       213x     213x             3x   1x 1x 1x 1x 1x   1x 1x             3x   296x 296x 168x 168x 168x       3x 604x 604x 604x 604x 604x 604x 604x   604x                           3x 516x 516x 516x 516x 350x   516x     3x 4x 4x 4x       3x   512x   512x 512x       512x   512x   508x   508x 162x   346x     4x   4x 4x 4x                   3x 212x 212x 212x         212x   212x 212x 212x   212x           3x 65x 65x 65x 65x 2x 2x 2x   63x   63x 63x 67x 67x   63x   67x   63x     3x 221x 221x 221x         221x 221x       221x 221x           221x 221x 308x 308x     308x     308x 308x   308x 308x     308x           221x        
/*!
 * 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");
var gpii = fluid.registerNamespace("gpii");
 
(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
        },
        components: {
            variableResolver: {
                type: "gpii.lifecycleManager.variableResolver"
            },
            nameResolver: {
                type: "gpii.lifecycleManager.nameResolver"
            }
        },
        dynamicComponents: {
            sessions: {
                type: "{arguments}.0", // a session grade derived from gpii.lifecycleManager.session
                options: {
                    userToken: "{arguments}.1"
                },
                createOnEvent: "onSessionStart"
            }
        },
        members: {
            sessionIndex: {}, // map of userToken to session component member name, managed by gpii.lifecycleManager.sessionIndexer
            /* queue for high-level lifecycle manager tasks (start, stop or update).
             * The entries in the 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
             */
            queue: [],
            isProcessingQueue: false // if queue is currently being processed
        },
        model: {
            logonChange: {
                type: undefined, // "login"/"logout"
                inProgress: false, // boolean
                userToken: undefined, // string with user token
                timeStamp: 0
            }
        },
        events: {
            onSessionStart: null,          // fired with [gradeName, userToken]
            onSessionSnapshotUpdate: null, // fired with [{lifecycleManager}, {session}, originalSettings]
            onSessionStop: null            // fired with [{lifecycleManager}, {session}
        },
        listeners: {
            "onSessionSnapshotUpdate.log": "gpii.lifecycleManager.logSnapshotUpdate",
            "onCreate.createQueueFunctions": "gpii.lifecycleManager.createQueueFunctions"
        },
        queueFunctions: {
            expander: { // manually expanded via the createQueueFunctions function
                type: "fluid.noexpand",
                value: {
                    stop: "{that}.processStop",
                    start: "{that}.processStart",
                    update: "{that}.processUpdate"
                }
            }
        },
        invokers: {
            getActiveSessionTokens: {
                funcName: "gpii.lifecycleManager.getActiveSessionTokens",
                args: "{that}"
            },
            addLifecycleInstructionsToPayload: {
                funcName: "gpii.lifecycleManager.addLifecycleInstructionsToPayload",
                args: [ "{arguments}.0" ] // fullPayload
            },
            /** Accepts an array of user tokens, of which all but the first will currently be ignored. Returns the session
              * component corresponding to that user, if they have an active session. A typical usage pattern is to call
              * "getActiveSessionTokens" and send its return to "getSession"
              */
            getSession: {
                funcName: "gpii.lifecycleManager.getSession",
                args: ["{that}", "{arguments}.0"] // user token
            },
            processQueue: {
                funcName: "gpii.lifecycleManager.processQueue",
                args: ["{that}", "{that}.queue"]
            },
            addToQueue: {
                funcName: "gpii.lifecycleManager.addToQueue",
                args: ["{that}", "{that}.queue", "{arguments}.0"]
            },
            // 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)
            // 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}", "{arguments}.0"]
            },
            processStop: { // should not be used directly, use the manually created 'stop' invoker instead
                funcName: "gpii.lifecycleManager.processStop",
                args: [ "{that}", "{arguments}.0"]
            },
            processUpdate: { // should not be used directly, use the manually created 'update' invoker instead
                funcName: "gpii.lifecycleManager.processUpdate",
                args: [ "{that}", "{arguments}.0"]
            },
            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"]
                                  // solutionId, settingsHandlers
            },
            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"
        }
    });
 
    // Manually constructs the functions in the queueFunctions options.
    // This done manually to avoid resolving the "{that}" part of the function
    // to be called (e.g. "{that}.processStart")
    gpii.lifecycleManager.createQueueFunctions = function (that) {
        fluid.each(that.options.queueFunctions, function (queueFunc, invokerName) {
            that[invokerName] = function (arg) {
                return that.addToQueue({
                    func: queueFunc,
                    invokerName: invokerName,
                    arg: arg
                });
            };
        });
    };
 
    gpii.lifecycleManager.addLifecycleInstructionsToPayload = function (fullPayload) {
        fullPayload.activeConfiguration = {
            inferredConfiguration: fluid.extend(true, {}, fullPayload.matchMakerOutput.inferredConfiguration[fullPayload.activeContextName])
        };
        var lifecycleInstructions = gpii.transformer.configurationToSettings(fullPayload.activeConfiguration.inferredConfiguration, fullPayload.solutionsRegistryEntries);
        fluid.set(fullPayload, "activeConfiguration.lifecycleInstructions", lifecycleInstructions);
        return fullPayload;
    };
 
 
    gpii.lifecycleManager.logSnapshotUpdate = function (lifecycleManager, session, originalSettings) {
        fluid.log("Settings for session " + session.id + " created at " + session.createTime + " updated to ", originalSettings);
    };
 
    // Will return one of the user's token keys for an active session
    // TODO: We need to implement logic to ensure at most one of these is set, or
    // to manage logic for superposition of sessions if we permit several (see GPII-102)
    gpii.lifecycleManager.getActiveSessionTokens = function (that) {
        return fluid.keys(that.sessionIndex);
    };
 
    gpii.lifecycleManager.getSession = function (that, userTokens) {
        userTokens = fluid.makeArray(userTokens);
        Iif (userTokens.length === 0) {
            fluid.fail("Attempt to get sessions without keys");
        } else {
            var memberKey = that.sessionIndex[userTokens[0]];
            if (memberKey === undefined) {
                return undefined;
            }
            return that[memberKey];
        }
    };
 
    /** 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 handlerSpec {Object} A single settings handler specification
    * 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) {
        // 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();
 
        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);
        }, togo.reject);
 
        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));
 
            // d 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 Eif (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 that {Object} The lifecycle manager component
     * @param solutionId {String} the ID of the solution for which to execute the settings
     * @param solutionRecord {Object} The solution registry entry for the solution
     * @param session {Component} The current session component. This function will attach the
     *     solution record to the 'appliedSolutions' of the session's model (if successful)
     * @param handlerType {String} The name of the handler block (i.e. "settings" or "launchers")
     * @param settingsHandlerBlockName {String} should be a reference to a settings block from the
     *     settingsHandlers section.
     * @param rootAction {String} 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];
        Iif (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);
 
                settingsPromise.then(function (snapshot) {
                    session.applier.change(["appliedSolutions", solutionId], solutionRecord);
                    gpii.lifecycleManager.recordSnapshotInSession(that, snapshot, solutionId, solutionRecord, session,
                        handlerType, settingsHandlerBlockName, rootAction);
                });
                return gpii.lifecycleManager.wrapRestorePromise(settingsPromise, rootAction);
            }
        };
    };
 
    /** For the complete entry for a single solution, transform each settingsHandler block by a supplied function - traditionally
     * either gpii.settingsHandlers.changesToSettings or gpii.settingsHandlers.settingsToChanges .
     * This is traditionally called during the "stop" action to unarmour all the settingsHandler blocks by converting from changes
     * back to settings. In a future version of the SettingsHandler API, this will not be necessary.
     * This is called during the "stop" action to convert the snapshotted "originalSettings" model material back to material
     * suitable for being sent to executeSettingsAction, as well as at the corresponding point during the "journal restore"
     * operationf
     * @param solutionSettings {Object} A settings block for a single solution, holding a member named `settingsHandlers`
     * @param transformer {Function} A function which will transform one settingsHandlers block in the supplied `solutionSettings`
     */
    gpii.lifecycleManager.transformSolutionSettings = function (solutionSettings, transformer) {
        var togo = fluid.copy(solutionSettings); // safe since armoured
        fluid.each(["settingsHandlers", "launchHandlers"], function (handlersBlock) {
            togo[handlersBlock] = fluid.transform(solutionSettings[handlersBlock], function (handler) {
                return gpii.settingsHandlers.transformOneSolutionSettings(handler, transformer);
            });
            // avoid the handlerBlock to be `undefined` if it's not defined in solutionsRegistry entry. This is necessary for testing assertion purposes
            if (togo[handlersBlock] === undefined) {
                delete togo[handlersBlock];
            }
        });
 
        return togo;
    };
 
    /** As for gpii.lifecycleManager.transformSolutionSettings, only transforms the complete collection of stored solutions
      * (e.g. a value like session.model.originalSettings)
      */
    gpii.lifecycleManager.transformAllSolutionSettings = function (allSettings, transformer) {
        return fluid.transform(allSettings, function (solutionSettings) {
            return gpii.lifecycleManager.transformSolutionSettings(solutionSettings, transformer);
        });
    };
 
    /** Upgrades a promise rejection payload (or Error) by suffixing an additional "while" reason into its `message` field. If the
     * payload is already an Error, its `message` field will be updated in place, otherwise a shallow clone of the original payload
     * will be taken to perform the update.
     * @param originError {Object|Error} A rejection payload. This should (at least) have the member `isError: true` set, as well
     *     as a String `message` holding a rejection reason.
     * @param whileMsg {String} A message describing the activity which led to this error
     * @return {Object} The rejected payload formed by shallow cloning the supplied argument (if it is not an `Error`) and
     *     suffixing its `message` member
     */
    // TODO: Duplicate of kettle.upgradeError to avoid dependence on Kettle in this file. This needs to go into a new module once
    // Kettle is factored up.
    gpii.upgradeError = function (originError, whileMsg) {
        var error = originError instanceof Error ? originError : fluid.extend({}, originError);
        error.message = originError.message + whileMsg;
        return error;
    };
 
    /** Transform a promise into one that always resolves, by intercepting its reject action and converting it to a logging action
     * plus a resolve with an optionally supplied value.
     * The error payload's message will be logged to `fluid.log` with the priority `fluid.logLevel.WARN`.
     * @param promise {Promise} The promise to be transformed
     * @param whileMsg {String} A suffix to be applied to the message, by the action of the utility `gpii.upgradeError`.
     *     This will typically begin with the text " while"
     * @param resolveValue {Any} [optional] An optional value to be supplied to `resolve` of the returned promise, when the
     *     underlying promise rejects.
     * @return {Promise} The wrapped promise which will resolve whether the supplied promise resolves or rejects.
     */
    // TODO: In theory this is a fairly generic promise algorithm, but in practice there are some further subtleties - for
    // example, a better system would instead replace fluid.promise.sequence with a version that allowed some form of
    // "soft rejection" so that there might be a chance to signal failures to the user.
    gpii.rejectToLog = function (promise, whileMsg, resolveValue) {
        var togo = fluid.promise();
        promise.then(function (value) {
            togo.resolve(value);
        }, function (error) {
            gpii.upgradeError(error, whileMsg);
            fluid.log(fluid.logLevel.WARN, error.message);
            fluid.log(fluid.logLevel.WARN, error.stack);
            togo.resolve(resolveValue);
        });
        return togo;
    };
 
    /**
     * Infer an action block such as "start", "stop", "configure", etc.
     *
     * If some lifecycle block is not present, the system will use default actions for each block. These are as follows:
     * * "start", "stop" and "isRunning": will by default be references to all launch handler blocks
     * * "configure" and "restore": will by default be references to all settings handler blocks
     * * "update": depends on the solutions "liveness" value where "live" means set settings only and
     *             "liveRestart"/"manualRestart"/"OSRestart" means a [stop, configure, start] cycle
     */
    gpii.lifecycleManager.inferActionBlockSteps = function (actionName, solutionRecord) {
        var steps = [];
        if (["start", "stop", "isRunning"].indexOf(actionName) !== -1 && solutionRecord.launchHandlers) {
            // grab all lifecycle block names and make these the steps
            steps = fluid.transform(Object.keys(solutionRecord.launchHandlers), function (stepName) {
                return "launchers." + stepName;
            });
        } else if (["configure", "restore"].indexOf(actionName) !== -1 && solutionRecord.settingsHandlers) {
            // grab all settingshandler block names and make these the steps
            steps = fluid.transform(Object.keys(solutionRecord.settingsHandlers), function (stepName) {
                return "settings." + stepName;
            });
        } else if (actionName === "update") {
            // check liveness:
            var liveness = gpii.matchMakerFramework.utils.getLeastLiveness([solutionRecord]);
            if (liveness === "live") {
                steps = [ "configure" ];
            } else if (["liveRestart", "manualRestart", "OSRestart"].indexOf(liveness) !== -1) {
                steps = [ "stop", "configure", "start"];
            }
        }
        return steps;
    };
 
    /** Called for each solution during "start", "stop" and "update" phases
     * Actions to be performed are held in array "actions" and the settingsHandlers block from "solutions" (either Transformer
     * output, or snapshot output from "start" phase) encodes the settings to be set.
     * Returns the results from the settings action present in the list, and builds up action returns in session state
     * "actionResults" field (so that these may be referenced from context expressions in further actions).
     * @param that {Component:LifecycleManager} The LifecycleManager instance of type `gpii.lifecycleManager`
     * @param solutionId {String} The id of the solution for which actions are to be performed
     * @param solutionRecord {Object} Either the solution's registry entry (on start) or the fabricated "restore" entry (on stop)
     * @param session {Component:LifecycleManagerSession} The LifecycleManager Session of type `gpii.lifecycleManager.session`
     * @param actionBlock {String} The key for the particular block in the solutions registry entry which is being acted on - this
     *    may differ from `rootAction` because, e.g. an "update" action may be serviced by stopping and starting the solution, etc.
     * @param rootAction {String} The root action on the LifecycleManager which is being serviced: "start", "stop", "update" or
     *    "restore"
     * @return {Promise} A promise that will resolve when all the sequential actions have been performed. Any returned values
     *     will be built up as a side-effect within session.appliedSolutions, session.originalSettings, session.actionResults etc.
     *     If the `rootAction` is "restore", this promise will never reject, and instead the system will continue to make best
     *     efforts to continue executing actions even in the face of failures.
     */
 
    gpii.lifecycleManager.executeActions = function (that, solutionId, solutionRecord, session, actionBlock, rootAction) {
        var steps = solutionRecord[actionBlock] || gpii.lifecycleManager.inferActionBlockSteps(actionBlock, solutionRecord);
        Iif (steps === undefined) {
            fluid.log("No " + actionBlock + " actions defined for solution " + solutionId);
            return fluid.promise().resolve();
        }
        var sequence = fluid.transform(steps, function (action) {
            if (typeof(action) === "string") {
                // if the action is a reference to a settings block (settings.<refName> where <refName> is a key to
                // the settings handler block)
                if (action.startsWith("settings.")) {
                    var settingsHandlerBlockName = action.substring("settings.".length);
                    return gpii.lifecycleManager.executeSettingsAction(that, solutionId, solutionRecord, session, "settingsHandlers", settingsHandlerBlockName, rootAction);
                } else if (action.startsWith("launchers.")) {
                    var launchHandlerBlockName = action.substring("launchers.".length);
 
                    if (actionBlock === "isRunning") {
                        // if we're just checking for the run state, don't actually modify the solutionRecord with settings:
                        solutionRecord = fluid.copy(solutionRecord);
                    }
                    // Set appropritate settings (i.e. { running: true } if we're in a start block, else { running: false }.
                    var launchSettings = { running: actionBlock === "start" ? true : false };
                    fluid.set(solutionRecord, [ "launchHandlers", launchHandlerBlockName, "settings"], launchSettings);
                    return gpii.lifecycleManager.executeSettingsAction(that, solutionId, solutionRecord, session, "launchHandlers", launchHandlerBlockName, rootAction);
                } else Eif (actionBlock === "update") {
                    // Keywords: "start", "stop", "configure" are allowed here as well, and
                    // and will result in evaluating the respective block
                    // TODO (GPII-1230) Fix this up so we don't always run the full start and stops (including)
                    // system restoration, etc.
                    Eif (action === "start" || action === "configure" || action === "stop") {
                        return function () {
                            return that.executeActions(solutionId, solutionRecord, session, action, rootAction);
                        };
                    } else {
                        fluid.fail("Unrecognised string action in LifecycleManager: " + action +
                            " inside 'update' section for solution " + solutionId);
                    }
                }
            } else { // We allow free lifecycle actions, but strongly discourage them, since we don't have control of logging state like with settings/launch handlers
                return function () {
                    var expanded = session.localResolver(action);
                    var result = gpii.lifecycleManager.invokeAction(expanded, that.nameResolver);
                    Eif (action.name) {
                        session.applier.change(["actionResults", action.name], result);
                    }
                    // TODO: It seems we have never supported asynchronous actions
                };
            }
        });
        return fluid.promise.sequence(sequence);
    };
 
    /** Invoked on "start", "update", "stop" and "restore" phases - in addition to forwarding to
     * gpii.lifecycleManager.executeActions, it is responsible for saving the settings that are being set (when the fullSnapshot
     * is true) and storing the list of applied solutions to the session state
     *
     * @param solutionId {String} the ID of the solution
     * @param solutionRecord {Object} a solution record with settings that are to be applied to the system
     * @param session {Object} the object holding the state of the system. This is updated in place by the settings application
     *     process, if `rootAction` is start.
     * @param lifecycleBlockKeys {Array} Array of ordered strings denoting which lifecycle blocks to run (supported values here
     *     are "configure", "start" and/or "update")
     * @param rootAction {String} Either "start", "update" or "stop" depending on the lifecycleManager phase which is executing
     * @return {Promise} The same promise yielded by executeActions - the stateful construction of the session state is tacked
     *     onto this promise as a side-effect
     */
    gpii.lifecycleManager.applySolution = function (that, solutionId, solutionRecord, session, lifecycleBlockKeys, rootAction) {
        var promises = fluid.transform(lifecycleBlockKeys, function (key) {
            // Courtesy to allow GPII-580 journalling tests to be expressed in-process - better expressed with FLUID-5790 cancellable promises
            Eif (!fluid.isDestroyed(that)) {
                return that.executeActions(solutionId, solutionRecord, session, key, rootAction);
            }
        });
        return fluid.promise.sequence(promises);
    };
 
    /**
     * Based on the current state of an application (ie. if it is running or not) and the desired state,
     * this function returns an array of actions for the lifecycle manager to run to get it to its
     * recorded state
     *
     * @param currentRunState {boolean}: True if application is currently running
     * @param desiredRunState {boolean}: True if application should be running
     * @param isRestore {boolean}: If this is true, a "restore" will be used as the configuration action
     *    in the array that is returned. Else a "configure" will be used. In general, isRestore should be provided and
     *    true only of this is called by the logout/restore functionality of the system.
     * @return {Array} An array of actions (strings) that needs to be run to restore the application
     *       to its original state
     */
    gpii.lifecycleManager.calculateLifecycleActions = function (currentRunState, desiredRunState, isRestore) {
        var configurationType = isRestore ? "restore" : "configure";
        var actions;
        if (currentRunState === true) { // if it's already running
            if (desiredRunState === false) { // and it was not running on start
                actions = [ "stop", configurationType ];
            } else { // else update it
                actions = [ "update" ];
            }
        } else { // if it is not running
            if (desiredRunState === true) { // and it was running when we started
                actions = [ configurationType, "start" ];
            } else { // just restore settings
                actions = [ "restore" ];
            }
        }
        return actions;
    };
 
    /** Common utility used by gpii.lifecycleManager.stop and gpii.lifecycleManager.restoreSnapshot
      * @param session {gpii.lifecycleManager.session} which must contain
      *   * A `originalSettings` snapshot in its model
      *   * A `localResolver` member for expanding material
      * @param rootAction {String} Must be either "stop" or "restore"
      * @return {Promise} A promise for the action of restoring the system
      */
    gpii.lifecycleManager.restoreSystem = function (that, session, rootAction) {
        var tasks = [];
        fluid.each(session.model.originalSettings, function (changesSolutionRecord, solutionId) {
            tasks.push(function () {
                // check the current state of the solution to decide whether we should run the
                // "restore", "update, or "stop"
                return that.getSolutionRunningState(solutionId, changesSolutionRecord, session);
            });
            tasks.push(function () {
                Eif (!fluid.isDestroyed(that)) { // See above comment for GPII-580
                    var solutionRecord = gpii.lifecycleManager.transformSolutionSettings(changesSolutionRecord, gpii.settingsHandlers.changesToSettings);
                    var recordedRunState = gpii.lifecycleManager.getSolutionRunningStateFromSnapshot(solutionRecord);
                    var currentRunState = session.model.runningOnLogin[solutionId];
 
                    var actions = gpii.lifecycleManager.calculateLifecycleActions(currentRunState, recordedRunState, true);
                    // build structure for returned values (for later reset)
                    return that.applySolution(solutionId, solutionRecord, session, actions, rootAction);
                }
            });
        });
        var sequence = fluid.promise.sequence(tasks);
 
        // // TODO: In theory we could stop all solutions in parallel
        return sequence;
    };
 
    /** Restore a snapshot of settings, perhaps captured in the journal. This constructs a "fake" session using the special user token "restore"
     * @param originalSettings {Object} The system snapshot to be restored
     * @return A promise for the action of restoring the system
     */
    gpii.lifecycleManager.restoreSnapshot = function (that, originalSettings) {
        // TODO: document/ensure that this token, as well as the special "reset", is reserved
        that.events.onSessionStart.fire("gpii.lifecycleManager.restoreSession", "restore");
        var session = that[that.sessionIndex.restore];
        session.applier.change("originalSettings", originalSettings);
        var restorePromise = gpii.lifecycleManager.restoreSystem(that, session, "restore");
        restorePromise.then(session.destroy, session.destroy);
 
        return fluid.promise.map(restorePromise, function () {
            return { // TODO: The standard response yield is unhelpful, consisting of the returns of any actions in "stop"
                message: "The system's settings were restored from a snapshot",
                payload: originalSettings
            };
        });
    };
 
    gpii.lifecycleManager.getSolutionRunningStateFromSnapshot = function (solutionSnapshot) {
        // get isRunning entry from snapshot and block to run:
        var isRunningBlock = fluid.makeArray(solutionSnapshot.isRunning || gpii.lifecycleManager.inferActionBlockSteps("isRunning", solutionSnapshot))[0];
        if (isRunningBlock && isRunningBlock.indexOf("launchers.") === 0) {
            var settingsHandlerBlockName = isRunningBlock.substring("launchers.".length);
            var recordedState = fluid.get(solutionSnapshot, ["launchHandlers", settingsHandlerBlockName, "settings", "running"]);
            return recordedState;
        }
    };
 
    gpii.lifecycleManager.getSolutionRunningState = function (that, solutionId, solutionRecord, session) {
        Eif (!fluid.isDestroyed(that)) {
            var promise = that.executeActions(solutionId, solutionRecord, session, "isRunning", "isRunning");
            var togo = fluid.promise();
            promise.then(function (val) {
                var isRunning = fluid.get(val, [0, solutionId, 0, "settings", "running"]);
                session.applier.change(["runningOnLogin", solutionId], isRunning);
                togo.resolve(isRunning);
            });
            return togo;
        }
    };
 
    /**
     * The lifecycleManager queue is used to hold the high-level actions that needs to happen,
     * such as starting the login process, starting logout process, starting the update process.
     * The entries in the queue are of the format { func: <unresolvedFunctionToCall>, invokerName: <invokerName>, arg: <argument> } where
     * <unresolvedFunctionToCall> is the name of a single-argument function that returns a promise. The promise should be resolved when
     * the function is complete (including side-effects). <InvokerName> is the name of the invoker that was called on the lifecycleManager
     * for triggering this addToQueue function.
     * The queue is run sequentially, and an item is considered "done" once the promise returned by its
     * function is resolved.
     */
    gpii.lifecycleManager.addToQueue = function (that, queue, item) {
        var newItem = fluid.copy(item);
        newItem.promise = fluid.promise();
        queue.push(newItem);
        if (!that.isProcessingQueue) {
            that.processQueue();
        }
        return newItem.promise;
    };
 
    gpii.lifecycleManager.clearQueue = function (queue) {
        while (queue.length > 0) {
            var item = queue.shift();
            item.promise.reject("clearQueue: clearing lifecycleManager promise from queue - remaining items: " + queue.length);
        }
    };
 
    gpii.lifecycleManager.processQueue = function (that, queue) {
        // mark that we're currently processing queue
        that.isProcessingQueue = true;
        // pick first item and process
        var item = queue.shift();
        var func = fluid.makeInvoker(that, {
            func: item.func,
            args: [ item.arg ]
        }, "");
        var promise = func();
 
        promise.then(function (val) {
            // resolve the original promise of the item;
            item.promise.resolve(val);
            // process next item in queue if it exists
            if (queue.length > 0) {
                that.processQueue(queue);
            } else {
                that.isProcessingQueue = false;
            }
        }, function (error) {
            fluid.log(fluid.logLevel.ERROR, "An error occurred in an item of the lifecyclemanager's queue" +
                "(invoker: " + item.invokerName + ", func: " + item.func + "), so clearing queue. Error was", error);
            item.promise.reject();
            gpii.lifecycleManager.clearQueue(queue);
            that.isProcessingQueue = false;
        });
    };
 
    /**
     * Structure of lifecycleManager options:
     * userid: userid,
     * actions: either start or stop configuration from solutions registry
     * settingsHandlers: transformed settings handler blocks
     */
    gpii.lifecycleManager.processStop = function (that, options) {
        var userToken = options.userToken;
        var session = that.getSession([userToken]);
        Iif (!session) {
            var failPromise = fluid.promise();
            failPromise.reject("No session was found when attempting keyout");
            return failPromise;
        }
        var restorePromise = gpii.lifecycleManager.restoreSystem(that, session, "stop");
 
        restorePromise.then(function () {
            that.events.onSessionStop.fire(that, session);
            session.destroy();
        });
        return restorePromise;
    };
 
    /**
     * Update user preferences.
     */
    gpii.lifecycleManager.processUpdate = function (that, finalPayload) {
        var userToken = finalPayload.userToken,
            lifecycleInstructions = finalPayload.activeConfiguration.lifecycleInstructions;
        var session = that.getSession([userToken]);
        if (!session) { // if user has logged out since the update was added to queue
            var msg = "User with token " + userToken + " has no active session, so ignoring update request";
            fluid.log(msg);
            return fluid.promise().resolve(msg);
        }
        var appliedSolutions = session.model.appliedSolutions;
 
        var promises = [];
        fluid.each(lifecycleInstructions, function (solution, solutionId) {
            var sol = fluid.copy(solution);
            if (appliedSolutions[solutionId]) {
                // merge already applied settings with the updates
                sol = fluid.extend(true, {}, appliedSolutions[solutionId], sol);
            }
            promises.push(that.applySolution(solutionId, sol, session, [ "update" ], "update"));
        });
        return fluid.promise.sequence(promises);
    };
 
    gpii.lifecycleManager.processStart = function (that, finalPayload) {
        var userToken = finalPayload.userToken,
            lifecycleInstructions = finalPayload.activeConfiguration.lifecycleInstructions;
        Iif (that.sessionIndex[userToken]) {
            var failPromise = fluid.promise();
            failPromise.reject("User already logged in when processing start item in queue. Aborting");
            return failPromise;
        }
        that.events.onSessionStart.fire("gpii.lifecycleManager.userSession", userToken);
        var session = that[that.sessionIndex[userToken]];
        // TODO: Make global map of all users of session state
        //   activeConfiguration: Assigned in contextManager.updateActiveContextName, consumed in UserUpdate
        //   userToken, solutionsRegistryEntries: Assigned in initialPayload, consumed in UserUpdate
        var filteredPayload = fluid.filterKeys(finalPayload, ["userToken", "preferences", "activeContextName", "activeConfiguration", "solutionsRegistryEntries", "matchMakerOutput"]);
        session.applier.change("", filteredPayload); // TODO: One day after the applier is refactored this will explicitly need to be a "MERGE"
 
        // This "start" action will result in the original settings of the system (i.e. those that
        // were on the system before the user logged in) being stored inside session.model.originalSettings.
        // When "stop" is called this payload will be used to restore the settings back to their
        // original state.
        var tasks = [];
        fluid.each(lifecycleInstructions, function (solution, solutionId) {
            tasks.push(function () {
                Eif (!fluid.isDestroyed(that)) { // See above comment for GPII-580
                    // check the current state of the solution to decide whether we should run the
                    // "update", "start" or "stop" directive
                    return that.getSolutionRunningState(solutionId, solution, session);
                }
            });
            tasks.push(function () {
                Eif (!fluid.isDestroyed(that)) { // See above comment for GPII-580
                    // if solution is already running, call "update" directive - else use "start" directive
                    var isRunning = session.model.runningOnLogin[solutionId];
                    var actions = gpii.lifecycleManager.calculateLifecycleActions(isRunning, solution.active);
 
                    // build structure for returned values (for later reset)
                    return that.applySolution(solutionId, solution, session, actions, "start");
                }
            });
        });
        // Note that these promises are only sequenced for their side-effects (the ones on session state within applySolution and on the system at large
        // via the settings handlers)
        return fluid.promise.sequence(tasks);;
    };
 
})();