Rework Function node module integration

This commit is contained in:
Nick O'Leary 2021-02-12 18:14:13 +00:00
parent 4a1d66f210
commit 9c09ee3b71
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
20 changed files with 800 additions and 1384 deletions

View File

@ -67,11 +67,6 @@ module.exports = {
adminApp.get(/^\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.read"),nodes.getSet,apiUtil.errorHandler);
adminApp.put(/^\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.write"),nodes.putSet,apiUtil.errorHandler);
// NPM Modules
adminApp.get("/modules", needsPermission("nodes.read"), nodes.listNPMModules, apiUtil.errorHandler);
adminApp.delete("/modules/:spec", needsPermission("nodes.write"), nodes.uninstallNPMModule, apiUtil.errorHandler);
adminApp.post("/modules", needsPermission("nodes.write"), nodes.updateNPMModule, apiUtil.errorHandler);
// Context
adminApp.get("/context/:scope(global)",needsPermission("context.read"),context.get,apiUtil.errorHandler);
adminApp.get("/context/:scope(global)/*",needsPermission("context.read"),context.get,apiUtil.errorHandler);

View File

@ -191,45 +191,5 @@ module.exports = {
runtimeAPI.nodes.getIconList(opts).then(function(list) {
res.json(list);
});
},
listNPMModules: function(req, res) {
var opts = {
user: req.user,
req: apiUtils.getRequestLogObject(req)
}
runtimeAPI.nodes.listNPMModules(opts).then(function(list) {
res.json(list);
}).catch(err => {
apiUtils.rejectHandler(req,res,err);
});;
},
uninstallNPMModule: function(req, res) {
var opts = {
user: req.user,
spec: req.params.spec,
req: apiUtils.getRequestLogObject(req)
}
runtimeAPI.nodes.uninstallNPMModule(opts).then(function(result) {
res.json(result);
}).catch(err => {
apiUtils.rejectHandler(req,res,err);
});
},
updateNPMModule: function(req, res) {
var body = req.body;
var opts = {
user: req.user,
spec: body.spec,
update: body.update,
req: apiUtils.getRequestLogObject(req)
}
runtimeAPI.nodes.updateNPMModule(opts).then(function(result) {
res.json(result);
}).catch(err => {
apiUtils.rejectHandler(req,res,err);
});
}
};

View File

@ -142,6 +142,7 @@
"nodeActionDisabled": "node actions disabled",
"nodeActionDisabledSubflow": "node actions disabled within subflow",
"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>",
"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>",

View File

@ -248,6 +248,7 @@ var RED = (function() {
id: notificationId
}
if (notificationId === "runtime-state") {
RED.events.emit("runtime-state",msg);
if (msg.error === "safe-mode") {
options.buttons = [
{
@ -280,6 +281,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") {
if (RED.settings.theme("projects.enabled",false)) {
// projects enabled
@ -370,6 +381,9 @@ var RED = (function() {
} else if (persistentNotifications.hasOwnProperty(notificationId)) {
persistentNotifications[notificationId].close();
delete persistentNotifications[notificationId];
if (notificationId === 'runtime-state') {
RED.events.emit("runtime-state",msg);
}
}
});
RED.comms.subscribe("status/#",function(topic,msg) {

View File

@ -509,8 +509,8 @@ RED.tabs = (function() {
li.attr('id',"red-ui-tab-"+(tab.id.replace(".","-")));
li.data("tabId",tab.id);
if (options.maximumTabWidth) {
li.css("maxWidth",options.maximumTabWidth+"px");
if (options.maximumTabWidth || tab.maximumTabWidth) {
li.css("maxWidth",(options.maximumTabWidth || tab.maximumTabWidth) +"px");
}
var link = $("<a/>",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li);
if (tab.icon) {

View File

@ -331,7 +331,7 @@ RED.palette.editor = (function() {
if (v.modules) {
var a = false;
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;
m.index = [m.id];
if (m.keywords) {
@ -445,68 +445,6 @@ RED.palette.editor = (function() {
var installAllowList = ['*'];
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() {
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
return;
@ -517,8 +455,8 @@ RED.palette.editor = (function() {
installAllowList = settingsAllowList;
installDenyList = settingsDenyList
}
installAllowList = parseModuleList(installAllowList);
installDenyList = parseModuleList(installDenyList);
installAllowList = RED.utils.parseModuleList(installAllowList);
installDenyList = RED.utils.parseModuleList(installDenyList);
createSettingsPane();

View File

@ -1122,6 +1122,67 @@ RED.utils = (function() {
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 {
createObjectElement: buildMessageElement,
getMessageProperty: getMessageProperty,
@ -1141,6 +1202,8 @@ RED.utils = (function() {
sanitize: sanitize,
renderMarkdown: renderMarkdown,
createNodeIcon: createNodeIcon,
getDarkerColor: getDarkerColor
getDarkerColor: getDarkerColor,
parseModuleList: parseModuleList,
checkModuleAllowed: checkModuleAllowed
}
})();

View File

@ -143,6 +143,13 @@
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 {
font-family: $monospace-font;
font-size: $primary-font-size;

View File

@ -1,20 +1,67 @@
<script type="text/html" data-template-name="function">
<div class="form-row">
<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>
#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;
}
<div class="form-row">
.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" style="margin-top: -20px">
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
</div>
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
<div id="func-tab-init" style="display:none">
<div class="form-row" style="margin-bottom: 0px;">
<input type="hidden" id="node-input-initialize" autofocus="autofocus">
<div id="func-tab-config" style="display:none">
<div class="form-row">
<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>
<div class="form-row">
<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">
</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="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>
@ -22,46 +69,77 @@
</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>
<input id="node-input-outputs" style="width: 60px;" value="1">
</div>
</div>
<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 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; margin-top: 30px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
</div>
</div>
<div id="func-tab-config" style="display:none">
<div class="form-row" style="margin-bottom: 0px;">
<label><i class="fa fa-wrench"></i> <span>Use Library</span></label>
</div>
<div class="form-row" style="height: 250px; min-height: 150px">
<ol id="node-input-libs-container"></ol>
</div>
</div>
</div>
</script>
<script type="text/javascript">
(function() {
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 = [];
@ -73,523 +151,123 @@
return [module, undefined];
}
/**
* Add library descriptor
* @param {string} name - library name
*/
function addLib(info) {
allLibs.push(info);
}
/**
* Validate library spec including conflicts with other specs
* @param {string} name - library name
*/
function checkLib(spec) {
var [name, ver] = moduleName(spec);
var libs = $("#node-input-libs-container").editableList("items");
var count = 0;
libs.each(function(i) {
if (count > 1) {
return;
}
var item = $(this);
var n = item.find(".node-input-libs-val").val();
if (n && (n !== "")) {
var [name1, ver1] = moduleName(n);
if (name1 === name) {
count++;
}
}
});
return (count <= 1);
}
/**
* Get library list
*/
function getModules() {
var root = RED.settings.apiRootUrl || "";
var promise = $.ajax({
url: root + "/modules",
type: "GET",
cache: false
});
return promise;
}
/**
* Check and update in progress state of modules
*/
function checkInProgress(allLibs) {
var waitTime = 1000;
function update() {
map = {};
allLibs.forEach(function (lib) {
if (lib.inProgress) {
map[lib.name] = lib;
}
});
getModules().then(function (data,textStatus,xhr) {
var count = 0;
data.forEach(lib => {
var name = lib.name;
if (lib.inProgress) {
count++;
}
else if (name in map) {
var lib2 = map[name];
if (lib2) {
delete map[name];
var cb = lib2.onReady;
if (cb) {
lib2.version = lib.version;
lib2.status["installed"] = true;
lib2.inProgress = false;
cb();
}
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);
}
}
});
if (count > 0) {
setTimeout(update, waitTime);
}
});
}
}
setTimeout(update, waitTime);
}
/**
* Update library list
*/
function updateLib(node, done) {
var names = {};
var specs = {};
var libs = node.libs || [];
allLibs = [];
libs.forEach(lib => {
var vname = lib.vname;
var spec = lib.spec;
var [name, ver] = moduleName(spec);
var item = {
vname: vname,
name: name,
spec: spec,
status: { "edit":true }
};
addLib(item);
specs[spec] = true;
names[name] = true;
});
RED.nodes.eachNode(function(n) {
if ((n.type === "function") && (n !== node)) {
var libs = n.libs || [];
libs.forEach(lib => {
var spec = lib.spec;
var [name, ver] = moduleName(spec);
if (name in names) {
var items = allLibs.filter(item => (item.name === name));
items.forEach(item => {
item.status["used"] = true;
});
if (spec in specs) {
// olready found
return;
}
// spec conflict
}
var item = {
name: name,
spec: spec,
status: { "used": true }
};
addLib(item);
names[name] = true;
specs[spec] = true;
});
}
});
var inProgressCount = 0;
getModules().then(function(data,textStatus,xhr) {
data.forEach(lib => {
var name = lib.name;
var version = lib.version;
var preinstalled = (lib.status == "preinstalled");
var spec = lib.spec;
var items = allLibs.filter(item => (item.name === name));
if (items.length > 0) {
items.forEach(item => {
item.version = version;
item.status["installed"] = true;
item.status["preinstalled"] = preinstalled;
item.inProgress = lib.inProgress;
});
}
else {
var item = {
name: name,
spec: spec,
version: version,
status: {
"installed": true,
"preinstalled": preinstalled
},
inProgress: lib.inProgress
};
addLib(item);
names[name] = true;
specs[spec] = true;
}
if (lib.inProgress) {
inProgressCount++;
}
});
$("#node-input-libs-container").editableList("empty");
allLibs.forEach(function(lib) {
$("#node-input-libs-container").editableList("addItem", lib);
});
if (inProgressCount > 0) {
checkInProgress(allLibs);
}
if (done) {
done();
}
});
}
/**
* Uninstall NPM module
*/
function uninstallModule(node, spec, name, version,
moduleInfo, installButton,
installInfoRow, installedInfo,
removeButton,
progressRow,
done) {
var root = RED.settings.apiRootUrl || "";
progressRow.show();
$.ajax({
url: root + "/modules/" +spec,
type: "DELETE",
}).then(function(data,textStatus,xhr) {
moduleInfo.attr("disabled", false);
installButton.attr("disabled", false);
installInfoRow.hide();
installedInfo.val("");
removeButton.show();
progressRow.hide();
RED.notify("Successfully uninstalled:" +name);
if(done) {
done();
}
}).fail(function(xhr,textStatus,err) {
progressRow.hide();
RED.notify("Failed to uninstall: " +name);
if (done) {
done();
}
});
}
/**
* Updaate or Install NPM module
*/
function updateModule(node, spec, update,
moduleInfo, installButton,
installInfoRow, installedInfo, removeButton,
progressRow,
done) {
var [name, version] = moduleName(spec);
var root = RED.settings.apiRootUrl || "";
var errorMessage = "failed to "+(update ? "update": "install") +": " +name;
progressRow.show();
$.ajax({
url: root + "/modules",
type: "POST",
contentType: "application/json; charset=utf-8",
cache: false,
data: JSON.stringify({
spec: spec,
update: update
})
}).then(function(data) {
getModules().then(function(data,textStatus,xhr) {
var item = data.find(lib => (lib.name === name));
progressRow.hide();
if (item) {
moduleInfo.attr("disabled", true);
installButton.attr("disabled", true);
installInfoRow.show();
installedInfo.val(item.name+"@"+item.version);
removeButton.hide();
var msg = "Successfully "
+(update ? "updated": "installed")
+": " +name;
RED.notify(msg);
}
if (done) {
done();
}
}).fail(function(xhr,textStatus,err) {
progressRow.hide();
RED.notify(errorMessage);
if (done) {
done();
}
});
}).fail(function(xhr,textStatus,err) {
progressRow.hide();
RED.notify(errorMessage);
if (done) {
done();
}
});
}
/**
*
*/
function withNotification(msg, text, cb) {
var notification = RED.notify(msg, {
modal: true,
fixed: true,
buttons: [
{
text: RED._("common.label.cancel"),
click: function() {
notification.close();
}
},
{
text: text,
class: "primary red-ui-palette-module-install-confirm-button-update",
click: function() {
notification.close();
cb();
}
},
]
});
var modules = Array.from(moduleSet);
modules.sort();
return modules;
}
function prepareLibraryConfig(node) {
$("#node-input-libs-container").css('min-height','250px').css('min-width','450px').editableList({
$(".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 removeButton = parent.find(".red-ui-editableList-item-remove");
var disabled = (opt && !!opt.unused);
var row0 = $("<div/>").appendTo(container);
var fvar = $("<input/>", {
class: "node-input-libs-var",
placeholder: RED._("node-red:function.require.var"),
type: "text",
disabled: disabled
}).css({
width: "90px",
"margin-left": "5px"
}).appendTo(row0);
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);
$('<code> = require(</code>').appendTo(row0);
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text",
disabled: disabled
type: "text"
}).css({
width: fieldWidth,
"margin-left": "5px"
}).appendTo(row0);
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);
}
var buttonWidth = "70px";
var install = $("<button/>", {
class: "red-ui-button red-ui-button-small"
}).css({
"margin-left": "10px",
width: buttonWidth
}).appendTo(row0);;
$("<i/>", {
class: "fa fa-cube"
}).appendTo(install);
$("<span/>").css({
"margin-left": "5px"
}).text("Install").appendTo(install);
$('<code>)</code>').appendTo(row0);
var warn = $("<span/>").css({
"margin-left": "10px"
}).appendTo(row0);
$("<i/>", {
class: "fa fa-warning"
}).appendTo(warn);
RED.popover.tooltip(warn, "module spec conflict");
warn.hide();
var row1 = $("<div/>").css({
"margin-top": "5px"
}).appendTo(container);
var linstalled = $("<label/>").css({
width: "90px",
"text-align": "right",
"margin-right": "10px"
}).appendTo(row1);
$("<i/>", {
class: "fa fa-cube"
}).appendTo(linstalled);
var status = "Installed";
$("<span/>").css({
"margin-left": "5px"
}).text(status).appendTo(linstalled);
var finstalled = $("<input/>", {
type: "text",
disabled: true
}).css({
width: fieldWidth
}).appendTo(row1);
var uninstall = $("<button/>", {
class: "red-ui-button red-ui-button-small"
}).css({
"margin-left": "10px",
width: buttonWidth
}).appendTo(row1);
$("<i/>", {
class: "fa fa-trash"
}).appendTo(uninstall);
$("<span/>").css({
"margin-left": "5px"
}).text("Uninstall").appendTo(uninstall);
var update = $("<button/>", {
class: "red-ui-button red-ui-button-small"
}).css({
"margin-left": "10px",
width: buttonWidth
}).appendTo(row1);
$("<i/>", {
class: "fa fa-refresh"
}).appendTo(update);
$("<span/>").css({
"margin-left": "5px"
}).text("Update").appendTo(update);
var row2 = $("<div/>", {
class: "red-ui-palette-module-shade"
}).appendTo(container);
$("<img/>", {
src: "red/images/spin.svg",
class: "red-ui-palette-spinner"
}).appendTo(row2);
row2.hide();
var warning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
RED.popover.tooltip(warning.find("i"),function() {
var val = fmodule.typedInput("type");
if (val === "_custom_") {
val = fmodule.val();
}
if (!RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList)) {
return "Module not allowed"
} else {
return "Module not installed: "+missingModuleReasons[val]
}
})
fvar.on("change", function (e) {
opt.vname = $(this).val();
var v = $(this).val().trim();
if (v === "" || / /.test(v)) {
fvar.addClass("input-error");
} else {
fvar.removeClass("input-error");
}
});
fmodule.on("change", function (e) {
var val = $(this).val();
if (!checkLib(val, opt.id)) {
$(this).addClass("input-error");
warn.show();
var val = $(this).typedInput("type");
if (val === "_custom_") {
val = $(this).val();
}
else {
$(this).removeClass("input-error");
warn.hide();
if (val && (val !== "") &&
(!opt.version || (opt.version === ""))) {
install.attr("disabled", false);
}
}
opt.spec = val;
});
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/]./g, function(v) { return v[1].toUpperCase() });
fvar.val(varName);
fvar.trigger("change");
install.on("click", function (e) {
withNotification(
"Install NPM Module " +opt.spec,
"Install",
function (cb) {
updateModule(
node, opt.spec, false,
fmodule, install,
row1, finstalled, removeButton,
row2,
cb);
});
});
uninstall.on("click", function (e) {
withNotification(
"Uninstall NPM Module "+opt.spec,
"Uninstall",
function (cb) {
uninstallModule(
node, opt.spec, opt.name, opt.version,
fmodule, install,
row1, finstalled,
removeButton,
row2,
cb);
});
});
update.on("click", function (e) {
withNotification(
"Update NPM Module " +opt.spec,
"Update",
function (cb) {
updateModule(
node, opt.spec, true,
fmodule, install,
row1, finstalled, removeButton,
row2,
cb);
});
});
install.attr("disabled", true);
if (opt) {
function updateData(data) {
if (data.vname && (data.vname !== "")) {
fvar.val(data.vname);
}
if (data.spec && (data.spec !== "")) {
fmodule.val(data.spec);
}
if (data.version && (data.version !== "")) {
fmodule.prop("disabled", true);
row1.show();
removeButton.hide();
finstalled.val(data.name+"@"+data.version)
if (data.status["preinstalled"]) {
uninstall.attr("disabled", true);
update.attr("disabled", true);
}
}
else {
install.attr("disabled", false);
row1.hide();
removeButton.show();
}
}
updateData(opt);
if (opt.inProgress) {
row2.show();
opt.onReady = function () {
row2.hide();
updateData(opt);
opt.onReady = null;
};
if (RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList) && (missingModules.indexOf(val) === -1)) {
fmodule.removeClass("input-error");
warning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
warning.addClass("input-error");
}
});
if (RED.utils.checkModuleAllowed(opt.module,null,installAllowList,installDenyList) && (missingModules.indexOf(opt.module) === -1)) {
fmodule.removeClass("input-error");
warning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
warning.addClass("input-error");
}
if (opt.var) {
fvar.trigger("change");
}
},
removable: true,
sortable: true
});
updateLib(node, function () {
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',{
@ -602,7 +280,22 @@
noerr: {value:0,required:true,validate:function(v) { return !v; }},
initialize: {value:""},
finalize: {value:""},
libs: {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;
}
}
return true;
}}
},
inputs:1,
outputs:1,
@ -623,21 +316,24 @@
$("#" + tab.id).show();
}
});
tabs.addTab({
id: "func-tab-config",
iconClass: "fa fa-cog",
maximumTabWidth: 38
});
tabs.addTab({
id: "func-tab-init",
label: that._("function.label.initialize")
label: "On Start", //that._("function.label.initialize")
});
tabs.addTab({
id: "func-tab-body",
label: that._("function.label.function")
label: "On Message"//that._("function.label.function")
});
tabs.addTab({
id: "func-tab-finalize",
label: that._("function.label.finalize")
});
tabs.addTab({
id: "func-tab-config",
label: "Config"
label: "On Stop"//that._("function.label.finalize")
});
tabs.activateTab("func-tab-body");
@ -748,7 +444,9 @@
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"));
prepareLibraryConfig(that);
if (RED.settings.functionExternalModules !== false) {
prepareLibraryConfig(that);
}
},
oneditsave: function() {
var node = this;
@ -780,24 +478,28 @@
$("#node-input-noerr").val(noerr);
this.noerr = noerr;
var libs = $("#node-input-libs-container").editableList("items");
var oldLibs = node.libs || [];
var newLibs = [];
node.libs = newLibs;
libs.each(function(i) {
var item = $(this);
var v = item.find(".node-input-libs-var").val();
var n = item.find(".node-input-libs-val").val();
if ((!v || (v === "")) ||
(!n || (n === ""))) {
return;
}
newLibs.push({
vname: v,
spec: n
if (RED.settings.functionExternalModules !== false) {
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() {
var node = this;
@ -823,15 +525,16 @@
this.editor.resize();
var height = size.height;
$("#node-input-init-editor").css("height", (height -105)+"px");
$("#node-input-func-editor").css("height", (height -145)+"px");
$("#node-input-finalize-editor").css("height", (height -105)+"px");
$("#node-input-init-editor").css("height", (height -45)+"px");
$("#node-input-func-editor").css("height", (height -45)+"px");
$("#node-input-finalize-editor").css("height", (height -45)+"px");
this.initEditor.resize();
this.editor.resize();
this.finalizeEditor.resize();
$("#node-input-libs-container").css("height", (height -155)+"px");
$("#node-input-libs-container").css("height", (height - 185)+"px");
}
});
})();
</script>

View File

@ -91,13 +91,17 @@ module.exports = function(RED) {
function FunctionNode(n) {
var libs = n.libs || [];
n.modules = libs.map(x => x.spec).filter(x => (x && (x !== "")));
var loadPromise = RED.nodes.createNode(this,n);
RED.nodes.createNode(this,n);
var node = this;
node.name = n.name;
node.func = n.func;
node.ini = n.initialize ? n.initialize.trim() : "";
node.fin = n.finalize ? n.finalize.trim() : "";
node.libs = libs;
node.libs = libs || [];
if (RED.settings.functionExternalModules === false && node.libs.length > 0) {
throw new Error("Function node not allowed to load external modules");
}
var handleNodeDoneCall = true;
@ -287,173 +291,173 @@ module.exports = function(RED) {
}
});
// wait for module installation
loadPromise.catch(()=>{
}).finally(function () {
if (node.hasOwnProperty("libs")) {
var modules = node.libs;
modules.forEach(module => {
var vname = module.hasOwnProperty("vname") ? module.vname : null;
if (vname && (vname !== "")) {
sandbox[vname] = null;
try {
var spec = module.spec;
if (spec && (spec !== "")) {
var lib = RED.require(module.spec);
sandbox[vname] = lib;
if (node.hasOwnProperty("libs")) {
var modules = node.libs;
modules.forEach(module => {
var vname = module.hasOwnProperty("var") ? module.var : null;
if (vname && (vname !== "")) {
sandbox[vname] = null;
try {
var spec = module.spec;
if (spec && (spec !== "")) {
var lib = RED.require(module.spec);
sandbox[vname] = lib;
}
}
catch (e) {
console.log(e);
node.warn("failed to load library: "+ module.spec);
}
}
});
}
var context = vm.createContext(sandbox);
try {
var iniScript = null;
var iniOpt = null;
if (node.ini && (node.ini !== "")) {
var iniText = `
(async function(__send__) {
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__.send(__send__, RED.util.generateId(), msgs, cloneMsg);
}
};
`+ node.ini +`
})(__initSend__);`;
iniOpt = createVMOpt(node, " setup");
iniScript = new vm.Script(iniText, iniOpt);
}
node.script = vm.createScript(functionText, createVMOpt(node, ""));
if (node.fin && (node.fin !== "")) {
var finText = "(function () {\n"+node.fin +"\n})();";
finOpt = createVMOpt(node, " cleanup");
finScript = new vm.Script(finText, finOpt);
}
var promise = Promise.resolve();
if (iniScript) {
context.__initSend__ = function(msgs) { node.send(msgs); };
promise = iniScript.runInContext(context, iniOpt);
}
processMessage = function (msg, send, done) {
var start = process.hrtime();
context.msg = msg;
context.__send__ = send;
context.__done__ = done;
node.script.runInContext(context);
context.results.then(function(results) {
sendResults(node,send,msg._msgid,results,false);
if (handleNodeDoneCall) {
done();
}
var duration = process.hrtime(start);
var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100;
node.metric("duration", msg, converted);
if (process.env.NODE_RED_FUNCTION_TIME) {
node.status({fill:"yellow",shape:"dot",text:""+converted});
}
}).catch(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+")";
}
}
}
catch (e) {
node.warn("failed to load library: "+ module.spec);
if (!errorMessage) {
errorMessage = err.toString();
}
done(errorMessage);
}
else if (typeof err === "string") {
done(err);
}
else {
done(JSON.stringify(err));
}
});
}
var context = vm.createContext(sandbox);
try {
var iniScript = null;
var iniOpt = null;
if (node.ini && (node.ini !== "")) {
var iniText = `
(async function(__send__) {
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__.send(__send__, RED.util.generateId(), msgs, cloneMsg);
}
};
`+ node.ini +`
})(__initSend__);`;
iniOpt = createVMOpt(node, " setup");
iniScript = new vm.Script(iniText, iniOpt);
node.on("close", function() {
if (finScript) {
try {
finScript.runInContext(context, finOpt);
}
catch (err) {
node.error(err);
}
}
node.script = vm.createScript(functionText, createVMOpt(node, ""));
if (node.fin && (node.fin !== "")) {
var finText = "(function () {\n"+node.fin +"\n})();";
finOpt = createVMOpt(node, " cleanup");
finScript = new vm.Script(finText, finOpt);
while (node.outstandingTimers.length > 0) {
clearTimeout(node.outstandingTimers.pop());
}
var promise = Promise.resolve();
if (iniScript) {
context.__initSend__ = function(msgs) { node.send(msgs); };
promise = iniScript.runInContext(context, iniOpt);
while (node.outstandingIntervals.length > 0) {
clearInterval(node.outstandingIntervals.pop());
}
if (node.clearStatus) {
node.status({});
}
});
processMessage = function (msg, send, done) {
var start = process.hrtime();
context.msg = msg;
context.__send__ = send;
context.__done__ = done;
node.script.runInContext(context);
context.results.then(function(results) {
sendResults(node,send,msg._msgid,results,false);
if (handleNodeDoneCall) {
done();
}
var duration = process.hrtime(start);
var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100;
node.metric("duration", msg, converted);
if (process.env.NODE_RED_FUNCTION_TIME) {
node.status({fill:"yellow",shape:"dot",text:""+converted});
}
}).catch(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));
}
promise.then(function (v) {
var msgs = messages;
messages = [];
while (msgs.length > 0) {
msgs.forEach(function (s) {
processMessage(s.msg, s.send, s.done);
});
msgs = messages;
messages = [];
}
state = RESOLVED;
}).catch((error) => {
messages = [];
state = ERROR;
node.error(error);
});
node.on("close", function() {
if (finScript) {
try {
finScript.runInContext(context, finOpt);
}
catch (err) {
node.error(err);
}
}
while (node.outstandingTimers.length > 0) {
clearTimeout(node.outstandingTimers.pop());
}
while (node.outstandingIntervals.length > 0) {
clearInterval(node.outstandingIntervals.pop());
}
if (node.clearStatus) {
node.status({});
}
});
promise.then(function (v) {
var msgs = messages;
messages = [];
while (msgs.length > 0) {
msgs.forEach(function (s) {
processMessage(s.msg, s.send, s.done);
});
msgs = messages;
messages = [];
}
state = RESOLVED;
}).catch((error) => {
messages = [];
state = ERROR;
node.error(error);
});
}
catch(err) {
// eg SyntaxError - which v8 doesn't include line number information
// so we can't do better than this
updateErrorInfo(err);
node.error(err);
}
});
}
catch(err) {
// eg SyntaxError - which v8 doesn't include line number information
// so we can't do better than this
updateErrorInfo(err);
node.error(err);
}
}
RED.nodes.registerType("function",FunctionNode, {
dynamicModuleList: "modules"
dynamicModuleList: "libs",
settings: {
functionExternalModules: { value: true, exportable: true }
}
});
RED.library.register("functions");
};

View File

@ -220,7 +220,7 @@
"finalize": "// Code added here will be run when the\n// node is being stopped or re-deployed.\n"
},
"require": {
"var": "name",
"var": "variable",
"module": "module"
},
"error": {

View File

@ -0,0 +1,209 @@
// 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 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 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;
path.resolve(settings.userDir || process.env.NODE_RED_HOME || ".");
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 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) {
await refreshExternalModules();
const checkedModules = {};
const promises = [];
const errors = [];
flowConfig.forEach(n => {
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(() => {
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",
"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;
var lookForVersionNotFound = new RegExp("version not found: ","m");
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("------------------------------------------");
throw new Error(log._("server.install.install-failed"));
}
})
}
module.exports = {
init: init,
register: register,
checkFlowDependencies: checkFlowDependencies,
require: requireModule
}

View File

@ -28,6 +28,7 @@ var registry = require("./registry");
var loader = require("./loader");
var installer = require("./installer");
var library = require("./library");
const externalModules = require("./externalModules")
/**
* Initialise the registry with a reference to a runtime object
@ -42,6 +43,7 @@ function init(runtime) {
loader.init(runtime);
registry.init(runtime.settings,loader);
library.init();
externalModules.init(runtime.settings);
}
/**
@ -297,6 +299,8 @@ module.exports = {
*/
getNodeExampleFlowPath: library.getExampleFlowPath,
checkFlowDependencies: externalModules.checkFlowDependencies,
deprecated: require("./deprecated")
};

View File

@ -21,6 +21,7 @@ var fs = require("fs");
var library = require("./library");
const {events} = require("@node-red/util")
var subflows = require("./subflow");
var externalModules = require("./externalModules")
var settings;
var loader;
@ -28,6 +29,7 @@ var nodeConfigCache = {};
var moduleConfigs = {};
var nodeList = [];
var nodeConstructors = {};
var nodeOptions = {};
var subflowModules = {};
var nodeTypeToId = {};
@ -36,12 +38,7 @@ var moduleNodes = {};
function init(_settings,_loader) {
settings = _settings;
loader = _loader;
moduleNodes = {};
nodeTypeToId = {};
nodeConstructors = {};
subflowModules = {};
nodeList = [];
nodeConfigCache = {};
clear();
}
function load() {
@ -234,6 +231,7 @@ function removeNode(id) {
if (typeId === id) {
delete subflowModules[t];
delete nodeConstructors[t];
delete nodeOptions[t];
delete nodeTypeToId[t];
}
});
@ -411,7 +409,7 @@ function getCaller(){
return stack[0].getFileName();
}
function registerNodeConstructor(nodeSet,type,constructor) {
function registerNodeConstructor(nodeSet,type,constructor,options) {
if (nodeConstructors.hasOwnProperty(type)) {
throw new Error(type+" already registered");
}
@ -431,6 +429,12 @@ function registerNodeConstructor(nodeSet,type,constructor) {
}
nodeConstructors[type] = constructor;
nodeOptions[type] = options;
if (options) {
if (options.dynamicModuleList) {
externalModules.register(type,options.dynamicModuleList);
}
}
events.emit("type-registered",type);
}
@ -525,6 +529,7 @@ function clear() {
moduleConfigs = {};
nodeList = [];
nodeConstructors = {};
nodeOptions = {};
subflowModules = {};
nodeTypeToId = {};
}

View File

@ -17,6 +17,7 @@
const path = require("path");
const semver = require("semver");
const {events,i18n,log} = require("@node-red/util");
var runtime;
function copyObjectProperties(src,dst,copyList,blockList) {
@ -45,13 +46,8 @@ function requireModule(name) {
var relPath = path.relative(__dirname, moduleInfo.path);
return require(relPath);
} else {
var npm = runtime.nodes.loadNPMModule(name);
if (npm) {
return npm;
}
var err = new Error(`Cannot find module '${name}'`);
err.code = "MODULE_NOT_FOUND";
throw err;
// Require it here to avoid the circular dependency
return require("./externalModules").require(name);
}
}
@ -129,7 +125,6 @@ function checkAgainstList(module,version,list) {
}
function checkModuleAllowed(module,version,allowList,denyList) {
// console.log("checkModuleAllowed",module,version);//,allowList,denyList)
if (!allowList && !denyList) {
// Default to allow
return true;

View File

@ -447,47 +447,5 @@ var api = module.exports = {
} else {
return null
}
},
/**
* Gets list of NPM modules
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.req - the request to log (optional)
* @return {Promise<Buffer>} - list of installed NPM modules
* @memberof @node-red/runtime_nodes
*/
listNPMModules: async function(opts) {
var promise = runtime.nodes.listNPMModules();
return promise;
},
/**
* Uninstall NPM modules
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.req - the request to log (optional)
* @return {Promise<Object>} - object for request result
* @memberof @node-red/runtime_nodes
*/
uninstallNPMModule: async function(opts) {
var spec = opts.spec;
var promise = runtime.nodes.uninstallNPMModule(spec);
return promise;
},
/**
* Update NPM modules
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.req - the request to log (optional)
* @return {Promise<Object>} - object for request result
* @memberof @node-red/runtime_nodes
*/
updateNPMModule: async function(opts) {
var spec = opts.spec;
var isUpdate = opts.update;
var promise = runtime.nodes.updateNPMModule(spec, isUpdate);
return promise;
}
}

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 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});
}
});
return configSavePromise.then(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 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) {
@ -246,7 +246,7 @@ function getFlows() {
return activeConfig;
}
function start(type,diff,muteLog) {
async function start(type,diff,muteLog) {
type = type||"full";
started = true;
var i;
@ -271,7 +271,21 @@ function start(type,diff,muteLog) {
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});
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
@ -280,7 +294,7 @@ function start(type,diff,muteLog) {
log.info(log._("nodes.flows.safe-mode"));
log.info("*****************************************************************")
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) {
@ -370,7 +384,7 @@ function start(type,diff,muteLog) {
log.info(log._("nodes.flows.started-flows"));
}
}
return Promise.resolve();
return;
}
function stop(type,diff,muteLog) {

View File

@ -26,7 +26,6 @@ var flows = require("../flows");
var flowUtil = require("../flows/util")
var context = require("./context");
var Node = require("./Node");
var npmModule = require("./npmModule");
var log;
const events = require("@node-red/util").events;
@ -50,7 +49,6 @@ function registerType(nodeSet,type,constructor,opts) {
type = nodeSet;
nodeSet = "";
}
var dynModule = null;
if (opts) {
if (opts.credentials) {
credentials.register(type,opts.credentials);
@ -62,11 +60,7 @@ function registerType(nodeSet,type,constructor,opts) {
log.warn("["+type+"] "+err.message);
}
}
if (opts.dynamicModuleList) {
dynModule = opts.dynamicModuleList;
}
}
npmModule.register(type, dynModule);
if(!(constructor.prototype instanceof Node)) {
if(Object.getPrototypeOf(constructor.prototype) === Object.prototype) {
util.inherits(constructor,Node);
@ -87,7 +81,7 @@ function registerType(nodeSet,type,constructor,opts) {
}
}
}
registry.registerType(nodeSet,type,constructor);
registry.registerType(nodeSet,type,constructor,opts);
}
/**
@ -116,7 +110,6 @@ function createNode(node,def) {
} else if (credentials.getDefinition(node.type)) {
node.credentials = {};
}
return npmModule.checkInstall(def);
}
function registerSubflow(nodeSet, subflow) {
@ -145,7 +138,6 @@ function init(runtime) {
flows.init(runtime);
registry.init(runtime);
context.init(runtime.settings);
npmModule.init(runtime);
}
function disableNode(id) {
@ -270,10 +262,4 @@ module.exports = {
loadContextsPlugin: context.load,
closeContextsPlugin: context.close,
listContextStores: context.listStores,
// NPM modules
listNPMModules: npmModule.list,
uninstallNPMModule: npmModule.uninstall,
updateNPMModule: npmModule.update,
loadNPMModule: npmModule.load
};

View File

@ -1,460 +0,0 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var path = require("path");
var fs = require("fs-extra");
var os = require("os");
var util = require("@node-red/registry/lib/util");
var api;
var runtime;
var settings;
var exec;
var log;
var npmCommand = (process.platform === "win32") ? "npm.cmd" : "npm";
var metadataFileName = "npm-modules.json";
var moduleProp = {};
var moduleBase = null;
var allowInstall = true;
var allowList = ["*"];
var denyList = [];
var inProgress = {};
/**
* Initialise npm install module.
* @param {Object} _runtime - runtime object
*/
function init(_runtime) {
runtime = _runtime;
settings = _runtime.settings;
exec = _runtime.exec;
log = _runtime.log;
moduleProp = {};
inProgress = {};
moduleBase = settings.userDir || process.env.NODE_RED_HOME || ".";
if (settings.hasOwnProperty("externalModules")) {
var em = settings.externalModules;
if (em && em.hasOwnProperty("modules")) {
var mod = em.modules;
if (mod.hasOwnProperty("allowInstall")) {
allowInstall = mod.allowInstall;
}
if (mod.hasOwnProperty("allowList")) {
var alist = mod.allowList;
if (Array.isArray(alist)) {
allowList = alist;
}
else {
log.warn("unexpected value of externalModule.allowList in settings.js");
}
}
if (mod.hasOwnProperty("denyList")) {
var dlist = mod.denyList;
if (Array.isArray(dlist)) {
denyList = dlist;
}
else {
log.warn("unexpected value of externalModule.denyList in settings.js");
}
}
}
}
}
/**
* Register dynamic module installation property.
* @param {string} type - node type
* @param {string} prop - property name
*/
function register(type, prop) {
if (prop) {
moduleProp[type] = prop;
}
else {
delete moduleProp[prop]
}
}
/**
* Get path to install modules
*/
function modulePath() { // takes variable length arguments in `arguments`
var result = moduleBase;
for(var i = 0; i < arguments.length; i++) {
result = path.join(result, arguments[i]);
}
return result;
}
/**
* Decompose NPM module specification string
* @param {string} module - module specification
* @return {Object} array [name, version], where name is name part and version is version part of the module
*/
function moduleName(module) {
var match = /^([^@]+)@(.+)/.exec(module);
if (match) {
return [match[1], match[2]];
}
return [module, undefined];
}
/**
* Get NPM module info
* @param {string} name - module name
* @return {Object} package.json for specified NPM module
*/
function infoNPM(name) {
var path = modulePath("node_modules", name, "package.json");
try {
var pkg = fs.readFileSync(path);
return JSON.parse(pkg);
}
catch (e) {
}
return null;
}
/**
* Load NPM module metadata
* @return {object} module metadata object
*/
function loadMetadata() {
var path = modulePath(metadataFileName);
try {
var pkg = fs.readFileSync(path);
return JSON.parse(pkg);
}
catch (e) {
}
return {
modules: []
};
}
/**
* Save NPM module metadata
* @param {string} data - module metadata object
*/
function saveMetadata(data) {
var path = modulePath(metadataFileName);
var str = JSON.stringify(data, null, 4);
fs.writeFileSync(path, str);
}
/**
* Find item in metadata
* @param {Object} meta - metadata
* @param {string} name - module name
* @return {object} metadata item
*/
function findModule(meta, name) {
var modules = meta.modules;
var item = modules.find(item => (item.name === name));
return item;
}
function setInProgress(name) {
inProgress[name] = true;
}
function clearInProgress(name) {
inProgress[name] = false;
}
/**
* Install NPM module
* @param {string} module - module specification
*/
function installNPM(module) {
var [name, ver] = moduleName(module);
setInProgress(name);
return new Promise((resolve, reject) => {
var pkg = infoNPM(name);
if (!pkg) {
var args = ["install", module];
var dir = modulePath();
return exec.run(npmCommand, args, {
cwd: dir
}, true).then(result => {
if (result && (result.code === 0)) {
pkg = infoNPM(name);
var spec = name +(pkg ? "@"+pkg.version : "");
log.info("successfully installed: "+spec);
var meta = loadMetadata();
var item = {
name: name,
spec: module,
status: "installed",
};
meta.modules.push(item);
saveMetadata(meta);
clearInProgress(name);
resolve(true);
}
else {
clearInProgress(name);
var msg = "failed to install: "+name;
log.warn(msg);
reject(msg);
}
}).catch(e => {
clearInProgress(name);
var msg = "failed to install: "+name
log.warn(msg);
reject(msg);
});
}
else {
var meta = loadMetadata();
if (!findModule(meta, name)) {
var item = {
name: name,
spec: module,
status: "preinstalled",
};
meta.modules.push(item);
saveMetadata(meta);
}
clearInProgress(name);
var spec = name +(pkg ? ("@"+pkg.version) : "");
log.info("already installed: "+spec);
}
resolve(true);
});
}
/**
* Check allowance of NPM module installation
* @param {string} name - module specification
*/
function isAllowed(name) {
if (!allowInstall) {
return false;
}
var [module, ver] = moduleName(name);
var aList = util.parseModuleList(allowList);
var dList = util.parseModuleList(denyList);
return util.checkModuleAllowed(module, ver, aList, dList);
}
/**
* Check and install NPM module according to dynamic module specification
* @param {Object} node - node object
*/
function checkInstall(node) {
var name = null;
if(moduleProp.hasOwnProperty(node.type)) {
name = moduleProp[node.type];
}
var promises = [];
if (name && node.hasOwnProperty(name)) {
var modules = node[name];
modules.forEach(module => {
var name = module;
if ((typeof module === "object") &&
module &&
module.hasOwnProperty("name")) {
name = module.name;
}
if (isAllowed(name)) {
var [n, v] = moduleName(name);
setInProgress(name);
promises.push(installNPM(name));
}
else {
log.info("installation not allowed: "+name);
}
});
}
return Promise.all(promises);
}
/**
* Load NPM module
* @param {string} module - module to load
*/
function load(module) {
try {
var [name, ver] = moduleName(module);
var path = modulePath("node_modules", name);
var npm = require(path);
return npm;
}
catch (e) {
return null;
}
}
/**
* Get list of installed modules
*/
function listModules() {
return new Promise((resolve, reject) => {
var meta = loadMetadata();
var modules = meta.modules;
modules.forEach(item => {
var name = item.name;
var info = infoNPM(name);
if (info) {
item.version = info.version;
}
item.inProgress = ((name in inProgress) && inProgress[name]);
});
Object.keys(inProgress).forEach(name => {
if (inProgress[name] &&
!modules.find(item => (item.name === name))) {
modules.push({
name: name,
spec: name,
state: "inprogress",
inProgress: true
});
}
});
resolve(meta.modules);
});
}
/**
* Uninstall NPM modules
*/
function uninstall(module) {
var [name, ver] = moduleName(module);
setInProgress(name);
return new Promise((resolve, reject) => {
var pkg = infoNPM(name);
var meta = loadMetadata();
var item = findModule(meta, name);
if (pkg && item) {
if (item.status === "preinstalled") {
clearInProgress(name);
var msg = "can't uninstall preinstalled: "+name;
log.warn(msg);
reject(msg);
}
else {
var args = ["uninstall", module];
var dir = modulePath();
return exec.run(npmCommand, args, {
cwd: dir
}, true).then(result => {
if (result && (result.code === 0)) {
log.info("successfully uninstalled: "+name);
var meta = loadMetadata();
var items = meta.modules.filter(item => (item.name !== name));
meta.modules = items;
saveMetadata(meta);
clearInProgress(name);
resolve(true);
}
else {
clearInProgress(name);
var msg = "failed to uninstall: "+name;
log.warn(msg);
reject(msg);
}
}).catch(e => {
clearInProgress(name);
var msg = "failed to uninstall: "+name;
log.warn(msg);
reject(msg);
});
}
}
else {
clearInProgress(name);
var msg = "module not installed: "+name;
log.info(msg);
reject(msg);
}
});
}
/**
* Update NPM modules
*/
function update(module, isUpdate) {
var act = (isUpdate ? "updated": "install")
var acted = (isUpdate ? "updated": "installed")
var [name, ver] = moduleName(module);
setInProgress(name);
return new Promise((resolve, reject) => {
var pkg = infoNPM(name);
if (!pkg || isUpdate) {
var args = ["install", module];
var dir = modulePath();
return exec.run(npmCommand, args, {
cwd: dir
}, true).then(result => {
if (result && (result.code === 0)) {
pkg = infoNPM(name);
var spec = name +(pkg ? "@"+pkg.version : "");
log.info("successfully "+acted+": "+spec);
var meta = loadMetadata();
var items = meta.modules.filter(item => (item.name !== name));
var item = {
name: name,
spec: module,
status: "installed",
};
items.push(item);
meta.modules = items;
saveMetadata(meta);
clearInProgress(name);
resolve(true);
}
else {
clearInProgress(name);
var msg = "failed to "+act+": "+name;
log.warn(msg);
reject(msg);
}
}).catch(e => {
clearInProgress(name);
var msg = "failed to "+act+": "+name;
log.warn(msg);
reject(msg);
});
}
else {
clearInProgress(name);
var msg = "not "+acted+": "+name;
log.info(msg);
reject(msg);
}
});
}
api = {
init: init,
register: register,
checkInstall: checkInstall,
load: load,
list: listModules,
uninstall: uninstall,
update: update
};
module.exports = api;

View File

@ -0,0 +1,20 @@
// init: init,
// register: register,
// checkFlowDependencies: checkFlowDependencies,
// require: requireModule
//
const should = require("should");
const fs = require("fs");
const path = require("path");
var NR_TEST_UTILS = require("nr-test-utils");
var externalModules = NR_TEST_UTILS.require("@node-red/registry/lib/externalModules");
describe("externalModules api", function() {
});