1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge pull request #2873 from node-red/function-modules

Function node external modules
This commit is contained in:
Nick O'Leary 2021-03-01 21:35:31 +00:00 committed by GitHub
commit 3d23d1de4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1323 additions and 223 deletions

View File

@ -106,7 +106,7 @@
"marked": "2.0.0", "marked": "2.0.0",
"minami": "1.2.3", "minami": "1.2.3",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"node-red-node-test-helper": "^0.2.6", "node-red-node-test-helper": "^0.2.7",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"nodemon": "2.0.6", "nodemon": "2.0.6",
"should": "13.2.3", "should": "13.2.3",

View File

@ -143,6 +143,7 @@
"nodeActionDisabled": "node actions disabled", "nodeActionDisabled": "node actions disabled",
"nodeActionDisabledSubflow": "node actions disabled within subflow", "nodeActionDisabledSubflow": "node actions disabled within subflow",
"missing-types": "<p>Flows stopped due to missing node types.</p>", "missing-types": "<p>Flows stopped due to missing node types.</p>",
"missing-modules": "<p>Flows stopped due to missing modules.</p>",
"safe-mode":"<p>Flows stopped in safe mode.</p><p>You can modify your flows and deploy the changes to restart.</p>", "safe-mode":"<p>Flows stopped in safe mode.</p><p>You can modify your flows and deploy the changes to restart.</p>",
"restartRequired": "Node-RED must be restarted to enable upgraded modules", "restartRequired": "Node-RED must be restarted to enable upgraded modules",
"credentials_load_failed": "<p>Flows stopped as the credentials could not be decrypted.</p><p>The flow credential file is encrypted, but the project's encryption key is missing or invalid.</p>", "credentials_load_failed": "<p>Flows stopped as the credentials could not be decrypted.</p><p>The flow credential file is encrypted, but the project's encryption key is missing or invalid.</p>",

View File

