mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
commit
5de078dc61
@ -165,6 +165,7 @@ module.exports = function(grunt) {
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/actions.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/diff.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/keyboard.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js",
|
||||
|
23
packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js
vendored
Normal file
23
packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
let runtimeAPI;
|
||||
let settings;
|
||||
const apiUtil = require("../util");
|
||||
module.exports = {
|
||||
init: function(_settings, _runtimeAPI) {
|
||||
settings = _settings;
|
||||
runtimeAPI = _runtimeAPI;
|
||||
},
|
||||
getReport: function(req, res) {
|
||||
const diagnosticsOpts = settings.diagnostics || {};
|
||||
const opts = {
|
||||
user: req.user,
|
||||
scope: diagnosticsOpts.level || "basic"
|
||||
}
|
||||
if(diagnosticsOpts.enabled === false || diagnosticsOpts.enabled === "false") {
|
||||
apiUtil.rejectHandler(req, res, {message: "diagnostics are disabled", status: 403, code: "diagnostics.disabled" })
|
||||
} else {
|
||||
runtimeAPI.diagnostics.get(opts)
|
||||
.then(function(result) { res.json(result); })
|
||||
.catch(err => apiUtil.rejectHandler(req, res, err))
|
||||
}
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ var context = require("./context");
|
||||
var auth = require("../auth");
|
||||
var info = require("./settings");
|
||||
var plugins = require("./plugins");
|
||||
var diagnostics = require("./diagnostics");
|
||||
|
||||
var apiUtil = require("../util");
|
||||
|
||||
@ -34,6 +35,7 @@ module.exports = {
|
||||
context.init(runtimeAPI);
|
||||
info.init(settings,runtimeAPI);
|
||||
plugins.init(runtimeAPI);
|
||||
diagnostics.init(settings, runtimeAPI);
|
||||
|
||||
var needsPermission = auth.needsPermission;
|
||||
|
||||
@ -95,6 +97,8 @@ module.exports = {
|
||||
adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler);
|
||||
adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler);
|
||||
|
||||
adminApp.get("/diagnostics", needsPermission("diagnostics.read"), diagnostics.getReport, apiUtil.errorHandler);
|
||||
|
||||
return adminApp;
|
||||
}
|
||||
}
|
||||
|
@ -940,6 +940,8 @@
|
||||
"format": "format JSON",
|
||||
"rawMode": "Edit JSON",
|
||||
"uiMode": "Visual editor",
|
||||
"rawMode-readonly": "JSON",
|
||||
"uiMode-readonly": "Visual",
|
||||
"insertAbove": "Insert above",
|
||||
"insertBelow": "Insert below",
|
||||
"addItem": "Add item",
|
||||
@ -1154,6 +1156,9 @@
|
||||
"start": "Start",
|
||||
"next": "Next"
|
||||
},
|
||||
"diagnostics": {
|
||||
"title": "System Info"
|
||||
},
|
||||
"languages" : {
|
||||
"de": "German",
|
||||
"en-US": "English",
|
||||
|
@ -940,6 +940,8 @@
|
||||
"format": "JSONフォーマット",
|
||||
"rawMode": "JSONを編集",
|
||||
"uiMode": "ビジュアルエディタ",
|
||||
"rawMode-readonly": "JSON",
|
||||
"uiMode-readonly": "ビジュアル",
|
||||
"insertAbove": "上に挿入",
|
||||
"insertBelow": "下に挿入",
|
||||
"addItem": "要素を追加",
|
||||
@ -1298,22 +1300,23 @@
|
||||
"zoom-in": "ズームイン",
|
||||
"zoom-out": "ズームアウト",
|
||||
"zoom-reset": "ズームリセット",
|
||||
"toggle-navigator": "ナビゲータ表示切替"
|
||||
"toggle-navigator": "ナビゲータ表示切替",
|
||||
"show-system-info": "システムインフォメーション"
|
||||
},
|
||||
"validator": {
|
||||
"errors": {
|
||||
"invalid-json": "JSONデータが不正: __error__",
|
||||
"invalid-json-prop": "__prop__: JSONデータが不正: __error__",
|
||||
"invalid-prop": "プロパティ式が不正",
|
||||
"invalid-prop-prop": "__prop__: プロパティ式が不正",
|
||||
"invalid-num": "数値が不正",
|
||||
"invalid-num-prop": "__prop__: 数値が不正",
|
||||
"invalid-regexp": "入力パターンが不正",
|
||||
"invalid-regex-prop": "__prop__: 入力パターンが不正",
|
||||
"missing-required-prop": "__prop__: プロパティが未設定",
|
||||
"invalid-config": "__prop__: 設定ノードが不正",
|
||||
"missing-config": "__prop__: 設定ノードが存在しません",
|
||||
"validation-error": "__prop__: チェックエラー: __node__, __id__: __error__"
|
||||
}
|
||||
"invalid-json": "JSONデータが不正: __error__",
|
||||
"invalid-json-prop": "__prop__: JSONデータが不正: __error__",
|
||||
"invalid-prop": "プロパティ式が不正",
|
||||
"invalid-prop-prop": "__prop__: プロパティ式が不正",
|
||||
"invalid-num": "数値が不正",
|
||||
"invalid-num-prop": "__prop__: 数値が不正",
|
||||
"invalid-regexp": "入力パターンが不正",
|
||||
"invalid-regex-prop": "__prop__: 入力パターンが不正",
|
||||
"missing-required-prop": "__prop__: プロパティが未設定",
|
||||
"invalid-config": "__prop__: 設定ノードが不正",
|
||||
"missing-config": "__prop__: 設定ノードが存在しません",
|
||||
"validation-error": "__prop__: チェックエラー: __node__, __id__: __error__"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -730,6 +730,7 @@ var RED = (function() {
|
||||
RED.search.init();
|
||||
RED.actionList.init();
|
||||
RED.editor.init();
|
||||
RED.diagnostics.init();
|
||||
RED.diff.init();
|
||||
|
||||
|
||||
|
61
packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js
vendored
Normal file
61
packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
RED.diagnostics = (function () {
|
||||
|
||||
function init() {
|
||||
if (RED.settings.get('diagnostics.ui', true) === false) {
|
||||
return;
|
||||
}
|
||||
RED.actions.add("core:show-system-info", function () { show(); });
|
||||
}
|
||||
|
||||
function show() {
|
||||
$.ajax({
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
},
|
||||
cache: false,
|
||||
url: 'diagnostics',
|
||||
success: function (data) {
|
||||
var json = JSON.stringify(data || {}, "", 4);
|
||||
if (json === "{}") {
|
||||
json = "{\n\n}";
|
||||
}
|
||||
RED.editor.editJSON({
|
||||
title: RED._('diagnostics.title'),
|
||||
value: json,
|
||||
requireValid: true,
|
||||
readOnly: true,
|
||||
toolbarButtons: [
|
||||
{
|
||||
text: RED._('clipboard.export.copy'),
|
||||
icon: 'fa fa-copy',
|
||||
click: function () {
|
||||
RED.clipboard.copyText(json, $(this), RED._('clipboard.copyMessageValue'))
|
||||
}
|
||||
},
|
||||
{
|
||||
text: RED._('clipboard.download'),
|
||||
icon: 'fa fa-download',
|
||||
click: function () {
|
||||
var element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(json));
|
||||
element.setAttribute('download', "system-info.json");
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log("Unexpected error loading system info:", jqXHR.status, textStatus, errorThrown);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
};
|
||||
})();
|
@ -21,7 +21,9 @@
|
||||
'<ul id="red-ui-editor-type-json-tabs"></ul>'+
|
||||
'<div id="red-ui-editor-type-json-tab-raw" class="red-ui-editor-type-json-tab-content hide">'+
|
||||
'<div class="form-row" style="margin-bottom: 3px; text-align: right;">'+
|
||||
'<button id="node-input-json-reformat" class="red-ui-button red-ui-button-small"><span data-i18n="jsonEditor.format"></span></button>'+
|
||||
'<span class="button-group">'+
|
||||
'<button id="node-input-json-reformat" class="red-ui-button red-ui-button-small"><span data-i18n="jsonEditor.format"></span></button>'+
|
||||
'<span class="button-group">'+
|
||||
'</div>'+
|
||||
'<div class="form-row node-text-editor-row">'+
|
||||
'<div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-json"></div>'+
|
||||
@ -34,7 +36,7 @@
|
||||
|
||||
var activeTab;
|
||||
|
||||
function insertNewItem(parent,index,copyIndex) {
|
||||
function insertNewItem(parent,index,copyIndex,readOnly) {
|
||||
var newValue = "";
|
||||
|
||||
if (parent.children.length > 0) {
|
||||
@ -60,26 +62,26 @@
|
||||
newKey = keyRoot+"-"+(keySuffix++);
|
||||
}
|
||||
}
|
||||
var newItem = handleItem(newKey,newValue,parent.depth+1,parent);
|
||||
var newItem = handleItem(newKey,newValue,parent.depth+1,parent,readOnly);
|
||||
parent.treeList.insertChildAt(newItem, index, true);
|
||||
parent.treeList.expand();
|
||||
}
|
||||
function showObjectMenu(button,item) {
|
||||
function showObjectMenu(button,item,readOnly) {
|
||||
var elementPos = button.offset();
|
||||
var options = [];
|
||||
if (item.parent) {
|
||||
options.push({id:"red-ui-editor-type-json-menu-insert-above", icon:"fa fa-toggle-up", label:RED._('jsonEditor.insertAbove'),onselect:function(){
|
||||
var index = item.parent.children.indexOf(item);
|
||||
insertNewItem(item.parent,index,index);
|
||||
insertNewItem(item.parent,index,index,readOnly);
|
||||
}});
|
||||
options.push({id:"red-ui-editor-type-json-menu-insert-below", icon:"fa fa-toggle-down", label:RED._('jsonEditor.insertBelow'),onselect:function(){
|
||||
var index = item.parent.children.indexOf(item)+1;
|
||||
insertNewItem(item.parent,index,index-1);
|
||||
insertNewItem(item.parent,index,index-1,readOnly);
|
||||
}});
|
||||
}
|
||||
if (item.type === 'array' || item.type === 'object') {
|
||||
options.push({id:"red-ui-editor-type-json-menu-add-child", icon:"fa fa-plus", label:RED._('jsonEditor.addItem'),onselect:function(){
|
||||
insertNewItem(item,item.children.length,item.children.length-1);
|
||||
insertNewItem(item,item.children.length,item.children.length-1,readOnly);
|
||||
}});
|
||||
}
|
||||
if (item.parent) {
|
||||
@ -121,7 +123,7 @@
|
||||
newKey = keyRoot+"-"+(keySuffix++);
|
||||
}
|
||||
}
|
||||
var newItem = handleItem(newKey,convertToObject(item),item.parent.depth+1,item.parent);
|
||||
var newItem = handleItem(newKey,convertToObject(item),item.parent.depth+1,item.parent,readOnly);
|
||||
var index = item.parent.children.indexOf(item)+1;
|
||||
|
||||
item.parent.treeList.insertChildAt(newItem, index, true);
|
||||
@ -171,24 +173,24 @@
|
||||
menuOptionMenu.show();
|
||||
}
|
||||
|
||||
function parseObject(obj,depth,parent) {
|
||||
function parseObject(obj,depth,parent,readOnly) {
|
||||
var result = [];
|
||||
for (var prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
result.push(handleItem(prop,obj[prop],depth,parent));
|
||||
result.push(handleItem(prop,obj[prop],depth,parent,readOnly));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function parseArray(obj,depth,parent) {
|
||||
function parseArray(obj,depth,parent,readOnly) {
|
||||
var result = [];
|
||||
var l = obj.length;
|
||||
for (var i=0;i<l;i++) {
|
||||
result.push(handleItem(i,obj[i],depth,parent));
|
||||
result.push(handleItem(i,obj[i],depth,parent,readOnly));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function handleItem(key,val,depth,parent) {
|
||||
function handleItem(key,val,depth,parent,readOnly) {
|
||||
var item = {depth:depth, type: typeof val};
|
||||
var container = $('<span class="red-ui-editor-type-json-editor-label">');
|
||||
if (key != null) {
|
||||
@ -204,11 +206,14 @@
|
||||
if (parent && parent.type === "array") {
|
||||
keyLabel.addClass("red-ui-editor-type-json-editor-label-array-key")
|
||||
}
|
||||
|
||||
if(readOnly) {
|
||||
keyLabel.addClass("readonly")
|
||||
}
|
||||
keyLabel.on("click", function(evt) {
|
||||
if (item.parent.type === 'array') {
|
||||
return;
|
||||
}
|
||||
if (readOnly) { return; }
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
var w = Math.max(150,keyLabel.width());
|
||||
@ -253,10 +258,10 @@
|
||||
item.expanded = depth < 2;
|
||||
item.type = "array";
|
||||
item.deferBuild = depth >= 2;
|
||||
item.children = parseArray(val,depth+1,item);
|
||||
item.children = parseArray(val,depth+1,item,readOnly);
|
||||
} else if (val !== null && item.type === "object") {
|
||||
item.expanded = depth < 2;
|
||||
item.children = parseObject(val,depth+1,item);
|
||||
item.children = parseObject(val,depth+1,item,readOnly);
|
||||
item.deferBuild = depth >= 2;
|
||||
} else {
|
||||
item.value = val;
|
||||
@ -287,7 +292,11 @@
|
||||
//
|
||||
var orphanedChildren;
|
||||
var valueLabel = $('<span class="red-ui-editor-type-json-editor-label-value">').addClass(valClass).text(valValue).appendTo(container);
|
||||
if (readOnly) {
|
||||
valueLabel.addClass("readonly")
|
||||
}
|
||||
valueLabel.on("click", function(evt) {
|
||||
if (readOnly) { return; }
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
if (valType === 'str') {
|
||||
@ -395,17 +404,19 @@
|
||||
valueLabel.hide();
|
||||
})
|
||||
item.gutter = $('<span class="red-ui-editor-type-json-editor-item-gutter"></span>');
|
||||
|
||||
if (parent) {//red-ui-editor-type-json-editor-item-handle
|
||||
$('<span class="red-ui-editor-type-json-editor-item-handle"><i class="fa fa-bars"></span>').appendTo(item.gutter);
|
||||
} else {
|
||||
$('<span></span>').appendTo(item.gutter);
|
||||
if(!readOnly) {
|
||||
if (parent) {
|
||||
$('<span class="red-ui-editor-type-json-editor-item-handle"><i class="fa fa-bars"></span>').appendTo(item.gutter);
|
||||
} else {
|
||||
$('<span></span>').appendTo(item.gutter);
|
||||
}
|
||||
$('<button type="button" class="editor-button editor-button-small"><i class="fa fa-caret-down"></button>').appendTo(item.gutter).on("click", function(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
showObjectMenu($(this), item, readOnly);
|
||||
});
|
||||
}
|
||||
$('<button type="button" class="editor-button editor-button-small"><i class="fa fa-caret-down"></button>').appendTo(item.gutter).on("click", function(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
showObjectMenu($(this), item);
|
||||
});
|
||||
|
||||
item.element = container;
|
||||
return item;
|
||||
}
|
||||
@ -501,7 +512,25 @@
|
||||
open: function(tray) {
|
||||
var trayBody = tray.find('.red-ui-tray-body');
|
||||
var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor');
|
||||
|
||||
var toolbarButtons = options.toolbarButtons || [];
|
||||
if (toolbarButtons.length) {
|
||||
toolbarButtons.forEach(function (button) {
|
||||
var element = $('<button type="button" class="red-ui-button red-ui-button-small"> </button>')
|
||||
.insertBefore("#node-input-json-reformat")
|
||||
.on("click", function (evt) {
|
||||
evt.preventDefault();
|
||||
if (button.click !== undefined) {
|
||||
button.click.call(element, evt);
|
||||
}
|
||||
});
|
||||
if (button.id) { element.attr("id", button.id); }
|
||||
if (button.title) { element.attr("title", button.title); }
|
||||
if (button.icon) { element.append($("<i></i>").attr("class", button.icon)); }
|
||||
if (button.label || button.text) {
|
||||
element.append($("<span></span>").text(" " + (button.label || button.text)));
|
||||
}
|
||||
});
|
||||
}
|
||||
var container = $("#red-ui-editor-type-json-tab-ui-container").css({"height":"100%"});
|
||||
var filterDepth = Infinity;
|
||||
var list = $('<div class="red-ui-debug-msg-payload red-ui-editor-type-json-editor">').appendTo(container).treeList({
|
||||
@ -531,11 +560,13 @@
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
expressionEditor = RED.editor.createEditor({
|
||||
id: 'node-input-json',
|
||||
value: "",
|
||||
mode:"ace/mode/json",
|
||||
value: value||"",
|
||||
mode:"ace/mode/json",
|
||||
readOnly: !!options.readOnly,
|
||||
stateId: options.stateId,
|
||||
focus: true
|
||||
});
|
||||
@ -576,7 +607,7 @@
|
||||
var raw = expressionEditor.getValue().trim() ||"{}";
|
||||
try {
|
||||
var parsed = JSON.parse(raw);
|
||||
rootNode = handleItem(null,parsed,0,null);
|
||||
rootNode = handleItem(null,parsed,0,null,options.readOnly);
|
||||
rootNode.class = "red-ui-editor-type-json-root-node"
|
||||
list.treeList('data',[rootNode]);
|
||||
} catch(err) {
|
||||
@ -594,12 +625,12 @@
|
||||
|
||||
tabs.addTab({
|
||||
id: 'json-raw',
|
||||
label: RED._('jsonEditor.rawMode'),
|
||||
label: options.readOnly ? RED._('jsonEditor.rawMode-readonly') : RED._('jsonEditor.rawMode'),
|
||||
content: $("#red-ui-editor-type-json-tab-raw")
|
||||
});
|
||||
tabs.addTab({
|
||||
id: 'json-ui',
|
||||
label: RED._('jsonEditor.uiMode'),
|
||||
label: options.readOnly ? RED._('jsonEditor.uiMode-readonly') : RED._('jsonEditor.uiMode'),
|
||||
content: $("#red-ui-editor-type-json-tab-ui")
|
||||
});
|
||||
finishedBuild = true;
|
||||
|
@ -701,6 +701,10 @@ div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle {
|
||||
border-color: $list-item-background-hover;
|
||||
border-style: dashed;
|
||||
}
|
||||
&.readonly {
|
||||
cursor: pointer;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.red-ui-editor-type-json-editor-item-gutter {
|
||||
width: 48px;
|
||||
@ -720,6 +724,10 @@ div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle {
|
||||
> span, > button {
|
||||
display: none;
|
||||
}
|
||||
&.readonly {
|
||||
cursor: pointer;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
202
packages/node_modules/@node-red/runtime/lib/api/diagnostics.js
vendored
Normal file
202
packages/node_modules/@node-red/runtime/lib/api/diagnostics.js
vendored
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
|
||||
let runtime;
|
||||
let isContainerCached;
|
||||
let isWSLCached;
|
||||
|
||||
const isInWsl = () => {
|
||||
if (isWSLCached === undefined) {
|
||||
isWSLCached = getIsInWSL();
|
||||
}
|
||||
return isWSLCached;
|
||||
function getIsInWSL() {
|
||||
if (process.platform !== 'linux') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (os.release().toLowerCase().includes('microsoft')) {
|
||||
if (isInContainer()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft') ? !isInContainer() : false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isInContainer = () => {
|
||||
if (isContainerCached === undefined) {
|
||||
isContainerCached = hasDockerEnv() || hasDockerCGroup();
|
||||
}
|
||||
return isContainerCached;
|
||||
function hasDockerEnv() {
|
||||
try {
|
||||
fs.statSync('/.dockerenv');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function hasDockerCGroup() {
|
||||
try {
|
||||
const s = fs.readFileSync('/proc/self/cgroup', 'utf8');
|
||||
if (s.includes('docker')) {
|
||||
return "docker"
|
||||
} else if (s.includes('kubepod')) {
|
||||
return "kubepod"
|
||||
} else if (s.includes('lxc')) {
|
||||
return "lxc"
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildDiagnosticReport(scope, callback) {
|
||||
const modules = {};
|
||||
const nl = runtime.nodes.getNodeList();
|
||||
for (let i = 0; i < nl.length; i++) {
|
||||
if (modules[nl[i].module]) {
|
||||
continue;
|
||||
}
|
||||
modules[nl[i].module] = nl[i].version
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const {locale, timeZone} = Intl.DateTimeFormat().resolvedOptions();
|
||||
const report = {
|
||||
report: "diagnostics",
|
||||
scope: scope,
|
||||
time: {
|
||||
utc: now.toUTCString(),
|
||||
local: now.toLocaleString(),
|
||||
},
|
||||
intl: {
|
||||
locale, timeZone
|
||||
},
|
||||
nodejs: {
|
||||
version: process.version,
|
||||
arch: process.arch,
|
||||
platform: process.platform,
|
||||
memoryUsage: process.memoryUsage(),
|
||||
},
|
||||
os: {
|
||||
containerised: isInContainer(),
|
||||
wsl: isInWsl(),
|
||||
totalmem: os.totalmem(),
|
||||
freemem: os.freemem(),
|
||||
arch: os.arch(),
|
||||
loadavg: os.loadavg(),
|
||||
platform: os.platform(),
|
||||
release: os.release(),
|
||||
type: os.type(),
|
||||
uptime: os.uptime(),
|
||||
version: os.version(),
|
||||
},
|
||||
runtime: {
|
||||
isStarted: runtime.isStarted(),
|
||||
modules: modules,
|
||||
version: runtime.settings.version,
|
||||
settings: {
|
||||
available: runtime.settings.available(),
|
||||
apiMaxLength: runtime.settings.apiMaxLength || "UNSET",
|
||||
//coreNodesDir: runtime.settings.coreNodesDir,
|
||||
disableEditor: runtime.settings.disableEditor,
|
||||
contextStorage: listContextModules(),
|
||||
debugMaxLength: runtime.settings.debugMaxLength || "UNSET",
|
||||
editorTheme: runtime.settings.editorTheme || "UNSET",
|
||||
flowFile: runtime.settings.flowFile || "UNSET",
|
||||
mqttReconnectTime: runtime.settings.mqttReconnectTime || "UNSET",
|
||||
serialReconnectTime: runtime.settings.serialReconnectTime || "UNSET",
|
||||
|
||||
adminAuth: runtime.settings.adminAuth ? "SET" : "UNSET",
|
||||
|
||||
httpAdminRoot: runtime.settings.httpAdminRoot || "UNSET",
|
||||
httpAdminCors: runtime.settings.httpAdminCors ? "SET" : "UNSET",
|
||||
httpNodeAuth: runtime.settings.httpNodeAuth ? "SET" : "UNSET",
|
||||
|
||||
httpNodeRoot: runtime.settings.httpNodeRoot || "UNSET",
|
||||
httpNodeCors: runtime.settings.httpNodeCors ? "SET" : "UNSET",
|
||||
|
||||
httpStatic: runtime.settings.httpStatic ? "SET" : "UNSET",
|
||||
httpStaticRoot: runtime.settings.httpStaticRoot || "UNSET",
|
||||
httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET",
|
||||
|
||||
uiHost: runtime.settings.uiHost ? "SET" : "UNSET",
|
||||
uiPort: runtime.settings.uiPort ? "SET" : "UNSET",
|
||||
userDir: runtime.settings.userDir ? "SET" : "UNSET",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if (scope == "admin") {
|
||||
// const moreSettings = {
|
||||
// adminAuth_type: (runtime.settings.adminAuth && runtime.settings.adminAuth.type) ? runtime.settings.adminAuth.type : "UNSET",
|
||||
// httpAdminCors: runtime.settings.httpAdminCors ? runtime.settings.httpAdminCors : "UNSET",
|
||||
// httpNodeCors: runtime.settings.httpNodeCors ? runtime.settings.httpNodeCors : "UNSET",
|
||||
// httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET",
|
||||
// settingsFile: runtime.settings.settingsFile ? runtime.settings.settingsFile : "UNSET",
|
||||
// uiHost: runtime.settings.uiHost ? runtime.settings.uiHost : "UNSET",
|
||||
// uiPort: runtime.settings.uiPort ? runtime.settings.uiPort : "UNSET",
|
||||
// userDir: runtime.settings.userDir ? runtime.settings.userDir : "UNSET",
|
||||
// }
|
||||
// const moreNodejs = {
|
||||
// execPath: process.execPath,
|
||||
// pid: process.pid,
|
||||
// }
|
||||
// const moreOs = {
|
||||
// cpus: os.cpus(),
|
||||
// homedir: os.homedir(),
|
||||
// hostname: os.hostname(),
|
||||
// networkInterfaces: os.networkInterfaces(),
|
||||
// }
|
||||
// report.runtime.settings = Object.assign({}, report.runtime.settings, moreSettings);
|
||||
// report.nodejs = Object.assign({}, report.nodejs, moreNodejs);
|
||||
// report.os = Object.assign({}, report.os, moreOs);
|
||||
// }
|
||||
|
||||
callback(report);
|
||||
|
||||
/** gets a sanitised list containing only the module name */
|
||||
function listContextModules() {
|
||||
const keys = Object.keys(runtime.settings.contextStorage);
|
||||
const result = {};
|
||||
keys.forEach(e => {
|
||||
result[e] = {
|
||||
module: String(runtime.settings.contextStorage[e].module)
|
||||
}
|
||||
})
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
init: function (_runtime) {
|
||||
runtime = _runtime;
|
||||
},
|
||||
/**
|
||||
* Gets the node-red diagnostics report
|
||||
* @param {{scope: string}} opts - settings
|
||||
* @return {Promise} the diagnostics information
|
||||
* @memberof @node-red/diagnostics
|
||||
*/
|
||||
get: async function (opts) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
opts = opts || {}
|
||||
try {
|
||||
runtime.log.audit({ event: "diagnostics.get", scope: opts.scope }, opts.req);
|
||||
buildDiagnosticReport(opts.scope, (report) => resolve(report));
|
||||
} catch (error) {
|
||||
error.status = 500;
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
@ -29,6 +29,7 @@ var api = module.exports = {
|
||||
api.projects.init(runtime);
|
||||
api.context.init(runtime);
|
||||
api.plugins.init(runtime);
|
||||
api.diagnostics.init(runtime);
|
||||
},
|
||||
|
||||
comms: require("./comms"),
|
||||
@ -39,6 +40,7 @@ var api = module.exports = {
|
||||
projects: require("./projects"),
|
||||
context: require("./context"),
|
||||
plugins: require("./plugins"),
|
||||
diagnostics: require("./diagnostics"),
|
||||
|
||||
isStarted: async function(opts) {
|
||||
return runtime.isStarted();
|
||||
|
@ -142,6 +142,13 @@ var api = module.exports = {
|
||||
}
|
||||
|
||||
safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType();
|
||||
|
||||
safeSettings.diagnostics = {
|
||||
//unless diagnostics.ui and diagnostics.enabled are explicitly false, they will default to true.
|
||||
enabled: (runtime.settings.diagnostics && runtime.settings.diagnostics.enabled === false) ? false : true,
|
||||
ui: (runtime.settings.diagnostics && runtime.settings.diagnostics.ui === false) ? false : true
|
||||
}
|
||||
|
||||
runtime.settings.exportNodeSettings(safeSettings);
|
||||
runtime.plugins.exportPluginSettings(safeSettings);
|
||||
}
|
||||
|
@ -400,6 +400,11 @@ module.exports = {
|
||||
*/
|
||||
version: externalAPI.version,
|
||||
|
||||
/**
|
||||
* @memberof @node-red/diagnostics
|
||||
*/
|
||||
diagnostics:externalAPI.diagnostics,
|
||||
|
||||
storage: storage,
|
||||
events: events,
|
||||
hooks: hooks,
|
||||
|
9
packages/node_modules/node-red/lib/red.js
vendored
9
packages/node_modules/node-red/lib/red.js
vendored
@ -229,5 +229,12 @@ module.exports = {
|
||||
* @see @node-red/editor-api_auth
|
||||
* @memberof node-red
|
||||
*/
|
||||
auth: api.auth
|
||||
auth: api.auth,
|
||||
|
||||
/**
|
||||
* The editor authentication api.
|
||||
* @see @node-red/editor-api_auth
|
||||
* @memberof node-red
|
||||
*/
|
||||
get diagnostics() { return api.diagnostics }
|
||||
};
|
||||
|
14
packages/node_modules/node-red/settings.js
vendored
14
packages/node_modules/node-red/settings.js
vendored
@ -242,6 +242,7 @@ module.exports = {
|
||||
/*******************************************************************************
|
||||
* Runtime Settings
|
||||
* - lang
|
||||
* - diagnostics
|
||||
* - logging
|
||||
* - contextStorage
|
||||
* - exportGlobalContextKeys
|
||||
@ -254,6 +255,19 @@ module.exports = {
|
||||
*/
|
||||
// lang: "de",
|
||||
|
||||
/** Configure diagnostics options
|
||||
* - enabled: When `enabled` is `true` (or unset), diagnostics data will
|
||||
* be available at http://localhost:1880/diagnostics
|
||||
* - ui: When `ui` is `true` (or unset), the action `show-system-info` will
|
||||
* be available to logged in users of node-red editor
|
||||
*/
|
||||
diagnostics: {
|
||||
/** enable or disable diagnostics endpoint. Must be set to `false` to disable */
|
||||
enabled: true,
|
||||
/** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */
|
||||
ui: true,
|
||||
},
|
||||
|
||||
/** Configure the logging output */
|
||||
logging: {
|
||||
/** Only console logging is currently supported */
|
||||
|
119
test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js
Normal file
119
test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js
Normal file
@ -0,0 +1,119 @@
|
||||
const should = require("should");
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const bodyParser = require("body-parser");
|
||||
const sinon = require('sinon');
|
||||
|
||||
let app;
|
||||
|
||||
const NR_TEST_UTILS = require("nr-test-utils");
|
||||
const diagnostics = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/diagnostics");
|
||||
|
||||
describe("api/editor/diagnostics", function() {
|
||||
before(function() {
|
||||
app = express();
|
||||
app.use(bodyParser.json());
|
||||
app.get("/diagnostics",diagnostics.getReport);
|
||||
});
|
||||
|
||||
it('returns the diagnostics report when explicitly enabled', function(done) {
|
||||
const settings = { diagnostics: { ui: true, enabled: true } }
|
||||
const runtimeAPI = {
|
||||
diagnostics: {
|
||||
get: async function (opts) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
opts = opts || {}
|
||||
try {
|
||||
resolve({ opts: opts, a:1, b:2});
|
||||
} catch (error) {
|
||||
error.status = 500;
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.init(settings, runtimeAPI);
|
||||
|
||||
request(app)
|
||||
.get("/diagnostics")
|
||||
.expect(200)
|
||||
.end(function(err,res) {
|
||||
if (err || typeof res.error === "object") {
|
||||
return done(err || res.error);
|
||||
}
|
||||
res.should.have.property("statusCode",200);
|
||||
res.body.should.have.property("a",1);
|
||||
res.body.should.have.property("b",2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('returns the diagnostics report when not explicitly enabled (implicitly enabled)', function(done) {
|
||||
const settings = { diagnostics: { enabled: undefined } }
|
||||
const runtimeAPI = {
|
||||
diagnostics: {
|
||||
get: async function (opts) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
opts = opts || {}
|
||||
try {
|
||||
resolve({ opts: opts, a:3, b:4});
|
||||
} catch (error) {
|
||||
error.status = 500;
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.init(settings, runtimeAPI);
|
||||
|
||||
request(app)
|
||||
.get("/diagnostics")
|
||||
.expect(200)
|
||||
.end(function(err,res) {
|
||||
if (err || typeof res.error === "object") {
|
||||
return done(err || res.error);
|
||||
}
|
||||
res.should.have.property("statusCode",200);
|
||||
res.body.should.have.property("a",3);
|
||||
res.body.should.have.property("b",4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should error when setting is disabled', function(done) {
|
||||
const settings = { diagnostics: { ui: true, enabled: false } }
|
||||
const runtimeAPI = {
|
||||
diagnostics: {
|
||||
get: async function (opts) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
opts = opts || {}
|
||||
try {
|
||||
resolve({ opts: opts});
|
||||
} catch (error) {
|
||||
error.status = 500;
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.init(settings, runtimeAPI);
|
||||
|
||||
request(app)
|
||||
.get("/diagnostics")
|
||||
.expect(403)
|
||||
.end(function(err,res) {
|
||||
if (!err && typeof res.error !== "object") {
|
||||
return done(new Error("accessing diagnostics endpoint while disabled should raise error"));
|
||||
}
|
||||
res.should.have.property("statusCode",403);
|
||||
res.body.should.have.property("message","diagnostics are disabled");
|
||||
res.body.should.have.property("code","diagnostics.disabled");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
126
test/unit/@node-red/runtime/lib/api/diagnostics_spec.js
Normal file
126
test/unit/@node-red/runtime/lib/api/diagnostics_spec.js
Normal file
@ -0,0 +1,126 @@
|
||||
|
||||
var should = require("should");
|
||||
var sinon = require("sinon");
|
||||
var NR_TEST_UTILS = require("nr-test-utils");
|
||||
var diagnostics = NR_TEST_UTILS.require("@node-red/runtime/lib/api/diagnostics")
|
||||
|
||||
var mockLog = () => ({
|
||||
log: sinon.stub(),
|
||||
debug: sinon.stub(),
|
||||
trace: sinon.stub(),
|
||||
warn: sinon.stub(),
|
||||
info: sinon.stub(),
|
||||
metric: sinon.stub(),
|
||||
audit: sinon.stub(),
|
||||
_: function() { return "abc"}
|
||||
})
|
||||
|
||||
describe("runtime-api/diagnostics", function() {
|
||||
|
||||
describe("get", function() {
|
||||
before(function() {
|
||||
diagnostics.init({
|
||||
isStarted: () => true,
|
||||
nodes: {
|
||||
getNodeList: () => [{module:"node-red", version:"9.9.9"},{module:"node-red-node-inject", version:"8.8.8"}]
|
||||
},
|
||||
settings: {
|
||||
version: "7.7.7",
|
||||
available: () => true,
|
||||
//apiMaxLength: xxx, deliberately left blank. Should arrive in report as "UNSET"
|
||||
debugMaxLength: 1111,
|
||||
disableEditor: false,
|
||||
flowFile: "flows.json",
|
||||
mqttReconnectTime: 321,
|
||||
serialReconnectTime: 432,
|
||||
adminAuth: {},//should be sanitised to "SET"
|
||||
httpAdminRoot: "/admin/root/",
|
||||
httpAdminCors: {},//should be sanitised to "SET"
|
||||
httpNodeAuth: {},//should be sanitised to "SET"
|
||||
httpNodeRoot: "/node/root/",
|
||||
httpNodeCors: {},//should be sanitised to "SET"
|
||||
httpStatic: "/var/static/",//should be sanitised to "SET"
|
||||
httpStaticRoot: "/static/root/",
|
||||
httpStaticCors: {},//should be sanitised to "SET"
|
||||
uiHost: "something.secret.com",//should be sanitised to "SET"
|
||||
uiPort: 1337,//should be sanitised to "SET"
|
||||
userDir: "/var/super/secret/",//should be sanitised to "SET",
|
||||
contextStorage: {
|
||||
default : { module: "memory" },
|
||||
file: { module: "localfilesystem" },
|
||||
secured: { module: "secure_store", user: "fred", pass: "super-duper-secret" },
|
||||
},
|
||||
editorTheme: {}
|
||||
},
|
||||
log: mockLog()
|
||||
});
|
||||
})
|
||||
it("returns basic user settings", function() {
|
||||
return diagnostics.get({scope:"fake_scope"}).then(result => {
|
||||
should(result).be.type("object");
|
||||
|
||||
//result.xxxxx
|
||||
Object.keys(result)
|
||||
const reportPropCount = Object.keys(result).length;
|
||||
reportPropCount.should.eql(7);//ensure no more than 7 keys are present in the report (avoid leakage of extra info)
|
||||
result.should.have.property("report","diagnostics");
|
||||
result.should.have.property("scope","fake_scope");
|
||||
result.should.have.property("time").type("object");
|
||||
result.should.have.property("intl").type("object");
|
||||
result.should.have.property("nodejs").type("object");
|
||||
result.should.have.property("os").type("object");
|
||||
result.should.have.property("runtime").type("object");
|
||||
|
||||
//result.runtime.xxxxx
|
||||
const runtimeCount = Object.keys(result.runtime).length;
|
||||
runtimeCount.should.eql(4);//ensure no more than 4 keys are present in runtime
|
||||
result.runtime.should.have.property('isStarted',true)
|
||||
result.runtime.should.have.property('modules').type("object");
|
||||
result.runtime.should.have.property('settings').type("object");
|
||||
result.runtime.should.have.property('version','7.7.7');
|
||||
|
||||
//result.runtime.modules.xxxxx
|
||||
const moduleCount = Object.keys(result.runtime.modules).length;
|
||||
moduleCount.should.eql(2);//ensure no more than the 2 modules specified are present
|
||||
result.runtime.modules.should.have.property('node-red','9.9.9');
|
||||
result.runtime.modules.should.have.property('node-red-node-inject','8.8.8');
|
||||
|
||||
//result.runtime.settings.xxxxx
|
||||
const settingsCount = Object.keys(result.runtime.settings).length;
|
||||
settingsCount.should.eql(21);//ensure no more than the 21 settings listed below are present in the settings object
|
||||
result.runtime.settings.should.have.property('available',true);
|
||||
result.runtime.settings.should.have.property('apiMaxLength', "UNSET");//deliberately disabled to ensure UNSET is returned
|
||||
result.runtime.settings.should.have.property('debugMaxLength', 1111);
|
||||
result.runtime.settings.should.have.property('disableEditor', false);
|
||||
result.runtime.settings.should.have.property('editorTheme', {});
|
||||
result.runtime.settings.should.have.property('flowFile', "flows.json");
|
||||
result.runtime.settings.should.have.property('mqttReconnectTime', 321);
|
||||
result.runtime.settings.should.have.property('serialReconnectTime', 432);
|
||||
result.runtime.settings.should.have.property("adminAuth", "SET"); //should be sanitised to "SET"
|
||||
result.runtime.settings.should.have.property("httpAdminCors", "SET"); //should be sanitised to "SET"
|
||||
result.runtime.settings.should.have.property('httpAdminRoot', "/admin/root/");
|
||||
result.runtime.settings.should.have.property("httpNodeAuth", "SET"); //should be sanitised to "SET"
|
||||
result.runtime.settings.should.have.property("httpNodeCors", "SET"); //should be sanitised to "SET"
|
||||
result.runtime.settings.should.have.property('httpNodeRoot', "/node/root/");
|
||||
result.runtime.settings.should.have.property("httpStatic", "SET"); //should be sanitised to "SET"
|
||||
result.runtime.settings.should.have.property('httpStaticRoot', "/static/root/");
|
||||
result.runtime.settings.should.have.property("httpStaticCors", "SET"); //should be sanitised to "SET"
|
||||
result.runtime.settings.should.have.property("uiHost", "SET"); //should be sanitised to "SET"
|
||||
result.runtime.settings.should.have.property("uiPort", "SET"); //should be sanitised to "SET"
|
||||
result.runtime.settings.should.have.property("userDir", "SET"); //should be sanitised to "SET"
|
||||
result.runtime.settings.should.have.property('contextStorage').type("object");
|
||||
|
||||
//result.runtime.settings.contextStorage.xxxxx
|
||||
const contextCount = Object.keys(result.runtime.settings.contextStorage).length;
|
||||
contextCount.should.eql(3);//ensure no more than the 3 settings listed below are present in the contextStorage object
|
||||
result.runtime.settings.contextStorage.should.have.property('default', {module:"memory"});
|
||||
result.runtime.settings.contextStorage.should.have.property('file', {module:"localfilesystem"});
|
||||
result.runtime.settings.contextStorage.should.have.property('secured', {module:"secure_store"}); //only module should be present, other fields are dropped for security
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
Loading…
Reference in New Issue
Block a user