Turn errors from scripts and extensions into faults

This commit is contained in:
Zaid Abdulla committed 2016-12-04 19:04:22 -08:00
1 parent 7ac94a5304
commit 9f85ead558
8 files changed
+254 -105

No files matched your search

+1
View File
@@ -42,6 +42,7 @@ options = {
FS_IP : {type : 'string', default : '192.168.0.1'}, FS_IP : {type : 'string', default : '192.168.0.1'},
DOWNLOAD_TIMEOUT: {type : 'int', default : 3600}, DOWNLOAD_TIMEOUT: {type : 'int', default : 3600},
EXT_TIMEOUT: {type: 'int', default: 3000},
PRESETS_CACHE_DURATION : {type : 'int', default : 86400}, PRESETS_CACHE_DURATION : {type : 'int', default : 86400},
LOG_INFORMS : {type : 'bool', default : true}, LOG_INFORMS : {type : 'bool', default : true},
DEBUG : {type : 'bool', default : false}, DEBUG : {type : 'bool', default : false},
+33 -3
View File
@@ -207,9 +207,12 @@ applyPresets = (currentRequest) ->
for k, v of parameters for k, v of parameters
declarations.push([v, 1, {value: 1}]) declarations.push([v, 1, {value: 1}])
session.rpcRequest(currentRequest.sessionData, declarations, (err, id, rpcRequest) -> session.rpcRequest(currentRequest.sessionData, declarations, (err, faults, id, rpcRequest) ->
throw err if err throw err if err
if faults?
throw new Error('A fault occured while trying to discover parameters to test preset preconditions: ' + JSON.stringify(faults))
if rpcRequest? if rpcRequest?
return sendRpcRequest(currentRequest, id, rpcRequest) return sendRpcRequest(currentRequest, id, rpcRequest)
@@ -227,9 +230,18 @@ applyPresets = (currentRequest) ->
if query.testFilter(parameters, p.precondition) if query.testFilter(parameters, p.precondition)
session.addProvisions(currentRequest.sessionData, p.channel, p.provisions) session.addProvisions(currentRequest.sessionData, p.channel, p.provisions)
session.rpcRequest(currentRequest.sessionData, null, (err, id, rpcRequest, doneProvisions) -> session.rpcRequest(currentRequest.sessionData, null, (err, faults, id, rpcRequest, doneProvisions) ->
throw err if err throw err if err
if faults?
for channel, fault of faults
if not fault.retries? and Object.keys(faults).length == 1
fault.retries = 0
currentRequest.sessionData.faults[channel] = fault
util.log("#{currentRequest.sessionData.deviceId}: Fault response for channel #{channel} (retries: #{fault.retries ? 0})")
return nextRpc(currentRequest)
for p, i in doneProvisions or [] for p, i in doneProvisions or []
delete currentRequest.sessionData.faults[currentRequest.sessionData.channels[i]] delete currentRequest.sessionData.faults[currentRequest.sessionData.channels[i]]
@@ -240,8 +252,26 @@ applyPresets = (currentRequest) ->
nextRpc = (currentRequest) -> nextRpc = (currentRequest) ->
session.rpcRequest(currentRequest.sessionData, null, (err, id, rpcRequest, doneProvisions) -> session.rpcRequest(currentRequest.sessionData, null, (err, faults, id, rpcRequest, doneProvisions) ->
throw err if err throw err if err
if faults?
for channel, fault of faults
if channel.startsWith('_task_')
taskId = channel.slice(6)
for t, j in currentRequest.sessionData.tasks
if t._id == taskId
currentRequest.sessionData.tasks.splice(j, 1)
fault.expiry = t.expiry if t.expiry?
break
if not fault.retries? and Object.keys(faults).length == 1
fault.retries = 0
currentRequest.sessionData.faults[channel] = fault
util.log("#{currentRequest.sessionData.deviceId}: Fault response for channel #{channel} (retries: #{fault.retries ? 0})")
return nextRpc(currentRequest)
if rpcRequest? if rpcRequest?
return sendRpcRequest(currentRequest, id, rpcRequest) return sendRpcRequest(currentRequest, id, rpcRequest)
-26
View File
@@ -1,26 +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 <http://www.gnu.org/licenses/>.
###
process.chdir(process.argv[2])
script = require("#{process.argv[2]}/#{process.argv[3]}")
process.on('message', (message) ->
script[message[1][0]](message[1].slice(1), (err, res) ->
process.send([message[0], err, res])
)
)
+62
View File
@@ -0,0 +1,62 @@
/**
* 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/>.
*/
"use strict";
const jobs = new Set();
const fileName = process.argv[2];
let script;
function errorToFault(err) {
if (!err)
return null;
return {
code: `ext.${err.name}`,
message: err.message,
detail: {
name: err.name,
message: err.message,
stack: err.stack
}
}
}
process.on("uncaughtException", function(err) {
let fault = errorToFault(err);
jobs.forEach(function(jobId) {
process.send([jobId, fault]);
});
jobs.clear();
process.disconnect();
});
process.on("message", function(message) {
jobs.add(message[0]);
if (!script) {
let cwd = process.env["GENIEACS_CONFIG_DIR"] + "/ext";
process.chdir(cwd);
script = require(`${cwd}/${fileName}`);
}
script[message[1][0]](message[1].slice(1), function(err, res) {
if (!jobs.delete(message[0]))
return;
process.send([message[0], errorToFault(err), res]);
});
});
-55
View File
@@ -1,55 +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 <http://www.gnu.org/licenses/>.
###
childProcess = require 'child_process'
crypto = require 'crypto'
path = require 'path'
config = require './config'
TIMEOUT = 5000
processes = {}
queue = {}
messageHandler = (message) ->
func = queue[message[0]]
delete queue[message[0]]
func(message[1], message[2])
run = (args, callback) ->
scriptName = args[0]
if not processes[scriptName]?
cwd = path.resolve(config.get('CONFIG_DIR'), 'ext')
p = childProcess.fork('lib/extension-wrapper', [cwd, scriptName])
p.on('message', messageHandler)
processes[scriptName] = p
id = crypto.randomBytes(8).toString('hex')
queue[id] = callback
setTimeout(() ->
if id of queue
delete queue[id]
return callback(new Error('Extension timeout'))
, TIMEOUT)
processes[scriptName].send([id, args.slice(1)])
exports.run = run
+98
View File
@@ -0,0 +1,98 @@
/**
* 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/>.
*/
"use strict";
const childProcess = require("child_process");
const crypto = require ("crypto");
const config = require("./config");
const TIMEOUT = config.get('EXT_TIMEOUT');
const processes = {};
const jobs = {};
function messageHandler(message) {
let func = jobs[message[0]];
if (func) {
delete jobs[message[0]];
func(null, message[1], message[2]);
}
}
function run(args, callback) {
let scriptName = args[0];
if (!processes[scriptName]) {
let p = childProcess.fork('lib/extension-wrapper', [scriptName]);
p.on('error', function() {
kill(processes[scriptName]);
delete processes[scriptName];
});
p.on('disconnect', function() {
kill(processes[scriptName]);
delete processes[scriptName];
});
p.on('message', messageHandler);
processes[scriptName] = p;
}
let id = crypto.randomBytes(8).toString('hex');
jobs[id] = callback;
setTimeout(function() {
if (id in jobs) {
delete jobs[id];
return callback(null, {code: "timeout", message: "Extension timed out"});
}
}, TIMEOUT);
return processes[scriptName].send([id, args.slice(1)]);
};
function kill(process) {
if (process.signalCode !== null || process.exitCode !== null)
return;
let timeToKill = Date.now() + 5000;
process.kill();
let t = setInterval(function() {
if (process.signalCode !== null || process.exitCode !== null) {
clearInterval(t);
}
else if (Date.now() > timeToKill) {
process.kill('SIGKILL');
clearInterval(t);
}
}, 100);
}
function killAll() {
for (let n in processes)
kill(processes[n]);
}
exports.run = run;
exports.killAll = killAll;
+22 -6
View File
@@ -204,6 +204,22 @@ Object.defineProperty(sandbox.context, 'commit', {value: commit});
Object.defineProperty(sandbox.context, 'ext', {value: ext}); Object.defineProperty(sandbox.context, 'ext', {value: ext});
function errorToFault(err) {
if (!err)
return null;
return {
code: `script.${err.name}`,
message: err.message,
detail: {
name: err.name,
message: err.message,
stack: err.stack
}
}
}
function run(script, globals, timestamp, deviceData, extensionsCache, startRevision, maxRevision, callback) { function run(script, globals, timestamp, deviceData, extensionsCache, startRevision, maxRevision, callback) {
sandbox.timestamp = timestamp; sandbox.timestamp = timestamp;
sandbox.deviceData = deviceData; sandbox.deviceData = deviceData;
@@ -233,7 +249,7 @@ function run(script, globals, timestamp, deviceData, extensionsCache, startRevis
else if (err === EXT) else if (err === EXT)
status = 2; status = 2;
else else
return callback(err); return callback(null, errorToFault(err));
} }
let ext = sandbox.extensions; let ext = sandbox.extensions;
@@ -245,17 +261,17 @@ function run(script, globals, timestamp, deviceData, extensionsCache, startRevis
let counter = 1; let counter = 1;
for (let key in sandbox.extensions) { for (let key in sandbox.extensions) {
++ counter; ++ counter;
extensions.run(sandbox.extensions[key], function(err, res) { extensions.run(sandbox.extensions[key], function(err, fault, res) {
if (err && counter > 0) { if ((err || fault) && counter > 0) {
counter = 0; counter = 0;
return callback(err); return callback(err, fault);
} }
extCache[key] = res; extCache[key] = res;
if (-- counter === 0) if (-- counter === 0)
if (status === 2) if (status === 2)
return run.apply(null, args); return run.apply(null, args);
else else
return callback(null, clr, decs, status === 0, ret); return callback(null, null, clr, decs, status === 0, ret);
}); });
} }
@@ -263,7 +279,7 @@ function run(script, globals, timestamp, deviceData, extensionsCache, startRevis
if (status === 2) if (status === 2)
return run.apply(null, args); return run.apply(null, args);
else else
return callback(null, clr, decs, status === 0, ret); return callback(null, null, clr, decs, status === 0, ret);
} }
+38 -15
View File
@@ -21,7 +21,6 @@ db = require './db'
device = require './device' device = require './device'
sandbox = require './sandbox' sandbox = require './sandbox'
cache = require './cache' cache = require './cache'
extensions = require './extensions'
PathSet = require './path-set' PathSet = require './path-set'
VersionedMap = require './versioned-map' VersionedMap = require './versioned-map'
InstanceSet = require './instance-set' InstanceSet = require './instance-set'
@@ -379,12 +378,12 @@ runProvisions = (sessionData, provisions, startRevision, endRevision, callback)
sandbox.run(sessionData.cache.provisions[provision[0]].script, sandbox.run(sessionData.cache.provisions[provision[0]].script,
{args: provision[1]}, sessionData.timestamp, sessionData.deviceData, {args: provision[1]}, sessionData.timestamp, sessionData.deviceData,
sessionData.extensionsCache, startRevision, endRevision, sessionData.extensionsCache, startRevision, endRevision,
(err, _clear, _declarations, _done) -> (err, _fault, _clear, _declarations, _done) ->
-- counter -- counter
if err if err or _fault
if counter >= 0 if counter >= 0
counter = 0 counter = 0
return callback(err) return callback(err, _fault)
return return
done &&= _done done &&= _done
@@ -396,11 +395,11 @@ runProvisions = (sessionData, provisions, startRevision, endRevision, callback)
allClear = allClear.concat(_clear) allClear = allClear.concat(_clear)
if counter == 0 if counter == 0
return callback(null, done, allDeclarations, allClear) return callback(null, null, done, allDeclarations, allClear)
) )
if -- counter == 0 if -- counter == 0
return callback(null, done, allDeclarations, allClear) return callback(null, null, done, allDeclarations, allClear)
runVirtualParameters = (sessionData, provisions, startRevision, endRevision, callback) -> runVirtualParameters = (sessionData, provisions, startRevision, endRevision, callback) ->
@@ -415,12 +414,12 @@ runVirtualParameters = (sessionData, provisions, startRevision, endRevision, cal
sandbox.run(sessionData.cache.virtualParameters[provision[0]].script, globals, sandbox.run(sessionData.cache.virtualParameters[provision[0]].script, globals,
sessionData.timestamp, sessionData.deviceData, sessionData.timestamp, sessionData.deviceData,
sessionData.extensionsCache, startRevision, endRevision, sessionData.extensionsCache, startRevision, endRevision,
(err, _clear, _declarations, _done, _returnValue) -> (err, _fault, _clear, _declarations, _done, _returnValue) ->
-- counter -- counter
if err if err or _fault
if counter >= 0 if counter >= 0
counter = 0 counter = 0
return callback(err) return callback(err, _fault)
return return
done &&= _done done &&= _done
@@ -441,7 +440,7 @@ runVirtualParameters = (sessionData, provisions, startRevision, endRevision, cal
toClear = commitVirtualParameter(sessionData, provisions[i], vpu, toClear) toClear = commitVirtualParameter(sessionData, provisions[i], vpu, toClear)
clear(sessionData, toClear, (err) -> clear(sessionData, toClear, (err) ->
return callback(err, done, allDeclarations, allClear) return callback(err, null, done, allDeclarations, allClear)
) )
) )
@@ -452,7 +451,7 @@ runVirtualParameters = (sessionData, provisions, startRevision, endRevision, cal
toClear = commitVirtualParameter(sessionData, provisions[i], vpu, toClear) toClear = commitVirtualParameter(sessionData, provisions[i], vpu, toClear)
clear(sessionData, toClear, (err) -> clear(sessionData, toClear, (err) ->
return callback(err, done, allDeclarations, allClear) return callback(err, null, done, allDeclarations, allClear)
) )
@@ -586,7 +585,7 @@ runDeclarations = (sessionData, declarations) ->
rpcRequest = (sessionData, _declarations, callback) -> rpcRequest = (sessionData, _declarations, callback) ->
if sessionData.rpcRequest? if sessionData.rpcRequest?
return callback(null, generateRpcId(sessionData), sessionData.rpcRequest) return callback(null, null, generateRpcId(sessionData), sessionData.rpcRequest)
if sessionData.declarations.length < sessionData.provisions.length if sessionData.declarations.length < sessionData.provisions.length
inception = sessionData.declarations.length inception = sessionData.declarations.length
@@ -595,9 +594,33 @@ rpcRequest = (sessionData, _declarations, callback) ->
sessionData.deviceData.attributes.revision = revision sessionData.deviceData.attributes.revision = revision
run = if inception == 0 then runProvisions else runVirtualParameters run = if inception == 0 then runProvisions else runVirtualParameters
return run(sessionData, sessionData.provisions[inception], sessionData.revisions[inception - 1] ? 0, sessionData.revisions[inception], (err, done, decs, toClear) -> return run(sessionData, sessionData.provisions[inception], sessionData.revisions[inception - 1] ? 0, sessionData.revisions[inception], (err, fault, done, decs, toClear) ->
return callback(err) if err return callback(err) if err
if fault
faults = {}
for p, i in sessionData.provisions[0]
channel = sessionData.channels[i]
f = faults[channel]
if not f?
f = {
provisions: []
timestamp: sessionData.timestamp
code: fault.code
message: fault.message
detail: fault.detail
}
if sessionData.faults[channel]?
f.retries = (sessionData.faults[channel].retries or 0) + 1
faults[channel] = f
f.provisions.push(p)
clearProvisions(sessionData)
return callback(null, faults)
sessionData.declarations.push(decs) sessionData.declarations.push(decs)
sessionData.doneProvisions |= 1 << inception if done or not decs.length sessionData.doneProvisions |= 1 << inception if done or not decs.length
@@ -777,7 +800,7 @@ rpcRequest = (sessionData, _declarations, callback) ->
return rpcRequest(sessionData, null, callback) return rpcRequest(sessionData, null, callback)
if sessionData.rpcRequest if sessionData.rpcRequest
return callback(null, generateRpcId(sessionData), sessionData.rpcRequest) return callback(null, null, generateRpcId(sessionData), sessionData.rpcRequest)
++ sessionData.revisions[inception] ++ sessionData.revisions[inception]
sessionData.declarations.pop() sessionData.declarations.pop()
@@ -791,7 +814,7 @@ rpcRequest = (sessionData, _declarations, callback) ->
sessionData.deviceData.timestamps.collapse(1) sessionData.deviceData.timestamps.collapse(1)
sessionData.deviceData.attributes.collapse(1) sessionData.deviceData.attributes.collapse(1)
sessionData.extensionsCache = {} sessionData.extensionsCache = {}
return callback(null, null, null, doneProvisions) return callback(null, null, null, null, doneProvisions)
rev = sessionData.revisions[sessionData.revisions.length - 1] rev = sessionData.revisions[sessionData.revisions.length - 1]
sessionData.deviceData.timestamps.collapse(rev + 1) sessionData.deviceData.timestamps.collapse(rev + 1)