@ -315,6 +315,7 @@ var RED = (function() {
id: notificationId id: notificationId
} }
if (notificationId === "runtime-state") { if (notificationId === "runtime-state") {
RED.events.emit("runtime-state",msg);
if (msg.error === "safe-mode") { if (msg.error === "safe-mode") {
options.buttons = [ options.buttons = [
{ {
@ -347,6 +348,16 @@ var RED = (function() {
} }
] ]
} }
} else if (msg.error === "missing-modules") {
text+="<ul><li>"+msg.modules.map(function(m) { return RED.utils.sanitize(m.module)+(m.error?(" - <small>"+RED.utils.sanitize(""+m.error)+"</small>"):"")}).join("</li><li>")+"</li></ul>";
options.buttons = [
{
text: RED._("common.label.close"),
click: function() {
persistentNotifications[notificationId].hideNotification();
}
}
]
} else if (msg.error === "credentials_load_failed") { } else if (msg.error === "credentials_load_failed") {
if (RED.settings.theme("projects.enabled",false)) { if (RED.settings.theme("projects.enabled",false)) {
// projects enabled // projects enabled
@ -437,6 +448,9 @@ var RED = (function() {
} else if (persistentNotifications.hasOwnProperty(notificationId)) { } else if (persistentNotifications.hasOwnProperty(notificationId)) {
persistentNotifications[notificationId].close(); persistentNotifications[notificationId].close();
delete persistentNotifications[notificationId]; delete persistentNotifications[notificationId];
if (notificationId === 'runtime-state') {
RED.events.emit("runtime-state",msg);
}
} }
}); });
RED.comms.subscribe("status/#",function(topic,msg) { RED.comms.subscribe("status/#",function(topic,msg) {

View File

@ -29,6 +29,7 @@ RED.tabs = (function() {
var currentTabWidth; var currentTabWidth;
var currentActiveTabWidth = 0; var currentActiveTabWidth = 0;
var collapsibleMenu; var collapsibleMenu;
var mousedownTab;
var preferredOrder = options.order; var preferredOrder = options.order;
var ul = options.element || $("#"+options.id); var ul = options.element || $("#"+options.id);
var wrapper = ul.wrap( "<div>" ).parent(); var wrapper = ul.wrap( "<div>" ).parent();
@ -207,6 +208,11 @@ RED.tabs = (function() {
if (dragActive) { if (dragActive) {
return return
} }
if (evt.currentTarget !== mousedownTab) {
mousedownTab = null;
return;
}
mousedownTab = null;
if (dblClickTime && Date.now()-dblClickTime < 400) { if (dblClickTime && Date.now()-dblClickTime < 400) {
dblClickTime = 0; dblClickTime = 0;
dblClickArmed = true; dblClickArmed = true;
@ -445,6 +451,7 @@ RED.tabs = (function() {
} }
ul.find("li.red-ui-tab a") ul.find("li.red-ui-tab a")
.on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
.on("mouseup",onTabClick) .on("mouseup",onTabClick)
.on("click", function(evt) {evt.preventDefault(); }) .on("click", function(evt) {evt.preventDefault(); })
.on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); }) .on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); })
@ -509,8 +516,8 @@ RED.tabs = (function() {
li.attr('id',"red-ui-tab-"+(tab.id.replace(".","-"))); li.attr('id',"red-ui-tab-"+(tab.id.replace(".","-")));
li.data("tabId",tab.id); li.data("tabId",tab.id);
if (options.maximumTabWidth) { if (options.maximumTabWidth || tab.maximumTabWidth) {
li.css("maxWidth",options.maximumTabWidth+"px"); li.css("maxWidth",(options.maximumTabWidth || tab.maximumTabWidth) +"px");
} }
var link = $("<a/>",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li); var link = $("<a/>",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li);
if (tab.icon) { if (tab.icon) {
@ -636,6 +643,7 @@ RED.tabs = (function() {
} }
} }
link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
link.on("mouseup",onTabClick); link.on("mouseup",onTabClick);
link.on("click", function(evt) { evt.preventDefault(); }) link.on("click", function(evt) { evt.preventDefault(); })
link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); })

View File

@ -344,6 +344,16 @@
that.element.val(that.value()); that.element.val(that.value());
that.element.trigger('change',[that.propertyType,that.value()]); that.element.trigger('change',[that.propertyType,that.value()]);
}); });
this.input.on('keyup', function(evt) {
that.validate();
that.element.val(that.value());
that.element.trigger('keyup',evt);
});
this.input.on('paste', function(evt) {
that.validate();
that.element.val(that.value());
that.element.trigger('paste',evt);
});
this.input.on('keydown', function(evt) { this.input.on('keydown', function(evt) {
if (evt.keyCode >= 37 && evt.keyCode <= 40) { if (evt.keyCode >= 37 && evt.keyCode <= 40) {
evt.stopPropagation(); evt.stopPropagation();

View File

@ -369,7 +369,7 @@ RED.palette.editor = (function() {
if (v.modules) { if (v.modules) {
var a = false; var a = false;
v.modules = v.modules.filter(function(m) { v.modules = v.modules.filter(function(m) {
if (checkModuleAllowed(m.id,m.version,installAllowList,installDenyList)) { if (RED.utils.checkModuleAllowed(m.id,m.version,installAllowList,installDenyList)) {
loadedIndex[m.id] = m; loadedIndex[m.id] = m;
m.index = [m.id]; m.index = [m.id];
if (m.keywords) { if (m.keywords) {
@ -483,68 +483,6 @@ RED.palette.editor = (function() {
var installAllowList = ['*']; var installAllowList = ['*'];
var installDenyList = []; var installDenyList = [];
function parseModuleList(list) {
list = list || ["*"];
return list.map(function(rule) {
var m = /^(.+?)(?:@(.*))?$/.exec(rule);
var wildcardPos = m[1].indexOf("*");
wildcardPos = wildcardPos===-1?Infinity:wildcardPos;
return {
module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"),
version: m[2],
wildcardPos: wildcardPos
}
})
}
function checkAgainstList(module,version,list) {
for (var i=0;i<list.length;i++) {
var rule = list[i];
if (rule.module.test(module)) {
// Without a full semver library in the editor,
// we skip the version check.
// Not ideal - but will get caught in the runtime
// if the user tries to install.
return rule;
}
}
}
function checkModuleAllowed(module,version,allowList,denyList) {
if (!allowList && !denyList) {
// Default to allow
return true;
}
if (allowList.length === 0 && denyList.length === 0) {
return true;
}
var allowedRule = checkAgainstList(module,version,allowList);
var deniedRule = checkAgainstList(module,version,denyList);
// console.log("A",allowedRule)
// console.log("D",deniedRule)
if (allowedRule && !deniedRule) {
return true;
}
if (!allowedRule && deniedRule) {
return false;
}
if (!allowedRule && !deniedRule) {
return true;
}
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
return allowedRule.wildcardPos > deniedRule.wildcardPos
} else {
// First wildcard in same position.
// Go with the longer matching rule. This isn't going to be 100%
// right, but we are deep into edge cases at this point.
return allowedRule.module.toString().length > deniedRule.module.toString().length
}
return false;
}
function init() { function init() {
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
return; return;
@ -555,8 +493,8 @@ RED.palette.editor = (function() {
installAllowList = settingsAllowList; installAllowList = settingsAllowList;
installDenyList = settingsDenyList installDenyList = settingsDenyList
} }
installAllowList = parseModuleList(installAllowList); installAllowList = RED.utils.parseModuleList(installAllowList);
installDenyList = parseModuleList(installDenyList); installDenyList = RED.utils.parseModuleList(installDenyList);
createSettingsPane(); createSettingsPane();

View File

@ -1171,6 +1171,67 @@ RED.utils = (function() {
return '#'+'000000'.slice(0, 6-s.length)+s; return '#'+'000000'.slice(0, 6-s.length)+s;
} }
function parseModuleList(list) {
list = list || ["*"];
return list.map(function(rule) {
var m = /^(.+?)(?:@(.*))?$/.exec(rule);
var wildcardPos = m[1].indexOf("*");
wildcardPos = wildcardPos===-1?Infinity:wildcardPos;
return {
module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"),
version: m[2],
wildcardPos: wildcardPos
}
})
}
function checkAgainstList(module,version,list) {
for (var i=0;i<list.length;i++) {
var rule = list[i];
if (rule.module.test(module)) {
// Without a full semver library in the editor,
// we skip the version check.
// Not ideal - but will get caught in the runtime
// if the user tries to install.
return rule;
}
}
}
function checkModuleAllowed(module,version,allowList,denyList) {
if (!allowList && !denyList) {
// Default to allow
return true;
}
if (allowList.length === 0 && denyList.length === 0) {
return true;
}
var allowedRule = checkAgainstList(module,version,allowList);
var deniedRule = checkAgainstList(module,version,denyList);
// console.log("A",allowedRule)
// console.log("D",deniedRule)
if (allowedRule && !deniedRule) {
return true;
}
if (!allowedRule && deniedRule) {
return false;
}
if (!allowedRule && !deniedRule) {
return true;
}
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
return allowedRule.wildcardPos > deniedRule.wildcardPos
} else {
// First wildcard in same position.
// Go with the longer matching rule. This isn't going to be 100%
// right, but we are deep into edge cases at this point.
return allowedRule.module.toString().length > deniedRule.module.toString().length
}
return false;
}
return { return {
createObjectElement: buildMessageElement, createObjectElement: buildMessageElement,
getMessageProperty: getMessageProperty, getMessageProperty: getMessageProperty,
@ -1190,6 +1251,8 @@ RED.utils = (function() {
sanitize: sanitize, sanitize: sanitize,
renderMarkdown: renderMarkdown, renderMarkdown: renderMarkdown,
createNodeIcon: createNodeIcon, createNodeIcon: createNodeIcon,
getDarkerColor: getDarkerColor getDarkerColor: getDarkerColor,
parseModuleList: parseModuleList,
checkModuleAllowed: checkModuleAllowed
} }
})(); })();

View File

@ -146,6 +146,13 @@ body {
background-size: contain background-size: contain
} }
.red-ui-font-code {
font-family: $monospace-font;
font-size: $primary-font-size;
color: $info-text-code-color;
white-space: nowrap;
}
code { code {
font-family: $monospace-font; font-family: $monospace-font;
font-size: $primary-font-size; font-size: $primary-font-size;

View File

@ -174,8 +174,8 @@ button.red-ui-tray-resize-button {
.red-ui-editor .red-ui-tray { .red-ui-editor .red-ui-tray {
.dialog-form, #dialog-form, #node-config-dialog-edit-form { .dialog-form, #dialog-form, #node-config-dialog-edit-form {
margin: 20px; margin: 10px 20px;
height: calc(100% - 40px); height: calc(100% - 20px);
} }
} }

View File

@ -1,60 +1,318 @@
<script type="text/html" data-template-name="function"> <script type="text/html" data-template-name="function">
<style>
.func-tabs-row {
margin-bottom: 0;
}
#node-input-libs-container-row .red-ui-editableList-container {
padding: 0px;
}
#node-input-libs-container-row .red-ui-editableList-container li {
padding:5px;
}
#node-input-libs-container-row .red-ui-editableList-item-remove {
right: 5px;
}
.node-libs-entry {
display: flex;
}
.node-libs-entry .node-input-libs-var, .node-libs-entry .red-ui-typedInput-container {
flex-grow: 1;
}
.node-libs-entry > code,.node-libs-entry > span {
line-height: 30px;
}
.node-libs-entry > input[type=text] {
border-radius: 0;
border-left: none;
border-top: none;
border-right: none;
padding-top: 2px;
padding-bottom: 2px;
margin-top: 4px;
margin-bottom: 2px;
height: 26px;
}
.node-libs-entry > span > i {
display: none;
}
.node-libs-entry > span.input-error > i {
display: inline;
}
</style>
<input type="hidden" id="node-input-func">
<input type="hidden" id="node-input-noerr">
<input type="hidden" id="node-input-finalize">
<input type="hidden" id="node-input-initialize">
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div> <div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div>
</div> </div>
<div class="form-row">
<div class="form-row func-tabs-row">
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul> <ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
</div> </div>
<div id="func-tabs-content" style="min-height: calc(100% - 95px);"> <div id="func-tabs-content" style="min-height: calc(100% - 95px);">
<div id="func-tab-init" style="display:none"> <div id="func-tab-config" style="display:none">
<div class="form-row" style="margin-bottom: 0px;"> <div class="form-row">
<input type="hidden" id="node-input-initialize" autofocus="autofocus">
</div>
<div class="form-row node-text-editor-row" style="position:relative">
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
<div style="height: 250px; min-height:150px; margin-top: 30px;" class="node-text-editor" id="node-input-init-editor" ></div>
</div>
</div>
<div id="func-tab-body" style="display:none">
<div class="form-row" style="margin-bottom: 0px;">
<input type="hidden" id="node-input-func" autofocus="autofocus">
<input type="hidden" id="node-input-noerr">
</div>
<div class="form-row node-text-editor-row" style="position:relative">
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
<div style="height: 220px; min-height:120px; margin-top: 30px;" class="node-text-editor" id="node-input-func-editor" ></div>
</div>
<div class="form-row" style="margin-bottom: 0px">
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label> <label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
<input id="node-input-outputs" style="width: 60px;" value="1"> <input id="node-input-outputs" style="width: 60px;" value="1">
</div> </div>
<div class="form-row node-input-libs-row hide" style="margin-bottom: 0px;">
<label><i class="fa fa-cubes"></i> <span>Modules</span></label>
</div>
<div class="form-row node-input-libs-row hide" id="node-input-libs-container-row">
<ol id="node-input-libs-container"></ol>
</div>
</div>
<div id="func-tab-init" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-init-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
<div id="func-tab-body" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 220px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div> </div>
<div id="func-tab-finalize" style="display:none"> <div id="func-tab-finalize" style="display:none">
<div class="form-row" style="margin-bottom: 0px;">
<input type="hidden" id="node-input-finalize" autofocus="autofocus">
</div>
<div class="form-row node-text-editor-row" style="position:relative"> <div class="form-row node-text-editor-row" style="position:relative">
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
<div style="height: 250px; min-height:150px; margin-top: 30px;" class="node-text-editor" id="node-input-finalize-editor" ></div> <div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div> </div>
</div> </div>
</div> </div>
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
(function() {
var invalidModuleVNames = [
'console',
'util',
'Buffer',
'Date',
'RED',
'node',
'__node__',
'context',
'flow',
'global',
'env',
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'promisify'
]
var knownFunctionNodes = {};
RED.events.on("nodes:add", function(n) {
if (n.type === "function") {
knownFunctionNodes[n.id] = n;
}
})
RED.events.on("nodes:remove", function(n) {
if (n.type === "function") {
delete knownFunctionNodes[n.id];
}
})
var missingModules = [];
var missingModuleReasons = {};
RED.events.on("runtime-state", function(event) {
if (event.error === "missing-modules") {
missingModules = event.modules.map(function(m) { missingModuleReasons[m.module] = m.error; return m.module });
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
RED.editor.validateNode(knownFunctionNodes[id])
}
}
} else if (!event.text) {
missingModuleReasons = {};
missingModules = [];
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
RED.editor.validateNode(knownFunctionNodes[id])
}
}
}
RED.view.redraw();
});
var installAllowList = ['*'];
var installDenyList = [];
var modulesEnabled = true;
if (RED.settings.get('externalModules.modules.allowInstall', true) === false) {
modulesEnabled = false;
}
var settingsAllowList = RED.settings.get("externalModules.modules.allowList")
var settingsDenyList = RED.settings.get("externalModules.modules.denyList")
if (settingsAllowList || settingsDenyList) {
installAllowList = settingsAllowList;
installDenyList = settingsDenyList
}
installAllowList = RED.utils.parseModuleList(installAllowList);
installDenyList = RED.utils.parseModuleList(installDenyList);
// object that maps from library name to its descriptor
var allLibs = [];
function moduleName(module) {
var match = /^([^@]+)@(.+)/.exec(module);
if (match) {
return [match[1], match[2]];
}
return [module, undefined];
}
function getAllUsedModules() {
var moduleSet = new Set();
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id)) {
if (knownFunctionNodes[id].libs) {
for (var i=0, l=knownFunctionNodes[id].libs.length; i<l; i++) {
if (RED.utils.checkModuleAllowed(knownFunctionNodes[id].libs[i].module,null,installAllowList,installDenyList)) {
moduleSet.add(knownFunctionNodes[id].libs[i].module);
}
}
}
}
}
var modules = Array.from(moduleSet);
modules.sort();
return modules;
}
function prepareLibraryConfig(node) {
$(".node-input-libs-row").show();
var usedModules = getAllUsedModules();
var typedModules = usedModules.map(function(l) {
return {icon:"fa fa-cube", value:l,label:l,hasValue:false}
})
typedModules.push({
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
})
var libList = $("#node-input-libs-container").css('min-height','100px').css('min-width','450px').editableList({
addItem: function(container,i,opt) {
var parent = container.parent();
var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container);
var fieldWidth = "260px";
$('<code>const </code>').appendTo(row0);
var fvar = $("<input/>", {
class: "node-input-libs-var red-ui-font-code",
placeholder: RED._("node-red:function.require.var"),
type: "text"
}).css({
width: "120px",
"margin-left": "5px"
}).appendTo(row0).val(opt.var);
var vnameWarning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
RED.popover.tooltip(vnameWarning.find("i"),function() {
var val = fvar.val();
if (invalidModuleVNames.indexOf(val) !== -1) {
return RED._("node-red:function.error.moduleNameReserved",{name:val})
} else {
return RED._("node-red:function.error.moduleNameError",{name:val})
}
})
$('<code> = require(</code>').appendTo(row0);
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text"
}).css({
width: "180px",
}).appendTo(row0).typedInput({
types: typedModules,
default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_"
});
if (usedModules.indexOf(opt.module) === -1) {
fmodule.typedInput('value', opt.module);
}
$('<code>)</code>').appendTo(row0);
var moduleWarning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
RED.popover.tooltip(moduleWarning.find("i"),function() {
var val = fmodule.typedInput("type");
if (val === "_custom_") {
val = fmodule.val();
}
var errors = [];
if (!RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList)) {
return RED._("node-red:function.error.moduleNotAllowed",{module:val});
} else {
return RED._("node-red:function.error.moduleLoadError",{module:val,error:missingModuleReasons[val]});
}
})
fvar.on("change keyup paste", function (e) {
var v = $(this).val().trim();
if (v === "" || / /.test(v) || invalidModuleVNames.indexOf(v) !== -1) {
fvar.addClass("input-error");
vnameWarning.addClass("input-error");
} else {
fvar.removeClass("input-error");
vnameWarning.removeClass("input-error");
}
});
fmodule.on("change keyup paste", function (e) {
var val = $(this).typedInput("type");
if (val === "_custom_") {
val = $(this).val();
}
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/]./g, function(v) { return v[1].toUpperCase() });
fvar.val(varName);
fvar.trigger("change");
if (RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList) && (missingModules.indexOf(val) === -1)) {
fmodule.removeClass("input-error");
moduleWarning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
moduleWarning.addClass("input-error");
}
});
if (RED.utils.checkModuleAllowed(opt.module,null,installAllowList,installDenyList) && (missingModules.indexOf(opt.module) === -1)) {
fmodule.removeClass("input-error");
moduleWarning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
moduleWarning.addClass("input-error");
}
if (opt.var) {
fvar.trigger("change");
}
},
removable: true
});
var libs = node.libs || [];
for (var i=0,l=libs.length;i<l; i++) {
libList.editableList('addItem',libs[i])
}
}
RED.nodes.registerType('function',{ RED.nodes.registerType('function',{
color:"#fdd0a2", color:"#fdd0a2",
category: 'function', category: 'function',
@ -64,7 +322,26 @@
outputs: {value:1}, outputs: {value:1},
noerr: {value:0,required:true,validate:function(v) { return !v; }}, noerr: {value:0,required:true,validate:function(v) { return !v; }},
initialize: {value:""}, initialize: {value:""},
finalize: {value:""} finalize: {value:""},
libs: {value: [], validate: function(v) {
if (!v) { return true; }
for (var i=0,l=v.length;i<l;i++) {
var m = v[i];
if (!RED.utils.checkModuleAllowed(m.module,null,installAllowList,installDenyList)) {
return false
}
if (m.var === "" || / /.test(m.var)) {
return false;
}
if (missingModules.indexOf(m.module) > -1) {
return false;
}
if (invalidModuleVNames.indexOf(m.var) !== -1){
return false;
}
}
return true;
}}
}, },
inputs:1, inputs:1,
outputs:1, outputs:1,
@ -85,6 +362,12 @@
$("#" + tab.id).show(); $("#" + tab.id).show();
} }
}); });
tabs.addTab({
id: "func-tab-config",
iconClass: "fa fa-cog",
label: that._("function.label.setup")
});
tabs.addTab({ tabs.addTab({
id: "func-tab-init", id: "func-tab-init",
label: that._("function.label.initialize") label: that._("function.label.initialize")
@ -97,6 +380,7 @@
id: "func-tab-finalize", id: "func-tab-finalize",
label: that._("function.label.finalize") label: that._("function.label.finalize")
}); });
tabs.activateTab("func-tab-body"); tabs.activateTab("func-tab-body");
$( "#node-input-outputs" ).spinner({ $( "#node-input-outputs" ).spinner({
@ -205,7 +489,9 @@
RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand")); RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand"));
RED.popover.tooltip($("#node-finalize-expand-js"), RED._("node-red:common.label.expand")); RED.popover.tooltip($("#node-finalize-expand-js"), RED._("node-red:common.label.expand"));
if (RED.settings.functionExternalModules !== false) {
prepareLibraryConfig(that);
}
}, },
oneditsave: function() { oneditsave: function() {
var node = this; var node = this;
@ -237,7 +523,28 @@
$("#node-input-noerr").val(noerr); $("#node-input-noerr").val(noerr);
this.noerr = noerr; this.noerr = noerr;
if (RED.settings.functionExternalModules === true) {
var libs = $("#node-input-libs-container").editableList("items");
node.libs = [];
libs.each(function(i) {
var item = $(this);
var v = item.find(".node-input-libs-var").val();
var n = item.find(".node-input-libs-val").typedInput("type");
if (n === "_custom_") {
n = item.find(".node-input-libs-val").val();
}
if ((!v || (v === "")) ||
(!n || (n === ""))) {
return;
}
node.libs.push({
var: v,
module: n
});
});
} else {
node.libs = [];
}
}, },
oneditcancel: function() { oneditcancel: function() {
var node = this; var node = this;
@ -259,18 +566,19 @@
} }
var editorRow = $("#dialog-form>div.node-text-editor-row"); var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px"); $("#dialog-form .node-text-editor").css("height",height+"px");
this.editor.resize();
var height = size.height; var height = size.height;
$("#node-input-init-editor").css("height", (height -105)+"px"); $("#node-input-init-editor").css("height", (height -45-48)+"px");
$("#node-input-func-editor").css("height", (height -145)+"px"); $("#node-input-func-editor").css("height", (height -45-48)+"px");
$("#node-input-finalize-editor").css("height", (height -105)+"px"); $("#node-input-finalize-editor").css("height", (height -45-48)+"px");
this.initEditor.resize(); this.initEditor.resize();
this.editor.resize(); this.editor.resize();
this.finalizeEditor.resize(); this.finalizeEditor.resize();
$("#node-input-libs-container").css("height", (height - 185)+"px");
} }
}); });
})();
</script> </script>

