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