diff --git a/.gitignore b/.gitignore
index c8a7b48..dd9e4a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
*~
lib/*.map
-lib/session.js
node_modules
debug
config/auth.js
diff --git a/lib/instance-set.js b/lib/instance-set.js
index 3047745..20805b8 100644
--- a/lib/instance-set.js
+++ b/lib/instance-set.js
@@ -118,6 +118,10 @@ class InstanceSet {
return res;
}
+ [Symbol.iterator]() {
+ return this.set.values();
+ }
+
forEach(callback) {
return this.set.forEach(callback);
}
diff --git a/lib/session.coffee b/lib/session.coffee
deleted file mode 100644
index af38b4e..0000000
--- a/lib/session.coffee
+++ /dev/null
@@ -1,1654 +0,0 @@
-###
-# Copyright 2013-2017 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 .
-###
-
-config = require './config'
-common = require './common'
-db = require './db'
-device = require './device'
-sandbox = require './sandbox'
-localCache = require './local-cache'
-PathSet = require './path-set'
-VersionedMap = require './versioned-map'
-InstanceSet = require './instance-set'
-defaultProvisions = require './default-provisions'
-gpnHeuristic = require './gpn-heuristic'
-
-# Multiply by two because every iteration is two phases: read and update
-MAX_ITERATIONS = config.get('MAX_COMMIT_ITERATIONS') * 2
-
-provisionsCache = new WeakMap()
-virtualParametersCache = new WeakMap()
-
-
-initDeviceData = () ->
- return {
- paths : new PathSet()
- timestamps : new VersionedMap()
- attributes : new VersionedMap()
- loaded : new Map()
- trackers : new Map()
- changes : new Set()
- }
-
-
-init = (deviceId, cwmpVersion, timeout, callback) ->
- timestamp = Date.now()
-
- 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) ->
- return callback(err) if err
-
- sessionContext.presetsHash = hash
- provisionsCache.set(sessionContext, provisions)
- virtualParametersCache.set(sessionContext, virtualParameters)
-
- return callback(null, sessionContext)
- )
-
-
-loadParameters = (sessionContext, callback) ->
- if not sessionContext.toLoad?.size
- return callback()
-
- toLoad = Array.from(sessionContext.toLoad.entries())
- db.fetchDevice(sessionContext.deviceId, sessionContext.timestamp, toLoad, (err, parameters, loaded) ->
- return callback(err) if err
-
- if not parameters?
- # Device not available in database, mark as new
- sessionContext.new = true
- loaded = [[[], (1 << config.get('MAX_DEPTH', sessionContext.deviceId)) - 1]]
- parameters = []
-
- for p in loaded
- path = sessionContext.deviceData.paths.add(p[0])
-
- if p[1]
- l = sessionContext.deviceData.loaded.get(path) | 0
- sessionContext.deviceData.loaded.set(path, l | p[1])
-
- for p in parameters
- 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
-
- return callback()
- )
-
-
-generateRpcId = (sessionContext) ->
- return sessionContext.timestamp.toString(16) +
- "0#{sessionContext.cycle.toString(16)}".slice(-2) +
- "0#{sessionContext.rpcCount.toString(16)}".slice(-2)
-
-
-inform = (sessionContext, rpcReq, callback) ->
- timestamp = sessionContext.timestamp + sessionContext.iteration + 1
-
- params = []
- params.push([['DeviceID', 'Manufacturer'], timestamp,
- {object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [rpcReq.deviceId.Manufacturer, 'xsd:string']]}])
-
- params.push([['DeviceID', 'OUI'], timestamp,
- {object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [rpcReq.deviceId.OUI, 'xsd:string']]}])
-
- params.push([['DeviceID', 'ProductClass'], timestamp,
- {object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [rpcReq.deviceId.ProductClass, 'xsd:string']]}])
-
- params.push([['DeviceID', 'SerialNumber'], timestamp,
- {object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [rpcReq.deviceId.SerialNumber, 'xsd:string']]}])
-
- for p in rpcReq.parameterList
- 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 e in rpcReq.event
- params.push([['Events', e.replace(' ', '_')], timestamp,
- {object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [sessionContext.timestamp, 'xsd:dateTime']]}])
-
- # Preload DeviceID params
- loadPath(sessionContext, ['DeviceID', '*'])
-
- for p in params
- loadPath(sessionContext, p[0])
-
- loadParameters(sessionContext, (err) ->
- return callback(err) if 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
-
- toClear = null
- for p in 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) ->
- return callback(err, {name: 'InformResponse'})
- )
- )
-
-
-transferComplete = (sessionContext, rpcReq, callback) ->
- revision = (sessionContext.revisions[sessionContext.revisions.length - 1] or 0) + 1
- sessionContext.deviceData.timestamps.revision = revision
- sessionContext.deviceData.attributes.revision = revision
- commandKey = rpcReq.commandKey
- operation = sessionContext.operations[commandKey]
-
- if not operation?
- return callback(null, {name: 'TransferCompleteResponse'})
-
- instance = operation.args.instance
-
- delete sessionContext.operations[commandKey]
- sessionContext.operationsTouched ?= {}
- sessionContext.operationsTouched[commandKey] = 1
-
- if rpcReq.faultStruct? and rpcReq.faultStruct.faultCode != '0'
- return revertDownloadParameters(sessionContext, operation.args.instance, (err) ->
- fault = {
- code: "cwmp.#{rpcReq.faultStruct.faultCode}"
- message: rpcReq.faultStruct.faultString
- detail: rpcReq.faultStruct
- timestamp: operation.timestamp
- }
-
- return callback(err, {name: 'TransferCompleteResponse'}, operation, fault)
- )
-
- loadPath(sessionContext, ['Downloads', instance, '*'])
-
- loadParameters(sessionContext, (err) ->
- return callback(err) if err
-
- toClear = null
- timestamp = sessionContext.timestamp + sessionContext.iteration + 1
-
- 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) ->
- return callback(err, {name: 'TransferCompleteResponse'}, operation)
- )
- )
-
-
-revertDownloadParameters = (sessionContext, instance, callback) ->
- loadPath(sessionContext, ['Downloads', instance, '*'])
-
- loadParameters(sessionContext, (err) ->
- return callback(err) if err
-
- timestamp = sessionContext.timestamp + sessionContext.iteration + 1
-
- p = sessionContext.deviceData.paths.add(['Downloads', instance, 'LastDownload'])
- LastDownload = sessionContext.deviceData.attributes.get(p)
-
- p = sessionContext.deviceData.paths.add(['Downloads', instance, 'Download'])
- toClear = device.set(sessionContext.deviceData, p, timestamp,
- {value: [timestamp, [LastDownload?.value[1]?[0] or 0, 'xsd:dateTime']]}, toClear)
-
- clear(sessionContext, toClear, callback)
- )
-
-timeoutOperations = (sessionContext, callback) ->
- revision = (sessionContext.revisions[sessionContext.revisions.length - 1] or 0) + 1
- sessionContext.deviceData.timestamps.revision = revision
- sessionContext.deviceData.attributes.revision = revision
- faults = []
- operations = []
- counter = 3
-
- for commandKey, operation of sessionContext.operations
- if operation.name isnt 'Download'
- return callback(new Error("Unknown operation name #{operation.name}"))
-
- DOWNLOAD_TIMEOUT = config.get('DOWNLOAD_TIMEOUT', sessionContext.deviceId) * 1000
-
- if sessionContext.timestamp > operation.timestamp + DOWNLOAD_TIMEOUT
- delete sessionContext.operations[commandKey]
- 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
- callback(err) if counter & 1
- return counter = 0
-
- return callback(null, faults, operations) if (counter -= 2) == 1
- )
-
- return callback(null, faults, operations) if (counter -= 2) == 1
-
-
-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 provision, i in provisions
- channels = [channel]
- # Remove duplicate provisions
- provisionStr = JSON.stringify(provision)
- for p, j in sessionContext.provisions
- if JSON.stringify(p) == provisionStr
- sessionContext.provisions.splice(j, 1)
- for c of sessionContext.channels
- channels.push(c) if sessionContext.channels[c] & (1 << j)
- a = sessionContext.channels[c] >> (j + 1)
- sessionContext.channels[c] &= ((1 << j) - 1)
- sessionContext.channels[c] |= (a << j)
-
- for c in channels
- sessionContext.channels[c] |= (1 << sessionContext.provisions.length)
- sessionContext.provisions.push(provision)
-
-
-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 = {}
-
-
-runProvisions = (sessionContext, provisions, startRevision, endRevision, callback) ->
- done = true
- allDeclarations = []
- allClear = []
- counter = 3
- allProvisions = provisionsCache.get(sessionContext)
- for provision, j in provisions
- if not allProvisions[provision[0]]?
- allDeclarations[j] = []
- allClear[j] = []
- if defaultProvisions[provision[0]]
- done = defaultProvisions[provision[0]](sessionContext, provision, allDeclarations[j], startRevision, endRevision) and done
- continue
-
- counter += 2
- do (j) -> sandbox.run(allProvisions[provision[0]].script,
- {args: provision.slice(1)}, sessionContext, startRevision, endRevision,
- (err, _fault, _clear, _declarations, _done) ->
- if err or _fault
- callback(err, _fault) if counter & 1
- return counter = 0
-
- done &&= _done
- allDeclarations[j] = _declarations or []
- allClear[j] = _clear or []
-
- if (counter -= 2) == 1
- allDeclarations = Array.prototype.concat.apply([], allDeclarations)
- allClear = Array.prototype.concat.apply([], allClear)
- return callback(null, null, done, allDeclarations, allClear)
- )
-
- if (counter -= 2) == 1
- allDeclarations = Array.prototype.concat.apply([], allDeclarations)
- allClear = Array.prototype.concat.apply([], allClear)
- return callback(null, null, done, allDeclarations, allClear)
-
-
-runVirtualParameters = (sessionContext, provisions, startRevision, endRevision, callback) ->
- done = true
- virtualParameterUpdates = []
- allDeclarations = []
- allClear = []
- counter = 3
- allVirtualParameters = virtualParametersCache.get(sessionContext)
- for provision, j in provisions
- counter += 2
- globals = {args: provision.slice(1)}
- do (provision, j) -> sandbox.run(allVirtualParameters[provision[0]].script, globals,
- sessionContext, startRevision, endRevision,
- (err, _fault, _clear, _declarations, _done, _returnValue) ->
- if err or _fault
- callback(err, _fault) if counter & 1
- return counter = 0
-
- done &&= _done
- allDeclarations[j] = _declarations or []
- allClear[j] = _clear or []
- if _done
- if not _returnValue
- if counter & 1
- callback(null, {code: 'script', message: 'Invalid virtual parameter return value'})
- return counter = 0
-
- ret = {}
-
- if _returnValue.writable?
- ret.writable = +!!_returnValue.writable
- else if provision[1].writable? or provision[2].writable?
- if counter & 1
- callback(null, {code: 'script', message: 'Virtual parameter must provide declared attributes'})
- return counter = 0
-
- if _returnValue.value?
- if not Array.isArray(_returnValue.value)
- _returnValue.value = [_returnValue.value]
-
- if not _returnValue.value[1]
- if typeof _returnValue.value[0] is 'number'
- _returnValue.value[1] = 'xsd:int'
- else if typeof _returnValue.value[0] is 'boolean'
- _returnValue.value[1] = 'xsd:boolean'
- else if _returnValue.value[0] instanceof Date
- _returnValue.value[1] = 'xsd:dateTime'
- else
- _returnValue.value[1] = 'xsd:string'
-
- if not _returnValue.value[0]? or
- _returnValue.value[1] not in ['xsd:int', 'xsd:unsignedInt', 'xsd:boolean', 'xsd:string', 'xsd:dateTime', 'xsd:base64', 'xsd:hexBinary']
- 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? or provision[2].value?
- 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, (if done then virtualParameterUpdates else null), allDeclarations, allClear)
- )
-
- if (counter -= 2) == 1
- allDeclarations = Array.prototype.concat.apply([], allDeclarations)
- allClear = Array.prototype.concat.apply([], allClear)
- return callback(null, null, (if done then virtualParameterUpdates else null), allDeclarations, allClear)
-
-
-runDeclarations = (sessionContext, declarations) ->
- 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()
- }
-
- allDeclareTimestamps = new Map()
- allDeclareAttributeTimestamps = new Map()
- allDeclareAttributeValues = new Map()
-
- allVirtualParameters = virtualParametersCache.get(sessionContext)
-
- mergeAttributeTimestamps = (p, attrs) ->
- if cur = allDeclareAttributeTimestamps.get(p)
- cur = Object.assign({}, cur)
- cur[k] = Math.max(v, cur[k] or 0) for k, v of attrs
- allDeclareAttributeTimestamps.set(p, cur)
- else
- allDeclareAttributeTimestamps.set(p, attrs)
-
- mergeAttributeValues = (p, attrs) ->
- if cur = allDeclareAttributeValues.get(p)
- cur = Object.assign({}, cur, attrs)
- allDeclareAttributeValues.set(p, cur)
- else
- allDeclareAttributeValues.set(p, attrs)
-
- for declaration, i in declarations
- path = common.addPathMeta(declaration[0])
- unpacked = null
-
- if (path.alias | path.wildcard) & 1 or path[0] == 'VirtualParameters'
- sessionContext.deviceData.paths.add(['VirtualParameters'])
- if (path.alias | path.wildcard) & 2
- sessionContext.deviceData.paths.add(['VirtualParameters', '*'])
- for k of allVirtualParameters
- sessionContext.deviceData.paths.add(['VirtualParameters', k])
-
- if (path.alias | path.wildcard) & 1 or path[0] == 'Reboot'
- sessionContext.deviceData.paths.add(['Reboot'])
-
- if (path.alias | path.wildcard) & 1 or path[0] == 'FactoryReset'
- sessionContext.deviceData.paths.add(['FactoryReset'])
-
- if path.alias
- aliasDecs = device.getAliasDeclarations(path, declaration[1] or 1)
- for ad in aliasDecs
- p = sessionContext.deviceData.paths.add(ad[0])
- allDeclareTimestamps.set(p, Math.max(ad[1] or 1, allDeclareTimestamps.get(p) or 0))
- attrTrackers = null
- 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 u in unpacked
- allDeclareTimestamps.set(u, Math.max(declaration[1] or 1, allDeclareTimestamps.get(u) or 0))
- if declaration[2]
- mergeAttributeTimestamps(u, declaration[2])
- else
- path = sessionContext.deviceData.paths.add(path)
- allDeclareTimestamps.set(path, Math.max(declaration[1] or 1, allDeclareTimestamps.get(path) or 0))
- if declaration[2]
- mergeAttributeTimestamps(path, declaration[2])
- device.track(sessionContext.deviceData, path, 'prerequisite')
-
- if declaration[4]
- if path.alias | path.wildcard
- unpacked ?= device.unpack(sessionContext.deviceData, path)
- for u in unpacked
- mergeAttributeValues(u, declaration[4])
- else
- mergeAttributeValues(path, declaration[4])
-
- if declaration[3]?
- if Array.isArray(declaration[3])
- minInstances = declaration[3][0]
- maxInstances = declaration[3][1]
- else
- minInstances = maxInstances = declaration[3]
-
- parent = common.addPathMeta(path.slice(0, -1))
-
- keys = null
- if Array.isArray(path[path.length - 1])
- keys = {}
- for p, i in path[path.length - 1] by 2
- keys[p.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)
- unpacked ?= device.unpack(sessionContext.deviceData, path)
- processInstances(sessionContext, parent, unpacked, keys, minInstances, maxInstances)
- else
- parentsUnpacked = device.unpack(sessionContext.deviceData, parent)
- for parent in parentsUnpacked
- parent = sessionContext.deviceData.paths.add(parent)
- processInstances(sessionContext, parent, device.unpack(sessionContext.deviceData, common.addPathMeta(parent.concat([path[parent.length]]))), keys, minInstances, maxInstances)
-
- return processDeclarations(sessionContext, allDeclareTimestamps, allDeclareAttributeTimestamps, allDeclareAttributeValues)
-
-
-rpcRequest = (sessionContext, _declarations, callback) ->
- if sessionContext.rpcRequest?
- return callback(null, null, generateRpcId(sessionContext), sessionContext.rpcRequest)
-
- if sessionContext.virtualParameters.length == 0 and
- sessionContext.declarations.length == 0 and
- not _declarations?.length and
- sessionContext.provisions.length == 0
- return callback()
-
- if sessionContext.declarations.length <= sessionContext.virtualParameters.length
- inception = sessionContext.declarations.length
- revision = (sessionContext.revisions[inception] or 0) + 1
- sessionContext.deviceData.timestamps.revision = revision
- sessionContext.deviceData.attributes.revision = revision
-
- if inception == 0
- run = runProvisions
- provisions = sessionContext.provisions
- else
- run = runVirtualParameters
- provisions = sessionContext.virtualParameters[inception - 1]
-
- return run(sessionContext, provisions, sessionContext.revisions[inception - 1] or 0, sessionContext.revisions[inception], (err, fault, ret, decs, toClear) ->
- return callback(err) if err
-
- if fault
- fault.timestamp = sessionContext.timestamp
- return callback(null, fault)
-
- # Enforce max clear timestamp
- for c in toClear
- c[1] = sessionContext.timestamp if c[1] > sessionContext.timestamp
- for k, v of c[2]
- c[2][k] = sessionContext.timestamp if v > sessionContext.timestamp
-
- sessionContext.declarations.push(decs)
- sessionContext.provisionsRet[inception] = ret
-
- for d in decs
- # Enforce max timestamp
- d[1] = sessionContext.timestamp if d[1] > sessionContext.timestamp
- for k, v of d[2]
- d[2][k] = sessionContext.timestamp if v > sessionContext.timestamp
-
- for ad in device.getAliasDeclarations(d[0], 1)
- loadPath(sessionContext, ad[0])
-
- return clear(sessionContext, toClear, (err) ->
- return callback(err) if err
- loadParameters(sessionContext, (err) ->
- return callback(err) if err
- rpcRequest(sessionContext, _declarations, callback)
- )
- )
- )
-
- if _declarations?.length
- delete sessionContext.syncState
- sessionContext.declarations[0] ?= []
- sessionContext.declarations[0] = sessionContext.declarations[0].concat(_declarations)
-
- for d in _declarations
- for ad in device.getAliasDeclarations(d[0], 1)
- loadPath(sessionContext, ad[0])
-
- return loadParameters(sessionContext, (err) ->
- return callback(err) if err
- return 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?.virtualParameterDeclarations?.length or 0) < sessionContext.declarations.length
- inception = sessionContext.syncState?.virtualParameterDeclarations?.length or 0
- # Avoid unnecessary increment of iteration when using vparams
- sessionContext.iteration += 2 if inception == sessionContext.declarations.length - 1
- vpd = runDeclarations(sessionContext, sessionContext.declarations[inception])
- timestamp = sessionContext.timestamp + sessionContext.iteration
- toClear = null
-
- 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 not 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 k, v of 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 declaration[0][1] of allVirtualParameters
- # Avoid setting on every inform as "exist" timestamp is not saved in DB
- if not sessionContext.deviceData.attributes.has(declaration[0])
- toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, {object: [timestamp, 0]}, toClear)
- return true
-
- for p in sessionContext.deviceData.paths.find(declaration[0], false, true)
- if sessionContext.deviceData.attributes.has(p)
- toClear ?= []
- toClear.push([declaration[0], timestamp])
- break
-
- return false
- )
-
- return clear(sessionContext, toClear, (err) ->
- return callback(err) if err
- sessionContext.syncState.virtualParameterDeclarations[inception] = vpd
- return rpcRequest(sessionContext, null, callback)
- )
-
- if not sessionContext.syncState?
- return callback()
-
- inception = sessionContext.declarations.length - 1
-
- provisions = generateGetVirtualParameterProvisions(sessionContext, sessionContext.syncState.virtualParameterDeclarations[inception])
- if not provisions
- sessionContext.rpcRequest = generateGetRpcRequest(sessionContext)
- if not 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)
-
- toClear = null
- timestamp = sessionContext.timestamp + sessionContext.iteration + 1
-
- # Update tags
- sessionContext.syncState.tags.forEach((v, p) ->
- c = sessionContext.deviceData.attributes.get(p)
- if v and not c?
- toClear = device.set(sessionContext.deviceData, p, timestamp, {object: [timestamp, false], writable: [timestamp, true], value: [timestamp, [true, 'xsd:boolean']]}, toClear)
- else if c? and not v
- toClear = device.set(sessionContext.deviceData, p, timestamp, null, toClear)
- )
-
- # Downloads
- index = null
- sessionContext.syncState.downloadsToCreate.forEach((instance) ->
- if not index?
- index = 0
- for p in sessionContext.deviceData.paths.find(['Downloads', '*'], false, true)
- if +p[1] > index and 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)
-
- params = {
- 'FileType': {writable: 1, value: [instance.FileType or '', 'xsd:string']}
- 'FileName': {writable: 1, value: [instance.FileName or '', 'xsd:string']}
- 'TargetFileName': {writable: 1, value: [instance.TargetFileName or '', 'xsd:string']}
- 'Download': {writable: 1, value: [instance.Download or 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 k, v of 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()
-
- sessionContext.syncState.downloadsToDelete.forEach((instance) ->
- toClear = device.set(sessionContext.deviceData, instance, timestamp, null, toClear)
- sessionContext.syncState.downloadsValues.forEach((v, p) ->
- if p[1] == instance[1]
- sessionContext.syncState.downloadsValues.delete(p)
- )
- )
- sessionContext.syncState.downloadsToDelete.clear()
-
- sessionContext.syncState.downloadsValues.forEach((v, p) ->
- if attrs = sessionContext.deviceData.attributes.get(p)
- if attrs.writable?[1] and attrs.value?
- v = device.sanitizeParameterValue([v, attrs.value[1][1]])
- if v[0] != attrs.value[1][0]
- toClear = device.set(sessionContext.deviceData, p, timestamp, {value: [timestamp, v]}, toClear)
- )
-
- if toClear or sessionContext.deviceData.changes.has('prerequisite')
- return clear(sessionContext, toClear, (err) ->
- return callback(err) if err
- rpcRequest(sessionContext, null, callback)
- )
-
- provisions = generateSetVirtualParameterProvisions(sessionContext, sessionContext.syncState.virtualParameterDeclarations[inception])
- if not 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()
-
- ret = sessionContext.provisionsRet.splice(inception)[0]
- if not ret
- return rpcRequest(sessionContext, null, callback)
-
- sessionContext.revisions.pop()
- rev = sessionContext.revisions[sessionContext.revisions.length - 1] or 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 k of sessionContext.extensionsCache
- if rev < Number(k.split(':', 1)[0])
- delete sessionContext.extensionsCache[k]
-
- vparams = sessionContext.virtualParameters.pop()
-
- if not vparams
- return callback()
-
- timestamp = sessionContext.timestamp + sessionContext.iteration
- toClear = null
- for vpu, i in ret
- vpu[k] = [timestamp + (if vparams[i][2][k]? then 1 else 0), v] for k, v of vpu
- toClear = device.set(sessionContext.deviceData, ['VirtualParameters', vparams[i][0]], timestamp, vpu, toClear)
-
- clear(sessionContext, toClear, (err) ->
- return callback(err) if err
- rpcRequest(sessionContext, null, callback)
- )
-
-
-generateGetRpcRequest = (sessionContext) ->
- if not (syncState = sessionContext.syncState)?
- return
-
- iter = syncState.refreshAttributes.exist.values()
- while (path = iter.next().value)
- found = false
- for p in sessionContext.deviceData.paths.find(path, false, true, 99)
- if syncState.refreshAttributes.value.has(p) or
- syncState.refreshAttributes.object.has(p) or
- syncState.refreshAttributes.writable.has(p) or
- syncState.gpn.has(p)
- found = true
- break
- if not found
- p = sessionContext.deviceData.paths.add(path.slice(0, -1))
- syncState.gpn.add(p)
- f = 1 << p.length
- syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p))
- syncState.refreshAttributes.exist.clear()
-
- iter = syncState.refreshAttributes.object.values()
- while (path = iter.next().value)
- found = false
- for p in sessionContext.deviceData.paths.find(path, false, true, 99)
- if syncState.refreshAttributes.value.has(p) or
- (p.length > path.length and
- (syncState.refreshAttributes.object.has(p) or
- syncState.refreshAttributes.writable.has(p)))
- found = true
- break
- if not found
- p = sessionContext.deviceData.paths.add(path.slice(0, -1))
- syncState.gpn.add(p)
- f = 1 << p.length
- syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p))
- syncState.refreshAttributes.object.clear()
-
- iter = syncState.refreshAttributes.writable.values()
- while (path = iter.next().value)
- p = sessionContext.deviceData.paths.add(path.slice(0, -1))
- syncState.gpn.add(p)
- f = 1 << p.length
- syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p))
- syncState.refreshAttributes.writable.clear()
-
- if syncState.gpn.size
- GPN_NEXT_LEVEL = config.get('GPN_NEXT_LEVEL', sessionContext.deviceId)
-
- paths = Array.from(syncState.gpn.keys()).sort((a,b) -> b.length - a.length)
- path = paths.pop()
- while path and path.length and not sessionContext.deviceData.attributes.has(path)
- syncState.gpn.delete(path)
- path = paths.pop()
-
- if path
- if path.length >= GPN_NEXT_LEVEL
- patterns = [[path, 0]]
- for p in sessionContext.deviceData.paths.find(path, true, false, 99)
- if v = syncState.gpnPatterns.get(p)
- patterns.push([p, (v >> path.length) << path.length])
-
- est = gpnHeuristic.estimateGpnCount(patterns)
- else
- est = 0
-
- if est < Math.pow(2, Math.max(0, 8 - path.length))
- nextLevel = true
- syncState.gpn.delete(path)
- else
- nextLevel = false
- for p in 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
- GPV_BATCH_SIZE = config.get('GPV_BATCH_SIZE', sessionContext.deviceId)
-
- parameterNames = []
- iter = syncState.refreshAttributes.value.values()
- while (path = iter.next().value) and
- parameterNames.length < GPV_BATCH_SIZE
- syncState.refreshAttributes.value.delete(path)
- # Need to check in case param is deleted or changed to object
- attrs = sessionContext.deviceData.attributes.get(path)
- if attrs and attrs.object and attrs.object[1] == 0
- parameterNames.push(path)
-
- if parameterNames.length
- return {
- name: 'GetParameterValues'
- parameterNames: (p.join('.') for p in parameterNames)
- }
-
- return null
-
-
-generateSetRpcRequest = (sessionContext) ->
- if not (syncState = sessionContext.syncState)?
- return
-
- deviceData = sessionContext.deviceData
-
- # Delete instance
- iter = syncState.instancesToDelete.values()
- while instances = iter.next().value
- if (instance = instances.values().next().value) and
- sessionContext.deviceData.attributes.has(instance)
- return {
- name: 'DeleteObject'
- objectName: instance.concat('').join('.')
- }
-
- # Create instance
- iter = syncState.instancesToCreate.entries()
- while pair = iter.next().value
- if sessionContext.deviceData.attributes.has(pair[0]) and
- instance = pair[1].values().next().value
- pair[1].delete(instance)
- return {
- name: 'AddObject'
- objectName: pair[0].concat('').join('.')
- instanceValues: instance
- next: 'getInstanceKeys'
- }
-
- # Set values
- GPV_BATCH_SIZE = config.get('GPV_BATCH_SIZE', sessionContext.deviceId)
- DATETIME_MILLISECONDS = config.get('DATETIME_MILLISECONDS', sessionContext.deviceId)
- BOOLEAN_LITERAL = config.get('BOOLEAN_LITERAL', sessionContext.deviceId)
-
- parameterValues = []
- syncState.spv.forEach((v, k) ->
- return if parameterValues.length >= GPV_BATCH_SIZE
- syncState.spv.delete(k)
- attrs = sessionContext.deviceData.attributes.get(k)
- if (curVal = attrs.value?[1])? and attrs.writable?[1]
- val = v.slice()
- val[1] = curVal[1] if not val[1]?
- device.sanitizeParameterValue(val)
-
- # Strip milliseconds
- if val[1] == 'xsd:dateTime' and not DATETIME_MILLISECONDS and typeof val[0] == 'number'
- val[0] -= val[0] % 1000
-
- if val[0] != curVal[0] or val[1] != curVal[1]
- parameterValues.push([k, val[0], val[1]])
- )
-
- if parameterValues.length
- return {
- name: 'SetParameterValues'
- parameterList: ([p[0].join('.'), p[1], p[2]] for p in parameterValues)
- DATETIME_MILLISECONDS: DATETIME_MILLISECONDS
- BOOLEAN_LITERAL: BOOLEAN_LITERAL
- }
-
- # Download
- iter = syncState.downloadsDownload.entries()
- while pair = iter.next().value
- if not (pair[1] <= deviceData.attributes.get(pair[0])?.value?[1][0])
- fileTypePath = deviceData.paths.get(pair[0].slice(0, -1).concat('FileType'))
- fileNamePath = deviceData.paths.get(pair[0].slice(0, -1).concat('FileName'))
- targetFileNamePath = deviceData.paths.get(pair[0].slice(0, -1).concat('TargetFileName'))
- return {
- name: 'Download'
- commandKey: generateRpcId(sessionContext)
- instance: pair[0][1]
- fileType: deviceData.attributes.get(fileTypePath)?.value?[1][0]
- fileName: deviceData.attributes.get(fileNamePath)?.value?[1][0]
- targetFileName: deviceData.attributes.get(targetFileNamePath)?.value?[1][0]
- }
-
- # Reboot
- if syncState.reboot?
- p = sessionContext.deviceData.paths.get(['Reboot'])
- if not (p? and sessionContext.deviceData.attributes.get(p)?.value?[1][0] >= syncState.reboot)
- delete syncState.reboot
- return {
- name: 'Reboot'
- }
-
- # Factory reset
- if syncState.factoryReset?
- p = sessionContext.deviceData.paths.get(['FactoryReset'])
- if not (p? and sessionContext.deviceData.attributes.get(p)?.value?[1][0] >= syncState.factoryReset)
- delete syncState.factoryReset
- return {
- name: 'FactoryReset'
- }
-
- return null
-
-
-generateGetVirtualParameterProvisions = (sessionContext, virtualParameterDeclarations) ->
- provisions = null
- for declaration in virtualParameterDeclarations
- if declaration[1]
- currentTimestamps = {}
- currentValues = {}
- dec = {}
- attrs = sessionContext.deviceData.attributes.get(declaration[0]) or {}
- for k, v of declaration[1]
- continue if k != 'value' and k != 'writable'
- if not attrs[k] or v > attrs[k][0]
- dec[k] = v
- for k, v of attrs
- currentTimestamps[k] = v[0]
- currentValues[k] = v[1]
- if (Object.keys(dec).length)
- provisions ?= []
- provisions.push([declaration[0][1], dec, {}, currentTimestamps, currentValues])
-
- return provisions
-
-
-generateSetVirtualParameterProvisions = (sessionContext, virtualParameterDeclarations) ->
- provisions = null
- for declaration in virtualParameterDeclarations
- if declaration[2]?.value?
- attrs = sessionContext.deviceData.attributes.get(declaration[0])
- if (curVal = attrs.value?[1])? and attrs.writable?[1]
- val = declaration[2].value.slice()
- val[1] = curVal[1] if not val[1]?
- device.sanitizeParameterValue(val)
-
- if val[0] != curVal[0] or val[1] != curVal[1]
- provisions ?= []
- currentTimestamps = {}
- currentValues = {}
- for k, v of attrs
- currentTimestamps[k] = v[0]
- currentValues[k] = v[1]
- provisions.push([declaration[0][1], {}, {value: val}, currentTimestamps, currentValues])
-
- return provisions
-
-
-processDeclarations = (sessionContext, allDeclareTimestamps, allDeclareAttributeTimestamps, allDeclareAttributeValues) ->
- deviceData = sessionContext.deviceData
- syncState = sessionContext.syncState
-
- root = sessionContext.deviceData.paths.add([])
- 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
- )
-
- toClear = null
- virtualParameterDeclarations = []
-
- func = (leafParam, leafIsObject, leafTimestamp, paths) ->
- currentPath = paths[0]
- children = {}
- declareTimestamp = 0
- declareAttributeTimestamps = null
- declareAttributeValues = null
-
- currentTimestamp = 0
- currentAttributes = null
- if currentPath.wildcard == 0
- currentAttributes = deviceData.attributes.get(currentPath)
-
- for path in paths
- if path.length > currentPath.length
- fragment = path[currentPath.length]
- if not 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
- 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
- if attrs = allDeclareAttributeTimestamps.get(path)
- if declareAttributeTimestamps
- declareAttributeTimestamps = Object.assign({}, declareAttributeTimestamps)
- declareAttributeTimestamps[k] = Math.max(v, declareAttributeTimestamps[k] or 0) for k, v of attrs
- else
- declareAttributeTimestamps = attrs
-
- if attrs = allDeclareAttributeValues.get(path)
- declareAttributeValues = attrs
-
- if currentAttributes
- leafParam = currentPath
- leafIsObject = currentAttributes.object?[1]
- if leafIsObject == 0
- leafTimestamp = Math.max(leafTimestamp, currentAttributes.object[0])
- else
- leafTimestamp = Math.max(leafTimestamp, currentTimestamp)
-
- switch (if currentPath[0] != '*' then currentPath[0] else leafParam[0])
- when 'Reboot'
- if currentPath.length == 1
- if declareAttributeValues?.value?
- syncState.reboot = +(new Date(declareAttributeValues.value[0]))
- when 'FactoryReset'
- if currentPath.length == 1
- if declareAttributeValues?.value?
- syncState.factoryReset = +(new Date(declareAttributeValues.value[0]))
- when 'Tags'
- if currentPath.length == 2 and currentPath.wildcard == 0 and declareAttributeValues?.value?
- syncState.tags.set(currentPath, device.sanitizeParameterValue([declareAttributeValues.value[0], 'xsd:boolean'])[0])
- when 'Events', 'DeviceID' then
- # Do nothing
- when 'Downloads'
- if currentPath.length == 3 and currentPath.wildcard == 0 and declareAttributeValues?.value?
- if currentPath[2] == 'Download'
- syncState.downloadsDownload.set(currentPath, declareAttributeValues.value[0])
- else
- syncState.downloadsValues.set(currentPath, declareAttributeValues.value[0])
- when 'VirtualParameters'
- if currentPath.length <= 2
- d = null
- if not (declareTimestamp <= currentTimestamp)
- d = [currentPath]
-
- if currentPath.wildcard == 0
- if declareAttributeTimestamps
- for attrName of declareAttributeTimestamps
- if not (declareAttributeTimestamps[attrName] <= currentAttributes?[attrName]?[0])
- d ?= [currentPath]
- d[1] ?= {}
- d[1][attrName] = declareAttributeTimestamps[attrName]
-
- if declareAttributeValues
- d ?= [currentPath]
- d[2] = declareAttributeValues
-
- virtualParameterDeclarations.push(d) if d
- else
- if declareTimestamp > currentTimestamp and declareTimestamp > leafTimestamp
- if currentPath == leafParam
- syncState.refreshAttributes.exist.add(leafParam)
- else if leafIsObject
- syncState.gpn.add(leafParam)
- if leafTimestamp > 0
- f = 1 << leafParam.length
- syncState.gpnPatterns.set(leafParam, f | syncState.gpnPatterns.get(leafParam))
- else
- 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 not leafIsObject?
- f = ((1 << syncState.gpnPatterns.length) - 1) ^ ((1 << leafParam.length) - 1)
- syncState.gpnPatterns.set(currentPath, f | syncState.gpnPatterns.get(currentPath))
-
- if currentAttributes
- for attrName of declareAttributeTimestamps
- if not (declareAttributeTimestamps[attrName] <= currentAttributes[attrName]?[0])
- if attrName == 'value'
- if not currentAttributes.object?[1]?
- syncState.refreshAttributes.object.add(currentPath)
- else if currentAttributes.object[1] == 0
- syncState.refreshAttributes.value.add(currentPath)
- else
- syncState.refreshAttributes[attrName].add(currentPath)
-
- if declareAttributeValues?.value?
- syncState.spv.set(currentPath, declareAttributeValues.value)
-
- for child of children
- # This fine expression avoids duplicate visits
- if ((currentPath.wildcard ^ children[child][0].wildcard) & ((1 << currentPath.length) - 1)) >> leafParam.length == 0
- if child != '*' and children['*']?
- children[child] = children[child].concat(children['*'])
- func(leafParam, leafIsObject, leafTimestamp, children[child])
-
- if allDeclareTimestamps.size or allDeclareAttributeTimestamps.size or allDeclareAttributeValues.size
- func(root, 1, 0, paths)
-
- return virtualParameterDeclarations
-
-
-loadPath = (sessionContext, path, depth) ->
- depth = depth or (1 << path.length) - 1
- return true if sessionContext.new or not depth
-
- sessionContext.toLoad ?= new Map()
-
- # Trim trailing wildcards
- trimWildcard = path.length
- -- trimWildcard while trimWildcard and path[trimWildcard - 1] == '*'
- path = path.slice(0, trimWildcard) if trimWildcard < path.length
-
- for i in [0..path.length] by 1
- d = if i == path.length then 99 else i
- for sup in sessionContext.deviceData.paths.find(path.slice(0, i), true, false, d)
- v = sessionContext.deviceData.loaded.get(sup) | sessionContext.toLoad.get(sup)
- if sup.length > i
- v &= (1 << i) - 1
- depth &= depth ^ v
- return true if depth == 0
-
- path = sessionContext.deviceData.paths.add(path)
- depth |= sessionContext.toLoad.get(path)
- sessionContext.toLoad.set(path, depth)
- return false
-
-
-processInstances = (sessionContext, parent, parameters, keys, minInstances, maxInstances) ->
- if parent[0] == 'Downloads'
- return if parent.length != 1
- instancesToDelete = sessionContext.syncState.downloadsToDelete
- instancesToCreate = sessionContext.syncState.downloadsToCreate
- else
- instancesToDelete = sessionContext.syncState.instancesToDelete.get(parent)
- if not instancesToDelete?
- instancesToDelete = new Set()
- sessionContext.syncState.instancesToDelete.set(parent, instancesToDelete)
-
- instancesToCreate = sessionContext.syncState.instancesToCreate.get(parent)
- if not instancesToCreate?
- instancesToCreate = new InstanceSet()
- sessionContext.syncState.instancesToCreate.set(parent, instancesToCreate)
-
- counter = 0
- for p in 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
- return if not keys
-
- superset = instancesToCreate.superset(keys)
- for inst in superset
- ++ counter
- if counter > maxInstances
- instancesToCreate.delete(inst)
-
- subset = instancesToCreate.subset(keys)
- for inst in subset
- ++ 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)))
-
- return
-
-
-clear = (sessionContext, toClear, callback) ->
- return callback() if not toClear?.length
-
- MAX_DEPTH = config.get('MAX_DEPTH', sessionContext.deviceId)
-
- toClear.forEach((c) ->
- if c[1]
- 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] and c[2].object)
- loadPath(sessionContext, c[0], (((1 << c[0].length) - 1) >> 1) ^ ((1 << MAX_DEPTH) - 1))
- else
- loadPath(sessionContext, c[0], (1 << c[0].length) >> 1)
- )
-
- loadParameters(sessionContext, (err) ->
- return callback(err) if err
-
- toClear.forEach((c) ->
- device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3])
- )
- return callback()
- )
-
-
-rpcResponse = (sessionContext, id, rpcRes, callback) ->
- if id != generateRpcId(sessionContext)
- return callback(new Error('Request ID not recognized'))
-
- ++ sessionContext.rpcCount
-
- rpcReq = sessionContext.rpcRequest
- if not rpcReq.next?
- sessionContext.rpcRequest = null
- else if rpcReq.next == 'getInstanceKeys'
- instanceNumber = rpcRes.instanceNumber
- parameterNames = []
- instanceValues = {}
- for k, v of rpcReq.instanceValues
- n = "#{rpcReq.objectName}#{rpcRes.instanceNumber}.#{k}"
- parameterNames.push(n)
- instanceValues[n] = v
-
- if parameterNames.length == 0
- sessionContext.rpcRequest = null
- else
- sessionContext.rpcRequest = {
- name: 'GetParameterValues'
- parameterNames: parameterNames
- next: 'setInstanceKeys'
- instanceValues: instanceValues
- }
- else if rpcReq.next == 'setInstanceKeys'
- parameterList = []
- for p in rpcRes.parameterList
- if p[1] != rpcReq.instanceValues[p[0]]
- parameterList.push(
- [p[0]].concat(device.sanitizeParameterValue([rpcReq.instanceValues[p[0]], p[2]])))
-
- if not parameterList.length
- sessionContext.rpcRequest = null
- else
- sessionContext.rpcRequest = {
- name: 'SetParameterValues'
- parameterList: parameterList
- }
-
- timestamp = sessionContext.timestamp + sessionContext.iteration
-
- revision = (sessionContext.revisions[sessionContext.revisions.length - 1] or 0) + 1
- sessionContext.deviceData.timestamps.revision = revision
- sessionContext.deviceData.attributes.revision = revision
-
- toClear = null
-
- switch rpcRes.name
- when 'GetParameterValuesResponse'
- return callback(new Error('Response name does not match request name')) if rpcReq.name isnt 'GetParameterValues'
-
- for p in rpcRes.parameterList
- toClear = device.set(sessionContext.deviceData, common.parsePath(p[0]), timestamp,
- {object: [timestamp, 0], value: [timestamp, p.slice(1)]}, toClear)
-
- when 'GetParameterNamesResponse'
- return callback(new Error('Response name does not match request name')) if rpcReq.name isnt 'GetParameterNames'
-
- if rpcReq.parameterPath.endsWith('.')
- root = common.parsePath(rpcReq.parameterPath.slice(0, -1))
- else
- root = common.parsePath(rpcReq.parameterPath)
-
- params = []
- params.push([root.concat('*'), timestamp])
-
- # Some clients don't report all ancestors explicitly
- missing = {}
-
- for p in rpcRes.parameterList
- 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][0...-1]] |= 1
- path = common.parsePath(p[0][0...-1])
- if not rpcReq.nextLevel
- params.push([path.concat('*'), timestamp])
-
- params.push([path, timestamp,
- {object: [timestamp, 1], writable: [timestamp, if p[1] then 1 else 0]}])
-
- else
- missing[p[0]] |= 1
- params.push([common.parsePath(p[0]), timestamp,
- {object: [timestamp, 0], writable: [timestamp, if p[1] then 1 else 0]}])
-
- for k, v of missing when v == 0
- # TODO consider showing a warning
- 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) ->
- al = a[0].length
- bl = b[0].length
- bl *= -1 if b[0][bl - 1] == '*'
- al *= -1 if a[0][al - 1] == '*'
- return bl - al
- )
-
- if rpcReq.nextLevel
- loadPath(sessionContext, root, (1 << (root.length + 1)) - 1)
- else
- loadPath(sessionContext, root, (1 << config.get('MAX_DEPTH', sessionContext.deviceId)) - 1)
-
- loadParameters(sessionContext, (err) ->
- return callback(err) if err
-
- if root.length == 0
- for n in ['DeviceID', 'Events', 'Tags', 'Reboot', 'FactoryReset', 'VirtualParameters', 'Downloads']
- if p = sessionContext.deviceData.paths.get([n])
- if sessionContext.deviceData.attributes.has(p)
- sessionContext.deviceData.timestamps.set(p, timestamp)
-
- for p in params
- toClear = device.set(sessionContext.deviceData, p[0], p[1], p[2], toClear)
-
- clear(sessionContext, toClear, callback)
- )
- return
-
- when 'SetParameterValuesResponse'
- return callback(new Error('Response name does not match request name')) if rpcReq.name isnt 'SetParameterValues'
-
- for p in 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)
-
- when 'AddObjectResponse'
- toClear = device.set(sessionContext.deviceData, common.parsePath(rpcReq.objectName + rpcRes.instanceNumber),
- timestamp + 1, {object: [timestamp + 1, 1]}, toClear)
-
- when 'DeleteObjectResponse'
- toClear = device.set(sessionContext.deviceData, common.parsePath(rpcReq.objectName.slice(0, -1)),
- timestamp + 1, null, toClear)
-
- when 'RebootResponse'
- toClear = device.set(sessionContext.deviceData, common.parsePath('Reboot'),
- timestamp + 1, {value: [timestamp + 1, [sessionContext.timestamp, 'xsd:dateTime']]}, toClear)
-
- when 'FactoryResetResponse'
- toClear = device.set(sessionContext.deviceData, common.parsePath('FactoryReset'),
- timestamp + 1, {value: [timestamp + 1, [sessionContext.timestamp, 'xsd:dateTime']]}, toClear)
-
- when '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
- 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 channel of sessionContext.channels
- if sessionContext.retries[channel]?
- operation.retries[channel] = sessionContext.retries[channel]
-
- sessionContext.operations[rpcReq.commandKey] = operation
- sessionContext.operationsTouched ?= {}
- sessionContext.operationsTouched[rpcReq.commandKey] = 1
-
- else
- return callback(new Error('Response name not recognized'))
-
- return clear(sessionContext, toClear, callback)
-
-
-rpcFault = (sessionContext, id, faultResponse, callback) ->
- # TODO Consider handling invalid parameter faults by automatically refreshing
- # relevant data model portions
-
- fault = {
- code: "cwmp.#{faultResponse.detail.faultCode}"
- message: faultResponse.detail.faultString
- detail: faultResponse.detail
- timestamp: sessionContext.timestamp
- }
-
- delete sessionContext.syncState
- return callback(null, fault)
-
-
-deserialize = (sessionContextString, callback) ->
- localCache.getProvisionsAndVirtualParameters((err, hash, provisions, virtualParameters) ->
- return callback(err) if err
-
- sessionContext = JSON.parse(sessionContextString)
-
- if sessionContext.presetsHash? and sessionContext.presetsHash != hash
- return callback(new Error('Preset hash mismatch'))
-
- provisionsCache.set(sessionContext, provisions)
- virtualParametersCache.set(sessionContext, virtualParameters)
-
- for decs in sessionContext.declarations
- common.addPathMeta(d[0]) for d in decs
-
- deviceData = initDeviceData()
-
- for r in sessionContext.deviceData
- 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
-
- return callback(null, sessionContext)
- )
-
-
-serialize = (sessionContext, callback) ->
- deviceData = []
-
- for path in sessionContext.deviceData.paths.find([], false, false, 99)
- 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
-
- sessionContextString = JSON.stringify(sessionContext)
-
- return 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
diff --git a/lib/session.js b/lib/session.js
new file mode 100644
index 0000000..ac51096
--- /dev/null
+++ b/lib/session.js
@@ -0,0 +1,2602 @@
+/**
+ * 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 .
+ */
+"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;