From b4bbd8f58cf5c4ed0f7fc10f92dd5faef905a21a Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Mon, 7 Nov 2016 00:29:38 -0800 Subject: [PATCH] Simplify declare function in scripts Using declare function is made easier by accept a single path and returning a promise to read parameter attributes. --- lib/cache.coffee | 4 +- lib/sandbox.coffee | 152 ------------------------ lib/sandbox.js | 281 +++++++++++++++++++++++++++++++++++++++++++++ lib/session.coffee | 129 +++++++++++---------- 4 files changed, 347 insertions(+), 219 deletions(-) delete mode 100644 lib/sandbox.coffee create mode 100644 lib/sandbox.js diff --git a/lib/cache.coffee b/lib/cache.coffee index d0bd9f6..905c0a3 100644 --- a/lib/cache.coffee +++ b/lib/cache.coffee @@ -167,7 +167,7 @@ refresh = (callback) -> for r in res provisions[r._id] = {} provisions[r._id].md5 = crypto.createHash('md5').update(r.script).digest('hex') - provisions[r._id].script = new vm.Script(r.script, {filename: r._id, timeout: 50}) + provisions[r._id].script = new vm.Script("\"use strict\";(function(){\n#{r.script}\n})();", {filename: r._id, lineOffset: 1, timeout: 50}) if -- counter == 0 computeHash() @@ -188,7 +188,7 @@ refresh = (callback) -> for r in res virtualParameters[r._id] = {} virtualParameters[r._id].md5 = crypto.createHash('md5').update(r.script).digest('hex') - virtualParameters[r._id].script = new vm.Script(r.script, {filename: r._id, timeout: 50}) + virtualParameters[r._id].script = new vm.Script("\"use strict\";(function(){\n#{r.script}\n})();", {filename: r._id, lineOffset: 1, timeout: 50}) if -- counter == 0 computeHash() diff --git a/lib/sandbox.coffee b/lib/sandbox.coffee deleted file mode 100644 index 34bf90a..0000000 --- a/lib/sandbox.coffee +++ /dev/null @@ -1,152 +0,0 @@ -### -# 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 . -### - -vm = require 'vm' - -common = require './common' -device = require './device' - - -# Used for throwing to exit user script -class Exit; - - -sandbox = { - timestamp: null - deviceData: null - revision: null - maxRevision: null - declarations: null - extensionsCache: null - extensions: null - context: vm.createContext() -} - -Object.defineProperty(sandbox.context, 'Date', {value: class - constructor: (arg) -> - if arguments.length - return new (Function.prototype.bind.apply(Date, arguments)) - - return new Date(sandbox.timestamp) - - @now: () -> - return sandbox.timestamp -}) - - -Object.defineProperty(sandbox.context, 'ext', {value: () -> - if sandbox.extensions?.length - throw new Error('Ext function should not be called from within a try/catch block') - - extCall = (String(a) for a in arguments) - key = JSON.stringify(extCall) - if not sandbox.extensionsCache[sandbox.revision]? or key not of sandbox.extensionsCache[sandbox.revision] - sandbox.extensions = [extCall] - throw new Exit() - - return sandbox.extensionsCache[sandbox.revision][key] -}) - - -Object.defineProperty(sandbox.context, 'declare', {value: (decs) -> - if ++ sandbox.revision > sandbox.maxRevision + 1 - throw new Error('Declare function should not be called from within a try/catch block') - - if sandbox.revision == sandbox.maxRevision + 1 - for k, v of decs - timestamp = 1 - exist = null - attributeTimestamps = {} - attributeValues = {} - for attrName, dec of v - if attrName is 'exist' - if Array.isArray(dec) - timestamp = dec[0] - exist = dec[1] if 1 of dec - else - timestamp = dec - continue - - if Array.isArray(dec) - attributeTimestamps[attrName] = dec[0] - attributeValues[attrName] = dec[1] if 1 of dec - else - attributeTimestamps[attrName] = dec - - sandbox.declarations.push([common.parsePath(k), timestamp, attributeTimestamps, exist, attributeValues]) - - throw new Exit() - - res = {} - for k, v of decs - path = common.parsePath(k) - single = true - for p in path - if p == '*' or common.typeOf(p) isnt common.STRING_TYPE - single = false - break - - unpacked = device.unpack(sandbox.deviceData, path, sandbox.revision) - for param in unpacked - r = {} - attrs = sandbox.deviceData.attributes.get(param, sandbox.revision) - for attrName, dec of v - if attrName of attrs - r[attrName] = attrs[attrName] - - if single - res[k] = r - else - res[k] ?= {} - res[k][param.join('.')] = r - - return res -}) - - -Object.defineProperty(sandbox.context, 'clear', {value: (pattern, timestamp) -> - if sandbox.revision == sandbox.maxRevision - sandbox.clear ?= [] - sandbox.clear.push([common.parsePath(pattern), timestamp]) - return -}) - - -run = (script, globals, timestamp, deviceData, extensionsCache, startRevision, maxRevision) -> - sandbox.timestamp = timestamp - sandbox.deviceData = deviceData - sandbox.extensionsCache = extensionsCache - sandbox.revision = startRevision - sandbox.maxRevision = maxRevision - sandbox.declarations = [] - sandbox.extensions = null - sandbox.clear = null - - for k, v of globals - sandbox.context[k] = v - - try - ret = script.runInNewContext(sandbox.context, {displayErrors: false}) - delete sandbox.context[k] for k of sandbox.context - return {done: true, declarations: sandbox.declarations, extensions: sandbox.extensions, clear: sandbox.clear, returnValue: ret} - catch err - delete sandbox.context[k] for k of sandbox.context - throw err if err not instanceof Exit - return {done: false, declarations: sandbox.declarations, extensions: sandbox.extensions, clear: sandbox.clear} - - -exports.run = run diff --git a/lib/sandbox.js b/lib/sandbox.js new file mode 100644 index 0000000..1b63b99 --- /dev/null +++ b/lib/sandbox.js @@ -0,0 +1,281 @@ +/** + * 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 . + */ +"use strict"; + +const vm = require("vm"); + +const common = require("./common"); +const device = require("./device"); +const extensions = require("./extensions"); + + +// Used for throwing to exit user script and commit +const COMMIT = Symbol(); + +// Used to execute extensions and restart +const EXT = Symbol(); + +const sandbox = { + timestamp: null, + deviceData: null, + revision: null, + maxRevision: null, + uncommitted: false, + declarations: null, + extensionsCache: null, + extensions: null, + context: vm.createContext() +} + +class SandboxDate { + constructor(arg) { + if (arguments.length) + return new (Function.prototype.bind.apply(Date, arguments)); + + return new Date(sandbox.timestamp); + } + + static now() { + return sandbox.timestamp; + } +}; + + +class ParameterWrapper { + constructor(path, attributes, unpacked, unpackedRevision) { + for (let attrName of attributes) { + Object.defineProperty(this, attrName, {get: function() { + if (sandbox.uncommitted) + commit(); + + if (sandbox.revision !== unpackedRevision) { + unpackedRevision = sandbox.revision; + unpacked = device.unpack(sandbox.deviceData, path, sandbox.revision + 1); + } + + if (!unpacked.length) + return undefined; + + let attr = sandbox.deviceData.attributes.get(unpacked[0], sandbox.revision + 1)[attrName]; + + if (!attr) + return undefined; + + return attr[1]; + }}); + } + + Object.defineProperty(this, "path", {get: function() { + if (sandbox.uncommitted) + commit(); + + if (sandbox.revision !== unpackedRevision) { + unpackedRevision = sandbox.revision; + unpacked = device.unpack(sandbox.deviceData, path, sandbox.revision + 1); + } + + if (!unpacked.length) + return undefined; + + return unpacked[0].join('.'); + }}); + + Object.defineProperty(this, "size", {get: function() { + if (sandbox.uncommitted) + commit(); + + if (sandbox.revision !== unpackedRevision) { + unpackedRevision = sandbox.revision; + unpacked = device.unpack(sandbox.deviceData, path, sandbox.revision + 1); + } + + if (!unpacked.length) + return undefined; + + return unpacked.length; + }}); + + this[Symbol.iterator] = function*() { + if (sandbox.uncommitted) + commit(); + + if (sandbox.revision !== unpackedRevision) { + unpackedRevision = sandbox.revision; + unpacked = device.unpack(sandbox.deviceData, path, sandbox.revision + 1); + } + + for (let p of unpacked) + yield new ParameterWrapper(p, attributes, [p], sandbox.revision); + } + } +} + + +function declare(path, timestamps, values) { + sandbox.uncommitted = true; + if (!timestamps) + timestamps = {}; + + let parsedPath = common.parsePath(path); + + sandbox.declarations[path] = sandbox.declarations[path] || [parsedPath, 1, {}, null, {}]; + + let attrs = {}; + for (let attrName in timestamps) { + if (attrName === "path") { + if (timestamps.path > sandbox.declarations[path][1]) + sandbox.declarations[path][1] = timestamps.path; + } + else { + attrs[attrName] = 1; + if (!(timestamps[attrName] <= sandbox.declarations[path][2][attrName])) + sandbox.declarations[path][2][attrName] = timestamps[attrName]; + } + } + + if (values) { + for (let attrName in values) { + if (attrName === "path") + sandbox.declarations[path][3] = values.path; + else { + attrs[attrName] = 1; + if (attrName === 'value' && !Array.isArray(values[attrName])) + sandbox.declarations[path][4].value = [values[attrName]]; + else + sandbox.declarations[path][4].value = values[attrName]; + } + } + } + + return new ParameterWrapper(parsedPath, Object.keys(attrs)); +} + + +function clear(path, timestamp, attributes) { + sandbox.uncommitted = true; + + if (sandbox.revision === sandbox.maxRevision) { + sandbox.clear = sandbox.clear || []; + sandbox.clear.push([common.parsePath(path), timestamp, attributes]); + } +} + + +function commit() { + ++ sandbox.revision; + sandbox.uncommitted = false; + + if (sandbox.revision === sandbox.maxRevision + 1) + throw COMMIT; + else if (sandbox.revision > sandbox.maxRevision + 1) + throw new Error("Declare function should not be called from within a try/catch block"); +} + + +function ext() { + let extCall = Array.from(arguments).map(String); + let key = `${sandbox.revision}: ${JSON.stringify(extCall)}`; + + if (key in sandbox.extensionsCache) + return sandbox.extensionsCache[key]; + + sandbox.extensions[key] = extCall; + throw EXT; +} + + +Object.defineProperty(sandbox.context, 'Date', {value: SandboxDate}); +Object.defineProperty(sandbox.context, 'declare', {value: declare}); +Object.defineProperty(sandbox.context, 'clear', {value: clear}); +Object.defineProperty(sandbox.context, 'commit', {value: commit}); +Object.defineProperty(sandbox.context, 'ext', {value: ext}); + + +function run(script, globals, timestamp, deviceData, extensionsCache, startRevision, maxRevision, callback) { + sandbox.timestamp = timestamp; + sandbox.deviceData = deviceData; + sandbox.extensionsCache = extensionsCache; + sandbox.revision = startRevision; + sandbox.maxRevision = maxRevision; + sandbox.uncommitted = false; + sandbox.declarations = {}; + sandbox.extensions = {}; + sandbox.clear = null; + + for (let n in sandbox.context) + delete sandbox.context[n]; + + for (let n in globals) + sandbox.context[n] = globals[n]; + + try { + let ret = script.runInContext(sandbox.context, {displayErrors: false}); + + let declarations = []; + for (let p in sandbox.declarations) + declarations.push(sandbox.declarations[p]); + + let counter = 1; + for (let key in sandbox.extensions) { + ++ counter; + extensions.run(sandbox.extensions[key], function(err, res) { + sandbox.extensionsCache[key] = res; + if (-- counter === 0) + return callback(null, sandbox.clear, declarations, true, ret); + }); + } + if (-- counter === 0) + return callback(null, sandbox.clear, declarations, true, ret); + } + catch (err) { + if (err === COMMIT) { + let declarations = []; + for (let p in sandbox.declarations) + declarations.push(sandbox.declarations[p]); + + let counter = 1; + for (let key in sandbox.extensions) { + ++ counter; + extensions.run(sandbox.extensions[key], function(err, res) { + sandbox.extensionsCache[key] = res; + if (-- counter === 0) + return callback(null, sandbox.clear, declarations, false); + }); + } + if (-- counter === 0) + return callback(null, sandbox.clear, declarations, false); + } + else if (err === EXT) { + let counter = 1; + for (let key in sandbox.extensions) { + ++ counter; + extensions.run(sandbox.extensions[key], function(err, res) { + sandbox.extensionsCache[key] = res; + if (-- counter === 0) + return run(script, globals, timestamp, deviceData, extensionsCache, startRevision, maxRevision, callback); + }); + } + if (-- counter === 0) + return run(script, globals, timestamp, deviceData, extensionsCache, startRevision, maxRevision, callback); + } + else + return callback(err); + } +} + + +exports.run = run; diff --git a/lib/session.coffee b/lib/session.coffee index a79bb5f..15839f1 100644 --- a/lib/session.coffee +++ b/lib/session.coffee @@ -52,7 +52,7 @@ init = (deviceId, cwmpVersion, timeout, callback) -> revisions: [] rpcCount: 0 iteration: 0 - extensionsCache: [] + extensionsCache: {} declarations: [] } @@ -180,7 +180,7 @@ addProvisions = (sessionData, channel, provisions) -> v.collapse(1) for k, v of sessionData.deviceData.attributes sessionData.rpcCount = 0 sessionData.revisions = [0] - sessionData.extensionsCache.length = 0 + sessionData.extensionsCache = {} clearProvisions = (sessionData) -> @@ -195,7 +195,7 @@ clearProvisions = (sessionData) -> sessionData.deviceData.attributes.collapse(1) sessionData.rpcCount = 0 sessionData.revisions = [0] - sessionData.extensionsCache.length = 0 + sessionData.extensionsCache = {} runProvisions = (sessionData, provisions, startRevision, endRevision, callback) -> @@ -203,6 +203,7 @@ runProvisions = (sessionData, provisions, startRevision, endRevision, callback) _extensions = [] _clear = [] allDeclarations = [] + counter = 1 for provision in provisions if not sessionData.cache.provisions[provision[0]]? switch provision[0] @@ -245,22 +246,27 @@ runProvisions = (sessionData, provisions, startRevision, endRevision, callback) allDeclarations.push([['FactoryReset'], 1, {value: 1}, null, {value: [sessionData.timestamp]}]) continue - ret = sandbox.run(sessionData.cache.provisions[provision[0]].script, {args: provision[1]}, sessionData.timestamp, sessionData.deviceData, sessionData.extensionsCache, startRevision, endRevision) + ++ counter + sandbox.run(sessionData.cache.provisions[provision[0]].script, + {args: provision[1]}, sessionData.timestamp, sessionData.deviceData, + sessionData.extensionsCache, startRevision, endRevision, + (err, _clear, _declarations, _done) -> + -- counter + if err + if counter >= 0 + counter = 0 + return callback(err) + return - if ret.extensions?.length - _extensions = _extensions.concat(ret.extensions) - if ret.clear?.length - _clear = _clear.concat(ret.clear) - done &&= ret.done - allDeclarations = allDeclarations.concat(ret.declarations) + done &&= _done + allDeclarations = allDeclarations.concat(_declarations) - if _extensions.length - return runExtensions(sessionData, endRevision, _extensions, (err) -> - return callback(err) if err - return runProvisions(sessionData, provisions, startRevision, endRevision, callback) - ) + if counter == 0 + return callback(null, done, allDeclarations, _clear) + ) - return callback(null, done, allDeclarations, _clear) + if -- counter == 0 + return callback(null, done, allDeclarations, _clear) runVirtualParameters = (sessionData, provisions, startRevision, endRevision, callback) -> @@ -269,36 +275,48 @@ runVirtualParameters = (sessionData, provisions, startRevision, endRevision, cal virtualParameterUpdates = [] _extensions = [] _clear = [] - + counter = 1 for provision in provisions + ++ counter globals = {TIMESTAMPS: provision[1], VALUES: provision[2]} - ret = sandbox.run(sessionData.cache.virtualParameters[provision[0]].script, globals, sessionData.timestamp, sessionData.deviceData, sessionData.extensionsCache, startRevision, endRevision) - if ret.extensions?.length - _extensions = _extensions.concat(ret.extensions) - if ret.clear?.length - _clear = _clear.concat(ret.clear) + sandbox.run(sessionData.cache.virtualParameters[provision[0]].script, globals, + sessionData.timestamp, sessionData.deviceData, + sessionData.extensionsCache, startRevision, endRevision, + (err, _clear, _declarations, _done, _returnValue) -> + -- counter + if err + if counter >= 0 + counter = 0 + return callback(err) + return - done &&= ret.done - decs = decs.concat(ret.declarations) + done &&= _done + decs = decs.concat(_declarations) - if ret.done - virtualParameterUpdates.push(ret.returnValue) + if _done + virtualParameterUpdates.push(_returnValue) - if _extensions.length - return runExtensions(sessionData, endRevision, _extensions, (err) -> - return callback(err) if err - return runVirtualParameters(sessionData, provisions, startRevision, endRevision, callback) + if counter == 0 + toClear = null + if virtualParameterUpdates.length == provisions.length + for vpu, i in virtualParameterUpdates + toClear = commitVirtualParameter(sessionData, provisions[i], vpu, toClear) + + clear(sessionData, toClear, (err) -> + return callback(err, done, decs, _clear) + ) + ) + + if -- counter == 0 + toClear = null + if virtualParameterUpdates.length == provisions.length + for vpu, i in virtualParameterUpdates + toClear = commitVirtualParameter(sessionData, provisions[i], vpu, toClear) + + clear(sessionData, toClear, (err) -> + return callback(err, done, decs, _clear) ) - toClear = null - if virtualParameterUpdates.length == provisions.length - for vpu, i in virtualParameterUpdates - toClear = commitVirtualParameter(sessionData, provisions[i], vpu, toClear) - - clear(sessionData, toClear, (err) -> - return callback(err, done, decs, _clear) - ) - runDeclarations = (sessionData, declarations) -> sessionData.iteration += 2 @@ -388,8 +406,8 @@ runDeclarations = (sessionData, declarations) -> if declaration[3]? if Array.isArray(declaration[3]) - minInstances = declaration[3].exist[0] - maxInstances = declaration[3].exist[1] + minInstances = declaration[3][0] + maxInstances = declaration[3][1] else minInstances = maxInstances = declaration[3] @@ -557,13 +575,16 @@ rpcRequest = (sessionData, _declarations, callback) -> delete sessionData.syncState sessionData.deviceData.timestamps.collapse(1) sessionData.deviceData.attributes.collapse(1) - sessionData.extensionsCache.splice(0) + sessionData.extensionsCache = {} return callback(null, null, null, doneProvisions) rev = sessionData.revisions[sessionData.revisions.length - 1] sessionData.deviceData.timestamps.collapse(rev + 1) sessionData.deviceData.attributes.collapse(rev + 1) - sessionData.extensionsCache.splice(rev) + for k of sessionData.extensionsCache + n = Number(k.split(':', 1)[0]) + if n > rev + delete sessionData.extensionsCache[k] sessionData.syncState.virtualParameterDeclarations.length = sessionData.declarations.length @@ -1039,28 +1060,6 @@ commitVirtualParameter = (sessionData, provision, update, toClear) -> return device.set(sessionData.deviceData, ['VirtualParameters', provision[0]], timestamp, attributes, toClear) -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'))