diff --git a/.gitignore b/.gitignore index 547beb4..f316df5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ -/node_modules +node_modules +modules +package-lock.json +.vscode *.log diff --git a/README.md b/README.md index 014b66a..19870db 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,21 @@ so if your code worked on a standard function node it should also work on `unsaf (like calling `require('fs')` and its operations). ## Additional Features -`nodeRedContribUnsafeFunctionAsyncSend`, `nodeRedContribUnsafeFunctionAsyncReceive` - +`nodeRedContribUnsafeFunctionAsyncSend`, `nodeRedContribUnsafeFunctionAsyncReceive` - Set these RED settings variables in order to make the sending\receiving of messages asynchronous. The current behavior of node-red is that sending a message pauses until all the following nodes in the flow finish handling the sent message (unless they specifically do it asynchronously). -For more information, see [issue 833](https://github.com/node-red/node-red/issues/833). +For more information, see [issue 833](https://github.com/node-red/node-red/issues/833). -`nodeRedContribUnsafeFunctionProfiling`: Set this RED settings variable to show +`nodeRedContribUnsafeFunctionProfiling`: Set this RED settings variable to show for each node a status message with basic profiling information: how many messages -were handled and what's the total\max execution time. +were handled and what's the total\max execution time. + +## Change Log +### From 0.4.0 to 1.0.0 +https://github.com/ozomer/node-red-contrib-unsafe-function/issues/10#issue-852665839 +* Updating package dependency `require-from-string` to version 2.0.2. +* Added package dependency `mkdirp` version 1.0.4. ## Change Log ### From 0.3.0 to 0.4.0 diff --git a/package.json b/package.json index 8b6c008..fe82fc0 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "node-red-contrib-unsafe-function", - "version": "0.8.0", + "version": "1.0.0", "description": "The fast and furious version of function nodes", "dependencies": { - "require-from-string": "1.2.1" + "mkdirp": "^1.0.4", + "require-from-string": "^2.0.2" }, "repository": { "type": "git", @@ -13,6 +14,13 @@ "name": "Oren Zomer", "email": "oren.zomer@gmail.com" }, + "contributors": [ + { + "name": "Daniele", + "email": "hanc2006@gmail.com", + "url": "https://github.com/hanc2006" + } + ], "license": "Apache-2.0", "keywords": [ "node-red", @@ -27,5 +35,8 @@ "undef": true, "unused": true, "node": true + }, + "scripts": { + "postinstall": "mkdirp ./modules" } } diff --git a/unsafe-function.html b/unsafe-function.html index bcf27d2..7b63b2d 100644 --- a/unsafe-function.html +++ b/unsafe-function.html @@ -15,179 +15,561 @@ See the License for the specific language governing permissions and limitations under the License. --> + + + + - - - diff --git a/unsafe-function.js b/unsafe-function.js index 8c6c011..3398921 100644 --- a/unsafe-function.js +++ b/unsafe-function.js @@ -15,344 +15,453 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ +const RED2 = require.main.require('node-red'); -module.exports = function(RED) { - "use strict"; - var util = require("util"); - var requireFromString = require('require-from-string'); +module.exports = function (RED) { + 'use strict'; + var util = require('util'); + var requireFromString = require('require-from-string'); + var fs = require('fs'); + var path = require('path'); + var Module = require('module'); + var { spawnSync } = require('child_process'); - function sendResults(node,_msgid,msgs) { - if (msgs === null) { - return; - } else if (!Array.isArray(msgs)) { - msgs = [msgs]; + function sendResults(node, send, _msgid, msgs, cloneFirstMessage) { + if (msgs == null) { + return; + } else if (!Array.isArray(msgs)) { + msgs = [msgs]; + } + var msgCount = 0; + for (var m = 0; m < msgs.length; m++) { + if (msgs[m]) { + if (!Array.isArray(msgs[m])) { + msgs[m] = [msgs[m]]; } - var msgCount = 0; - for (var m=0;m 0) { + send(msgs); + } + } + + function FunctionNode(n) { + RED.nodes.createNode(this, n); + var node = this; + node.name = n.name; + node.func = n.func; + + var functionText = + 'module.exports = function(console, util, Buffer, Date, RED, __node__, context, flow, global, env, setTimeout, clearTimeout, setInterval, clearInterval) { ' + + ' return function(msg, __send__, __done__) { ' + + ' var __msgid__ = msg._msgid;' + + ' var node = {' + + ' id:__node__.id,' + + ' type:__node__.type,' + + ' name:__node__.name,' + + ' log:__node__.log,' + + ' error:__node__.error,' + + ' warn:__node__.warn,' + + ' debug:__node__.debug,' + + ' trace:__node__.trace,' + + ' on:__node__.on,' + + ' status:__node__.status,' + + ' send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},' + + ' done:__done__' + + ' };\n' + + this.func + + '\n' + + ' }' + + '};'; + + node.topic = n.topic; + node.script = ''; + node.outstandingTimers = []; + node.outstandingIntervals = []; + node.clearStatus = false; + + var sandbox = { + console: console, + util: util, + Buffer: Buffer, + Date: Date, + RED: { + util: RED.util, + // NODE-RED runtime api available in function node + // https://nodered.org/docs/api/modules/v/1.0/@node-red_runtime.html + api: RED2.nodes, + }, + __node__: { + id: node.id, + name: node.name, + type: node.type, + log: function () { + node.log.apply(node, arguments); + }, + error: function () { + node.error.apply(node, arguments); + }, + warn: function () { + node.warn.apply(node, arguments); + }, + debug: function () { + node.debug.apply(node, arguments); + }, + trace: function () { + node.trace.apply(node, arguments); + }, + send: function (send, id, msgs, cloneMsg) { + sendResults(node, send, id, msgs, cloneMsg); + }, + on: function () { + if (arguments[0] === 'input') { + throw new Error(RED._('function.error.inputListener')); + } + node.on.apply(node, arguments); + }, + status: function () { + node.clearStatus = true; + node.status.apply(node, arguments); + }, + }, + context: { + set: function () { + node.context().set.apply(node, arguments); + }, + get: function () { + return node.context().get.apply(node, arguments); + }, + keys: function () { + return node.context().keys.apply(node, arguments); + }, + get global() { + return node.context().global; + }, + get flow() { + return node.context().flow; + }, + }, + flow: { + info: function () { + var id = RED.workspaces.active(); + var flow = RED.nodes.workspace(id); + if (!flow) { + // this is probably a subflow + flow = RED.nodes.subflow(id); + } + return flow; + }, + set: function () { + node.context().flow.set.apply(node, arguments); + }, + get: function () { + return node.context().flow.get.apply(node, arguments); + }, + keys: function () { + return node.context().flow.keys.apply(node, arguments); + }, + }, + global: { + set: function () { + node.context().global.set.apply(node, arguments); + }, + get: function () { + return node.context().global.get.apply(node, arguments); + }, + keys: function () { + return node.context().global.keys.apply(node, arguments); + }, + }, + env: { + get: function (envVar) { + var flow = node._flow; + return flow.getSetting(envVar); + }, + }, + setTimeout: function () { + var func = arguments[0]; + var timerId; + arguments[0] = function () { + sandbox.clearTimeout(timerId); + try { + func.apply(node, arguments); + } catch (err) { + node.error(err, {}); + } + }; + timerId = setTimeout.apply(node, arguments); + node.outstandingTimers.push(timerId); + return timerId; + }, + clearTimeout: function (id) { + clearTimeout(id); + var index = node.outstandingTimers.indexOf(id); + if (index > -1) { + node.outstandingTimers.splice(index, 1); } - if (msgCount <= 0) { - return; + }, + setInterval: function () { + var func = arguments[0]; + var timerId; + arguments[0] = function () { + try { + func.apply(node, arguments); + } catch (err) { + node.error(err, {}); + } + }; + timerId = setInterval.apply(node, arguments); + node.outstandingIntervals.push(timerId); + return timerId; + }, + clearInterval: function (id) { + clearInterval(id); + var index = node.outstandingIntervals.indexOf(id); + if (index > -1) { + node.outstandingIntervals.splice(index, 1); } - if (RED.settings.nodeRedContribUnsafeFunctionAsyncSend) { - // Create empty array of the same length. - var emptyArray = msgs.map(function() { - return null; - }); - msgs.forEach(function(wireMessages, wireIndex) { - if (!wireMessages) { - return; - } - (Array.isArray(wireMessages)?wireMessages:[wireMessages]).forEach(function(msg) { - // Fill with a single message. - var arr = emptyArray.slice(); - arr[wireIndex] = [msg]; - setImmediate(function() { - try { - node.send(arr); - } catch(err) { - var line = 0; - var errorMessage; - var stack = err.stack.split(/\r?\n/); - if (stack.length > 0) { - while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) { - line++; - } + }, + }; - if (line < stack.length) { - errorMessage = stack[line]; - var m = /:(\d+):(\d+)$/.exec(stack[line+1]); - if (m) { - var lineno = Number(m[1])-1; - var cha = m[2]; - errorMessage += " (line "+lineno+", col "+cha+")"; - } - } - } - if (!errorMessage) { - errorMessage = err.toString(); - } - node.error(errorMessage, msg); - } - }); - }); - }); - } else { - node.send(msgs); + node.script = requireFromString(functionText, '', { + prependPaths: [path.join(__dirname, 'modules')], + })( + sandbox.console, + sandbox.util, + sandbox.Buffer, + sandbox.Date, + sandbox.RED, + sandbox.__node__, + sandbox.context, + sandbox.flow, + sandbox.global, + sandbox.env, + sandbox.setTimeout, + sandbox.clearTimeout, + sandbox.setInterval, + sandbox.clearInterval + ); + + function setError(msg, done, err) { + if (typeof err === 'object' && err.hasOwnProperty('stack')) { + //remove unwanted part + var index = err.stack.search( + /\n\s*at ContextifyScript.Script.runInContext/ + ); + err.stack = err.stack + .slice(0, index) + .split('\n') + .slice(0, -1) + .join('\n'); + var stack = err.stack.split(/\r?\n/); + + //store the error in msg to be used in flows + msg.error = err; + + var line = 0; + var errorMessage; + if (stack.length > 0) { + while ( + line < stack.length && + stack[line].indexOf('ReferenceError') !== 0 + ) { + line++; + } + + if (line < stack.length) { + errorMessage = stack[line]; + var m = /:(\d+):(\d+)$/.exec(stack[line + 1]); + if (m) { + var lineno = Number(m[1]) - 1; + var cha = m[2]; + errorMessage += ' (line ' + lineno + ', col ' + cha + ')'; + } + } + } + if (!errorMessage) { + errorMessage = err.toString(); } + done(errorMessage); + } else if (typeof err === 'string') { + done(err); + } else { + done(JSON.stringify(err)); + } } - function FunctionNode(n) { - RED.nodes.createNode(this,n); - var node = this; - this.name = n.name; - this.func = n.func; - var functionText = "module.exports = function(util, RED, __node__, context, flow, global, env, setTimeout, clearTimeout, setInterval, clearInterval) { " + - " return function(msg) { " + - " var __msgid__ = msg._msgid;" + - " var node = {" + - " id:__node__.id," + - " name:__node__.name," + - " log:__node__.log," + - " error:__node__.error," + - " warn:__node__.warn," + - " debug:__node__.debug," + - " trace:__node__.trace," + - " on:__node__.on," + - " status:__node__.status," + - " send:function(msgs) { __node__.send(__msgid__,msgs);}" + - " };\n" + - this.func + "\n" + - " };" + - "};"; + function getModules(dir) { + var modules = []; + fs.readdirSync(dir).forEach((file) => { + modules.push({ + label: file, + sublabel: 'file', + icon: 'fa fa-file-text', + checkbox: false, + }); + }); + return modules; + } - this.topic = n.topic; - this.outstandingTimers = []; - this.outstandingIntervals = []; - var sandbox = { - util: util, - RED: { - util: RED.util - }, - __node__: { - id: node.id, - name: node.name, - log: function() { - node.log.apply(node, arguments); - }, - error: function() { - node.error.apply(node, arguments); - }, - warn: function() { - node.warn.apply(node, arguments); - }, - debug: function() { - node.debug.apply(node, arguments); - }, - trace: function() { - node.trace.apply(node, arguments); - }, - send: function(id, msgs) { - sendResults(node, id, msgs); - }, - on: function() { - if (arguments[0] === "input") { - throw new Error(RED._("function.error.inputListener")); - } - node.on.apply(node, arguments); - }, - status: function() { - node.status.apply(node, arguments); - } - }, - context: { - set: function() { - node.context().set.apply(node,arguments); - }, - get: function() { - return node.context().get.apply(node,arguments); - }, - keys: function() { - return node.context().keys.apply(node,arguments); - }, - get global() { - return node.context().global; - }, - get flow() { - return node.context().flow; - } - }, - flow: { - set: function() { - node.context().flow.set.apply(node,arguments); - }, - get: function() { - return node.context().flow.get.apply(node,arguments); - }, - keys: function() { - return node.context().flow.keys.apply(node,arguments); - } - }, - global: { - set: function() { - node.context().global.set.apply(node,arguments); - }, - get: function() { - return node.context().global.get.apply(node,arguments); - }, - keys: function() { - return node.context().global.keys.apply(node,arguments); - } - }, - env: { - get: function(envVar) { - var flow = node._flow; - return flow.getSetting(envVar); - } - }, - setTimeout: function () { - var func = arguments[0]; - var timerId; - arguments[0] = function() { - sandbox.clearTimeout(timerId); - try { - func.apply(this,arguments); - } catch(err) { - node.error(err,{}); - } - }; - timerId = setTimeout.apply(this,arguments); - node.outstandingTimers.push(timerId); - return timerId; - }, - clearTimeout: function(id) { - clearTimeout(id); - var index = node.outstandingTimers.indexOf(id); - if (index > -1) { - node.outstandingTimers.splice(index,1); - } - }, - setInterval: function() { - var func = arguments[0]; - var timerId; - arguments[0] = function() { - try { - func.apply(this,arguments); - } catch(err) { - node.error(err,{}); - } - }; - timerId = setInterval.apply(this,arguments); - node.outstandingIntervals.push(timerId); - return timerId; - }, - clearInterval: function(id) { - clearInterval(id); - var index = node.outstandingIntervals.indexOf(id); - if (index > -1) { - node.outstandingIntervals.splice(index,1); - } - } - }; - if (util.hasOwnProperty('promisify')) { - sandbox.setTimeout[util.promisify.custom] = function(after, value) { - return new Promise(function(resolve, reject) { - sandbox.setTimeout(function(){ resolve(value); }, after); - }); - }; + function getPackages() { + // delete cache to reload the package.json file + delete require.cache[ + Module._resolveFilename(path.join(__dirname, '../../package.json')) + ]; + + var dep = require('../../package.json').dependencies; + var packages = []; + for (var name in dep) { + // simple filter to exclude in Packages TAB + // all npm packages that start with node-red-xxx + // usually these modules are nodes + // TODO: use a new packages.json instead of using the main one used by NODE-RED + if (!name.startsWith('node-red')) { + packages.push({ + label: name, + sublabel: dep[name], + icon: 'fa fa-archive', + checkbox: false, + }); } + } + return packages; + } - try { - this.script = requireFromString(functionText)(sandbox.util, sandbox.RED, sandbox.__node__, sandbox.context, sandbox.flow, sandbox.global, sandbox.env, sandbox.setTimeout, sandbox.clearTimeout, sandbox.setInterval, sandbox.clearInterval); - if (RED.settings.nodeRedContribUnsafeFunctionAsyncReceive) { - this.on("input", function(msg) { - setImmediate(function() { - handle(msg); - }); - }); - } else { - this.on("input", handle); - } - this.on("close", function() { - while(node.outstandingTimers.length > 0) { - clearTimeout(node.outstandingTimers.pop()); - } - while(node.outstandingIntervals.length > 0) { - clearInterval(node.outstandingIntervals.pop()); - } - this.status({}); - }); - } catch(err) { - // eg SyntaxError - which v8 doesn't include line number information - // so we can't do better than this - this.error(err); + node.on('input', function (msg, send, done) { + try { + var result = node.script(msg, send, done); + // sync function has return + if (result) { + send(result); + done(); } + } catch (err) { + setError(msg, done, err); + } + }); - var profiling = { - "max": 0, - "total": 0, - "count": 0, - "debounce": null, - "status_count": -1 // current count in status message - }; - function handle(msg) { - try { - var start = process.hrtime(); - var results = node.script(msg); - sendResults(node, msg._msgid, results); + node.on('close', function () { + while (node.outstandingTimers.length > 0) { + clearTimeout(node.outstandingTimers.pop()); + } + while (node.outstandingIntervals.length > 0) { + clearInterval(node.outstandingIntervals.pop()); + } + if (node.clearStatus) { + node.status({}); + } + }); - var duration = process.hrtime(start); - var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100; - node.metric("duration", msg, converted); - if (RED.settings.nodeRedContribUnsafeFunctionProfiling) { - profiling.count += 1; - profiling.total += (duration[0] * 1e9 + duration[1]) / 1000000; - profiling.max = Math.max(profiling.max, converted); - if (!profiling.debounce) { - profiling.debounce = setInterval(function() { - if (profiling.status_count == profiling.count) { - // count hasn't changed. stop interval. - clearInterval(profiling.debounce); - profiling.debounce = null; - return; - } - profiling.status_count = profiling.count; - node.status({ - fill: "yellow", - shape: "dot", - text: "max: " + profiling.max + ", total: " + (Math.round(profiling.total * 100) / 100) + ", count: " + profiling.count - }); - }, 1000); // limited rate for status messages. - } - } else if (process.env.NODE_RED_FUNCTION_TIME) { - node.status({fill:"yellow",shape:"dot",text:""+converted}); + RED.httpAdmin.post('/unsafe-function/:action', function (req, res) { + try { + var dir = path.join(__dirname, 'modules'); + var file = path.join(dir, req.query.name); + var action = req.params.action; + + switch (action) { + case 'save': + var text = decodeURIComponent(req.body.text); + fs.writeFileSync(file, text); + // delete cache to get the latest changes + delete require.cache[Module._resolveFilename(file)]; + res.sendStatus(200); + break; + case 'create': + if (fs.existsSync(file)) res.sendStatus(200); + else { + fs.writeFileSync(file, ''); + res.send(getModules(dir)); } - } catch(err) { + break; + case 'delete': + fs.unlinkSync(file); + res.send(getModules(dir)); + break; + } + } catch (err) { + res.sendStatus(500); + node.error(err); + } + }); - var line = 0; - var errorMessage; - var stack = err.stack.split(/\r?\n/); - if (stack.length > 0) { - while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) { - line++; - } + RED.httpAdmin.get('/unsafe-function/:action', function (req, res) { + try { + var dir = path.join(__dirname, 'modules'); + var action = req.params.action; - if (line < stack.length) { - errorMessage = stack[line]; - var m = /:(\d+):(\d+)$/.exec(stack[line+1]); - if (m) { - var lineno = Number(m[1])-1; - var cha = m[2]; - errorMessage += " (line "+lineno+", col "+cha+")"; - } - } - } - if (!errorMessage) { - errorMessage = err.toString(); + switch (action) { + case 'file': + res.type('text/plain').sendFile(req.query.name, { + root: dir, + lastModified: false, + cacheControl: false, + dotfiles: 'allow', + }); + break; + case 'packages': + res.send(getPackages()); + break; + case 'modules': + res.send(getModules(dir)); + break; + case 'npm': + var proc = spawnSync( + 'npm', + [ + req.query.command, + req.query.package, + '--silent', + '--no-audit', + '--no-update-notifier', + '--no-progress', + ], + { stdio: 'inherit' } + ); + + if (proc.error) { + console.log(proc); + res.sendStatus(500); + } else { + res.send(getPackages()); } - node.error(errorMessage, msg); - } + break; } - } - RED.nodes.registerType("unsafe-function",FunctionNode); - RED.library.register("functions"); + } catch (err) { + res.sendStatus(500); + node.error(err); + } + }); + } + + RED.nodes.registerType('unsafe-function', FunctionNode); + RED.library.register('functions'); };