From bd59398cab675f9d750b734f95294e986fbbb92d Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Wed, 27 Apr 2016 22:32:58 +0100 Subject: [PATCH] Add optional timeout to exec node (both exec and spawn modes) and add test for it (both exec and spawn) also extra test for trigger node. --- nodes/core/core/75-exec.html | 7 +- nodes/core/core/75-exec.js | 89 ++++++++++++++----------- nodes/core/locales/en-US/messages.json | 6 +- test/nodes/core/core/75-exec_spec.js | 54 +++++++++++++++ test/nodes/core/core/89-trigger_spec.js | 29 ++++++++ 5 files changed, 142 insertions(+), 43 deletions(-) diff --git a/nodes/core/core/75-exec.html b/nodes/core/core/75-exec.html index 735ed984e..02c9b0f5f 100644 --- a/nodes/core/core/75-exec.html +++ b/nodes/core/core/75-exec.html @@ -31,7 +31,11 @@
- + +
+
+ + seconds
@@ -59,6 +63,7 @@ addpay: {value:true}, append: {value:""}, useSpawn: {value:""}, + timer: {value:""}, name: {value:""} }, inputs:1, diff --git a/nodes/core/core/75-exec.js b/nodes/core/core/75-exec.js index 97a1a3b63..da03bf61a 100644 --- a/nodes/core/core/75-exec.js +++ b/nodes/core/core/75-exec.js @@ -27,55 +27,64 @@ module.exports = function(RED) { this.addpay = n.addpay; this.append = (n.append || "").trim(); this.useSpawn = n.useSpawn; + this.timer = Number(n.timer || 0)*1000; this.activeProcesses = {}; + var cleanup = function(p) { + //console.log("CLEANUP!!!",p); + node.activeProcesses[p].kill(); + node.status({fill:"red",shape:"dot",text:"timeout"}); + node.error("Exec node timeout"); + } + var node = this; this.on("input", function(msg) { + var child; node.status({fill:"blue",shape:"dot",text:" "}); if (this.useSpawn === true) { // make the extra args into an array // then prepend with the msg.payload - var arg = node.cmd; - if (node.addpay) { - arg += " "+msg.payload; - } + if (node.addpay) { arg += " "+msg.payload; } arg += " "+node.append; // slice whole line by spaces (trying to honour quotes); arg = arg.match(/(?:[^\s"]+|"[^"]*")+/g); var cmd = arg.shift(); /* istanbul ignore else */ if (RED.settings.verbose) { node.log(cmd+" ["+arg+"]"); } - if (cmd.indexOf(" ") == -1) { - var ex = spawn(cmd,arg); - node.activeProcesses[ex.pid] = ex; - ex.stdout.on('data', function (data) { - //console.log('[exec] stdout: ' + data); - if (isUtf8(data)) { msg.payload = data.toString(); } - else { msg.payload = data; } - node.send([msg,null,null]); - }); - ex.stderr.on('data', function (data) { - //console.log('[exec] stderr: ' + data); - if (isUtf8(data)) { msg.payload = data.toString(); } - else { msg.payload = new Buffer(data); } - node.send([null,msg,null]); - }); - ex.on('close', function (code) { - //console.log('[exec] result: ' + code); - delete node.activeProcesses[ex.pid]; - msg.payload = code; - if (code === 0) { node.status({}); } - else if (code < 0) { node.status({fill:"red",shape:"dot",text:"rc: "+code}); } - else { node.status({fill:"yellow",shape:"dot",text:"rc: "+code}); } - node.send([null,null,msg]); - }); - ex.on('error', function (code) { - delete node.activeProcesses[ex.pid]; - node.error(code,msg); - }); + child = spawn(cmd,arg); + if (node.timer !== 0) { + child.tout = setTimeout(function() { cleanup(child.pid); }, node.timer); } - else { node.error(RED._("exec.spawnerr")); } + node.activeProcesses[child.pid] = child; + child.stdout.on('data', function (data) { + //console.log('[exec] stdout: ' + data); + if (isUtf8(data)) { msg.payload = data.toString(); } + else { msg.payload = data; } + node.send([msg,null,null]); + }); + child.stderr.on('data', function (data) { + //console.log('[exec] stderr: ' + data); + if (isUtf8(data)) { msg.payload = data.toString(); } + else { msg.payload = new Buffer(data); } + node.send([null,msg,null]); + }); + child.on('close', function (code) { + //console.log('[exec] result: ' + code); + delete node.activeProcesses[child.pid]; + if (child.tout) { clearTimeout(child.tout); } + msg.payload = code; + if (code === 0) { node.status({}); } + if (code === null) { node.status({fill:"red",shape:"dot",text:"timeout"}); } + else if (code < 0) { node.status({fill:"red",shape:"dot",text:"rc: "+code}); } + else { node.status({fill:"yellow",shape:"dot",text:"rc: "+code}); } + node.send([null,null,msg]); + }); + child.on('error', function (code) { + delete node.activeProcesses[child.pid]; + if (child.tout) { clearTimeout(child.tout); } + node.error(code,msg); + }); } else { var cl = node.cmd; @@ -83,13 +92,9 @@ module.exports = function(RED) { if (node.append.trim() !== "") { cl += " "+node.append; } /* istanbul ignore else */ if (RED.settings.verbose) { node.log(cl); } - var child = exec(cl, {encoding: 'binary', maxBuffer:10000000}, function (error, stdout, stderr) { + child = exec(cl, {encoding: 'binary', maxBuffer:10000000}, function (error, stdout, stderr) { msg.payload = new Buffer(stdout,"binary"); - try { - if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); } - } catch(e) { - node.log(RED._("exec.badstdout")); - } + if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); } var msg2 = {payload:stderr}; var msg3 = null; //console.log('[exec] stdout: ' + stdout); @@ -100,9 +105,13 @@ module.exports = function(RED) { } node.status({}); node.send([msg,msg2,msg3]); + if (child.tout) { clearTimeout(child.tout); } delete node.activeProcesses[child.pid]; }); child.on('error',function() {}); + if (node.timer !== 0) { + child.tout = setTimeout(function() { cleanup(child.pid); }, node.timer); + } node.activeProcesses[child.pid] = child; } }); @@ -110,10 +119,12 @@ module.exports = function(RED) { for (var pid in node.activeProcesses) { /* istanbul ignore else */ if (node.activeProcesses.hasOwnProperty(pid)) { + if (node.activeProcesses[pid].tout) { clearTimeout(node.activeProcesses[pid].tout); } node.activeProcesses[pid].kill(); } } node.activeProcesses = {}; + node.status({}); }); } RED.nodes.registerType("exec",ExecNode); diff --git a/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json index 1847c382a..bfb930f0e 100644 --- a/nodes/core/locales/en-US/messages.json +++ b/nodes/core/locales/en-US/messages.json @@ -124,11 +124,11 @@ } }, "exec": { - "spawnerr": "Spawn command must be just the command - no spaces or extra parameters", - "badstdout": "Bad STDOUT", "label": { "command": "Command", - "append": "Append" + "append": "Append", + "timeout": "Timeout", + "timeoutplace": "optional" }, "placeholder": { "extraparams": "extra input parameters" diff --git a/test/nodes/core/core/75-exec_spec.js b/test/nodes/core/core/75-exec_spec.js index 9572a99c4..ca1fc3edb 100644 --- a/test/nodes/core/core/75-exec_spec.js +++ b/test/nodes/core/core/75-exec_spec.js @@ -41,6 +41,7 @@ describe('exec node', function() { n1.should.have.property("cmd", ""); n1.should.have.property("append", ""); n1.should.have.property("addpay",true); + n1.should.have.property("timer",0); done(); }); }); @@ -139,6 +140,33 @@ describe('exec node', function() { n1.receive({}); }); }); + + it('should be able to timeout a long running command', function(done) { + var flow = [{id:"n1",type:"exec",wires:[["n2"],["n3"],["n4"]],command:"sleep", addpay:false, append:"1", timer:"0.3"}, + {id:"n2", type:"helper"},{id:"n3", type:"helper"},{id:"n4", type:"helper"}]; + + helper.load(execNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var n3 = helper.getNode("n3"); + var n4 = helper.getNode("n4"); + n4.on("input", function(msg) { + msg.should.have.property("payload"); + msg.payload.should.have.property("killed",true); + //done(); + }); + setTimeout(function() { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "exec"; + }); + logEvents.should.have.length(2); + logEvents[1][0].should.have.a.property('msg'); + logEvents[1][0].msg.toString().should.startWith("Exec node timeout"); + done(); + },400); + n1.receive({}); + }); + }); }); describe('calling spawn', function() { @@ -267,5 +295,31 @@ describe('exec node', function() { }); }); + it('should be able to timeout a long running command', function(done) { + var flow = [{id:"n1",type:"exec",wires:[["n2"],["n3"],["n4"]],command:"sleep", addpay:false, append:"1", timer:"0.3", useSpawn:true}, + {id:"n2", type:"helper"},{id:"n3", type:"helper"},{id:"n4", type:"helper"}]; + + helper.load(execNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var n3 = helper.getNode("n3"); + var n4 = helper.getNode("n4"); + n4.on("input", function(msg) { + msg.should.have.property("payload",null); + //done(); + }); + setTimeout(function() { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "exec"; + }); + logEvents.should.have.length(2); + logEvents[1][0].should.have.a.property('msg'); + logEvents[1][0].msg.toString().should.startWith("Exec node timeout"); + done(); + },400); + n1.receive({}); + }); + }); + }); }); diff --git a/test/nodes/core/core/89-trigger_spec.js b/test/nodes/core/core/89-trigger_spec.js index 3e1db2c9b..a333086ca 100644 --- a/test/nodes/core/core/89-trigger_spec.js +++ b/test/nodes/core/core/89-trigger_spec.js @@ -264,6 +264,35 @@ describe('trigger Node', function() { }); }); + it('should be able output the 2nd payload', function(done) { + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"false", op1type:"nul", op2type:"payl", op1:"false", op2:"true", duration:200, wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var c = 0; + n2.on("input", function(msg) { + if (c === 0) { + msg.should.have.a.property("payload", "Goodbye"); + c += 1; + } + else { + msg.should.have.a.property("payload", "World"); + (Date.now() - ss).should.be.greaterThan(380); + done(); + } + }); + var ss = Date.now(); + n1.emit("input", {payload:"Hello"}); + setTimeout( function() { + n1.emit("input", {payload:"Goodbye"}); + },100); + setTimeout( function() { + n1.emit("input", {payload:"World"}); + },400); + }); + }); + it('should be able to apply mustache templates to payloads', function(done) { var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"val", op2type:"val", op1:"{{payload}}", op2:"{{topic}}", duration:50, wires:[["n2"]] }, {id:"n2", type:"helper"} ];