diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index 0120d8c92..b5fcb14d4 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -14,6 +14,8 @@ * limitations under the License. **/ +const { log } = require("console"); + module.exports = function(RED) { "use strict"; @@ -399,6 +401,8 @@ module.exports = function(RED) { if(node.timeout>0){ finOpt.timeout = node.timeout; finOpt.breakOnSigint = true; + } else if(RED.settings.defaultFunctionTimeout > 0){ + finOpt.timeout = RED.settings.defaultFunctionTimeout * 1000 } } var promise = Promise.resolve(); @@ -415,8 +419,15 @@ module.exports = function(RED) { var opts = {}; if (node.timeout>0){ opts = node.timeoutOptions; + } else if(RED.settings.defaultFunctionTimeout > 0){ + opts.timeout = RED.settings.defaultFunctionTimeout * 1000 + } + try { + node.script.runInContext(context,opts); + } catch (err) { + node.error(err); + return done(err); } - node.script.runInContext(context,opts); context.results.then(function(results) { sendResults(node,send,msg._msgid,results,false); if (handleNodeDoneCall) { diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index e8bb01228..fdacb7102 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -473,6 +473,7 @@ module.exports = { * - fileWorkingDirectory * - functionGlobalContext * - functionExternalModules + * - defaultFunctionTimeout * - functionTimeout * - nodeMessageBufferMaxLength * - ui (for use with Node-RED Dashboard) @@ -499,8 +500,30 @@ module.exports = { /** Allow the Function node to load additional npm modules directly */ functionExternalModules: true, + + /** + * Default function timeout (in seconds) for the Function node. + * A value of 0 indicates no timeout is applied, meaning the function can run indefinitely. + * + * The default function timeout is designed to prevent blocking code in function nodes, + * which could otherwise lead to a stalled or unresponsive main thread. For example, + * the following code would block the event loop indefinitely: + * + * `while(1) {}` + * + * By specifying a `defaultFunctionTimeout`, such scenarios can be mitigated, + * ensuring that long-running or infinite loops are terminated automatically after + * the specified timeout duration. + * + * Note: If both `defaultFunctionTimeout` and `functionTimeout` are defined in the + * settings file, `functionTimeout` takes precedence, providing a more granular + * control for individual function nodes. + */ + + defaultFunctionTimeout: 5, + /** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */ - functionTimeout: 0, + functionTimeout: 2, /** The following property can be used to set predefined values in Global Context. * This allows extra node modules to be made available with in Function node. diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js index 56c4ec976..3f7eedafd 100644 --- a/test/nodes/core/function/10-function_spec.js +++ b/test/nodes/core/function/10-function_spec.js @@ -1437,7 +1437,7 @@ describe('function node', function() { var logEvents = helper.log().args.filter(function(evt) { return evt[0].type == "function"; }); - logEvents.should.have.length(1); + logEvents.should.have.length(2); var msg = logEvents[0][0]; msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', 'n1'); @@ -1451,7 +1451,7 @@ describe('function node', function() { }); }); - it('check if default function timeout settings are recognized', function (done) { + it('check if function timeout settings are recognized', function (done) { RED.settings.functionTimeout = 0.01; var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; helper.load(functionNode, flow, function () { @@ -1463,7 +1463,7 @@ describe('function node', function() { var logEvents = helper.log().args.filter(function (evt) { return evt[0].type == "function"; }); - logEvents.should.have.length(1); + logEvents.should.have.length(2); var msg = logEvents[0][0]; msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', 'n1'); @@ -1479,6 +1479,65 @@ describe('function node', function() { }); }); + it('check if default function timeout settings are recognized', function (done) { + RED.settings.defaultFunctionTimeout = 0.01; + var flow = [{id: "n1",type: "function",wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; + helper.load(functionNode, flow, function () { + var n1 = helper.getNode("n1"); + n1.receive({ payload: "foo", topic: "bar" }); + setTimeout(function () { + try { + helper.log().called.should.be.true(); + var logEvents = helper.log().args.filter(function (evt) { + return evt[0].type == "function"; + }); + logEvents.should.have.length(2); + var msg = logEvents[0][0]; + msg.should.have.property('level', helper.log().ERROR); + msg.should.have.property('id', 'n1'); + msg.should.have.property('type', 'function'); + should.equal(RED.settings.defaultFunctionTimeout, 0.01); + should.equal(msg.msg.message, 'Script execution timed out after 10ms'); + delete RED.settings.defaultFunctionTimeout; + done(); + } catch (err) { + done(err); + } + }, 500); + }); + }); + + it('check if functionTimeout has higher precedence over default function timeout setting', function (done) { + RED.settings.defaultFunctionTimeout = 0.02; + RED.settings.functionTimeout = 0.01; + var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; + helper.load(functionNode, flow, function () { + var n1 = helper.getNode("n1"); + n1.receive({ payload: "foo", topic: "bar" }); + setTimeout(function () { + try { + helper.log().called.should.be.true(); + var logEvents = helper.log().args.filter(function (evt) { + return evt[0].type == "function"; + }); + logEvents.should.have.length(2); + var msg = logEvents[0][0]; + msg.should.have.property('level', helper.log().ERROR); + msg.should.have.property('id', 'n1'); + msg.should.have.property('type', 'function'); + should.equal(RED.settings.functionTimeout, 0.01); + should.equal(RED.settings.defaultFunctionTimeout, 0.02); + should.equal(msg.msg.message, 'Script execution timed out after 10ms'); + delete RED.settings.functionTimeout; + delete RED.settings.defaultFunctionTimeout; + done(); + } catch (err) { + done(err); + } + }, 500); + }); + }); + describe("finalize function", function() { it('should execute', function(done) {