View File

@ -16,6 +16,7 @@
module.exports = function(RED) { module.exports = function(RED) {
"use strict"; "use strict";
var util = require("util"); var util = require("util");
var vm = require("vm"); var vm = require("vm");
@ -94,6 +95,11 @@ module.exports = function(RED) {
node.func = n.func; node.func = n.func;
node.ini = n.initialize ? n.initialize.trim() : ""; node.ini = n.initialize ? n.initialize.trim() : "";
node.fin = n.finalize ? n.finalize.trim() : ""; node.fin = n.finalize ? n.finalize.trim() : "";
node.libs = n.libs || [];
if (RED.settings.functionExternalModules !== true && node.libs.length > 0) {
throw new Error("Function node not allowed to load external modules");
}
var handleNodeDoneCall = true; var handleNodeDoneCall = true;
@ -105,23 +111,23 @@ module.exports = function(RED) {
} }
var functionText = "var results = null;"+ var functionText = "var results = null;"+
"results = (async function(msg,__send__,__done__){ "+ "results = (async function(msg,__send__,__done__){ "+
"var __msgid__ = msg._msgid;"+ "var __msgid__ = msg._msgid;"+
"var node = {"+ "var node = {"+
"id:__node__.id,"+ "id:__node__.id,"+
"name:__node__.name,"+ "name:__node__.name,"+
"log:__node__.log,"+ "log:__node__.log,"+
"error:__node__.error,"+ "error:__node__.error,"+
"warn:__node__.warn,"+ "warn:__node__.warn,"+
"debug:__node__.debug,"+ "debug:__node__.debug,"+
"trace:__node__.trace,"+ "trace:__node__.trace,"+
"on:__node__.on,"+ "on:__node__.on,"+
"status:__node__.status,"+ "status:__node__.status,"+
"send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},"+ "send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},"+
"done:__done__"+ "done:__done__"+
"};\n"+ "};\n"+
node.func+"\n"+ node.func+"\n"+
"})(msg,__send__,__done__);"; "})(msg,__send__,__done__);";
var finScript = null; var finScript = null;
var finOpt = null; var finOpt = null;
node.topic = n.topic; node.topic = n.topic;
@ -266,34 +272,96 @@ module.exports = function(RED) {
}; };
sandbox.promisify = util.promisify; sandbox.promisify = util.promisify;
} }
if (node.hasOwnProperty("libs")) {
let moduleErrors = false;
var modules = node.libs;
modules.forEach(module => {
var vname = module.hasOwnProperty("var") ? module.var : null;
if (vname && (vname !== "")) {
if (sandbox.hasOwnProperty(vname) || vname === 'node') {
node.error(RED._("function.error.moduleNameError",{name:vname}))
moduleErrors = true;
return;
}
sandbox[vname] = null;
try {
var spec = module.module;
if (spec && (spec !== "")) {
var lib = RED.require(module.module);
sandbox[vname] = lib;
}
} catch (e) {
//TODO: NLS error message
node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:e.toString()}))
moduleErrors = true;
}
}
});
if (moduleErrors) {
throw new Error("Function node failed to load external modules");
}
}
const RESOLVING = 0;
const RESOLVED = 1;
const ERROR = 2;
var state = RESOLVING;
var messages = [];
var processMessage = (() => {});
node.on("input", function(msg,send,done) {
if(state === RESOLVING) {
messages.push({msg:msg, send:send, done:done});
}
else if(state === RESOLVED) {
processMessage(msg, send, done);
}
});
var context = vm.createContext(sandbox); var context = vm.createContext(sandbox);
try { try {
var iniScript = null; var iniScript = null;
var iniOpt = null; var iniOpt = null;
if (node.ini && (node.ini !== "")) { if (node.ini && (node.ini !== "")) {
var iniText = ` var iniText = `
(async function(__send__) { (async function(__send__) {
var node = { var node = {
id:__node__.id, id:__node__.id,
name:__node__.name, name:__node__.name,
log:__node__.log, log:__node__.log,
error:__node__.error, error:__node__.error,
warn:__node__.warn, warn:__node__.warn,
debug:__node__.debug, debug:__node__.debug,
trace:__node__.trace, trace:__node__.trace,
status:__node__.status, status:__node__.status,
send: function(msgs, cloneMsg) { send: function(msgs, cloneMsg) {
__node__.send(__send__, RED.util.generateId(), msgs, cloneMsg); __node__.send(__send__, RED.util.generateId(), msgs, cloneMsg);
} }
}; };
`+ node.ini +` `+ node.ini +`
})(__initSend__);`; })(__initSend__);`;
iniOpt = createVMOpt(node, " setup"); iniOpt = createVMOpt(node, " setup");
iniScript = new vm.Script(iniText, iniOpt); iniScript = new vm.Script(iniText, iniOpt);
} }
node.script = vm.createScript(functionText, createVMOpt(node, "")); node.script = vm.createScript(functionText, createVMOpt(node, ""));
if (node.fin && (node.fin !== "")) { if (node.fin && (node.fin !== "")) {
var finText = "(function () {\n"+node.fin +"\n})();"; var finText = `(function () {
var node = {
id:__node__.id,
name:__node__.name,
log:__node__.log,
error:__node__.error,
warn:__node__.warn,
debug:__node__.debug,
trace:__node__.trace,
status:__node__.status,
send: function(msgs, cloneMsg) {
__node__.error("Cannot send from close function");
}
};
`+node.fin +`})();`;
finOpt = createVMOpt(node, " cleanup"); finOpt = createVMOpt(node, " cleanup");
finScript = new vm.Script(finText, finOpt); finScript = new vm.Script(finText, finOpt);
} }
@ -303,7 +371,7 @@ module.exports = function(RED) {
promise = iniScript.runInContext(context, iniOpt); promise = iniScript.runInContext(context, iniOpt);
} }
function processMessage(msg, send, done) { processMessage = function (msg, send, done) {
var start = process.hrtime(); var start = process.hrtime();
context.msg = msg; context.msg = msg;
context.__send__ = send; context.__send__ = send;
@ -363,20 +431,6 @@ module.exports = function(RED) {
}); });
} }
const RESOLVING = 0;
const RESOLVED = 1;
const ERROR = 2;
var state = RESOLVING;
var messages = [];
node.on("input", function(msg,send,done) {
if(state === RESOLVING) {
messages.push({msg:msg, send:send, done:done});
}
else if(state === RESOLVED) {
processMessage(msg, send, done);
}
});
node.on("close", function() { node.on("close", function() {
if (finScript) { if (finScript) {
try { try {
@ -422,7 +476,12 @@ module.exports = function(RED) {
node.error(err); node.error(err);
} }
} }
RED.nodes.registerType("function",FunctionNode); RED.nodes.registerType("function",FunctionNode, {
dynamicModuleList: "libs",
settings: {
functionExternalModules: { value: false, exportable: true }
}
});
RED.library.register("functions"); RED.library.register("functions");
}; };

View File

@ -209,16 +209,25 @@
"function": { "function": {
"function": "", "function": "",
"label": { "label": {
"function": "Function", "setup": "Setup",
"initialize": "Setup", "function": "On Message",
"finalize": "Close", "initialize": "On Start",
"finalize": "On Stop",
"outputs": "Outputs" "outputs": "Outputs"
}, },
"text": { "text": {
"initialize": "// Code added here will be run once\n// whenever the node is deployed.\n", "initialize": "// Code added here will be run once\n// whenever the node is deployed.\n",
"finalize": "// Code added here will be run when the\n// node is being stopped or re-deployed.\n" "finalize": "// Code added here will be run when the\n// node is being stopped or re-deployed.\n"
}, },
"require": {
"var": "variable",
"module": "module"
},
"error": { "error": {
"moduleNotAllowed": "Module __module__ not allowed",
"moduleLoadError": "Failed to load module __module__: __error__",
"moduleNameError": "Invalid module variable name: __name__",
"moduleNameReserved": "Reserved variable name: __name__",
"inputListener":"Cannot add listener to 'input' event within Function", "inputListener":"Cannot add listener to 'input' event within Function",
"non-message-returned":"Function tried to send a message of type __type__" "non-message-returned":"Function tried to send a message of type __type__"
} }

View File

@ -0,0 +1,223 @@
// This module handles the management of modules required by the runtime and flows.
// Essentially this means keeping track of what extra modules a flow requires,
// ensuring those modules are installed and providing a standard way for nodes
// to require those modules safely.
const fs = require("fs-extra");
const registryUtil = require("./util");
const path = require("path");
const clone = require("clone");
const exec = require("@node-red/util").exec;
const log = require("@node-red/util").log;
const BUILTIN_MODULES = require('module').builtinModules;
const EXTERNAL_MODULES_DIR = "externalModules";
// TODO: outsource running npm to a plugin
const NPM_COMMAND = (process.platform === "win32") ? "npm.cmd" : "npm";
let registeredTypes = {};
let subflowTypes = {};
let settings;
let knownExternalModules = {};
let installEnabled = true;
let installAllowList = ['*'];
let installDenyList = [];
function getInstallDir() {
return path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", "externalModules"));
}
async function refreshExternalModules() {
const externalModuleDir = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", EXTERNAL_MODULES_DIR));
try {
const pkgFile = JSON.parse(await fs.readFile(path.join(externalModuleDir,"package.json"),"utf-8"));
knownExternalModules = pkgFile.dependencies;
} catch(err) {
}
}
function init(_settings) {
settings = _settings;
knownExternalModules = {};
installEnabled = true;
if (settings.externalModules && settings.externalModules.modules) {
if (settings.externalModules.modules.allowList || settings.externalModules.modules.denyList) {
installAllowList = settings.externalModules.modules.allowList;
installDenyList = settings.externalModules.modules.denyList;
}
if (settings.externalModules.modules.hasOwnProperty("allowInstall")) {
installEnabled = settings.externalModules.modules.allowInstall
}
}
installAllowList = registryUtil.parseModuleList(installAllowList);
installDenyList = registryUtil.parseModuleList(installDenyList);
}
function register(type, dynamicModuleListProperty) {
registeredTypes[type] = dynamicModuleListProperty;
}
function registerSubflow(type, subflowConfig) {
subflowTypes[type] = subflowConfig;
}
function requireModule(module) {
if (!registryUtil.checkModuleAllowed( module, null,installAllowList,installDenyList)) {
const e = new Error("Module not allowed");
e.code = "module_not_allowed";
throw e;
}
if (BUILTIN_MODULES.indexOf(module) !== -1) {
return require(module);
}
if (!knownExternalModules[module]) {
const e = new Error("Module not allowed");
e.code = "module_not_allowed";
throw e;
}
const externalModuleDir = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", EXTERNAL_MODULES_DIR));
const moduleDir = path.join(externalModuleDir,"node_modules",module);
return require(moduleDir);
}
function parseModuleName(module) {
var match = /((?:@[^/]+\/)?[^/@]+)(?:@([\s\S]+))?/.exec(module);
if (match) {
return {
spec: module,
module: match[1],
version: match[2],
builtin: BUILTIN_MODULES.indexOf(match[1]) !== -1,
known: !!knownExternalModules[match[1]]
}
}
return null;
}
function isInstalled(moduleDetails) {
return moduleDetails.builtin || moduleDetails.known;
}
async function checkFlowDependencies(flowConfig) {
let nodes = clone(flowConfig);
await refreshExternalModules();
const checkedModules = {};
const promises = [];
const errors = [];
const checkedSubflows = {};
while (nodes.length > 0) {
let n = nodes.shift();
if (subflowTypes[n.type] && !checkedSubflows[n.type]) {
checkedSubflows[n.type] = true;
nodes = nodes.concat(subflowTypes[n.type].flow)
} else if (registeredTypes[n.type]) {
let nodeModules = n[registeredTypes[n.type]] || [];
if (!Array.isArray(nodeModules)) {
nodeModules = [nodeModules]
}
nodeModules.forEach(module => {
if (typeof module !== 'string') {
module = module.module || "";
}
if (module) {
let moduleDetails = parseModuleName(module)
if (moduleDetails && checkedModules[moduleDetails.module] === undefined) {
checkedModules[moduleDetails.module] = isInstalled(moduleDetails)
if (!checkedModules[moduleDetails.module]) {
if (installEnabled) {
promises.push(installModule(moduleDetails).catch(err => {
errors.push({module: moduleDetails,error:err});
}))
} else if (!installEnabled) {
const e = new Error("Module install disabled - externalModules.modules.allowInstall=false");
e.code = "install_not_allowed";
errors.push({module: moduleDetails,error:e});
}
} else if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) {
const e = new Error("Module not allowed");
e.code = "module_not_allowed";
errors.push({module: moduleDetails,error:e});
}
}
}
})
}
}
return Promise.all(promises).then(refreshExternalModules).then(() => {
if (errors.length > 0) {
throw errors;
}
})
}
async function ensureModuleDir() {
const installDir = getInstallDir();
if (!fs.existsSync(installDir)) {
await fs.ensureDir(installDir);
}
const pkgFile = path.join(installDir,"package.json");
if (!fs.existsSync(pkgFile)) {
await fs.writeFile(path.join(installDir,"package.json"),`{
"name": "Node-RED-External-Modules",
"description": "These modules are automatically installed by Node-RED to use in Function nodes.",
"version": "1.0.0",
"private": true,
"dependencies": {}
}`)
}
}
async function installModule(moduleDetails) {
let installSpec = moduleDetails.module;
if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) {
const e = new Error("Install not allowed");
e.code = "install_not_allowed";
throw e;
}
if (moduleDetails.version) {
installSpec = installSpec+"@"+moduleDetails.version;
}
log.info(log._("server.install.installing",{name: moduleDetails.module,version: moduleDetails.version||"latest"}));
const installDir = getInstallDir();
await ensureModuleDir();
var args = ["install", installSpec, "--production"];
return exec.run(NPM_COMMAND, args, {
cwd: installDir
},true).then(result => {
log.info("successfully installed: "+installSpec);
}).catch(result => {
var output = result.stderr;
var e;
if (/E404/.test(output) || /ETARGET/.test(output)) {
log.error(log._("server.install.install-failed-not-found",{name:installSpec}));
e = new Error("Module not found");
e.code = 404;
throw e;
} else {
log.error(log._("server.install.install-failed-long",{name:installSpec}));
log.error("------------------------------------------");
log.error(output);
log.error("------------------------------------------");
e = new Error(log._("server.install.install-failed"));
e.code = "unexpected_error";
throw e;
}
})
}
module.exports = {
init: init,
register: register,
registerSubflow: registerSubflow,
checkFlowDependencies: checkFlowDependencies,
require: requireModule
}

View File

@ -28,6 +28,7 @@ var registry = require("./registry");
var loader = require("./loader"); var loader = require("./loader");
var installer = require("./installer"); var installer = require("./installer");
var library = require("./library"); var library = require("./library");
const externalModules = require("./externalModules")
var plugins = require("./plugins"); var plugins = require("./plugins");
/** /**
@ -44,6 +45,7 @@ function init(runtime) {
plugins.init(runtime.settings); plugins.init(runtime.settings);
registry.init(runtime.settings,loader); registry.init(runtime.settings,loader);
library.init(); library.init();
externalModules.init(runtime.settings);
} }
/** /**
@ -299,6 +301,8 @@ module.exports = {
*/ */
getNodeExampleFlowPath: library.getExampleFlowPath, getNodeExampleFlowPath: library.getExampleFlowPath,
checkFlowDependencies: externalModules.checkFlowDependencies,
registerPlugin: plugins.registerPlugin, registerPlugin: plugins.registerPlugin,
getPlugin: plugins.getPlugin, getPlugin: plugins.getPlugin,
getPluginsByType: plugins.getPluginsByType, getPluginsByType: plugins.getPluginsByType,

View File

@ -21,6 +21,7 @@ var fs = require("fs");
var library = require("./library"); var library = require("./library");
const {events} = require("@node-red/util") const {events} = require("@node-red/util")
var subflows = require("./subflow"); var subflows = require("./subflow");
var externalModules = require("./externalModules")
var settings; var settings;
var loader; var loader;
@ -28,6 +29,7 @@ var nodeConfigCache = {};
var moduleConfigs = {}; var moduleConfigs = {};
var nodeList = []; var nodeList = [];
var nodeConstructors = {}; var nodeConstructors = {};
var nodeOptions = {};
var subflowModules = {}; var subflowModules = {};
var nodeTypeToId = {}; var nodeTypeToId = {};
@ -36,12 +38,7 @@ var moduleNodes = {};
function init(_settings,_loader) { function init(_settings,_loader) {
settings = _settings; settings = _settings;
loader = _loader; loader = _loader;
moduleNodes = {}; clear();
nodeTypeToId = {};
nodeConstructors = {};
subflowModules = {};
nodeList = [];
nodeConfigCache = {};
} }
function load() { function load() {
@ -241,6 +238,7 @@ function removeNode(id) {
if (typeId === id) { if (typeId === id) {
delete subflowModules[t]; delete subflowModules[t];
delete nodeConstructors[t]; delete nodeConstructors[t];
delete nodeOptions[t];
delete nodeTypeToId[t]; delete nodeTypeToId[t];
} }
}); });
@ -412,7 +410,7 @@ function getCaller(){
return stack[0].getFileName(); return stack[0].getFileName();
} }
function registerNodeConstructor(nodeSet,type,constructor) { function registerNodeConstructor(nodeSet,type,constructor,options) {
if (nodeConstructors.hasOwnProperty(type)) { if (nodeConstructors.hasOwnProperty(type)) {
throw new Error(type+" already registered"); throw new Error(type+" already registered");
} }
@ -432,6 +430,12 @@ function registerNodeConstructor(nodeSet,type,constructor) {
} }
nodeConstructors[type] = constructor; nodeConstructors[type] = constructor;
nodeOptions[type] = options;
if (options) {
if (options.dynamicModuleList) {
externalModules.register(type,options.dynamicModuleList);
}
}
events.emit("type-registered",type); events.emit("type-registered",type);
} }
@ -452,6 +456,9 @@ function registerSubflow(nodeSet, subflow) {
nodeSetInfo.config = result.config; nodeSetInfo.config = result.config;
} }
subflowModules[result.type] = result; subflowModules[result.type] = result;
externalModules.registerSubflow(result.type,subflow);
events.emit("type-registered",result.type); events.emit("type-registered",result.type);
return result; return result;
} }
@ -524,6 +531,7 @@ function clear() {
moduleConfigs = {}; moduleConfigs = {};
nodeList = []; nodeList = [];
nodeConstructors = {}; nodeConstructors = {};
nodeOptions = {};
subflowModules = {}; subflowModules = {};
nodeTypeToId = {}; nodeTypeToId = {};
} }

