| 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 |
5x
5x
5x
5x
5x
160x
160x
5x
189x
189x
189x
189x
5x
491x
491x
5x
5x
2106x
2154x
2230x
2230x
5x
1302x
1302x
1302x
6069x
6069x
1302x
5x
990x
990x
5x
220x
239x
5x
100x
74x
26x
2x
24x
90x
5x
883x
883x
988x
988x
988x
988x
883x
883x
883x
883x
988x
988x
883x
883x
5x
2471x
5x
1119x
5x
495x
5x
5x
495x
2379x
5x
56x
56x
56x
56x
56x
5x
56x
56x
56x
56x
56x
56x
56x
56x
5x
492x
492x
492x
492x
1x
1x
5x
492x
5x
56x
56x
56x
56x
56x
56x
56x
5x
8x
8x
8x
26x
26x
8x
5x
13x
13x
13x
13x
13x
13x
35x
35x
35x
35x
35x
13x
13x
13x
13x
5x
17x
17x
2x
2x
15x
15x
5x
7x
7x
5x
15x
15x
15x
15x
15x
15x
7x
7x
7x
15x
5x
13x
13x
5x
3x
6x
5x
3x
7x
5x
6x
6x
6x
10x
| /*!
GPII Settings Handler Utilities
Copyright 2013 OCAD University
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"),
fs = typeof(require) === "undefined" ? null : require("fs"), // courtesy to run web-based tests for partial file contents
kettle = fluid.registerNamespace("kettle"),
gpii = fluid.registerNamespace("gpii");
/***************************************************************************************************
* Low-level settings handler utilities, of use to practically anyone dealing with settings handlers
*/
// A useful utility to defer an activity by a fixed time, together with the necessary callback wrapping
// This or an equivalent MUST be used by anyone within the GPII who wishes to defer an activity
gpii.invokeLater = function (callback, time) {
var wrapped = kettle.wrapCallback ? kettle.wrapCallback(callback) : callback;
return time ? setTimeout(wrapped, time) : fluid.invokeLater(wrapped);
};
// Low-quality utility to force resolution of a value which is known to be either
// a plain value or a synchronous promise
gpii.resolveSync = function (response) {
Eif (fluid.isPromise(response)) {
response.then(function (resolved) {
response = resolved;
});
}
return response;
};
// Coerce a value or promise to a promise - this should really go in the core framework
gpii.toPromise = function (value) {
Eif (fluid.isPromise(value)) {
return value;
} else {
var togo = fluid.promise();
togo.resolve(value);
return togo;
}
};
fluid.registerNamespace("gpii.settingsHandlers");
// TODO: These utilities all clearly form part of some wider idiom of "payload handling" without some underpinnings
// for which they are rather hard to follow. For example, they could benefit from some form of "JSON type system"
// (whether provided via JSON schema or otherwise) in order to provide landmarks within the payloads, as well
// as actually validating arguments to these functions and payloads in general.
// cf. some vaguely related art in F# "Type Providers": http://fsharp.github.io/FSharp.Data/library/JsonProvider.html
/** A general utility function which helps in transforming settings handler payloads - these algorithms generally
* must iterate over the two levels of containment (per-solution, per-settings) and then apply some transform
* to the nested values
* @param payload {Object} A settings handler payload (either GET/SET response or return). The top-level keys will be solution ids
* @param handler {Function} A function whose signature is (oneSetting, [path], oneSolution) where -
* - oneSetting is the nested payload (i.e. the level containing "settings"/"options")
* - path is an array holding the path to this payload i.e. [solutionId, index]
* - oneSolution is the payload at the outer level of containment - i.e. payload[solutionId]
* @return The transformed payload-structured value
*/
gpii.settingsHandlers.transformPayload = function (payload, handler) {
return fluid.transform(payload, function (oneSolution, solutionPath) {
return fluid.transform(oneSolution, function (oneSetting, settingPath) {
var path = [solutionPath, settingPath];
return handler(oneSetting, path, oneSolution);
});
});
};
/** Transform the settings for one solution's worth of settings by a supplied transform.
* @param oneSolution {Object} One solution's worth of settings, with a member named `settings` containing a free hash of the settings indexed by settingsHandlerBlock name
* @param handler {Function} A transformer whose signature is the same as the 2nd argument to gpii.settingsHandlers.transformPayload
* @param path {Array of String} (optional) An array of path segments holding the path of oneSolution within an overall payload - this will be used to form the 2nd argument of `handler`
*/
gpii.settingsHandlers.transformOneSolutionSettings = function (oneSolution, handler, path) {
path = path || [];
var togo = fluid.censorKeys(oneSolution, "settings");
togo.settings = fluid.transform(oneSolution.settings, function (oneSetting, settingKey) {
var innerPath = path.concat(["settings", settingKey]);
return handler(oneSetting, innerPath, oneSolution);
});
return togo;
};
/** As for gpii.settingsHandlers.transformPayload, only the transform runs one level deeper and iterates over any
* "settings" block held within the payload.
*/
gpii.settingsHandlers.transformPayloadSettings = function (payload, handler) {
return gpii.settingsHandlers.transformPayload(payload, function (oneSolution, path) {
return gpii.settingsHandlers.transformOneSolutionSettings(oneSolution, handler, path);
});
};
/** Extract just the settings (eliminating "options") from the nested settingsHandler blocks for a full payload (top-level keys are solution ids)
* - this is used from gpii.settingsHandlers.comparePayloads and gpii.test.checkConfiguration
*/
gpii.settingsHandlers.extractSettingsBlocks = function (payload) {
return gpii.settingsHandlers.transformPayload(payload, function (oneSolution) {
return fluid.filterKeys(oneSolution, "settings");
});
};
// A utility which will accept a payload and convert all leaf values to numbers which may validly be. This
// is useful for settings handlers such as the INI settings handler which only really have a concept of strings
// as leaf values - without this postprocessing, roundtripping data through the handler is harder. This is
// primarly just a courtesy to make mock settings handlers easier to write.
gpii.settingsHandlers.numberify = function (payload) {
if (typeof(payload) === "string") {
return isFinite(payload) ? Number(payload) : payload;
} else if (fluid.isPrimitive(payload)) {
return payload;
} else {
return fluid.transform(payload, function (value) {
return gpii.settingsHandlers.numberify(value);
});
}
};
/** A general utility for invoking a settings handler, given a function for handling a single setting
* @param handler A function processing a single solution's entries
* @param payload The full payload as supplied to the settings handler's top-level method
* @return A promise yielding the combined payload expected from a top-level settings handler - this
* will resolve synchronously if the supplied handler function is synchronous ("ZALGO" notwithstanding)
**/
gpii.settingsHandlers.invokeSettingsHandler = function (handler, payload) {
var worklist = [];
var response = gpii.settingsHandlers.transformPayload(payload, function (element, path) {
// Note shallow copy performed within filterKeys
var directLoad = fluid.filterKeys(element, ["settings", "options"]);
var others = fluid.censorKeys(element, ["settings", "options"]);
worklist.push({
path: path,
result: handler(directLoad),
others: others
});
return {}; // construct isomorphic skeleton of response
});
var sequence = fluid.promise.sequence(fluid.getMembers(worklist, "result"));
var togo = fluid.promise();
sequence.then(function (resolved) {
fluid.each(resolved, function (oneResolve, i) {
// shallow copy to avoid trashing any "undefined" values in returned payload
var combined = fluid.extend({settings: oneResolve}, worklist[i].others);
fluid.set(response, worklist[i].path, combined);
});
togo.resolve(response);
}, togo.reject);
return togo;
};
/** Utilities for converting to and from concrete settings to ChangeApplier requests
* to enact those settings. On the "get" or "set" settingsHandler returns, we receive
* concrete settings, which need to be modelised so that they can safely be stored and persisted.
* In future, the "set" action will accept these requests as they are, but for now we need to
* convert them back using "changesToSettings" to adapt to the historical API.
*/
// Firstly two utilities which transform an individual settings value from one format to the other
gpii.settingsHandlers.settingsToChanges = function (element) {
return element === undefined ? {
type: "DELETE"
} : {
type: "ADD",
value: element
};
};
gpii.settingsHandlers.changesToSettings = function (element) {
return element.type === "DELETE" ? undefined : element.value;
};
// Secondly two utilities which do the same for an entire solution's worth of payloads as used in the LifecycleManager
gpii.settingsHandlers.settingsPayloadToChanges = function (response) {
return gpii.settingsHandlers.transformPayloadSettings(response, gpii.settingsHandlers.settingsToChanges);
};
gpii.settingsHandlers.changesPayloadToSettings = function (response) {
return gpii.settingsHandlers.transformPayloadSettings(response, gpii.settingsHandlers.changesToSettings);
};
/** Invoked by, e.g., the LifecycleManager to convert the SET response from a settingsHandler
* to a form where it can be immediately relayed back to another SET handler to
* restore the original settings values
*/
gpii.settingsHandlers.setResponseToSnapshot = function (response) {
return gpii.settingsHandlers.transformPayloadSettings(response, function (element) {
return element.oldValue;
});
};
/************************************************************************
* Utilities for automatically retrying settings handlers.
* the entry point "invokeRetryingHandler" accepts the materials for invoking
* a SET settings handler and converts them to a more deferred promise that will
* repeatedly reread the settings via GET until they match those expected.
*/
gpii.settingsHandlers.comparePayloads = function (payload1, payload2) {
var settings1 = gpii.settingsHandlers.extractSettingsBlocks(payload1);
var settings2 = gpii.settingsHandlers.extractSettingsBlocks(payload2);
var equal = fluid.model.diff(settings1, settings2);
Iif (!equal) {
fluid.log(fluid.logLevel.WARN, "Comparing payload ", JSON.stringify(settings1, null, 2), " with ", JSON.stringify(settings2, null, 2), " equal: " + equal);
}
return equal;
};
gpii.settingsHandlers.checkRereadSettings = function (that) {
Iif (that.retryOptions.rewriteEvery && !fluid.resolveContext("gpii.contexts.test", that)) {
fluid.fail("rewriteEvery is set to " + that.retryOptions.rewriteEvery
+ ": this facility should only be used in integration or unit tests");
}
// Note that this feature is now configured away as described in GPII-2522, although it is still (partially) tested
var writeAttempt = that.retryOptions.rewriteEvery && (that.retries % that.retryOptions.rewriteEvery === 0);
Iif (writeAttempt) {
fluid.log(fluid.logLevel.WARN,
"Stored settings have not settled to required values at attempt " + that.retries +
": attempting to rewrite");
}
// TODO: This will currently corrupt the SET response of a genuinely async set handler
var response = gpii.resolveSync(fluid.invokeGlobalFunction(that.resolvedName + (writeAttempt ? ".set" : ".get"), [that.payload]));
var getResponse = writeAttempt ? gpii.settingsHandlers.setResponseToSnapshot(response) : response;
var equal = gpii.settingsHandlers.comparePayloads(getResponse, that.payload);
Eif (equal) {
that.togo.resolve(that.originalSetPayload);
} else {
++that.retries;
if (that.retries === that.retryOptions.numRetries) {
fluid.log(fluid.logLevel.WARN, "Maximum retry count exceeded in settings handler " +
that.resolvedName + " at retry " + that.retries + ": rejecting settings action");
that.togo.reject({
isError: true,
message: "Failure to read settings for handler " + that.resolvedName + " as written after " + that.retries + " retries"
});
} else {
fluid.log(fluid.logLevel.WARN, "Settings have not settled to required values - retrying read at attempt " +
that.retries + " of " + that.retryOptions.numRetries + " in " + that.retryOptions.retryInterval + "ms");
gpii.invokeLater(that.checkRereadSettings, that.retryOptions.retryInterval);
}
}
};
/**
* Called to invoke settings handlers. Based on the first verifySettings option found in the payload,
* the settingshandler will be invoked with retrying enabled (if true) or without it (false).
* Structure of 'payload' argument is:
* {
* "some.app.id": [{
* "settings": { ... },
* "options": {
* ...,
* "verifySettings": true
* }
* }]
* }
* @param resolvedName {String} The resolved "trunk name" of the settings handler to be invoked - e.g. gpii.windows.registrySettingsHandler
* @param payload {Object} The full payload that would be sent to the SET method of the settings handler
* @param retryOptions {Object} (optional) Options governing the retry behaviour of the wrapped handler. This contains fields:
* numRetries {Integer} A positive integer holding the total number of retries to make
* retryInterval {Integer} A positive integer holder the delay in milliseconds between each retry
* @return a promise that will yield the original payload of the invoked SET method. In the case
* that retrying is enabled, a rejection occurs if the GET payloads never match after the end of the nominated retry period
*/
gpii.settingsHandlers.dispatchSettingsHandler = function (resolvedName, payload, retryOptions) {
// TODO "gpii.lifecycleManager.specToSettingsHandler" is the one responsible for this awkward
// layout of the settings handler payload - all of this infrastructure will have to be updated
// and cleaned up at some point
try {
var solutionIds = fluid.keys(payload);
var verifySettings = fluid.get(payload, [solutionIds[0], 0, "options", "verifySettings"]);
return verifySettings ?
gpii.settingsHandlers.invokeRetryingHandler(resolvedName, payload, retryOptions) :
gpii.settingsHandlers.invokeSetHandler(resolvedName, payload);
} catch (e) {
fluid.log(fluid.logLevel.WARN, "Error received when dispatching settingsHandler " + resolvedName + " with payload ",
payload, ": " + e);
return fluid.promise().reject(e);
}
};
gpii.settingsHandlers.invokeSetHandler = function (resolvedName, payload) {
return gpii.toPromise(fluid.invokeGlobalFunction(resolvedName + ".set", [payload]));
};
/** Main entry point for applying an automatically retrying settings handlers.
* @param resolvedName {String} The resolved "trunk name" of the settings handler to be invoked - e.g. gpii.windows.registrySettingsHandler
* @param payload {Object} The full payload that would be sent to the SET method of the settings handler
* @param retryOptions {Object} Options governing the retry behaviour of the wrapped handler. This contains fields:
* numRetries {Integer} A positive integer holding the total number of retries to make
* retryInterval {Integer} A positive integer holder the delay in milliseconds between each retry
* @return a promise that will yield the original payload of the originally invoked SET method if the GET calls eventually produce a result
* that matches, or else a rejection if the GET payloads never match after the end of the nominated retry period
*/
gpii.settingsHandlers.invokeRetryingHandler = function (resolvedName, payload, retryOptions) {
var that = {
retryOptions: retryOptions,
retries: 1,
payload: payload,
resolvedName: resolvedName,
togo: fluid.promise(),
checkRereadSettings: function () {
gpii.settingsHandlers.checkRereadSettings(that);
}
};
var originalResponse = gpii.settingsHandlers.invokeSetHandler(resolvedName, payload);
originalResponse.then(function (setPayload) {
that.originalSetPayload = setPayload;
that.checkRereadSettings();
});
return that.togo;
};
/****************************************************************
* Two moderately generic utilities for handling "standard" settings handlers whose
* data is backed by some kind of free-form in-memory JSON-like structure. These are called
* called by the file-based getters and setters.
*/
gpii.settingsHandlers.getSettings = function (solutionEntry, currentSettings) {
var newSettingsResponse = {};
var userRequestedSettings = solutionEntry.settings;
fluid.each(userRequestedSettings, function (settingVal, settingKey) {
var value = fluid.get(currentSettings, settingKey, fluid.model.escapedGetConfig);
newSettingsResponse[settingKey] = value;
});
return {
settings: newSettingsResponse
};
};
gpii.settingsHandlers.setSettings = function (solutionEntry, currentSettings) {
var newSettingsResponse = {};
var userRequestedSettings = solutionEntry.settings;
var options = solutionEntry.options;
var holder = {
model: currentSettings
};
var applier = fluid.makeHolderChangeApplier(holder, {
resolverGetConfig: fluid.model.escapedGetConfig,
resolverSetConfig: fluid.model.escapedSetConfig
});
// record differences between required and default settings
// so that the default settings can be restored
fluid.each(userRequestedSettings, function (settingVal, settingKey) {
var oldValue = fluid.get(holder.model, settingKey, fluid.model.escapedGetConfig);
var change = {
path: settingKey,
value: settingVal
};
change.type = settingVal === undefined ? "DELETE" : "ADD";
applier.fireChangeRequest(change);
newSettingsResponse[settingKey] = {
"oldValue": oldValue,
"newValue": settingVal
};
});
Eif (currentSettings !== holder.model) {
// TODO: Re-understand again exactly why this is, and eliminate it
fluid.clear(currentSettings); // We have a silly model based on object reference identity
fluid.extend(currentSettings, holder.model);
}
return { options: options, settings: newSettingsResponse };
};
/**************** FILE DEPENDENCE BELOW THIS POINT *********************/
gpii.settingsHandlers.readFile = function (options) {
Iif (!options || !options.filename) {
fluid.fail("readFile: expected an options block defining filename and encoding");
} else if (!fs.existsSync(options.filename)) {
fluid.log("readFile: No settingsfile '", options.filename, "'' exists. reporting " +
"'undefined' as current settings");
return undefined;
}
// TODO check for and handle read errors
var content = fs.readFileSync(options.filename, options.encoding || "utf-8");
return content;
};
gpii.settingsHandlers.writeFile = function (content, options) {
Iif (!options || !options.filename) {
fluid.fail("writeFile: expected an options block defining filename and encoding");
}
// TODO check for and handle write errors.
fs.writeFileSync(options.filename, content, options.encoding || "utf-8");
};
/**
* Utility for file-based settings handler. Handle individual solution entry data. The 'modifier' function passed as
* parameter is called to allow editing of the solutionEntry based on the
* current settings (both passed as parameters to the modifier function)
*/
gpii.settingsHandlers.handleFileSolutionEntry = function (solutionEntry, modifier, parser, isWrite) {
var options = solutionEntry.options;
Eif (options && (options.path || options.filename)) {
// read file - returns undefined if it doesn't exist
var content = gpii.settingsHandlers.readFile(options);
// if the file is non-existing, current settings are {}, else they're the parsed content of the file
var currentSettings = (content === undefined) ? {} : parser.parse(content, options);
// modification of the entry
var settings = modifier(solutionEntry, currentSettings);
if (isWrite) {
solutionEntry.settings = currentSettings;
var settingsData = parser.stringify(currentSettings, options);
gpii.settingsHandlers.writeFile(settingsData, options);
}
return settings;
}
return solutionEntry;
};
/**
* Function for looping through the payload sent to get/set calls. Looping
* is done by fluid.transform calls, and at the level of each solution
* entry.
*/
gpii.settingsHandlers.transformFilePayload = function (payload, modifier, parser, isWrite) {
return gpii.settingsHandlers.transformPayload(payload, function (solutionEntry) {
return gpii.settingsHandlers.handleFileSolutionEntry(solutionEntry, modifier, parser, isWrite);
});
};
gpii.settingsHandlers.makeFileGet = function (parser) {
return function (payload) {
return gpii.settingsHandlers.transformFilePayload(payload, gpii.settingsHandlers.getSettings, parser);
};
};
gpii.settingsHandlers.makeFileSet = function (parser) {
return function (payload) {
return gpii.settingsHandlers.transformFilePayload(payload, gpii.settingsHandlers.setSettings, parser, true);
};
};
/**
* Given two settingsHandler blocks, copy the settings (even those with a value of undefined) from
* the source to the target - overwriting the target settings. Note that the keys/values immediately
* within the source settingshandler block (i.e. the named settinghandler sub blocks) must be present
* in the target block structure
* Also note that the target object *will be modified*
*/
gpii.settingsHandlers.copySettings = function (target, source) {
for (var shName in source) {
var shBlock = source[shName];
for (var setting in shBlock.settings) { // will loop over undefined vals
target[shName].settings[setting] = shBlock.settings[setting];
}
}
};
|