UNPKG

gpii-universal

Version:

Cross platform, core components of the GPII personalization infrastructure.

274 lines (252 loc) 10.4 kB
/** * GPII Journal.js * * Responsible for journalling the state of the system's settings in order to be able to recover from system crashes or corruption of the settings state * * Copyright 2016 Raising the Floor - International * * Licensed under the New BSD license. You may not use this file except in * compliance with this License. * */ "use strict"; var fluid = require("infusion"), kettle = fluid.registerNamespace("kettle"), gpii = fluid.registerNamespace("gpii"), fs = require("fs"), writeFileAtomic = require("write-file-atomic"), glob = require("glob"); /** Manages reading and writing of journal files held within the "GPII settings directory", * managed in a suitable platform-specific area of the user's settings files. */ fluid.defaults("gpii.journal", { gradeNames: "fluid.component", maxOldJournals: 20, components: { settingsDir: { type: "gpii.settingsDir" } }, events: { onUpdateSnapshot: null }, listeners: { "onCreate.readJournals": "{that}.readJournals()", "onCreate.trimJournals": { funcName: "gpii.journal.trimJournals", args: "{that}", priority: "after:readJournals" }, "onCreate.findCrashed": { funcName: "gpii.journal.findCrashed", args: "{that}", priority: "after:trimJournals" } }, invokers: { readJournals: "gpii.journal.readJournals({that})", // args: {that}, {session}, {originalSettings}, closed writeJournal: "gpii.journal.writeJournal" } }); /** A mixin grade supplied to gpii.journal accounting for its interaction with a lifecycleManager. * In the main architecture this is supplied with the journal's definition in gpii.flowManager.local, * which also incidentally assures that this component will definitely be constructed after the * lifecycleManager. */ fluid.defaults("gpii.journalLifecycleManager", { gradeNames: "fluid.component", listeners: { "{lifecycleManager}.events.onSessionSnapshotUpdate": { namespace: "writeJournal", func: "{that}.writeJournal", args: ["{that}", "{arguments}.1", "{arguments}.1.model.originalSettings", false] }, "{lifecycleManager}.events.onSessionStop": { namespace: "writeJournal", func: "{that}.writeJournal", args: ["{that}", "{arguments}.1", "{arguments}.1.model.originalSettings", true] } }, invokers: { restoreJournal: { funcName: "gpii.journal.restoreJournal", args: ["{that}", "{lifecycleManager}", "{arguments}.0"] // journalId } } }); gpii.journal.restoreJournal = function (journalThat, lifecycleManager, journalId) { var parsed = gpii.journal.parseId(journalId); if (parsed.isError) { fluid.fail(parsed.message); } else { fluid.log("Restoring journal with id " + journalId); var journal = gpii.journal.fetchJournal(journalThat.journalFiles, parsed); if (!journal) { return fluid.promise().reject({ isError: true, message: "journal Id " + journalId + " could not be looked up to a journal" }); } else { fluid.log("Restoring journal snapshot with contents " + JSON.stringify(journal, null, 2)); return lifecycleManager.restoreSnapshot(journal.originalSettings); } } }; /** A mixin grade supplied to gpii.journal which converts it into a kettle app hosting an HTTP endpoint * currently just exposing the single function of restoring a particular journal */ fluid.defaults("gpii.journalApp", { gradeNames: "kettle.app", invokers: { dateToRestoreUrl: "gpii.journalApp.dateToRestoreUrl({that}, {kettle.server}, {arguments}.0)" }, serverUrl: "http://localhost", // TODO: provide a scheme either in Kettle or in universal for discovering this listeners: { "{lifecycleManager}.events.onSessionStart": { namespace: "logRestoreUrl", func: "gpii.journalApp.logRestoreUrl", args: ["{that}", "{arguments}.0"] } }, templates: { listJournals: "<html><head><title>Journals List</title></head><body><p>Click on a journal link to restore the system to its state at that time </p>%journals</body></html>", journalLink: "<p class=\"fl-snapshot\"><a href=\"%journalUrl\">System snapshot taken at %journalTime</a>%crashedString</p>" }, requestHandlers: { restore: { route: "/journal/restore/:journalId", method: "get", type: "gpii.journal.restoreJournal.handler" }, journals: { route: "/journal/journals.html", method: "get", type: "gpii.journal.journals.handler" } } }); gpii.journalApp.dateToRestoreUrl = function (that, server, date) { var baseUrl = that.options.serverUrl + ":" + server.options.port; var pathTemplate = that.options.requestHandlers.restore.route.replace(":", "%"); var journalId = ">" + new Date(date).toISOString(); var pathReplaced = kettle.dataSource.URL.resolveUrl(pathTemplate, {journalId: journalId}); var fullUrl = baseUrl + pathReplaced; return fullUrl; }; gpii.journalApp.logRestoreUrl = function (that, gradeName) { if (gradeName === "gpii.lifecycleManager.userSession") { var fullUrl = that.dateToRestoreUrl(Date.now()); fluid.log(fluid.logLevel.WARN, "New user session starting: In future, you can visit the URL " + fullUrl + " to restore the system's settings as they were at this point"); } }; fluid.defaults("gpii.journal.restoreJournal.handler", { gradeNames: ["kettle.request.http"], invokers: { handleRequest: { funcName: "gpii.journal.restoreJournal.handleRequest", args: [ "{gpii.journal}", "{that}"] } } }); gpii.journal.restoreJournal.handleRequest = function (journal, request) { var journalId = request.req.params.journalId; var restorePromise = journal.restoreJournal(journalId); fluid.promise.follow(restorePromise, request.handlerPromise); }; fluid.defaults("gpii.journal.journals.handler", { gradeNames: ["kettle.request.http"], invokers: { handleRequest: { funcName: "gpii.journal.journals.handleRequest", args: [ "{gpii.journal}", "{that}"] } } }); gpii.journal.journals.handleRequest = function (journal, request) { journal.readJournals(); var links = fluid.transform(journal.journalFiles, function (journalFile) { var terms = { journalUrl: journal.dateToRestoreUrl(journalFile.createTime), journalTime: new Date(journalFile.createTime).toLocaleString(), crashedString: journalFile.closed ? "" : ": crashed session" }; return fluid.stringTemplate(journal.options.templates.journalLink, terms); }); var linkBlock = links.join("\n"); var page = fluid.stringTemplate(journal.options.templates.listJournals, { journals: linkBlock }); request.res.header("Content-Type", "text/html"); request.events.onSuccess.fire(page); }; gpii.journal.writeJournal = function (that, session, snapshot, closed) { // This is a temporary fix to work around GPII-3791 issue before the proper fix is in place. if (!session.createTime) { fluid.log("Journal: stop proceeding with writeJournal() as session.createTime is undefined"); return; } var dir = that.settingsDir.getGpiiSettingsDir(); var filename = "journal-" + gpii.journal.formatTimestamp(session.createTime) + ".json"; var payload = { gpiiKey: session.model.gpiiKey, closed: closed, createTime: session.createTime, originalSettings: snapshot }; var formatted = JSON.stringify(payload, null, 4); var fullPath = dir + "/" + filename; fluid.log("Writing journal file to path " + fullPath); writeFileAtomic.sync(fullPath, formatted); }; // A comparator to sort journal entries in increasing order of age (newest first) gpii.journal.dateComparator = function (journalA, journalB) { return journalB.createTime - journalA.createTime; }; gpii.journal.readJournals = function (that) { var dir = that.settingsDir.getGpiiSettingsDir(); var journalFileNames = glob.sync(dir + "/journal-*.json"); fluid.log("Got journalFileNames ", journalFileNames); var journalFiles = fluid.transform(journalFileNames, function (journalFileName) { var readPromise = kettle.JSON.readFileSync(journalFileName, "reading journal file " + journalFileName); var togo; readPromise.then(function (read) { togo = read; }, function (err) { fluid.log(fluid.logLevel.WARN, "Error reading journal file " + journalFileName + ": " + err.message); togo = err; }); togo.journalFileName = journalFileName; return togo; }); journalFiles.sort(gpii.journal.dateComparator); that.journalFiles = journalFiles; }; gpii.journal.trimJournals = function (that) { if (that.journalFiles.length > that.options.maxOldJournals) { for (var i = that.options.maxOldJournals; i < that.journalFiles.length; ++i) { var journal = that.journalFiles[i]; var fileName = journal.journalFileName; fluid.log("Removing old journal file " + fileName + " since maximum number of old journals is " + that.options.maxOldJournals); try { fs.unlinkSync(fileName); } catch (e) { fluid.log(fluid.logLevel.WARN, "Error deleting old journal " + fileName + ": " + e); } } that.journalFiles.length = that.options.maxOldJournals; } }; gpii.journal.findCrashed = function (that) { var firstCrashed = fluid.find_if(that.journalFiles, function (journalFile) { return journalFile.closed === false; }); if (firstCrashed) { that.crashedJournal = firstCrashed; fluid.log(fluid.logLevel.WARN, "Found crashed journal file on startup: " + JSON.stringify(firstCrashed, null, 2)); } else { fluid.log("Found no crashed journals on startup"); } };