View File

@ -17,6 +17,7 @@
const path = require("path"); const path = require("path");
const semver = require("semver"); const semver = require("semver");
const {events,i18n,log} = require("@node-red/util"); const {events,i18n,log} = require("@node-red/util");
var runtime; var runtime;
function copyObjectProperties(src,dst,copyList,blockList) { function copyObjectProperties(src,dst,copyList,blockList) {
@ -45,9 +46,8 @@ function requireModule(name) {
var relPath = path.relative(__dirname, moduleInfo.path); var relPath = path.relative(__dirname, moduleInfo.path);
return require(relPath); return require(relPath);
} else { } else {
var err = new Error(`Cannot find module '${name}'`); // Require it here to avoid the circular dependency
err.code = "MODULE_NOT_FOUND"; return require("./externalModules").require(name);
throw err;
} }
} }
@ -90,7 +90,7 @@ function createNodeApi(node) {
httpAdmin: runtime.adminApp, httpAdmin: runtime.adminApp,
server: runtime.server server: runtime.server
} }
copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials" ]); copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials"]);
red.nodes.registerType = function(type,constructor,opts) { red.nodes.registerType = function(type,constructor,opts) {
runtime.nodes.registerType(node.id,type,constructor,opts); runtime.nodes.registerType(node.id,type,constructor,opts);
} }
@ -136,7 +136,6 @@ function checkAgainstList(module,version,list) {
} }
function checkModuleAllowed(module,version,allowList,denyList) { function checkModuleAllowed(module,version,allowList,denyList) {
// console.log("checkModuleAllowed",module,version);//,allowList,denyList)
if (!allowList && !denyList) { if (!allowList && !denyList) {
// Default to allow // Default to allow
return true; return true;

View File

@ -187,35 +187,35 @@ function setFlows(_config,_credentials,type,muteLog,forceStart,user) {
}); });
} }
return configSavePromise
.then(function(flowRevision) {
if (!isLoad) {
log.debug("saved flow revision: "+flowRevision);
}
activeConfig = {
flows:config,
rev:flowRevision
};
activeFlowConfig = newFlowConfig;
if (forceStart || started) {
// Flows are running (or should be)
// Stop the active flows (according to deploy type and the diff) return configSavePromise.then(flowRevision => {
return stop(type,diff,muteLog).then(() => { if (!isLoad) {
// Once stopped, allow context to remove anything no longer needed log.debug("saved flow revision: "+flowRevision);
return context.clean(activeFlowConfig) }
}).then(() => { activeConfig = {
// Start the active flows flows:config,
start(type,diff,muteLog).then(() => { rev:flowRevision
events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); };
}); activeFlowConfig = newFlowConfig;
// Return the new revision asynchronously to the actual start if (forceStart || started) {
return flowRevision; // Flows are running (or should be)
}).catch(function(err) { })
} else { // Stop the active flows (according to deploy type and the diff)
events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); return stop(type,diff,muteLog).then(() => {
} // Once stopped, allow context to remove anything no longer needed
}); return context.clean(activeFlowConfig)
}).then(() => {
// Start the active flows
start(type,diff,muteLog).then(() => {
events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true});
});
// Return the new revision asynchronously to the actual start
return flowRevision;
}).catch(function(err) { })
} else {
events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true});
}
});
} }
function getNode(id) { function getNode(id) {
@ -246,7 +246,7 @@ function getFlows() {
return activeConfig; return activeConfig;
} }
function start(type,diff,muteLog) { async function start(type,diff,muteLog) {
type = type||"full"; type = type||"full";
started = true; started = true;
var i; var i;
@ -271,7 +271,21 @@ function start(type,diff,muteLog) {
log.info(" "+settings.userDir); log.info(" "+settings.userDir);
} }
events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true}); events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true});
return Promise.resolve(); return;
}
try {
await typeRegistry.checkFlowDependencies(activeConfig.flows);
} catch(err) {
log.info("Failed to load external modules required by this flow:");
const missingModules = [];
for (i=0;i<err.length;i++) {
let errMessage = err[i].error.toString()
missingModules.push({module:err[i].module.module, error: err[i].error.code || err[i].error.toString()})
log.info(` - ${err[i].module.spec} [${err[i].error.code || "unknown_error"}]`);
}
events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true});
return;
} }
// In safe mode, don't actually start anything, emit the necessary runtime event and return // In safe mode, don't actually start anything, emit the necessary runtime event and return
@ -280,7 +294,7 @@ function start(type,diff,muteLog) {
log.info(log._("nodes.flows.safe-mode")); log.info(log._("nodes.flows.safe-mode"));
log.info("*****************************************************************") log.info("*****************************************************************")
events.emit("runtime-event",{id:"runtime-state",payload:{error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true}); events.emit("runtime-event",{id:"runtime-state",payload:{error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true});
return Promise.resolve(); return;
} }
if (!muteLog) { if (!muteLog) {
@ -370,7 +384,7 @@ function start(type,diff,muteLog) {
log.info(log._("nodes.flows.started-flows")); log.info(log._("nodes.flows.started-flows"));
} }
} }
return Promise.resolve(); return;
} }
function stop(type,diff,muteLog) { function stop(type,diff,muteLog) {

View File

@ -271,6 +271,7 @@ function stop() {
}); });
} }
// This is the internal api // This is the internal api
var runtime = { var runtime = {
version: getVersion, version: getVersion,

View File

@ -81,7 +81,7 @@ function registerType(nodeSet,type,constructor,opts) {
} }
} }
} }
registry.registerType(nodeSet,type,constructor); registry.registerType(nodeSet,type,constructor,opts);
} }
/** /**
@ -261,5 +261,5 @@ module.exports = {
// Contexts // Contexts
loadContextsPlugin: context.load, loadContextsPlugin: context.load,
closeContextsPlugin: context.close, closeContextsPlugin: context.close,
listContextStores: context.listStores listContextStores: context.listStores,
}; };

View File

@ -247,6 +247,10 @@ module.exports = {
// jfive:require("johnny-five"), // jfive:require("johnny-five"),
// j5board:require("johnny-five").Board({repl:false}) // j5board:require("johnny-five").Board({repl:false})
}, },
// Allow the Function node to load additional npm modules
functionExternalModules: false,
// `global.keys()` returns a list of all properties set in global context. // `global.keys()` returns a list of all properties set in global context.
// This allows them to be displayed in the Context Sidebar within the editor. // This allows them to be displayed in the Context Sidebar within the editor.
// In some circumstances it is not desirable to expose them to the editor. The // In some circumstances it is not desirable to expose them to the editor. The

View File

@ -93,9 +93,6 @@ describe('function node', function() {
/*
it('should be loaded', function(done) { it('should be loaded', function(done) {
var flow = [{id:"n1", type:"function", name: "function" }]; var flow = [{id:"n1", type:"function", name: "function" }];
helper.load(functionNode, flow, function() { helper.load(functionNode, flow, function() {
@ -1417,6 +1414,86 @@ describe('function node', function() {
}); });
}); });
describe('externalModules', function() {
it('should fail if using OS module without functionExternalModules set to true', function(done) {
var flow = [
{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload = os.type(); return msg;", "libs": [{var:"os", module:"os"}]},
{id:"n2", type:"helper"}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
should.not.exist(n1);
done();
}).catch(err => done(err));
})
it('should fail if using OS module without it listed in libs', function(done) {
var flow = [
{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload = os.type(); return msg;"},
{id:"n2", type:"helper"}
];
helper.settings({
functionExternalModules: true
})
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var messageReceived = false;
n2.on("input", function(msg) {
messageReceived = true;
});
n1.receive({payload:"foo",topic: "bar"});
setTimeout(function() {
try {
messageReceived.should.be.false();
done();
} catch(err) {
done(err);
}
},20);
}).catch(err => done(err));
})
it('should require the OS module', function(done) {
var flow = [
{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload = os.type(); return msg;", "libs": [{var:"os", module:"os"}]},
{id:"n2", type:"helper"}
];
helper.settings({
functionExternalModules: true
})
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('topic', 'bar');
msg.should.have.property('payload', require('os').type());
done();
} catch(err) {
done(err);
}
});
n1.receive({payload:"foo",topic: "bar"});
}).catch(err => done(err));
})
it('should fail if module variable name clashes with sandbox builtin', function(done) {
var flow = [
{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload = os.type(); return msg;", "libs": [{var:"flow", module:"os"}]},
{id:"n2", type:"helper"}
];
helper.settings({
functionExternalModules: true
})
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
should.not.exist(n1);
done();
}).catch(err => done(err));
})
})
describe('Logger', function () { describe('Logger', function () {
function testLog(initCode,funcCode,expectedLevel, done) { function testLog(initCode,funcCode,expectedLevel, done) {
@ -1603,5 +1680,4 @@ describe('function node', function() {
}); });
}) })
*/
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "test-subflow-mod", "name": "test-subflow-mod",
"version": "1.0.1", "version": "1.0.2",
"description": "", "description": "",
"keywords": [], "keywords": [],
"license": "ISC", "license": "ISC",
@ -13,6 +13,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"node-red-node-random": "*" "node-red-node-random": "*",
"cowsay2": "*"
} }
} }

