gpii-universal
Version:
Cross platform, core components of the GPII personalization infrastructure.
274 lines (252 loc) • 10.4 kB
JavaScript
/**
* 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");
}
};