Files
genieacs/lib/session.coffee
T
Zaid Abdulla 41fb97adef Use variable to store active sessions
Keep session data in process for as long as the TCP connectoin is
open. Use redis if connection closes before the session is over.
2016-08-29 00:17:01 -07:00

1185 lines
38 KiB
CoffeeScript

###
# Copyright 2013-2016 Zaid Abdulla
#
# GenieACS is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# GenieACS is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
###
config = require './config'
common = require './common'
db = require './db'
device = require './device'
sandbox = require './sandbox'
cache = require './cache'
extensions = require './extensions'
PathSet = require './path-set'
VersionedMap = require './versioned-map.js'
InstanceSet = require './instance-set'
initDeviceData = () ->
return {
paths : new PathSet()
timestamps : {exist: new VersionedMap(), object: new VersionedMap(), writable: new VersionedMap(), value: new VersionedMap()}
values : {exist: new VersionedMap(), object: new VersionedMap(), writable: new VersionedMap(), value: new VersionedMap()}
loaded : new Map()
}
init = (deviceId, cwmpVersion, timeout, callback) ->
timestamp = Date.now()
sessionData = {
timestamp : timestamp
deviceId : deviceId
deviceData : initDeviceData()
cwmpVersion : cwmpVersion
timeout : timeout
provisions: []
channels: []
revisions: [0]
rpcCount: 0
cycle: 0
extensionsCache: []
declarations: []
}
cache.getProvisionsAndVirtualParameters((err, hash, provisions, virtualParameters) ->
return callback(err) if err
sessionData.cache = {
hash: hash
provisions: provisions
virtualParameters: virtualParameters
}
return callback(null, sessionData)
)
loadParameters = (sessionData, callback) ->
if not sessionData.toLoad?.length
return callback()
db.fetchDevice(sessionData.deviceId, sessionData.timestamp, sessionData.toLoad, (err, parameters, loaded) ->
return callback(err) if err
if not parameters?
# Device not available in database, mark as new
sessionData.new = true
path = sessionData.deviceData.paths.add(['*'])
sessionData.deviceData.loaded.set(path, 99)
return callback()
for p in loaded
path = sessionData.deviceData.paths.add(p[0])
if p[1]
l = sessionData.deviceData.loaded.get(path) || 0
sessionData.deviceData.loaded.set(path, Math.max(l, p[1]))
for p in parameters
path = sessionData.deviceData.paths.add(p[0])
for k, v of p[1]
sessionData.deviceData.timestamps[k].set(path, v, 0)
for k, v of p[2]
sessionData.deviceData.values[k].set(path, v, 0)
# Virtual parameters
for p in sessionData.toLoad
p = sessionData.deviceData.paths.add(p)
if p[0] == '*' or p[0] == 'VirtualParameters'
if p.length == 1
sessionData.deviceData.timestamps.exist.set(p, sessionData.timestamp, 0)
sessionData.deviceData.timestamps.object.set(p, sessionData.timestamp, 0)
sessionData.deviceData.timestamps.writable.set(p, sessionData.timestamp, 0)
sessionData.deviceData.values.exist.set(p, 1, 0)
sessionData.deviceData.values.object.set(p, 1, 0)
sessionData.deviceData.values.writable.set(p, 0, 0)
else if p.length == 2
if not p[1]?
sessionData.deviceData.timestamps.exist.set(p, sessionData.timestamp, 0)
for k, v of sessionData.cache.virtualParameters
p = sessionData.deviceData.paths.add(p)
sessionData.deviceData.timestamps.exist.set(p, sessionData.timestamp, 0)
sessionData.deviceData.timestamps.object.set(p, sessionData.timestamp, 0)
sessionData.deviceData.values.exist.set(p, 1, 0)
sessionData.deviceData.values.object.set(p, 0, 0)
else if sessionData.cache.virtualParameters[p[1]]?
sessionData.deviceData.timestamps.exist.set(p, sessionData.timestamp, 0)
sessionData.deviceData.timestamps.object.set(p, sessionData.timestamp, 0)
sessionData.deviceData.values.exist.set(p, 1, 0)
sessionData.deviceData.values.object.set(p, 0, 0)
sessionData.toLoad = null
return callback()
)
generateRpcId = (sessionData) ->
if sessionData.rpcCount > 255 or sessionData.cycle > 15 or sessionData.revisions.length > 15
throw new Error('Too many RPCs')
return sessionData.timestamp.toString(16) + "0#{sessionData.rpcCount.toString(16)}".slice(-2)
inform = (sessionData, rpcReq, callback) ->
timestamp = sessionData.timestamp
params = []
params.push([['DeviceID', 'Manufacturer'],
{exist: timestamp, object: timestamp, writable: timestamp, value: timestamp},
{exist: 1, object: 0, writable: 0, value: [rpcReq.deviceId.Manufacturer, 'xsd:string']}])
params.push([['DeviceID', 'OUI'],
{exist: timestamp, object: timestamp, writable: timestamp, value: timestamp},
{exist: 1, object: 0, writable: 0, value: [rpcReq.deviceId.OUI, 'xsd:string']}])
params.push([['DeviceID', 'ProductClass'],
{exist: timestamp, object: timestamp, writable: timestamp, value: timestamp},
{exist: 1, object: 0, writable: 0, value: [rpcReq.deviceId.ProductClass, 'xsd:string']}])
params.push([['DeviceID', 'SerialNumber'],
{exist: timestamp, object: timestamp, writable: timestamp, value: timestamp},
{exist: 1, object: 0, writable: 0, value: [rpcReq.deviceId.SerialNumber, 'xsd:string']}])
for p in rpcReq.parameterList
path = common.parsePath(p[0])
params.push([path,
{exist: timestamp, object: timestamp, value: timestamp},
{exist: 1, object: 0, value: p.slice(1)}])
params.push([['Events', 'Inform'],
{exist: timestamp, object: timestamp, writable: timestamp, value: timestamp},
{exist: 1, object: 0, writable: 0, value: [timestamp, 'xsd:dateTime']}])
for e in rpcReq.event
params.push([['Events', e.replace(' ', '_')],
{exist: timestamp, object: timestamp, writable: timestamp, value: timestamp},
{exist: 1, object: 0, writable: 0, value: [timestamp, 'xsd:dateTime']}])
for p in params
loadPath(sessionData, p[0])
loadParameters(sessionData, (err) ->
return callback(err) if err
v.revision = 1 for k, v of sessionData.deviceData.timestamps
v.revision = 1 for k, v of sessionData.deviceData.values
for p in params
device.set(sessionData.deviceData, p[0], p[1], p[2])
return callback(null, {type : 'InformResponse'})
)
addProvisions = (sessionData, channel, provisions) ->
for provision, i in provisions
# Remove duplicate provisions
for p, j in sessionData.provisions
if channel == sessionData.channels[j] and JSON.stringify(p) == JSON.stringify(provision)
sessionData.provisions.splice(j, 1)
sessionData.channels.splice(j, 1)
sessionData.provisions.push(provision)
sessionData.channels.push(channel)
if sessionData.revisions.length > 1 or sessionData.revisions[0] > 0
v.collapse(1) for k, v of sessionData.timestamps
v.collapse(1) for k, v of sessionData.values
sessionData.cycle += 1
sessionData.rpcCount = 0
sessionData.revisions = [0]
sessionData.extensionsCache.length = 0
clearProvisions = (sessionData) ->
delete sessionData.syncState
sessionData.provisions = []
sessionData.channels = []
sessionData.declarations = []
if sessionData.revisions.length > 1 or sessionData.revisions[0] > 0
v.collapse(1) for k, v of sessionData.deviceData.timestamps
v.collapse(1) for k, v of sessionData.deviceData.values
sessionData.cycle += 1
sessionData.rpcCount = 0
sessionData.revisions = [0]
sessionData.extensionsCache.length = 0
runProvisions = (sessionData, callback) ->
done = true
_extensions = []
allDeclarations = []
for provision in sessionData.provisions
if not sessionData.cache.provisions[provision[0]]?
switch provision[0]
when 'refresh'
path = common.parsePath(provision[1]).slice()
l = path.length
path.length = 10
path.fill('*', l)
t = provision[2]
t += sessionData.timestamp if t <= 0
for i in [l...path.length] by 1
p = common.addPathMeta(path.slice(0, i))
allDeclarations.push([p, {exist: t, object: 1, writable: 1, value: t}])
when 'value'
allDeclarations.push([common.parsePath(provision[1]), {exist: 1, value: 1}, {value: [provision[2]]}])
when 'tag'
allDeclarations.push([['Tags', provision[1]], {exist: 1, value: 1}, {exist: 1, value: [provision[2], 'xsd:boolean']}])
when '_task'
# A special provision for tasks compatibility
switch provision[2]
when 'getParameterValues'
for i in [3...provision.length] by 1
allDeclarations.push([common.parsePath(provision[i]), {exist: 1, value: sessionData.timestamp}])
when 'setParameterValues'
for i in [3...provision.length] by 3
v = if provision[i + 2] then [provision[i + 1], provision[i + 2]] else [provision[i + 1]]
allDeclarations.push([common.parsePath(provision[i]), {exist: 1, value: 1}, {value: v}])
when 'refreshObject'
path = common.parsePath(provision[3]).slice()
l = path.length
path.length = 10
path.fill('*', l)
for i in [l...path.length] by 1
p = common.addPathMeta(path.slice(0, i))
allDeclarations.push([p, {exist: sessionData.timestamp, object: 1, writable: 1, value: sessionData.timestamp}])
continue
ret = sandbox.run(sessionData.cache.provisions[provision[0]].script, provision.slice(1), sessionData.timestamp, sessionData.deviceData, sessionData.extensionsCache, 0, sessionData.revisions[0] >> 1)
if ret.extensions?.length
_extensions = _extensions.concat(ret.extensions)
done &&= ret.done
allDeclarations = allDeclarations.concat(ret.declarations)
if _extensions.length
return runExtensions(sessionData, (sessionData.revisions[0] >> 1), _extensions, (err) ->
return callback(err) if err
return runProvisions(sessionData, callback)
)
for d in allDeclarations
state = applyDeclaration(sessionData, d[0], 0, d[1], d[2])
sessionData.declarations.push([d[0], state, d[1], d[2]])
return callback(null, done)
runVirtualParameters = (sessionData, inception, callback) ->
if not sessionData.syncState? or Object.keys(sessionData.syncState.virtualParameters).length == 0
return callback()
virtualParameterDeclarations = sessionData.syncState.virtualParameters
sessionData.syncState.virtualParameters = {}
revision = sessionData.revisions.reduce(((a, b) -> a + (b >> 1)), 0) + 1
sessionData.revisions[inception] ?= 0
decs = []
virtualParameterUpdates = {}
_extensions = []
firstRevision = revision - (sessionData.revisions[sessionData.revisions.length - 1] >> 1)
for vpName, vpDeclarations of virtualParameterDeclarations
ret = sandbox.run(sessionData.cache.virtualParameters[vpName].script, [vpDeclarations.timestamps, vpDeclarations.values], sessionData.timestamp, sessionData.deviceData, sessionData.extensionsCache, firstRevision, revision)
if ret.extensions?.length
_extensions = _extensions.concat(ret.extensions)
decs = decs.concat(ret.declarations)
if ret.done
virtualParameterUpdates[vpName] = ret.returnValue
if _extensions.length
return runExtensions(sessionData, revision, _extensions, (err) ->
return callback(err) if err
return runVirtualParameters(sessionData, inception, callback)
)
for d in decs
state = applyDeclaration(sessionData, d[0], 0, d[1], d[2])
sessionData.declarations.push([d[0], state, d[1], d[2]])
if Object.keys(virtualParameterUpdates).length == Object.keys(virtualParameterDeclarations).length
rev = sessionData.revisions.reduce(((a, b) -> a + (b >> 1)), 0) + 1
v.revision = rev for k, v of sessionData.deviceData.timestamps
v.revision = rev for k, v of sessionData.deviceData.values
for vpName, vpDeclarations of virtualParameterDeclarations
commitVirtualParameter(sessionData, vpName, vpDeclarations, virtualParameterUpdates[vpName])
if sessionData.revisions[inception] > 0
sessionData.revisions.length = inception
rev = sessionData.revisions.reduce(((a, b) -> a + (b >> 1)), 0) + 1
m.collapse(rev) for k, m of sessionData.deviceData.timestamps
m.collapse(rev) for k, m of sessionData.deviceData.values
if sessionData.extensionsCache.length > rev
sessionData.extensionsCache.length = rev
return runVirtualParameters(sessionData, inception + 1, callback)
rpcRequest = (sessionData, declarations, callback) ->
if sessionData.rpcRequest?
return callback(null, generateRpcId(sessionData), sessionData.rpcRequest)
revision = sessionData.revisions.reduce(((a, b) -> a + (b >> 1)), 0) + 1
v.revision = revision for k, v of sessionData.deviceData.timestamps
v.revision = revision for k, v of sessionData.deviceData.values
f = (cb) ->
return cb() if not sessionData.declarations.length
for d, i in sessionData.declarations
state = applyDeclaration(sessionData, d[0], d[1], d[2], d[3])
d[1] = state
if sessionData.toLoad?.length
return loadParameters(sessionData, (err) ->
return cb(err) if err
return f(cb)
)
runVirtualParameters(sessionData, 1, (err) ->
return cb(err) if err
if sessionData.toLoad?.length
return loadParameters(sessionData, (err) ->
return cb(err) if err
return f(cb)
)
return cb(null, generateRpcRequest(sessionData))
)
if declarations?
for d in declarations
sessionData.declarations.push([d[0], 0, d[1], d[2]])
if sessionData.declarations.length
return f((err, rpcReq) ->
return callback(err) if err
if rpcReq?
sessionData.declarations = [] if rpcReq.done
sessionData.rpcRequest = rpcReq
return callback(null, generateRpcId(sessionData), rpcReq)
sessionData.declarations = []
++ sessionData.revisions[sessionData.revisions.length - 1]
return rpcRequest(sessionData, null, callback)
)
if not sessionData.provisions.length
return callback()
runProvisions(sessionData, (err, done) ->
return callback(err) if err
return f((err, rpcReq) ->
return callback(err) if err
if rpcReq?
sessionData.declarations = [] if rpcReq.done
sessionData.rpcRequest = rpcReq
return callback(null, generateRpcId(sessionData), rpcReq)
sessionData.declarations = []
if done
return callback()
++ sessionData.revisions[sessionData.revisions.length - 1]
return rpcRequest(sessionData, null, callback)
)
)
generateRpcRequest = (sessionData) ->
syncState = sessionData.syncState
iter = syncState.refreshAttributes.exist.values()
while (path = iter.next().value)
descendantIter = syncState.paths.subset(path, 99)
found = false
while (p = descendantIter.next().value)?
if syncState.refreshAttributes.value.has(p) or
syncState.refreshAttributes.object.has(p) or
syncState.refreshAttributes.writable.has(p) or
syncState.refreshGpn.has(p)
found = true
break
if not found
path = syncState.paths.add(path.slice(0, -1))
syncState.refreshGpn.set(path, syncState.refreshGpn.get(path) or 1)
iter = syncState.refreshAttributes.object.values()
while (path = iter.next().value)
descendantIter = syncState.paths.subset(path, 99)
found = false
while (p = descendantIter.next().value)?
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
path = syncState.paths.add(path.slice(0, -1))
syncState.refreshGpn.set(path, syncState.refreshGpn.get(path) or 1)
iter = syncState.refreshAttributes.writable.values()
while (path = iter.next().value)
path = syncState.paths.add(path.slice(0, -1))
syncState.refreshGpn.set(path, syncState.refreshGpn.get(path) or 1)
iter = syncState.refreshGpn.keys()
while (path = iter.next().value)
descendantIter = syncState.paths.subset(path, 99)
found = false
while (p = descendantIter.next().value)?
continue if p == path
if (flags = syncState.refreshGpn.get(p))
flags <<= p.length - path.length
syncState.refreshGpn.set(path, syncState.refreshGpn.get(path) | flags)
syncState.refreshGpn.delete(p)
rpcReq = null
completed = true
# Delete instance
syncState.instancesToDelete.forEach((instances, parent) ->
instances.forEach((instance) ->
if rpcReq?
completed = false
else
rpcReq = {
type: 'DeleteObject'
objectName: instance.join('.') + '.'
}
)
)
# Create instance
syncState.instancesToCreate.forEach((instances, parent) ->
instances.forEach((instance) ->
if rpcReq?
completed = false
else
rpcReq = {
type: 'AddObject'
objectName: parent.join('.') + '.'
next: 'getInstanceKeys'
instanceValues: instance
}
)
)
if syncState.refreshGpn.size
if rpcReq?
completed = false
else
GET_PARAMETER_NAMES_DEPTH_THRESHOLD =
config.get('GET_PARAMETER_NAMES_DEPTH_THRESHOLD', sessionData.deviceId)
pair = syncState.refreshGpn.entries().next().value
nextLevel = true
if pair[0].length >= GET_PARAMETER_NAMES_DEPTH_THRESHOLD
nextLevel = false
else if common.hammingWeight(pair[1] >> 3) >= 3
nextLevel = false
if syncState.refreshGpn.size > 1 or (pair[1] > 1 and nextLevel)
completed = false
rpcReq = {
type: 'GetParameterNames'
parameterPath: pair[0].concat('').join('.')
nextLevel: nextLevel
}
if syncState.refreshAttributes.value.size
if rpcReq?
completed = false
else
TASK_PARAMETERS_BATCH_SIZE =
config.get('TASK_PARAMETERS_BATCH_SIZE', sessionData.deviceId)
parameterNames = []
iter = syncState.refreshAttributes.value.values()
while (path = iter.next().value) and
parameterNames.length < TASK_PARAMETERS_BATCH_SIZE
parameterNames.push(path)
if syncState.refreshAttributes.value.size > parameterNames.length
completed = false
rpcReq = {
type: 'GetParameterValues'
parameterNames: (p.join('.') for p in parameterNames)
}
TASK_PARAMETERS_BATCH_SIZE =
config.get('TASK_PARAMETERS_BATCH_SIZE', sessionData.deviceId)
parameterValues = []
syncState.setValues.forEach((v, k) ->
return if completed == false
if rpcReq?
completed = false
return
val = v[1].slice()
if not val[1]?
val[1] = v[0][1]
device.sanitizeParameterValue(val)
if val[0] != v[0][0] or val[1] != v[0][1]
if parameterValues.length >= TASK_PARAMETERS_BATCH_SIZE
completed = false
return
parameterValues.push([k, val[0], val[1]])
)
if parameterValues.length and not rpcReq?
rpcReq = {
type: 'SetParameterValues'
parameterList: ([p[0].join('.'), p[1], p[2]] for p in parameterValues)
}
if completed and rpcReq?
rpcReq.done = true
return rpcReq
processVirtualparameterDeclaration = (sessionData, path, timestamps, values) ->
ret = true
return ret if path.length != 2
deviceData = sessionData.deviceData
subIter = sessionData.deviceData.paths.subset(path)
while sub = subIter.next().value
continue if not deviceData.values.exist.get(sub)
isWritable = deviceData.values.writable.get(sub)
for k, v of timestamps
if not (v <= deviceData.timestamps[k].get(sub))
sessionData.syncState.virtualParameters[sub[1]] ?= {timestamps: {}, values: {}}
sessionData.syncState.virtualParameters[sub[1]].timestamps[k] =
Math.max(v, sessionData.syncState.virtualParameters[sub[1]].timestamps[k] ? 0)
ret = false
if values?.value?
if isWritable == 1
sessionData.syncState.virtualParameters[sub[1]] ?= {timestamps: {}, values: {}}
cur = sessionData.syncState.virtualParameters[sub[1]].values.value
if not cur?
cur = [deviceData.values.value.get(sub), values.value]
sessionData.syncState.virtualParameters[sub[1]].values.value = cur
else
cur[1] = values.value
else if not isWritable?
sessionData.syncState.virtualParameters[sub[1]] ?= {timestamps: {}, values: {}}
sessionData.syncState.virtualParameters[sub[1]].timestamps.writable =
Math.max(1, sessionData.syncState.virtualParameters[sub[1]].timestamps.writable ? 0)
ret = false
return ret
processDeclaration = (sessionData, path, _timestamps, values) ->
syncState = sessionData.syncState
deviceData = sessionData.deviceData
timestamps = {}
timestamps[k] = v for k, v of _timestamps
ret = true
if path[0] == '*' or path[0] == 'VirtualParameters'
ret &&= processVirtualparameterDeclaration(sessionData, path, _timestamps, values)
return ret if path[0] != '*'
iter = sessionData.deviceData.paths.superset(path)
param = null
existTimestamp = 0
while sup = iter.next().value
param = sup if not sup.wildcard
for k, v of timestamps
continue if not (t = deviceData.timestamps[k].get(sup))?
existTimestamp = Math.max(existTimestamp, t) if k is 'exist'
delete timestamps[k] if t >= v
if not Object.keys(timestamps).length and
not (values? and Object.keys(values).length)
return ret
if param?
if deviceData.values.exist.get(param)
param = syncState.paths.add(param)
isObject = deviceData.values.object.get(param)
isWritable = deviceData.values.writable.get(param)
for k of timestamps
if k isnt 'value' or isObject == 0
syncState.refreshAttributes[k].add(param)
ret = false
else if not isObject?
syncState.refreshAttributes.object.add(param)
ret = false
if values?.value?
if isObject == 0 and isWritable == 1
cur = syncState.setValues.get(param)
if not cur?
cur = [deviceData.values.value.get(param), values.value]
syncState.setValues.set(param, cur)
else
cur[1] = values.value
else
if not isObject?
syncState.refreshAttributes.object.add(param)
ret = false
if not isWritable?
syncState.refreshAttributes.writable.add(param)
ret = false
return ret
else if existTimestamp >= (timestamps.exist or 1)
return ret
else if path.indexOf('*') >= 0
subIter = deviceData.paths.subset(path)
while sub = subIter.next().value
continue if not deviceData.values.exist.get(sub)
isObject = deviceData.values.object.get(sub)
isWritable = deviceData.values.writable.get(sub)
for k, v of timestamps
if not (v <= deviceData.timestamps[k].get(sub))
if k isnt 'value' or isObject == 0
sub = syncState.paths.add(sub)
syncState.refreshAttributes[k].add(sub)
ret = false
else if not isObject?
sub = syncState.paths.add(sub)
syncState.refreshAttributes.object.add(sub)
ret = false
if values?.value?
if isObject == 0 and isWritable == 1
cur = syncState.setValues.get(sub)
if not cur?
cur = [deviceData.values.value.get(sub), values.value]
syncState.setValues.set(sub, cur)
else
cur[1] = values.value
else
if not isObject?
syncState.refreshAttributes.object.add(sub)
ret = false
if not isWritable?
syncState.refreshAttributes.writable.add(sub)
ret = false
if existTimestamp >= (timestamps.exist or 1)
return
childrenTimestamp = existTimestamp
for i in [path.length - 1...0] by -1
existTimestamp = 0
p = path.slice(0, i)
iter = deviceData.paths.superset(p)
param = null
while sup = iter.next().value
param = sup if not sup.wildcard
if (t = deviceData.timestamps.exist.get(sup))?
existTimestamp = Math.max(existTimestamp, t)
if param?
if deviceData.values.exist.get(param)
isObject = deviceData.values.object.get(param)
objectTimestamp = deviceData.timestamps.object.get(param)
if isObject
if childrenTimestamp == 0
param = syncState.paths.add(param)
syncState.refreshGpn.set(param, syncState.refreshGpn.get(param) | ((1 << path.length - i) - 1))
ret = false
else if childrenTimestamp < (timestamps.exist or 1)
param = syncState.paths.add(param)
syncState.refreshGpn.set(param, syncState.refreshGpn.get(param) or 1)
ret = false
if not (objectTimestamp >= (timestamps.exist or 1))
param = syncState.paths.add(param)
syncState.refreshAttributes.object.add(param)
ret = false
# return ret
# else if existTimestamp >= (timestamps.exist or 1)
# return ret
else if p.indexOf('*') >= 0
subIter = deviceData.paths.subset(p)
while sub = subIter.next().value
continue if not deviceData.values.exist.get(sub)
# TODO check if not object
pp = sub.concat(path.slice(sub.length))
ts = {}
ts[k] = v for k, v of timestamps
ret &&= processDeclaration(sessionData, pp, ts)
if existTimestamp >= (timestamps.exist or 1)
return ret
childrenTimestamp = existTimestamp
p = syncState.paths.add([])
ret = false
syncState.refreshGpn.set(p, syncState.refreshGpn.get(p) or 1)
return ret
loadPath = (sessionData, path) ->
tl = []
for i in [path.length...0] by -1
p = path.slice(0, i)
loaded = 0
iter = sessionData.deviceData.paths.superset(p)
while sup = iter.next().value
if (l = sessionData.deviceData.loaded.get(sup)) > loaded
loaded = l
if loaded
tl = tl.slice(0, tl.length - (loaded - 1))
break
tl.push(p)
if not tl.length
return true
sessionData.toLoad ?= []
sessionData.toLoad = sessionData.toLoad.concat(tl)
return false
processInstances = (sessionData, parent, parameters, keys, minInstances, maxInstances) ->
instancesToDelete = sessionData.syncState.instancesToDelete.get(parent)
if not instancesToDelete?
instancesToDelete = new Set()
sessionData.syncState.instancesToDelete.set(parent, instancesToDelete)
instancesToCreate = sessionData.syncState.instancesToCreate.get(parent)
if not instancesToCreate?
instancesToCreate = new InstanceSet()
sessionData.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)
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
applyDeclaration = (sessionData, path, state, timestamps, values) ->
if state == 100
if not values? or Object.keys(values).length == 0
return state
timestamps = {}
sessionData.syncState ?= {
paths: new PathSet()
refreshAttributes: {
exist: new Set()
object: new Set()
writable: new Set()
value: new Set()
}
setValues: new Map()
refreshGpn: new Map()
virtualParameters: {}
instancesToDelete: new Map()
instancesToCreate: new Map()
}
decs = device.getAliasDeclarations(path, timestamps.exist ? 1)
if state <= 1
loaded = true
for d in decs
loaded &&= loadPath(sessionData, d[0])
return 1 if not loaded
if decs.length == 1
satisfied = processDeclaration(sessionData, decs[0][0], timestamps, values)
if satisfied
return 100
else
return 2
else
if state <= 2
satisfied = true
for d in decs
satisfied &&= processDeclaration(sessionData, d[0], d[1], d[2])
return 2 if not satisfied
unpacked = device.unpack(sessionData.deviceData, path)
if values.exist?
if Array.isArray(values.exist)
minInstances = values.exist[0]
maxInstances = values.exist[1]
else
minInstances = maxInstances = values.exist
parent = path.slice(0, -1)
if Array.isArray(path[path.length - 1])
keys = {}
for p, i in path[path.length - 1] by 2
keys[p] = path[path.length - 1][i + 1]
else if path[path.length - 1] == '*'
keys = {}
if (path.wildcard | path.alias) & ((1 << (path.length - 1)) - 1) == 0
processInstances(sessionData, parent, unpacked, keys, minInstances, maxInstances)
else
parentsUnpacked = device.unpack(sessionData.deviceData, parent)
for parent in parentsUnpacked
processInstances(sessionData, parent, device.unpack(sessionData.deviceData, parent.concat([path[parent.length]])), keys, minInstances, maxInstances)
satisfied = true
for up in unpacked
satisfied &&= processDeclaration(sessionData, path, timestamps, values)
if satisfied
return 100
else
return 4
commitVirtualParameter = (sessionData, name, declaration, update) ->
t = {}
v = {}
if update.writable?
if update.writable[0] <= 0
t.writable = sessionData.timestamp + update.writable[0]
else
t.writable = Math.min(sessionData.timestamp, update.writable[0])
if declaration.timestamps.writable?
t.writable = Math.max(declaration.timestamps.writable, t.writable)
v.writable = +update.writable[1]
else if declaration.timestamps.writable?
throw new Error('Virtual parameter must provide declared attributes')
if update.value?
if update.value[0] <= 0
t.value = sessionData.timestamp + update.value[0]
else
t.value = Math.min(sessionData.timestamp, update.value[0])
if declaration.timestamps.value?
t.value = Math.max(declaration.timestamps.value, t.value)
v.value = device.sanitizeParameterValue(update.value[1])
else if declaration.timestmaps.value?
throw new Error('Virtual parameter must provide declared attributes')
device.set(sessionData.deviceData, ['VirtualParameters', name], t, v)
runExtensions = (sessionData, revision, _extensions, callback) ->
sessionData.extensionsCache[revision] ?= {}
obj = {}
for e in _extensions
obj[JSON.stringify(e)] = e
counter = 1
for k, v of obj
++ counter
do (k, v) ->
extensions.run(v, (err, res) ->
sessionData.extensionsCache[revision][k] = res
if -- counter == 0 or err
counter = 0
return callback(err)
)
if -- counter == 0
return callback()
rpcResponse = (sessionData, id, rpcRes, callback) ->
if id != generateRpcId(sessionData)
return callback(new Error('Request ID not recognized'))
++ sessionData.rpcCount
rpcReq = sessionData.rpcRequest
if not rpcReq.next?
sessionData.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
sessionData.rpcRequest = null
else
sessionData.rpcRequest = {
type: '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
sessionData.rpcRequest = null
else
sessionData.rpcRequest = {
type: 'SetParameterValues'
parameterList: parameterList
}
timestamp = sessionData.timestamp
revision = sessionData.revisions.reduce(((a, b) -> a + (b >> 1)), 0) + 1
v.revision = revision for k, v of sessionData.deviceData.timestamps
v.revision = revision for k, v of sessionData.deviceData.values
switch rpcRes.type
when 'GetParameterValuesResponse'
return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'GetParameterValues'
for p in rpcRes.parameterList
device.set(sessionData.deviceData, common.parsePath(p[0]),
{exist: timestamp, object: timestamp, value: timestamp},
{exist: 1, object: 0, value: p.slice(1)})
when 'GetParameterNamesResponse'
return callback(new Error('Response type does not match request type')) if rpcReq.type 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('*'), {exist: timestamp}])
for p in rpcRes.parameterList
if common.endsWith(p[0], '.')
path = common.parsePath(p[0][0...-1])
if not rpcReq.nextLevel
params.push([path.concat('*'), {exist: timestamp}])
params.push([path,
{exist: timestamp, object: timestamp, writable: timestamp},
{exist: 1, object: 1, writable: if p[1] then 1 else 0}])
else
params.push([common.parsePath(p[0]),
{exist: timestamp, object: timestamp, writable: timestamp},
{exist: 1, object: 0, writable: if p[1] then 1 else 0}])
# Sort such that actual parameters are set before wildcard ones
params.sort((a, b) ->
al = a[0].length
bl = b[0].length
-- bl if b[0][bl - 1] == '*'
-- al if a[0][al - 1] == '*'
return bl - al
)
p = root.slice()
p.length = params[params.length - 1].length
p.fill('*', root.length)
loadPath(sessionData, p)
loadParameters(sessionData, (err) ->
return callback(err) if err
for p in params
device.set(sessionData.deviceData, p[0], p[1], p[2])
return callback()
)
return
when 'SetParameterValuesResponse'
return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'SetParameterValues'
for p in rpcReq.parameterList
device.set(sessionData.deviceData, common.parsePath(p[0]),
{exist: timestamp, object: timestamp, writable: timestamp, value: timestamp},
{exist: 1, object: 0, writable: 1, value: p.slice(1)})
when 'AddObjectResponse'
device.set(sessionData.deviceData, common.parsePath(rpcReq.objectName + rpcRes.instanceNumber),
{exist: timestamp, object: timestamp},
{exist: 1, object: 1})
when 'DeleteObjectResponse'
device.set(sessionData.deviceData, common.parsePath(rpcReq.objectName.slice(0, -1)),
{exist: timestamp})
else
return callback(new Error('Response type not recognized'))
return callback()
rpcFault = (sessionData, id, faultResponse, callback) ->
# TODO Consider handling invalid parameter faults by automatically refreshing
# relevant data model portions
delete sessionData.rpcRequest
return callback(null, faultResponse)
deserialize = (sessionDataString, callback) ->
cache.getProvisionsAndVirtualParameters((err, hash, provisions, virtualParameters) ->
return callback(err) if err
sessionData = JSON.parse(sessionDataString)
if sessionData.presetsHash? and sessionData.presetsHash != hash
return callback(new Error('Preset hash mismatch'))
sessionData.cache = {
provisions: provisions
virtualParameters: virtualParameters
}
for d in sessionData.declarations
common.addPathMeta(d[0])
deviceData = initDeviceData()
keys = ['exist', 'object', 'writable', 'value']
for r in sessionData.deviceData
path = deviceData.paths.add(r[0])
if r[1]
deviceData.loaded.set(path, r[1])
for k, i in keys
if r[2 + i]?
deviceData.timestamps[k].setRevisions(path, r[2 + i])
if r[2 + keys.length + i]?
deviceData.values[k].setRevisions(path, r[2 + keys.length + i])
sessionData.deviceData = deviceData
return callback(null, sessionData)
)
serialize = (sessionData, callback) ->
deviceData = []
keys = ['exist', 'object', 'writable', 'value']
iter = sessionData.deviceData.paths.find([], 99)
while not (p = iter.next()).done
path = p.value
e = [path]
e[1] = sessionData.deviceData.loaded.get(path) || 0
for k, i in keys
if r = sessionData.deviceData.timestamps[k].getRevisions(path)
e[2 + i] = r
if r = sessionData.deviceData.values[k].getRevisions(path)
e[2 + keys.length + i] = r
deviceData.push(e)
oldDeviceData = sessionData.deviceData
oldSyncState = sessionData.syncState
oldToLoad = sessionData.toLoad
oldCache = sessionData.cache
sessionData.deviceData = deviceData
delete sessionData.syncState
delete sessionData.toLoad
delete sessionData.cache
sessionDataString = JSON.stringify(sessionData)
sessionData.deviceData = oldDeviceData
sessionData.syncState = oldSyncState
sessionData.toLoad = sessionData.toLoad
sessionData.cache = sessionData.cache
return callback(null, sessionDataString)
end = (sessionData, callback) ->
db.saveDevice(sessionData.deviceId, sessionData.deviceData, sessionData.new, (err) ->
return callback(err) if err
db.redisClient.del("session_#{sessionData.id}", (err) ->
callback(err, sessionData.new)
)
)
exports.init = init
exports.inform = inform
exports.addProvisions = addProvisions
exports.clearProvisions = clearProvisions
exports.rpcRequest = rpcRequest
exports.rpcResponse = rpcResponse
exports.rpcFault = rpcFault
exports.end = end
exports.serialize = serialize
exports.deserialize = deserialize