View File

@ -189,6 +189,7 @@
"noerr": 0, "noerr": 0,
"initialize": "", "initialize": "",
"finalize": "", "finalize": "",
"libs": [ {"var":"cowsay2","module":"cowsay2"}],
"x": 240, "x": 240,
"y": 100, "y": 100,
"wires": [ "wires": [

Binary file not shown.

View File

@ -0,0 +1,302 @@
// init: init,
// register: register,
// registerSubflow: registerSubflow,
// checkFlowDependencies: checkFlowDependencies,
// require: requireModule
//
const should = require("should");
const sinon = require("sinon");
const fs = require("fs-extra");
const path = require("path");
const os = require("os");
const NR_TEST_UTILS = require("nr-test-utils");
const externalModules = NR_TEST_UTILS.require("@node-red/registry/lib/externalModules");
const exec = NR_TEST_UTILS.require("@node-red/util/lib/exec");
let homeDir;
async function createUserDir() {
if (!homeDir) {
homeDir = path.join(os.tmpdir(),"nr-test-"+Math.floor(Math.random()*100000));
}
await fs.ensureDir(homeDir);
}
async function setupExternalModulesPackage(dependencies) {
await fs.ensureDir(path.join(homeDir,"externalModules"))
await fs.writeFile(path.join(homeDir,"externalModules","package.json"),`{
"name": "Node-RED-External-Modules",
"description": "These modules are automatically installed by Node-RED to use in Function nodes.",
"version": "1.0.0",
"private": true,
"dependencies": ${JSON.stringify(dependencies)}
}`)
}
describe("externalModules api", function() {
beforeEach(async function() {
await createUserDir()
})
afterEach(async function() {
await fs.remove(homeDir);
})
describe("checkFlowDependencies", function() {
beforeEach(function() {
sinon.stub(exec,"run", async function(cmd, args, options) {
let error;
if (args[1] === "moduleNotFound") {
error = new Error();
error.stderr = "E404";
} else if (args[1] === "moduleVersionNotFound") {
error = new Error();
error.stderr = "ETARGET";
} else if (args[1] === "moduleFail") {
error = new Error();
error.stderr = "Some unexpected install error";
}
if (error) {
throw error;
}
})
})
afterEach(function() {
exec.run.restore();
})
it("does nothing when no types are registered",async function() {
externalModules.init({userDir: homeDir});
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]}
])
exec.run.called.should.be.false();
});
it("skips install for modules already installed", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
await setupExternalModulesPackage({"foo": "1.2.3", "bar":"2.3.4"});
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]}
])
exec.run.called.should.be.false();
})
it("skips install for built-in modules", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "fs"}]}
])
exec.run.called.should.be.false();
})
it("installs missing modules", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
fs.existsSync(path.join(homeDir,"externalModules")).should.be.false();
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]}
])
exec.run.called.should.be.true();
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
})
it("installs missing modules from inside subflow module", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
externalModules.registerSubflow("sf", {"flow":[{type: "function", libs:[{module: "foo"}]}]});
await externalModules.checkFlowDependencies([
{type: "sf"}
])
exec.run.called.should.be.true();
})
it("reports install fail - 404", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "moduleNotFound"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.called.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","moduleNotFound");
err[0].should.have.property("error");
err[0].error.should.have.property("code",404);
}
})
it("reports install fail - target", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "moduleVersionNotFound"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.called.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","moduleVersionNotFound");
err[0].should.have.property("error");
err[0].error.should.have.property("code",404);
}
})
it("reports install fail - unexpected", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "moduleFail"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.called.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","moduleFail");
err[0].should.have.property("error");
err[0].error.should.have.property("code","unexpected_error");
}
})
it("reports install fail - multiple", async function() {
externalModules.init({userDir: homeDir});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "moduleNotFound"},{module: "moduleFail"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.called.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(2);
// Sort the array so we know the order to test for
err.sort(function(A,B) {
return A.module.module.localeCompare(B.module.module);
})
err[1].should.have.property("module");
err[1].module.should.have.property("module","moduleNotFound");
err[1].should.have.property("error");
err[1].error.should.have.property("code",404);
err[0].should.have.property("module");
err[0].module.should.have.property("module","moduleFail");
err[0].should.have.property("error");
err[0].error.should.have.property("code","unexpected_error");
}
})
it("reports install fail - install disabled", async function() {
externalModules.init({userDir: homeDir, externalModules: {
modules: {
allowInstall: false
}
}});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
// Should not try to install
exec.run.called.should.be.false();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","foo");
err[0].should.have.property("error");
err[0].error.should.have.property("code","install_not_allowed");
}
})
it("reports install fail - module disallowed", async function() {
externalModules.init({userDir: homeDir, externalModules: {
modules: {
denyList: ['foo']
}
}});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
// foo disallowed
// bar allowed
{type: "function", libs:[{module: "foo"},{module: "bar"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.calledOnce.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","foo");
err[0].should.have.property("error");
err[0].error.should.have.property("code","install_not_allowed");
}
})
it("reports install fail - built-in module disallowed", async function() {
externalModules.init({userDir: homeDir, externalModules: {
modules: {
denyList: ['fs']
}
}});
externalModules.register("function", "libs");
try {
await externalModules.checkFlowDependencies([
// foo disallowed
// bar allowed
{type: "function", libs:[{module: "fs"},{module: "bar"}]}
])
throw new Error("checkFlowDependencies did not reject after install fail")
} catch(err) {
exec.run.calledOnce.should.be.true();
Array.isArray(err).should.be.true();
err.should.have.length(1);
err[0].should.have.property("module");
err[0].module.should.have.property("module","fs");
err[0].should.have.property("error");
err[0].error.should.have.property("code","module_not_allowed");
}
})
})
describe("require", async function() {
it("requires built-in modules", async function() {
externalModules.init({userDir: homeDir});
const result = externalModules.require("fs")
result.should.eql(require("fs"));
})
it("rejects unknown modules", async function() {
externalModules.init({userDir: homeDir});
try {
externalModules.require("foo")
throw new Error("require did not reject after fail")
} catch(err) {
err.should.have.property("code","module_not_allowed");
}
})
it("rejects disallowed modules", async function() {
externalModules.init({userDir: homeDir, externalModules: {
modules: {
denyList: ['fs']
}
}});
try {
externalModules.require("fs")
throw new Error("require did not reject after fail")
} catch(err) {
err.should.have.property("code","module_not_allowed");
}
})
})
});

