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'))