Files
genieacs/lib/session.js
T

2603 lines
75 KiB
JavaScript

/**
* Copyright 2013-2018 Zaid Abdulla
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const config = require("./config");
const common = require("./common");
const db = require("./db");
const device = require("./device");
const sandbox = require("./sandbox");
const localCache = require("./local-cache");
const PathSet = require("./path-set");
const VersionedMap = require("./versioned-map");
const InstanceSet = require("./instance-set");
const defaultProvisions = require("./default-provisions");
const gpnHeuristic = require("./gpn-heuristic");
// Multiply by two because every iteration is two
// phases: read and update
const MAX_ITERATIONS = config.get("MAX_COMMIT_ITERATIONS") * 2;
const provisionsCache = new WeakMap();
const virtualParametersCache = new WeakMap();
function initDeviceData() {
return {
paths: new PathSet(),
timestamps: new VersionedMap(),
attributes: new VersionedMap(),
loaded: new Map(),
trackers: new Map(),
changes: new Set()
};
}
function init(deviceId, cwmpVersion, timeout, callback) {
const timestamp = Date.now();
const sessionContext = {
timestamp: timestamp,
deviceId: deviceId,
deviceData: initDeviceData(),
cwmpVersion: cwmpVersion,
timeout: timeout,
provisions: [],
channels: {},
virtualParameters: [],
revisions: [0],
rpcCount: 0,
iteration: 0,
cycle: 0,
extensionsCache: {},
declarations: []
};
localCache.getProvisionsAndVirtualParameters(
(err, hash, provisions, virtualParameters) => {
if (err) return callback(err);
sessionContext.presetsHash = hash;
provisionsCache.set(sessionContext, provisions);
virtualParametersCache.set(sessionContext, virtualParameters);
callback(null, sessionContext);
}
);
}
function loadParameters(sessionContext, callback) {
if (!sessionContext.toLoad || !sessionContext.toLoad.size) return callback();
const toLoad = Array.from(sessionContext.toLoad.entries());
db.fetchDevice(
sessionContext.deviceId,
sessionContext.timestamp,
toLoad,
(err, parameters, loaded) => {
if (err) return callback(err);
if (!parameters) {
// Device not available in database, mark as new
sessionContext.new = true;
loaded = [
[[], (1 << config.get("MAX_DEPTH", sessionContext.deviceId)) - 1]
];
parameters = [];
}
for (const p of loaded) {
const path = sessionContext.deviceData.paths.add(p[0]);
if (p[1]) {
const l = sessionContext.deviceData.loaded.get(path) | 0;
sessionContext.deviceData.loaded.set(path, l | p[1]);
}
}
for (const p of parameters) {
const path = sessionContext.deviceData.paths.add(p[0]);
sessionContext.deviceData.timestamps.set(path, p[1], 0);
if (p[2]) sessionContext.deviceData.attributes.set(path, p[2], 0);
}
delete sessionContext.toLoad;
callback();
}
);
}
function generateRpcId(sessionContext) {
return (
sessionContext.timestamp.toString(16) +
("0" + sessionContext.cycle.toString(16)).slice(-2) +
("0" + sessionContext.rpcCount.toString(16)).slice(-2)
);
}
function inform(sessionContext, rpcReq, callback) {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
const params = [
[
["DeviceID", "Manufacturer"],
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [rpcReq.deviceId.Manufacturer, "xsd:string"]]
}
],
[
["DeviceID", "OUI"],
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [rpcReq.deviceId.OUI, "xsd:string"]]
}
],
[
["DeviceID", "ProductClass"],
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [rpcReq.deviceId.ProductClass, "xsd:string"]]
}
],
[
["DeviceID", "SerialNumber"],
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [rpcReq.deviceId.SerialNumber, "xsd:string"]]
}
]
];
for (const p of rpcReq.parameterList) {
const path = common.parsePath(p[0]);
params.push([
path,
timestamp,
{
object: [timestamp, 0],
value: [timestamp, p.slice(1)]
}
]);
}
params.push([
["Events", "Inform"],
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [sessionContext.timestamp, "xsd:dateTime"]]
}
]);
for (const e of rpcReq.event) {
params.push([
["Events", e.replace(/\s+/g, "_")],
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [sessionContext.timestamp, "xsd:dateTime"]]
}
]);
}
// Preload DeviceID params
loadPath(sessionContext, ["DeviceID", "*"]);
for (const p of params) loadPath(sessionContext, p[0]);
loadParameters(sessionContext, err => {
if (err) return callback(err);
if (sessionContext.new) {
params.push([
["DeviceID", "ID"],
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [sessionContext.deviceId, "xsd:string"]]
}
]);
params.push([
["Events", "Registered"],
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [sessionContext.timestamp, "xsd:dateTime"]]
}
]);
}
sessionContext.deviceData.timestamps.revision = 1;
sessionContext.deviceData.attributes.revision = 1;
let toClear = null;
for (const p of params) {
// Don't need to clear wildcards for Events
if (p[0][0] === "Events") {
device.set(sessionContext.deviceData, p[0], p[1], p[2]);
} else {
toClear = device.set(
sessionContext.deviceData,
p[0],
p[1],
p[2],
toClear
);
}
}
clear(sessionContext, toClear, err => {
callback(err, { name: "InformResponse" });
});
});
}
function transferComplete(sessionContext, rpcReq, callback) {
const revision =
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision;
sessionContext.deviceData.attributes.revision = revision;
const commandKey = rpcReq.commandKey;
const operation = sessionContext.operations[commandKey];
if (!operation) return callback(null, { name: "TransferCompleteResponse" });
const instance = operation.args.instance;
delete sessionContext.operations[commandKey];
if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1;
if (rpcReq.faultStruct && rpcReq.faultStruct.faultCode !== "0") {
return revertDownloadParameters(
sessionContext,
operation.args.instance,
err => {
const fault = {
code: `cwmp.${rpcReq.faultStruct.faultCode}`,
message: rpcReq.faultStruct.faultString,
detail: rpcReq.faultStruct,
timestamp: operation.timestamp
};
callback(err, { name: "TransferCompleteResponse" }, operation, fault);
}
);
}
loadPath(sessionContext, ["Downloads", instance, "*"]);
loadParameters(sessionContext, err => {
if (err) return callback(err);
let toClear = null;
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let p;
p = sessionContext.deviceData.paths.add([
"Downloads",
instance,
"LastDownload"
]);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.timestamp, "xsd:dateTime"]] },
toClear
);
p = sessionContext.deviceData.paths.add([
"Downloads",
instance,
"LastFileType"
]);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.args.fileType, "xsd:string"]] },
toClear
);
p = sessionContext.deviceData.paths.add([
"Downloads",
instance,
"LastFileName"
]);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.args.fileName, "xsd:string"]] },
toClear
);
p = sessionContext.deviceData.paths.add([
"Downloads",
instance,
"LastTargetFileName"
]);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.args.targetFileName, "xsd:string"]] },
toClear
);
p = sessionContext.deviceData.paths.add([
"Downloads",
instance,
"StartTime"
]);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] },
toClear
);
p = sessionContext.deviceData.paths.add([
"Downloads",
instance,
"CompleteTime"
]);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] },
toClear
);
clear(sessionContext, toClear, err => {
callback(err, { name: "TransferCompleteResponse" }, operation);
});
});
}
function revertDownloadParameters(sessionContext, instance, callback) {
loadPath(sessionContext, ["Downloads", instance, "*"]);
loadParameters(sessionContext, err => {
if (err) return callback(err);
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let p;
p = sessionContext.deviceData.paths.add([
"Downloads",
instance,
"LastDownload"
]);
const lastDownload = sessionContext.deviceData.attributes.get(p);
p = sessionContext.deviceData.paths.add([
"Downloads",
instance,
"Download"
]);
let toClear = device.set(sessionContext.deviceData, p, timestamp, {
value: [
timestamp,
[
lastDownload && lastDownload.value[1] ? lastDownload.value[1][0] : 0,
"xsd:dateTime"
]
]
});
clear(sessionContext, toClear, callback);
});
}
function timeoutOperations(sessionContext, callback) {
const revision =
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision;
sessionContext.deviceData.attributes.revision = revision;
const faults = [];
const operations = [];
let counter = 3;
for (const [commandKey, operation] of Object.entries(operations)) {
if (operation.name !== "Download")
return callback(new Error(`Unknown operation name ${operation.name}`));
const DOWNLOAD_TIMEOUT =
config.get("DOWNLOAD_TIMEOUT", sessionContext.deviceId) * 1000;
if (sessionContext.timestamp > operation.timestamp + DOWNLOAD_TIMEOUT) {
delete sessionContext.operations[commandKey];
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1;
faults.push({
code: "timeout",
message: "Download operation timed out",
timestamp: operation.timestamp
});
operations.push(operation);
counter += 2;
revertDownloadParameters(sessionContext, operation.args.instance, err => {
if (err) {
if (counter & 1) callback(err);
return (counter = 0);
}
if ((counter -= 2) === 1) callback(null, faults, operations);
});
}
}
if ((counter -= 2) === 1) callback(null, faults, operations);
}
function addProvisions(sessionContext, channel, provisions) {
delete sessionContext.syncState;
delete sessionContext.rpcRequest;
sessionContext.declarations = [];
sessionContext.provisionsRet = [];
if (sessionContext.revisions[sessionContext.revisions.length - 1] > 0) {
sessionContext.deviceData.timestamps.collapse(1);
sessionContext.deviceData.attributes.collapse(1);
sessionContext.revisions = [0];
sessionContext.extensionsCache = {};
}
if (sessionContext.iteration !== sessionContext.cycle * MAX_ITERATIONS) {
sessionContext.cycle += 1;
sessionContext.rpcCount = 0;
sessionContext.iteration = sessionContext.cycle * MAX_ITERATIONS;
}
sessionContext.channels[channel] |= 0;
for (const provision of provisions) {
const channels = [channel];
// Remove duplicate provisions
const provisionStr = JSON.stringify(provision);
for (const [j, p] of sessionContext.provisions.entries()) {
if (JSON.stringify(p) === provisionStr) {
sessionContext.provisions.splice(j, 1);
for (const c of Object.keys(sessionContext.channels)) {
if (sessionContext.channels[c] & (1 << j)) channels.push(c);
const a = sessionContext.channels[c] >> (j + 1);
sessionContext.channels[c] &= (1 << j) - 1;
sessionContext.channels[c] |= a << j;
}
}
}
for (const c of channels)
sessionContext.channels[c] |= 1 << sessionContext.provisions.length;
sessionContext.provisions.push(provision);
}
}
function clearProvisions(sessionContext) {
if (sessionContext.revisions[sessionContext.revisions.length - 1] > 0) {
sessionContext.deviceData.timestamps.collapse(1);
sessionContext.deviceData.attributes.collapse(1);
}
if (sessionContext.iteration !== sessionContext.cycle * MAX_ITERATIONS) {
sessionContext.cycle += 1;
sessionContext.rpcCount = 0;
sessionContext.iteration = sessionContext.cycle * MAX_ITERATIONS;
}
delete sessionContext.syncState;
delete sessionContext.rpcRequest;
sessionContext.provisions = [];
sessionContext.virtualParameters = [];
sessionContext.channels = {};
sessionContext.declarations = [];
sessionContext.provisionsRet = [];
sessionContext.revisions = [0];
sessionContext.extensionsCache = {};
}
function runProvisions(
sessionContext,
provisions,
startRevision,
endRevision,
callback
) {
let done = true;
let allDeclarations = [];
let allClear = [];
let counter = 3;
const allProvisions = provisionsCache.get(sessionContext);
for (const [j, provision] of provisions.entries()) {
if (!allProvisions[provision[0]]) {
allDeclarations[j] = [];
allClear[j] = [];
if (defaultProvisions[provision[0]]) {
done =
defaultProvisions[provision[0]](
sessionContext,
provision,
allDeclarations[j],
startRevision,
endRevision
) && done;
}
continue;
}
counter += 2;
sandbox.run(
allProvisions[provision[0]].script,
{ args: provision.slice(1) },
sessionContext,
startRevision,
endRevision,
(err, _fault, _clear, _declarations, _done) => {
if (err || _fault) {
if (counter & 1) callback(err, _fault);
return (counter = 0);
}
done = done && _done;
allDeclarations[j] = _declarations || [];
allClear[j] = _clear || [];
if ((counter -= 2) === 1) {
allDeclarations = Array.prototype.concat.apply([], allDeclarations);
allClear = Array.prototype.concat.apply([], allClear);
callback(null, null, done, allDeclarations, allClear);
}
}
);
}
if ((counter -= 2) === 1) {
allDeclarations = Array.prototype.concat.apply([], allDeclarations);
allClear = Array.prototype.concat.apply([], allClear);
callback(null, null, done, allDeclarations, allClear);
}
}
function runVirtualParameters(
sessionContext,
provisions,
startRevision,
endRevision,
callback
) {
let done = true;
const virtualParameterUpdates = [];
let allDeclarations = [];
let allClear = [];
let counter = 3;
const allVirtualParameters = virtualParametersCache.get(sessionContext);
for (const [j, provision] of provisions.entries()) {
counter += 2;
const globals = {
args: provision.slice(1)
};
sandbox.run(
allVirtualParameters[provision[0]].script,
globals,
sessionContext,
startRevision,
endRevision,
(err, _fault, _clear, _declarations, _done, _returnValue) => {
if (err || _fault) {
if (counter & 1) callback(err, _fault);
return (counter = 0);
}
done = done && _done;
allDeclarations[j] = _declarations || [];
allClear[j] = _clear || [];
if (_done) {
if (!_returnValue) {
if (counter & 1) {
callback(null, {
code: "script",
message: "Invalid virtual parameter return value"
});
}
return (counter = 0);
}
const ret = {};
if (_returnValue.writable != null) {
ret.writable = +!!_returnValue.writable;
} else if (
provision[1].writable != null ||
provision[2].writable != null
) {
if (counter & 1) {
callback(null, {
code: "script",
message: "Virtual parameter must provide declared attributes"
});
}
return (counter = 0);
}
if (_returnValue.value != null) {
if (!Array.isArray(_returnValue.value))
_returnValue.value = [_returnValue.value];
if (!_returnValue.value[1]) {
if (typeof _returnValue.value[0] === "number")
_returnValue.value[1] = "xsd:int";
else if (typeof _returnValue.value[0] === "boolean")
_returnValue.value[1] = "xsd:boolean";
else if (_returnValue.value[0] instanceof Date)
_returnValue.value[1] = "xsd:dateTime";
else _returnValue.value[1] = "xsd:string";
}
const allowed = {
"xsd:int": 1,
"xsd:unsignedInt": 1,
"xsd:boolean": 1,
"xsd:string": 1,
"xsd:dateTime": 1,
"xsd:base64": 1,
"xsd:hexBinary": 1
};
if (
_returnValue.value[0] == null ||
!allowed[_returnValue.value[1]]
) {
if (counter & 1) {
callback(null, {
code: "script",
message: "Invalid virtual parameter value attribute"
});
}
return (counter = 0);
}
ret.value = device.sanitizeParameterValue(_returnValue.value);
} else if (provision[1].value != null || provision[2].value != null) {
if (counter & 1) {
callback(null, {
code: "script",
message: "Virtual parameter must provide declared attributes"
});
}
return (counter = 0);
}
virtualParameterUpdates[j] = ret;
}
if ((counter -= 2) === 1) {
allDeclarations = Array.prototype.concat.apply([], allDeclarations);
allClear = Array.prototype.concat.apply([], allClear);
return callback(
null,
null,
done ? virtualParameterUpdates : null,
allDeclarations,
allClear
);
}
}
);
}
if ((counter -= 2) === 1) {
allDeclarations = Array.prototype.concat.apply([], allDeclarations);
allClear = Array.prototype.concat.apply([], allClear);
return callback(
null,
null,
done ? virtualParameterUpdates : null,
allDeclarations,
allClear
);
}
}
function runDeclarations(sessionContext, declarations) {
if (!sessionContext.syncState) {
sessionContext.syncState = {
refreshAttributes: {
exist: new Set(),
object: new Set(),
writable: new Set(),
value: new Set()
},
spv: new Map(),
gpn: new Set(),
gpnPatterns: new Map(),
tags: new Map(),
virtualParameterDeclarations: [],
instancesToDelete: new Map(),
instancesToCreate: new Map(),
downloadsToDelete: new Set(),
downloadsToCreate: new InstanceSet(),
downloadsValues: new Map(),
downloadsDownload: new Map()
};
}
const allDeclareTimestamps = new Map();
const allDeclareAttributeTimestamps = new Map();
const allDeclareAttributeValues = new Map();
const allVirtualParameters = virtualParametersCache.get(sessionContext);
function mergeAttributeTimestamps(p, attrs) {
let cur = allDeclareAttributeTimestamps.get(p);
if (!cur) {
allDeclareAttributeTimestamps.set(p, attrs);
} else {
cur = Object.assign({}, cur);
for (const [k, v] of Object.entries(attrs))
cur[k] = Math.max(v, cur[k] || 0);
allDeclareAttributeTimestamps.set(p, cur);
}
}
function mergeAttributeValues(p, attrs) {
let cur = allDeclareAttributeValues.get(p);
if (!cur) {
allDeclareAttributeValues.set(p, attrs);
} else {
cur = Object.assign({}, cur, attrs);
allDeclareAttributeValues.set(p, cur);
}
}
for (const declaration of declarations) {
let path = common.addPathMeta(declaration[0]);
let unpacked;
if ((path.alias | path.wildcard) & 1 || path[0] === "VirtualParameters") {
sessionContext.deviceData.paths.add(["VirtualParameters"]);
if ((path.alias | path.wildcard) & 2) {
sessionContext.deviceData.paths.add(["VirtualParameters", "*"]);
for (const k of Object.keys(allVirtualParameters))
sessionContext.deviceData.paths.add(["VirtualParameters", k]);
}
}
if ((path.alias | path.wildcard) & 1 || path[0] === "Reboot")
sessionContext.deviceData.paths.add(["Reboot"]);
if ((path.alias | path.wildcard) & 1 || path[0] === "FactoryReset")
sessionContext.deviceData.paths.add(["FactoryReset"]);
if (path.alias) {
const aliasDecs = device.getAliasDeclarations(path, declaration[1] || 1);
for (const ad of aliasDecs) {
const p = sessionContext.deviceData.paths.add(ad[0]);
allDeclareTimestamps.set(
p,
Math.max(ad[1] || 1, allDeclareTimestamps.get(p) || 0)
);
let attrTrackers;
if (ad[2]) {
attrTrackers = Object.keys(ad[2]);
mergeAttributeTimestamps(p, ad[2]);
}
device.track(
sessionContext.deviceData,
p,
"prerequisite",
attrTrackers
);
}
unpacked = device.unpack(sessionContext.deviceData, path);
for (const u of unpacked) {
allDeclareTimestamps.set(
u,
Math.max(declaration[1] || 1, allDeclareTimestamps.get(u) || 0)
);
if (declaration[2]) mergeAttributeTimestamps(u, declaration[2]);
}
} else {
path = sessionContext.deviceData.paths.add(path);
allDeclareTimestamps.set(
path,
Math.max(declaration[1] || 1, allDeclareTimestamps.get(path) || 0)
);
if (declaration[2]) mergeAttributeTimestamps(path, declaration[2]);
device.track(sessionContext.deviceData, path, "prerequisite");
}
if (declaration[4]) {
if (path.alias | path.wildcard) {
if (!unpacked)
unpacked = device.unpack(sessionContext.deviceData, path);
for (const u of unpacked) mergeAttributeValues(u, declaration[4]);
} else {
mergeAttributeValues(path, declaration[4]);
}
}
if (declaration[3] != null) {
let minInstances, maxInstances;
if (Array.isArray(declaration[3])) {
minInstances = declaration[3][0];
maxInstances = declaration[3][1];
} else {
minInstances = maxInstances = declaration[3];
}
let parent = common.addPathMeta(path.slice(0, -1));
let keys;
if (Array.isArray(path[path.length - 1])) {
keys = {};
for (let i = 0; i < path[path.length - 1]; i += 2) {
keys[path[path.length - 1][i].join(".")] =
path[path.length - 1][i + 1];
}
} else if (path[path.length - 1] === "*") {
keys = {};
}
if (
((path.wildcard | path.alias) & ((1 << (path.length - 1)) - 1)) ===
0
) {
parent = sessionContext.deviceData.paths.add(parent);
if (!unpacked)
unpacked = device.unpack(sessionContext.deviceData, path);
processInstances(
sessionContext,
parent,
unpacked,
keys,
minInstances,
maxInstances
);
} else {
const parentsUnpacked = device.unpack(
sessionContext.deviceData,
parent
);
for (let par of parentsUnpacked) {
par = sessionContext.deviceData.paths.add(par);
processInstances(
sessionContext,
par,
device.unpack(
sessionContext.deviceData,
common.addPathMeta(par.concat([path[par.length]]))
),
keys,
minInstances,
maxInstances
);
}
}
}
}
return processDeclarations(
sessionContext,
allDeclareTimestamps,
allDeclareAttributeTimestamps,
allDeclareAttributeValues
);
}
function rpcRequest(sessionContext, _declarations, callback) {
if (sessionContext.rpcRequest != null) {
return callback(
null,
null,
generateRpcId(sessionContext),
sessionContext.rpcRequest
);
}
if (
!sessionContext.virtualParameters.length &&
!sessionContext.declarations.length &&
!(_declarations && _declarations.length) &&
!sessionContext.provisions.length
)
return callback();
if (
sessionContext.declarations.length <=
sessionContext.virtualParameters.length
) {
const inception = sessionContext.declarations.length;
const revision = (sessionContext.revisions[inception] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision;
sessionContext.deviceData.attributes.revision = revision;
let run, provisions;
if (inception === 0) {
run = runProvisions;
provisions = sessionContext.provisions;
} else {
run = runVirtualParameters;
provisions = sessionContext.virtualParameters[inception - 1];
}
return run(
sessionContext,
provisions,
sessionContext.revisions[inception - 1] || 0,
sessionContext.revisions[inception],
(err, fault, ret, decs, toClear) => {
if (err) return callback(err);
if (fault) {
fault.timestamp = sessionContext.timestamp;
return callback(null, fault);
}
// Enforce max clear timestamp
for (const c of toClear) {
if (c[1] > sessionContext.timestamp) c[1] = sessionContext.timestamp;
for (const [k, v] of Object.entries(c[2])) {
if (v > sessionContext.timestamp)
c[2][k] = sessionContext.timestamp;
}
}
sessionContext.declarations.push(decs);
sessionContext.provisionsRet[inception] = ret;
for (const d of decs) {
// Enforce max timestamp
if (d[1] > sessionContext.timestamp) d[1] = sessionContext.timestamp;
for (const [k, v] of Object.entries(d[2])) {
if (v > sessionContext.timestamp)
d[2][k] = sessionContext.timestamp;
}
for (const ad of device.getAliasDeclarations(d[0], 1))
loadPath(sessionContext, ad[0]);
}
clear(sessionContext, toClear, err => {
if (err) return callback(err);
loadParameters(sessionContext, err => {
if (err) return callback(err);
rpcRequest(sessionContext, _declarations, callback);
});
});
}
);
}
if (_declarations && _declarations.length) {
delete sessionContext.syncState;
if (!sessionContext.declarations[0]) sessionContext.declarations[0] = [];
sessionContext.declarations[0] = sessionContext.declarations[0].concat(
_declarations
);
for (const d of _declarations) {
for (const ad of device.getAliasDeclarations(d[0], 1))
loadPath(sessionContext, ad[0]);
}
return loadParameters(sessionContext, err => {
if (err) return callback(err);
rpcRequest(sessionContext, null, callback);
});
}
if (sessionContext.rpcCount >= 255) {
return callback(null, {
code: "too_many_rpcs",
message: "Too many RPC requests",
timestamp: sessionContext.timestamp
});
}
if (sessionContext.revisions.length >= 8) {
return callback(null, {
code: "deeply_nested_vparams",
message:
"Virtual parameters are referencing other virtual parameters in a deeply nested manner",
timestamp: sessionContext.timestamp
});
}
if (sessionContext.cycle >= 255) {
return callback(null, {
code: "too_many_cycles",
message: "Too many provision cycles",
timestamp: sessionContext.timestamp
});
}
if (sessionContext.iteration >= MAX_ITERATIONS * (sessionContext.cycle + 1)) {
return callback(null, {
code: "too_many_commits",
message: "Too many commit iterations",
timestamp: sessionContext.timestamp
});
}
if (
!(
sessionContext.syncState &&
sessionContext.syncState.virtualParameterDeclarations &&
sessionContext.syncState.virtualParameterDeclarations.length >=
sessionContext.declarations.length
)
) {
const inception =
sessionContext.syncState &&
sessionContext.syncState.virtualParameterDeclarations
? sessionContext.syncState.virtualParameterDeclarations.length
: 0;
// Avoid unnecessary increment of iteration when using vparams
if (inception === sessionContext.declarations.length - 1)
sessionContext.iteration += 2;
let vpd = runDeclarations(
sessionContext,
sessionContext.declarations[inception]
);
const timestamp = sessionContext.timestamp + sessionContext.iteration;
let toClear;
const allVirtualParameters = virtualParametersCache.get(sessionContext);
vpd = vpd.filter(declaration => {
if (Object.keys(allVirtualParameters).length) {
if (declaration[0].length === 1) {
// Avoid setting on every inform as "exist" timestamp
// is not saved in DB
if (!sessionContext.deviceData.attributes.has(declaration[0])) {
toClear = device.set(
sessionContext.deviceData,
declaration[0],
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] },
toClear
);
}
return false;
} else if (declaration[0].length === 2) {
if (declaration[0][1] === "*") {
for (const k of Object.keys(allVirtualParameters)) {
toClear = device.set(
sessionContext.deviceData,
["VirtualParameters", k],
timestamp,
{
object: [timestamp, 0]
},
toClear
);
}
toClear = device.set(
sessionContext.deviceData,
declaration[0],
timestamp,
null,
toClear
);
return false;
} else if (allVirtualParameters[declaration[0][1]]) {
// Avoid setting on every inform as "exist" timestamp
// is not saved in DB
if (!sessionContext.deviceData.attributes.has(declaration[0])) {
toClear = device.set(
sessionContext.deviceData,
declaration[0],
timestamp,
{ object: [timestamp, 0] },
toClear
);
}
return true;
}
}
}
for (const p of sessionContext.deviceData.paths.find(
declaration[0],
false,
true
)) {
if (sessionContext.deviceData.attributes.has(p)) {
if (!toClear) toClear = [];
toClear.push([declaration[0], timestamp]);
break;
}
}
return false;
});
return clear(sessionContext, toClear, err => {
if (err) return callback(err);
sessionContext.syncState.virtualParameterDeclarations[inception] = vpd;
rpcRequest(sessionContext, null, callback);
});
}
if (!sessionContext.syncState) return callback();
const inception = sessionContext.declarations.length - 1;
let provisions = generateGetVirtualParameterProvisions(
sessionContext,
sessionContext.syncState.virtualParameterDeclarations[inception]
);
if (!provisions) {
sessionContext.rpcRequest = generateGetRpcRequest(sessionContext);
if (!sessionContext.rpcRequest) {
// Only check after read stage is complete to minimize reprocessing of
// declarations especially during initial discovery of data model
if (sessionContext.deviceData.changes.has("prerequisite")) {
delete sessionContext.syncState;
device.clearTrackers(sessionContext.deviceData, "prerequisite");
return rpcRequest(sessionContext, null, callback);
}
let toClear;
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
// Update tags
for (const [p, v] of sessionContext.syncState.tags) {
const c = sessionContext.deviceData.attributes.get(p);
if (v && c) {
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{
object: [timestamp, false],
writable: [timestamp, true],
value: [timestamp, [true, "xsd:boolean"]]
},
toClear
);
} else if (c && !v) {
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
null,
toClear
);
}
}
// Downloads
let index;
for (const instance of sessionContext.syncState.downloadsToCreate) {
if (index == null) {
index = 0;
for (const p of sessionContext.deviceData.paths.find(
["Downloads", "*"],
false,
true
)) {
if (+p[1] > index && sessionContext.deviceData.attributes.has(p))
index = +p[1];
}
}
++index;
toClear = device.set(
sessionContext.deviceData,
["Downloads"],
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
["Downloads", "" + index],
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] },
toClear
);
const params = {
FileType: {
writable: 1,
value: [instance.FileType || "", "xsd:string"]
},
FileName: {
writable: 1,
value: [instance.FileName || "", "xsd:string"]
},
TargetFileName: {
writable: 1,
value: [instance.TargetFileName || "", "xsd:string"]
},
Download: {
writable: 1,
value: [instance.Download || 0, "xsd:dateTime"]
},
LastFileType: { writable: 0, value: ["", "xsd:string"] },
LastFileName: { writable: 0, value: ["", "xsd:string"] },
LastTargetFileName: { writable: 0, value: ["", "xsd:string"] },
LastDownload: { writable: 0, value: [0, "xsd:dateTime"] },
StartTime: { writable: 0, value: [0, "xsd:dateTime"] },
CompleteTime: { writable: 0, value: [0, "xsd:dateTime"] }
};
for (const [k, v] of Object.entries(params)) {
toClear = device.set(
sessionContext.deviceData,
["Downloads", `${index}`, k],
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, v.writable],
value: [timestamp, v.value]
},
toClear
);
}
toClear = device.set(
sessionContext.deviceData,
["Downloads", `${index}`, "*"],
timestamp,
null,
toClear
);
}
sessionContext.syncState.downloadsToCreate.clear();
for (const instance of sessionContext.syncState.downloadsToDelete) {
toClear = device.set(
sessionContext.deviceData,
instance,
timestamp,
null,
toClear
);
for (const p of sessionContext.syncState.downloadsValues.keys()) {
if (p[1] === instance[1])
sessionContext.syncState.downloadsValues.delete(p);
}
}
sessionContext.syncState.downloadsToDelete.clear();
for (const [p, v] of sessionContext.syncState.downloadsValues) {
const attrs = sessionContext.deviceData.attributes.get(p);
if (attrs) {
if (attrs.writable && attrs.writable[1] && attrs.value) {
const val = device.sanitizeParameterValue([v, attrs.value[1][1]]);
if (val[0] !== attrs.value[1][0]) {
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, val] },
toClear
);
}
}
}
}
if (toClear || sessionContext.deviceData.changes.has("prerequisite")) {
return clear(sessionContext, toClear, err => {
if (err) return callback(err);
rpcRequest(sessionContext, null, callback);
});
}
provisions = generateSetVirtualParameterProvisions(
sessionContext,
sessionContext.syncState.virtualParameterDeclarations[inception]
);
if (!provisions)
sessionContext.rpcRequest = generateSetRpcRequest(sessionContext);
}
}
if (provisions) {
sessionContext.virtualParameters.push(provisions);
sessionContext.revisions.push(sessionContext.revisions[inception]);
return rpcRequest(sessionContext, null, callback);
}
if (sessionContext.rpcRequest) {
return callback(
null,
null,
generateRpcId(sessionContext),
sessionContext.rpcRequest
);
}
++sessionContext.revisions[inception];
sessionContext.declarations.pop();
sessionContext.syncState.virtualParameterDeclarations.pop();
const ret = sessionContext.provisionsRet.splice(inception)[0];
if (!ret) return rpcRequest(sessionContext, null, callback);
sessionContext.revisions.pop();
const rev =
sessionContext.revisions[sessionContext.revisions.length - 1] || 0;
sessionContext.deviceData.timestamps.collapse(rev + 1);
sessionContext.deviceData.attributes.collapse(rev + 1);
sessionContext.deviceData.timestamps.revision = rev + 1;
sessionContext.deviceData.attributes.revision = rev + 1;
for (const k of Object.keys(sessionContext.extensionsCache)) {
if (rev < Number(k.split(":", 1)[0]))
delete sessionContext.extensionsCache[k];
}
const vparams = sessionContext.virtualParameters.pop();
if (!vparams) return callback();
const timestamp = sessionContext.timestamp + sessionContext.iteration;
let toClear;
for (const [i, vpu] of ret.entries()) {
for (const [k, v] of Object.entries(vpu))
vpu[k] = [timestamp + (vparams[i][2][k] != null ? 1 : 0), v];
toClear = device.set(
sessionContext.deviceData,
["VirtualParameters", vparams[i][0]],
timestamp,
vpu,
toClear
);
}
clear(sessionContext, toClear, err => {
if (err) return callback(err);
rpcRequest(sessionContext, null, callback);
});
}
function generateGetRpcRequest(sessionContext) {
const syncState = sessionContext.syncState;
if (!syncState) return null;
for (const path of syncState.refreshAttributes.exist) {
let found = false;
for (const p of sessionContext.deviceData.paths.find(
path,
false,
true,
99
)) {
if (
syncState.refreshAttributes.value.has(p) ||
syncState.refreshAttributes.object.has(p) ||
syncState.refreshAttributes.writable.has(p) ||
syncState.gpn.has(p)
) {
found = true;
break;
}
}
if (!found) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1));
syncState.gpn.add(p);
const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
}
}
syncState.refreshAttributes.exist.clear();
for (const path of syncState.refreshAttributes.object) {
let found = false;
for (const p of sessionContext.deviceData.paths.find(
path,
false,
true,
99
)) {
if (
syncState.refreshAttributes.value.has(p) ||
(p.length > path.length &&
(syncState.refreshAttributes.object.has(p) ||
syncState.refreshAttributes.writable.has(p)))
) {
found = true;
break;
}
}
if (!found) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1));
syncState.gpn.add(p);
const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
}
}
syncState.refreshAttributes.object.clear();
for (const path of syncState.refreshAttributes.writable) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1));
syncState.gpn.add(p);
const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
}
syncState.refreshAttributes.writable.clear();
if (syncState.gpn.size) {
const GPN_NEXT_LEVEL = config.get(
"GPN_NEXT_LEVEL",
sessionContext.deviceId
);
const paths = Array.from(syncState.gpn.keys()).sort(
(a, b) => b.length - a.length
);
let path = paths.pop();
while (
path &&
path.length &&
!sessionContext.deviceData.attributes.has(path)
) {
syncState.gpn.delete(path);
path = paths.pop();
}
if (path) {
let nextLevel;
let est = 0;
if (path.length >= GPN_NEXT_LEVEL) {
const patterns = [[path, 0]];
for (const p of sessionContext.deviceData.paths.find(
path,
true,
false,
99
)) {
const v = syncState.gpnPatterns.get(p);
if (v) patterns.push([p, (v >> path.length) << path.length]);
}
est = gpnHeuristic.estimateGpnCount(patterns);
}
if (est < Math.pow(2, Math.max(0, 8 - path.length))) {
nextLevel = true;
syncState.gpn.delete(path);
} else {
nextLevel = false;
for (const p of sessionContext.deviceData.paths.find(
path,
false,
true,
99
))
syncState.gpn.delete(p);
}
return {
name: "GetParameterNames",
parameterPath: path.concat("").join("."),
nextLevel: nextLevel
};
}
}
if (syncState.refreshAttributes.value.size) {
const GPV_BATCH_SIZE = config.get(
"GPV_BATCH_SIZE",
sessionContext.deviceId
);
const parameterNames = [];
for (const path of syncState.refreshAttributes.value) {
syncState.refreshAttributes.value.delete(path);
// Need to check in case param is deleted or changed to object
const attrs = sessionContext.deviceData.attributes.get(path);
if (attrs && attrs.object && attrs.object[1] === 0) {
parameterNames.push(path);
if (parameterNames.length >= GPV_BATCH_SIZE) break;
}
}
if (parameterNames.length) {
return {
name: "GetParameterValues",
parameterNames: parameterNames.map(p => p.join("."))
};
}
}
return null;
}
function generateSetRpcRequest(sessionContext) {
const syncState = sessionContext.syncState;
if (!syncState) return null;
const deviceData = sessionContext.deviceData;
// Delete instance
for (const instances of syncState.instancesToDelete.values()) {
const instance = instances.values().next().value;
if (instance && sessionContext.deviceData.attributes.has(instance)) {
return {
name: "DeleteObject",
objectName: instance.concat("").join(".")
};
}
}
// Create instance
for (const [param, instances] of syncState.instancesToCreate) {
if (sessionContext.deviceData.attributes.has(param)) {
const instance = instances.values().next().value;
if (instance) {
instances.delete(instance);
return {
name: "AddObject",
objectName: param.concat("").join("."),
instanceValues: instance,
next: "getInstanceKeys"
};
}
}
}
// Set values
const GPV_BATCH_SIZE = config.get("GPV_BATCH_SIZE", sessionContext.deviceId);
const DATETIME_MILLISECONDS = config.get(
"DATETIME_MILLISECONDS",
sessionContext.deviceId
);
const BOOLEAN_LITERAL = config.get(
"BOOLEAN_LITERAL",
sessionContext.deviceId
);
const parameterValues = [];
for (const [k, v] of syncState.spv) {
syncState.spv.delete(k);
const attrs = sessionContext.deviceData.attributes.get(k);
const curVal = attrs.value ? attrs.value[1] : null;
if (curVal && attrs.writable && attrs.writable[1]) {
const val = v.slice();
if (!val[1]) val[1] = curVal[1];
device.sanitizeParameterValue(val);
// Strip milliseconds
if (
val[1] === "xsd:dateTime" &&
!DATETIME_MILLISECONDS &&
typeof val[0] === "number"
)
val[0] -= val[0] % 1000;
if (val[0] !== curVal[0] || val[1] !== curVal[1])
parameterValues.push([k, val[0], val[1]]);
if (parameterValues.length >= GPV_BATCH_SIZE) break;
}
}
if (parameterValues.length) {
return {
name: "SetParameterValues",
parameterList: parameterValues.map(p => [p[0].join("."), p[1], p[2]]),
DATETIME_MILLISECONDS: DATETIME_MILLISECONDS,
BOOLEAN_LITERAL: BOOLEAN_LITERAL
};
}
// Downloads
for (const [p, t] of syncState.downloadsDownload) {
const attrs = deviceData.attrs.get(p);
if (!(attrs && attrs.value && t <= attrs.value[1][0])) {
const fileTypeAttrs = deviceData.attributes.get(
deviceData.paths.get(p.slice(0, -1).concat("FileType"))
);
const fileNameAttrs = deviceData.attributes.get(
deviceData.paths.get(p.slice(0, -1).concat("FileName"))
);
const targetFileNameAttrs = deviceData.attributes.get(
deviceData.paths.get(p.slice(0, -1).concat("TargetFileName"))
);
return {
name: "Download",
commandKey: generateRpcId(sessionContext),
instance: p[1],
fileType: fileTypeAttrs
? fileTypeAttrs.value
? fileTypeAttrs.value[1][0]
: null
: null,
fileName: fileNameAttrs
? fileNameAttrs.value
? fileNameAttrs.value[1][0]
: null
: null,
targetFileName: targetFileNameAttrs
? targetFileNameAttrs.value
? targetFileNameAttrs.value[1][0]
: null
: null
};
}
}
// Reboot
if (syncState.reboot) {
const p = sessionContext.deviceData.paths.get(["Reboot"]);
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
if (!(attrs && attrs.value && attrs.value[1][0] >= syncState.reboot)) {
delete syncState.reboot;
return { name: "Reboot" };
}
}
// Factory reset
if (syncState.reboot) {
const p = sessionContext.deviceData.paths.get(["FactoryReset"]);
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
if (
!(attrs && attrs.value && attrs.value[1][0] >= syncState.factoryReset)
) {
delete syncState.factoryReset;
return { name: "FactoryReset" };
}
}
return null;
}
function generateGetVirtualParameterProvisions(
sessionContext,
virtualParameterDeclarations
) {
let provisions;
if (virtualParameterDeclarations) {
for (const declaration of virtualParameterDeclarations) {
if (declaration[1]) {
const currentTimestamps = {};
const currentValues = {};
const dec = {};
const attrs =
sessionContext.deviceData.attributes.get(declaration[0]) || {};
for (const [k, v] of Object.entries(declaration[1])) {
if (k !== "value" && k !== "writable") continue;
if (!attrs[k] || v > attrs[k][0]) dec[k] = v;
}
for (const [k, v] of Object.entries(attrs)) {
currentTimestamps[k] = v[0];
currentValues[k] = v[1];
}
if (Object.keys(dec).length) {
if (!provisions) provisions = [];
provisions.push([
declaration[0][1],
dec,
{},
currentTimestamps,
currentValues
]);
}
}
}
}
return provisions;
}
function generateSetVirtualParameterProvisions(
sessionContext,
virtualParameterDeclarations
) {
let provisions;
if (virtualParameterDeclarations) {
for (const declaration of virtualParameterDeclarations) {
if (declaration[2] && declaration[2].value != null) {
const attrs = sessionContext.deviceData.attributes.get(declaration[0]);
if (
attrs &&
attrs.writable &&
attrs.writable[1] &&
attrs.value &&
attrs.value[1] != null
) {
const val = declaration[2].value.slice();
if (val[1] == null) val[1] = attrs.value[1][1];
device.sanitizeParameterValue(val);
if (val[0] !== attrs.value[1][0] || val[1] !== attrs.value[1][1]) {
if (!provisions) provisions = [];
const currentTimestamps = {};
const currentValues = {};
for (const [k, v] of Object.entries(attrs)) {
currentTimestamps[k] = v[0];
currentValues[k] = v[1];
}
provisions.push([
declaration[0][1],
{},
{ value: val },
currentTimestamps,
currentValues
]);
}
}
}
}
}
return provisions;
}
function processDeclarations(
sessionContext,
allDeclareTimestamps,
allDeclareAttributeTimestamps,
allDeclareAttributeValues
) {
const deviceData = sessionContext.deviceData;
const syncState = sessionContext.syncState;
const root = sessionContext.deviceData.paths.add([]);
const paths = sessionContext.deviceData.paths.find([], false, true, 99);
paths.sort((a, b) => {
if (a.wildcard === b.wildcard) return a.length - b.length;
return a.wildcard - b.wildcard;
});
const virtualParameterDeclarations = [];
function func(leafParam, leafIsObject, leafTimestamp, _paths) {
const currentPath = _paths[0];
const children = {};
let declareTimestamp = 0;
let declareAttributeTimestamps;
let declareAttributeValues;
let currentTimestamp = 0;
let currentAttributes;
if (currentPath.wildcard === 0)
currentAttributes = deviceData.attributes.get(currentPath);
for (const path of _paths) {
if (path.length > currentPath.length) {
const fragment = path[currentPath.length];
if (!children[fragment]) {
children[fragment] = [];
if (path.length > currentPath.length + 1) {
// This is to ensure we don't descend more than one step at a time
const p = common.addPathMeta(path.slice(0, currentPath.length + 1));
children[fragment].push(p);
}
}
children[fragment].push(path);
continue;
}
currentTimestamp = Math.max(
currentTimestamp,
deviceData.timestamps.get(path) || 0
);
declareTimestamp = Math.max(
declareTimestamp,
allDeclareTimestamps.get(path) || 0
);
if (currentPath.wildcard === 0) {
const attrs = allDeclareAttributeTimestamps.get(path);
if (attrs) {
if (declareAttributeTimestamps) {
declareAttributeTimestamps = Object.assign(
{},
declareAttributeTimestamps
);
for (const [k, v] of Object.entries(attrs)) {
declareAttributeTimestamps[k] = Math.max(
v,
declareAttributeTimestamps[k] || 0
);
}
} else {
declareAttributeTimestamps = attrs;
}
}
allDeclareAttributeValues.get(path) || declareAttributeValues;
}
}
if (currentAttributes) {
leafParam = currentPath;
leafIsObject = currentAttributes.object
? currentAttributes.object[1]
: null;
if (leafIsObject === 0)
leafTimestamp = Math.max(leafTimestamp, currentAttributes.object[0]);
} else {
leafTimestamp = Math.max(leafTimestamp, currentTimestamp);
}
switch (currentPath[0] !== "*" ? currentPath[0] : leafParam[0]) {
case "Reboot":
if (currentPath.length === 1) {
if (declareAttributeValues && declareAttributeValues.value)
syncState.reboot = +new Date(declareAttributeValues.value[0]);
}
break;
case "FactoryReset":
if (currentPath.length === 1) {
if (declareAttributeValues && declareAttributeValues.value)
syncState.factoryReset = +new Date(declareAttributeValues.value[0]);
}
break;
case "Tags":
if (
currentPath.length === 2 &&
currentPath.wildcard === 0 &&
declareAttributeValues &&
declareAttributeValues.value
) {
syncState.tags.set(
currentPath,
device.sanitizeParameterValue([
declareAttributeValues.value[0],
"xsd:boolean"
])[0]
);
}
break;
case "Events":
case "DeviceID":
// Do nothing
break;
case "Downloads":
if (
currentPath.length === 3 &&
currentPath.wildcard === 0 &&
declareAttributeValues &&
declareAttributeValues.value
) {
if (currentPath[2] === "Download") {
syncState.downloadsDownload.set(
currentPath,
declareAttributeValues.value[0]
);
} else {
syncState.downloadsValues.set(
currentPath,
declareAttributeValues.value[0]
);
}
}
break;
case "VirtualParameters":
if (currentPath.length <= 2) {
let d;
if (!(declareTimestamp <= currentTimestamp)) d = [currentPath];
if (currentPath.wildcard === 0) {
if (declareAttributeTimestamps) {
for (const [attrName, attrTimestamp] of Object.entries(
declareAttributeTimestamps
)) {
if (
!(
currentAttributes &&
currentAttributes[attrName] &&
attrTimestamp <= currentAttributes[attrName][0]
)
) {
if (!d) d = [currentPath];
if (!d[1]) d[1] = {};
d[1][attrName] = attrTimestamp;
}
}
}
if (declareAttributeValues) {
if (!d) d = [currentPath];
d[2] = declareAttributeValues;
}
}
if (d) virtualParameterDeclarations.push(d);
}
break;
default:
if (
declareTimestamp > currentTimestamp &&
declareTimestamp > leafTimestamp
) {
if (currentPath === leafParam) {
syncState.refreshAttributes.exist.add(leafParam);
} else if (leafIsObject) {
syncState.gpn.add(leafParam);
if (leafTimestamp > 0) {
const f = 1 << leafParam.length;
syncState.gpnPatterns.set(
leafParam,
f | syncState.gpnPatterns.get(leafParam)
);
} else {
const f =
((1 << currentPath.length) - 1) ^ ((1 << leafParam.length) - 1);
syncState.gpnPatterns.set(
currentPath,
f | syncState.gpnPatterns.get(currentPath)
);
}
} else {
syncState.refreshAttributes.object.add(leafParam);
if (leafIsObject == null) {
const f =
((1 << syncState.gpnPatterns.length) - 1) ^
((1 << leafParam.length) - 1);
syncState.gpnPatterns.set(
currentPath,
f | syncState.gpnPatterns.get(currentPath)
);
}
}
}
if (currentAttributes) {
if (declareAttributeTimestamps) {
for (const [attrName, attrTimestamp] of Object.entries(
declareAttributeTimestamps
)) {
if (
!(
currentAttributes[attrName] &&
attrTimestamp <= currentAttributes[attrName][0]
)
) {
if (attrName === "value") {
if (
!(
currentAttributes.object &&
currentAttributes.object[1] != null
)
)
syncState.refreshAttributes.object.add(currentPath);
else if (currentAttributes.object[1] === 0)
syncState.refreshAttributes.value.add(currentPath);
} else {
syncState.refreshAttributes[attrName].add(currentPath);
}
}
}
}
if (declareAttributeValues && declareAttributeValues.value != null)
syncState.spv.set(currentPath, declareAttributeValues.value);
}
}
for (const child of Object.keys(children)) {
// This fine expression avoids duplicate visits, don't ask.
if (
((currentPath.wildcard ^ children[child][0].wildcard) &
((1 << currentPath.length) - 1)) >>
leafParam.length ===
0
) {
if (child !== "*" && children["*"])
children[child] = children[child].concat(children["*"]);
func(leafParam, leafIsObject, leafTimestamp, children[child]);
}
}
}
if (
allDeclareTimestamps.size ||
allDeclareAttributeTimestamps.size ||
allDeclareAttributeValues.size
)
func(root, 1, 0, paths);
return virtualParameterDeclarations;
}
function loadPath(sessionContext, path, depth) {
depth = depth || (1 << path.length) - 1;
if (sessionContext.new || !depth) return true;
if (!sessionContext.toLoad) sessionContext.toLoad = new Map();
// Trim trailing wildcards
let trimWildcard = path.length;
while (trimWildcard && path[trimWildcard - 1] === "*") --trimWildcard;
if (trimWildcard < path.length) path = path.slice(0, trimWildcard);
for (let i = 0; i <= path.length; ++i) {
let d = i === path.length ? 99 : i;
for (const sup of sessionContext.deviceData.paths.find(
path.slice(0, i),
true,
false,
d
)) {
let v =
sessionContext.deviceData.loaded.get(sup) |
sessionContext.toLoad.get(sup);
if (sup.length > i) v &= (1 << i) - 1;
depth &= depth ^ v;
if (depth === 0) return true;
}
}
path = sessionContext.deviceData.paths.add(path);
depth |= sessionContext.toLoad.get(path);
sessionContext.toLoad.set(path, depth);
return false;
}
function processInstances(
sessionContext,
parent,
parameters,
keys,
minInstances,
maxInstances
) {
let instancesToCreate, instancesToDelete;
if (parent[0] === "Downloads") {
if (parent.length !== 1) return;
instancesToDelete = sessionContext.syncState.downloadsToDelete;
instancesToCreate = sessionContext.syncState.downloadsToCreate;
} else {
instancesToDelete = sessionContext.syncState.instancesToDelete.get(parent);
if (instancesToDelete == null) {
instancesToDelete = new Set();
sessionContext.syncState.instancesToDelete.set(parent, instancesToDelete);
}
instancesToCreate = sessionContext.syncState.instancesToCreate.get(parent);
if (instancesToCreate == null) {
instancesToCreate = new InstanceSet();
sessionContext.syncState.instancesToCreate.set(parent, instancesToCreate);
}
}
let counter = 0;
for (const p of parameters) {
++counter;
if (counter > maxInstances) instancesToDelete.add(p);
else if (counter <= minInstances) instancesToDelete.delete(p);
}
// Key is null if deleting a particular instance rather than use alias
if (!keys) return;
for (const inst of instancesToCreate.superset(keys)) {
++counter;
if (counter > maxInstances) instancesToCreate.delete(inst);
}
for (const inst of instancesToCreate.subset(keys)) {
++counter;
if (counter <= minInstances) {
instancesToCreate.delete(inst);
instancesToCreate.add(JSON.parse(JSON.stringify(keys)));
}
}
while (counter < minInstances) {
++counter;
instancesToCreate.add(JSON.parse(JSON.stringify(keys)));
}
}
function clear(sessionContext, toClear, callback) {
if (!toClear || !toClear.length) return callback();
const MAX_DEPTH = config.get("MAX_DEPTH", sessionContext.deviceId);
for (const c of toClear) {
if (c[1]) {
const p = c[0].slice(0, -1); // in order to include superset
loadPath(
sessionContext,
p,
((1 << p.length) - 1) ^ ((1 << MAX_DEPTH) - 1)
);
} else if (c[2] && c[2].object) {
loadPath(
sessionContext,
c[0],
(((1 << c[0].length) - 1) >> 1) ^ ((1 << MAX_DEPTH) - 1)
);
} else {
return loadPath(sessionContext, c[0], (1 << c[0].length) >> 1);
}
}
loadParameters(sessionContext, err => {
if (err) return callback(err);
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
callback();
});
}
function rpcResponse(sessionContext, id, rpcRes, callback) {
if (id !== generateRpcId(sessionContext))
return callback(new Error("Request ID not recognized"));
++sessionContext.rpcCount;
const rpcReq = sessionContext.rpcRequest;
if (!rpcReq.next) {
sessionContext.rpcRequest = null;
} else if (rpcReq.next === "getInstanceKeys") {
const parameterNames = [];
const instanceValues = {};
for (const [k, v] of Object.entries(rpcReq.instanceValues)) {
const n = `${rpcReq.objectName}${rpcRes.instanceNumber}.${k}`;
parameterNames.push(n);
instanceValues[n] = v;
}
if (!parameterNames.length) {
sessionContext.rpcRequest = null;
} else {
sessionContext.rpcRequest = {
name: "GetParameterValues",
parameterNames: parameterNames,
next: "setInstanceKeys",
instanceValues: instanceValues
};
}
} else if (rpcReq.next === "setInstanceKeys") {
const parameterList = [];
for (const p of rpcRes.parameterList) {
if (p[1] !== rpcReq.instanceValues[p[0]]) {
parameterList.push(
[p[0]].concat(
device.sanitizeParameterValue([rpcReq.instanceValues[p[0]], p[2]])
)
);
}
}
if (!parameterList.length) {
sessionContext.rpcRequest = null;
} else {
sessionContext.rpcRequest = {
name: "SetParameterValues",
parameterList: parameterList
};
}
}
const timestamp = sessionContext.timestamp + sessionContext.iteration;
const revision =
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision;
sessionContext.deviceData.attributes.revision = revision;
let toClear, root, missing, params;
switch (rpcRes.name) {
case "GetParameterValuesResponse":
if (rpcReq.name !== "GetParameterValues")
return callback(new Error("Response name does not match request name"));
for (const p of rpcRes.parameterList) {
toClear = device.set(
sessionContext.deviceData,
common.parsePath(p[0]),
timestamp,
{ object: [timestamp, 0], value: [timestamp, p.slice(1)] },
toClear
);
}
break;
case "GetParameterNamesResponse":
if (rpcReq.name !== "GetParameterNames")
return callback(new Error("Response name does not match request name"));
if (rpcReq.parameterPath.endsWith("."))
root = common.parsePath(rpcReq.parameterPath.slice(0, -1));
else root = common.parsePath(rpcReq.parameterPath);
params = [[root.concat("*"), timestamp]];
// Some clients don't report all ancestors explicitly
missing = {};
for (const p of rpcRes.parameterList) {
let i = p[0].length - 1;
while ((i = p[0].lastIndexOf(".", i - 1)) > rpcReq.parameterPath.length)
missing[p[0].slice(0, i)] |= 0;
if (p[0].endsWith(".")) {
missing[p[0].slice(0, -1)] |= 1;
const path = common.parsePath(p[0].slice(0, -1));
if (!rpcReq.nextLevel) params.push([path.concat("*"), timestamp]);
params.push([
path,
timestamp,
{ object: [timestamp, 1], writable: [timestamp, p[1] ? 1 : 0] }
]);
} else {
missing[p[0]] |= 1;
params.push([
common.parsePath(p[0]),
timestamp,
{ object: [timestamp, 0], writable: [timestamp, p[1] ? 1 : 0] }
]);
}
}
for (const [k, v] of Object.entries(missing)) {
if (v === 0) {
// TODO consider showing a warning
const path = common.parsePath(k);
params.push([
path,
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] }
]);
params.push([path.concat("*"), timestamp]);
}
}
// Sort such that:
// - Longer params come first in order to work around client issue
// where object paths can have no trailing dot.
// - Parameters come before wildcard paths.
params.sort((a, b) => {
let al = a[0].length;
let bl = b[0].length;
if (b[0][bl - 1] === "*") bl *= -1;
if (a[0][al - 1] === "*") al *= -1;
return bl - al;
});
if (rpcReq.nextLevel) {
loadPath(sessionContext, root, (1 << (root.length + 1)) - 1);
} else {
const MAX_DEPTH = config.get("MAX_DEPTH", sessionContext.deviceId);
loadPath(sessionContext, root, (1 << MAX_DEPTH) - 1);
}
loadParameters(sessionContext, err => {
if (err) return callback(err);
if (!root.length) {
for (const n of [
"DeviceID",
"Events",
"Tags",
"Reboot",
"FactoryReset",
"VirtualParameters",
"Downloads"
]) {
const p = sessionContext.deviceData.paths.get([n]);
if (p && sessionContext.deviceData.attributes.has(p))
sessionContext.deviceData.timestamps.set(p, timestamp);
}
}
for (const p of params) {
toClear = device.set(
sessionContext.deviceData,
p[0],
p[1],
p[2],
toClear
);
}
clear(sessionContext, toClear, callback);
});
return;
case "SetParameterValuesResponse":
if (rpcReq.name !== "SetParameterValues")
return callback(new Error("Response name does not match request name"));
for (const p of rpcReq.parameterList) {
toClear = device.set(
sessionContext.deviceData,
common.parsePath(p[0]),
timestamp + 1,
{
object: [timestamp + 1, 0],
writable: [timestamp + 1, 1],
value: [timestamp + 1, p.slice(1)]
},
toClear
);
}
break;
case "AddObjectResponse":
toClear = device.set(
sessionContext.deviceData,
common.parsePath(rpcReq.objectName + rpcRes.instanceNumber),
timestamp + 1,
{ object: [timestamp + 1, 1] },
toClear
);
break;
case "DeleteObjectResponse":
toClear = device.set(
sessionContext.deviceData,
common.parsePath(rpcReq.objectName.slice(0, -1)),
timestamp + 1,
null,
toClear
);
break;
case "RebootResponse":
toClear = device.set(
sessionContext.deviceData,
common.parsePath("Reboot"),
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear
);
break;
case "FactoryResetResponse":
toClear = device.set(
sessionContext.deviceData,
common.parsePath("FactoryReset"),
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear
);
break;
case "DownloadResponse":
toClear = device.set(
sessionContext.deviceData,
["Downloads", rpcReq.instance, "Download"],
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear
);
if (rpcRes.status === 0) {
toClear = device.set(
sessionContext.deviceData,
["Downloads", rpcReq.instance, "LastDownload"],
timestamp + 1,
{
value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]]
},
toClear
);
toClear = device.set(
sessionContext.deviceData,
["Downloads", rpcReq.instance, "LastFileType"],
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
["Downloads", rpcReq.instance, "LastFileName"],
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
["Downloads", rpcReq.instance, "LastTargetFileName"],
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
["Downloads", rpcReq.instance, "StartTime"],
timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
["Downloads", rpcReq.instance, "CompleteTime"],
timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] },
toClear
);
} else {
const operation = {
name: "Download",
timestamp: sessionContext.timestamp,
provisions: sessionContext.provisions,
channels: sessionContext.channels,
retries: {},
args: {
instance: rpcReq.instance,
fileType: rpcReq.fileType,
fileName: rpcReq.fileName,
targetFileName: rpcReq.targetFileName
}
};
for (const channel of Object.keys(sessionContext.channels)) {
if (sessionContext.retries[channel] != null)
operation.retries[channel] = sessionContext.retries[channel];
}
sessionContext.operations[rpcReq.commandKey] = operation;
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[rpcReq.commandKey] = 1;
}
break;
default:
return callback(new Error("Response name not recognized"));
}
clear(sessionContext, toClear, callback);
}
function rpcFault(sessionContext, id, faultResponse, callback) {
// TODO Consider handling invalid parameter faults by automatically refreshing
// relevant data model portions
const fault = {
code: `cwmp.${faultResponse.detail.faultCode}`,
message: faultResponse.detail.faultString,
detail: faultResponse.detail,
timestamp: sessionContext.timestamp
};
delete sessionContext.syncState;
callback(null, fault);
}
function deserialize(sessionContextString, callback) {
localCache.getProvisionsAndVirtualParameters(
(err, hash, provisions, virtualParameters) => {
if (err) return callback(err);
const sessionContext = JSON.parse(sessionContextString);
if (sessionContext.presetsHash && sessionContext.presetsHash !== hash)
return callback(new Error("Preset hash mismatch"));
provisionsCache.set(sessionContext, provisions);
virtualParametersCache.set(sessionContext, virtualParameters);
for (const decs of sessionContext.declarations)
for (const d of decs) common.addPathMeta(d[0]);
const deviceData = initDeviceData();
for (const r of sessionContext.deviceData) {
const path = deviceData.paths.add(r[0]);
if (r[1]) deviceData.loaded.set(path, r[1]);
if (r[2]) deviceData.trackers.set(path, r[2]);
if (r[3]) {
deviceData.timestamps.setRevisions(path, r[3]);
if (r[4]) deviceData.attributes.setRevisions(path, r[4]);
}
}
sessionContext.deviceData = deviceData;
callback(null, sessionContext);
}
);
}
function serialize(sessionContext, callback) {
const deviceData = [];
for (const path of sessionContext.deviceData.paths.find(
[],
false,
false,
99
)) {
const e = [path];
e[1] = sessionContext.deviceData.loaded.get(path) || 0;
e[2] = sessionContext.deviceData.trackers.get(path) || null;
e[3] = sessionContext.deviceData.timestamps.getRevisions(path) || null;
e[4] = sessionContext.deviceData.attributes.getRevisions(path) || null;
deviceData.push(e);
}
sessionContext = Object.assign({}, sessionContext);
sessionContext.deviceData = deviceData;
delete sessionContext.syncState;
delete sessionContext.toLoad;
delete sessionContext.httpRequest;
delete sessionContext.httpResponse;
const sessionContextString = JSON.stringify(sessionContext);
callback(null, sessionContextString);
}
exports.init = init;
exports.timeoutOperations = timeoutOperations;
exports.inform = inform;
exports.transferComplete = transferComplete;
exports.addProvisions = addProvisions;
exports.clearProvisions = clearProvisions;
exports.rpcRequest = rpcRequest;
exports.rpcResponse = rpcResponse;
exports.rpcFault = rpcFault;
exports.serialize = serialize;
exports.deserialize = deserialize;