View File

@ -40,7 +40,7 @@ describe('red/registry/index', function() {
stubs.push(sinon.stub(loader,"init")); stubs.push(sinon.stub(loader,"init"));
stubs.push(sinon.stub(typeRegistry,"init")); stubs.push(sinon.stub(typeRegistry,"init"));
registry.init({}); registry.init({settings:{}});
installer.init.called.should.be.true(); installer.init.called.should.be.true();
loader.init.called.should.be.true(); loader.init.called.should.be.true();
typeRegistry.init.called.should.be.true(); typeRegistry.init.called.should.be.true();

View File

@ -36,6 +36,7 @@ describe('flows/index', function() {
var flowCreate; var flowCreate;
var getType; var getType;
var checkFlowDependencies;
var mockLog = { var mockLog = {
log: sinon.stub(), log: sinon.stub(),
@ -52,9 +53,16 @@ describe('flows/index', function() {
getType = sinon.stub(typeRegistry,"get",function(type) { getType = sinon.stub(typeRegistry,"get",function(type) {
return type.indexOf('missing') === -1; return type.indexOf('missing') === -1;
}); });
checkFlowDependencies = sinon.stub(typeRegistry, "checkFlowDependencies", async function(flow) {
if (flow[0].id === "node-with-missing-modules") {
throw new Error("Missing module");
}
});
}); });
after(function() { after(function() {
getType.restore(); getType.restore();
checkFlowDependencies.restore();
}); });
@ -306,7 +314,7 @@ describe('flows/index', function() {
flows.init({log:mockLog, settings:{},storage:storage}); flows.init({log:mockLog, settings:{},storage:storage});
flows.load().then(function() { flows.load().then(function() {
flows.startFlows(); return flows.startFlows();
}); });
}); });
it('does not start if nodes missing', function(done) { it('does not start if nodes missing', function(done) {
@ -321,9 +329,14 @@ describe('flows/index', function() {
flows.init({log:mockLog, settings:{},storage:storage}); flows.init({log:mockLog, settings:{},storage:storage});
flows.load().then(function() { flows.load().then(function() {
flows.startFlows(); return flows.startFlows();
flowCreate.called.should.be.false(); }).then(() => {
done(); try {
flowCreate.called.should.be.false();
done();
} catch(err) {
done(err);
}
}); });
}); });
@ -339,9 +352,9 @@ describe('flows/index', function() {
} }
flows.init({log:mockLog, settings:{},storage:storage}); flows.init({log:mockLog, settings:{},storage:storage});
flows.load().then(function() { flows.load().then(function() {
flows.startFlows(); return flows.startFlows();
}).then(() => {
flowCreate.called.should.be.false(); flowCreate.called.should.be.false();
events.emit("type-registered","missing"); events.emit("type-registered","missing");
setTimeout(function() { setTimeout(function() {
flowCreate.called.should.be.false(); flowCreate.called.should.be.false();
@ -354,7 +367,44 @@ describe('flows/index', function() {
}); });
}); });
it('does not start if external modules missing', function(done) {
var originalConfig = [
{id:"node-with-missing-modules",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
storage.getFlows = function() {
return Promise.resolve({flows:originalConfig});
}
var receivedEvent = null;
var handleEvent = function(payload) {
receivedEvent = payload;
}
events.on("runtime-event",handleEvent);
//{id:"runtime-state",payload:{error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true});"
flows.init({log:mockLog, settings:{},storage:storage});
flows.load().then(flows.startFlows).then(() => {
events.removeListener("runtime-event",handleEvent);
try {
flowCreate.called.should.be.false();
receivedEvent.should.have.property('id','runtime-state');
receivedEvent.should.have.property('payload',
{ error: 'missing-modules',
type: 'warning',
text: 'notification.warnings.missing-modules',
modules: [] }
);
done();
}catch(err) {
done(err)
}
});
});
}); });