From b2516117f51817983be1b9a4b598f03d695c6fc5 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 18 Oct 2018 23:49:47 +0100 Subject: [PATCH] Add event log to editor Shows output from git pull/push and npm install in the editor --- Gruntfile.js | 1 + .../editor-client/locales/en-US/editor.json | 4 + .../editor-client/src/js/keymap.json | 3 +- .../@node-red/editor-client/src/js/red.js | 6 + .../editor-client/src/js/ui/event-log.js | 121 +++++++++++++++ .../editor-client/src/js/ui/palette-editor.js | 80 +++++++++- .../src/js/ui/projects/tab-versionControl.js | 12 ++ .../@node-red/editor-client/src/sass/ace.scss | 13 ++ .../@node-red/registry/lib/installer.js | 110 ++++++-------- .../@node-red/runtime/lib/api/comms.js | 17 +++ .../@node-red/runtime/lib/exec.js | 69 +++++++++ .../@node-red/runtime/lib/index.js | 3 + .../localfilesystem/projects/git/index.js | 140 ++++++++---------- packages/node_modules/@node-red/util/index.js | 5 +- packages/node_modules/node-red/lib/red.js | 13 -- .../@node-red/registry/lib/installer_spec.js | 140 +++++++++--------- test/unit/@node-red/runtime/lib/exec_spec.js | 22 +++ 17 files changed, 535 insertions(+), 224 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js create mode 100644 packages/node_modules/@node-red/runtime/lib/exec.js create mode 100644 test/unit/@node-red/runtime/lib/exec_spec.js diff --git a/Gruntfile.js b/Gruntfile.js index 272290495..411b6305f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -156,6 +156,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editor.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editors/*.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js", "packages/node_modules/@node-red/editor-client/src/js/ui/tray.js", "packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js", "packages/node_modules/@node-red/editor-client/src/js/ui/library.js", diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 304b9436d..9e0dcab33 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -215,6 +215,10 @@ "plusNMore": "+ __count__ more" } }, + "eventLog": { + "title": "Event log", + "view": "View log" + }, "diff": { "unresolvedCount": "__count__ unresolved conflict", "unresolvedCount_plural": "__count__ unresolved conflicts", diff --git a/packages/node_modules/@node-red/editor-client/src/js/keymap.json b/packages/node_modules/@node-red/editor-client/src/js/keymap.json index 5b7a94f1f..b94290862 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/keymap.json +++ b/packages/node_modules/@node-red/editor-client/src/js/keymap.json @@ -19,7 +19,8 @@ "ctrl-alt-r": "core:show-remote-diff", "ctrl-alt-n": "core:new-project", "ctrl-alt-o": "core:open-project", - "ctrl-g v": "core:show-version-control-tab" + "ctrl-g v": "core:show-version-control-tab", + "ctrl-shift-l": "core:show-event-log" }, "workspace": { "backspace": "core:delete-selection", diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 4d8012cf4..7c5bd3dda 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -398,6 +398,10 @@ var RED = (function() { // Refresh flow library to ensure any examples are updated RED.library.loadFlowLibrary(); }); + RED.comms.subscribe("event-log/#", function(topic,payload) { + var id = topic.substring(9); + RED.eventLog.log(id,payload); + }); } function showAbout() { @@ -436,6 +440,7 @@ var RED = (function() { // null, {id:"menu-item-palette",label:RED._("menu.label.palette.show"),toggle:true,onselect:"core:toggle-palette", selected: true}, {id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:"core:toggle-sidebar", selected: true}, + {id:"menu-item-event-log",label:RED._("eventLog.title"),onselect:"core:show-event-log"}, null ]}); menuOptions.push(null); @@ -483,6 +488,7 @@ var RED = (function() { RED.library.init(); RED.keyboard.init(); RED.palette.init(); + RED.eventLog.init(); if (RED.settings.theme('palette.editable') !== false) { RED.palette.editor.init(); } else { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js b/packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js new file mode 100644 index 000000000..c024b06fe --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js @@ -0,0 +1,121 @@ +/** + * 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. + **/ +RED.eventLog = (function() { + + var template = ''; + + var eventLogEditor; + var backlog = []; + var shown = false; + + function appendLogLine(line) { + backlog.push(line); + if (backlog.length > 500) { + backlog = backlog.slice(-500); + } + if (eventLogEditor) { + eventLogEditor.getSession().insert({ + row: eventLogEditor.getSession().getLength(), + column: 0 + }, "\n" + line); + eventLogEditor.scrollToLine(eventLogEditor.getSession().getLength()); + } + } + return { + init: function() { + $(template).appendTo(document.body); + RED.actions.add("core:show-event-log",RED.eventLog.show); + }, + show: function() { + if (shown) { + return; + } + shown = true; + var type = "_eventLog" + + var trayOptions = { + title: RED._("eventLog.title"), + width: Infinity, + buttons: [ + { + id: "node-dialog-close", + text: RED._("common.label.close"), + click: function() { + RED.tray.close(); + } + } + ], + resize: function(dimensions) { + var rows = $("#dialog-form>div:not(.node-text-editor-row)"); + var editorRow = $("#dialog-form>div.node-text-editor-row"); + var height = $("#dialog-form").height(); + for (var i=0;i').appendTo(spinner); + $('').text(RED._("eventLog.view")).appendTo(buttonRow).click(function(evt) { + evt.preventDefault(); + RED.actions.invoke("core:show-event-log"); + }); + RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.name+" "+version); installNodeModule(entry.name,version,function(xhr) { spinner.remove(); if (xhr) { if (xhr.responseJSON) { - RED.notify(RED._('palette.editor.errors.updateFailed',{module: entry.name,message:xhr.responseJSON.message})); + var notification = RED.notify(RED._('palette.editor.errors.updateFailed',{module: entry.name,message:xhr.responseJSON.message}),{ + type: 'error', + modal: true, + fixed: true, + buttons: [ + { + text: RED._("common.label.close"), + click: function() { + notification.close(); + } + },{ + text: RED._("eventLog.view"), + click: function() { + notification.close(); + RED.actions.invoke("core:show-event-log"); + } + } + ] + }); } } done(xhr); @@ -898,12 +922,35 @@ RED.palette.editor = (function() { class: "primary palette-module-install-confirm-button-remove", click: function() { var spinner = RED.utils.addSpinnerOverlay(container, true); + var buttonRow = $('
').appendTo(spinner); + $('').text(RED._("eventLog.view")).appendTo(buttonRow).click(function(evt) { + evt.preventDefault(); + RED.actions.invoke("core:show-event-log"); + }); + RED.eventLog.startEvent(RED._("palette.editor.confirm.button.remove")+" : "+entry.name); removeNodeModule(entry.name, function(xhr) { spinner.remove(); if (xhr) { if (xhr.responseJSON) { - RED.notify(RED._('palette.editor.errors.removeFailed',{module: entry.name,message:xhr.responseJSON.message})); - } + var notification = RED.notify(RED._('palette.editor.errors.removeFailed',{module: entry.name,message:xhr.responseJSON.message}),{ + type: 'error', + modal: true, + fixed: true, + buttons: [ + { + text: RED._("common.label.close"), + click: function() { + notification.close(); + } + },{ + text: RED._("eventLog.view"), + click: function() { + notification.close(); + RED.actions.invoke("core:show-event-log"); + } + } + ] + }); } } }) notification.close(); @@ -940,11 +987,36 @@ RED.palette.editor = (function() { class: "primary palette-module-install-confirm-button-install", click: function() { var spinner = RED.utils.addSpinnerOverlay(container, true); + + var buttonRow = $('
').appendTo(spinner); + $('').text(RED._("eventLog.view")).appendTo(buttonRow).click(function(evt) { + evt.preventDefault(); + RED.actions.invoke("core:show-event-log"); + }); + RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.id+" "+entry.version); installNodeModule(entry.id,entry.version,function(xhr) { spinner.remove(); if (xhr) { if (xhr.responseJSON) { - RED.notify(RED._('palette.editor.errors.installFailed',{module: entry.id,message:xhr.responseJSON.message})); + var notification = RED.notify(RED._('palette.editor.errors.installFailed',{module: entry.id,message:xhr.responseJSON.message}),{ + type: 'error', + modal: true, + fixed: true, + buttons: [ + { + text: RED._("common.label.close"), + click: function() { + notification.close(); + } + },{ + text: RED._("eventLog.view"), + click: function() { + notification.close(); + RED.actions.invoke("core:show-event-log"); + } + } + ] + }); } } done(xhr); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/tab-versionControl.js b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/tab-versionControl.js index 22fbc12f3..a153fe4da 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/tab-versionControl.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/tab-versionControl.js @@ -870,7 +870,13 @@ RED.sidebar.versionControl = (function() { .click(function(e) { e.preventDefault(); var spinner = utils.addSpinnerOverlay(remoteBox).addClass("projects-dialog-spinner-contain"); + var buttonRow = $('
').appendTo(spinner); + $('').text(RED._("eventLog.view")).appendTo(buttonRow).click(function(evt) { + evt.preventDefault(); + RED.actions.invoke("core:show-event-log"); + }); var activeProject = RED.projects.getActiveProject(); + RED.eventLog.startEvent("Push changes"+(activeProject.git.branches.remoteAlt?(" : "+activeProject.git.branches.remoteAlt):"")); var url = "projects/"+activeProject.name+"/push"; if (activeProject.git.branches.remoteAlt) { url+="/"+activeProject.git.branches.remoteAlt; @@ -914,7 +920,13 @@ RED.sidebar.versionControl = (function() { var pullRemote = function(options) { options = options || {}; var spinner = utils.addSpinnerOverlay(remoteBox).addClass("projects-dialog-spinner-contain"); + var buttonRow = $('
').appendTo(spinner); + $('').text(RED._("eventLog.view")).appendTo(buttonRow).click(function(evt) { + evt.preventDefault(); + RED.actions.invoke("core:show-event-log"); + }); var activeProject = RED.projects.getActiveProject(); + RED.eventLog.startEvent("Pull changes"+(activeProject.git.branches.remoteAlt?(" : "+activeProject.git.branches.remoteAlt):"")); var url = "projects/"+activeProject.name+"/pull"; if (activeProject.git.branches.remoteAlt) { url+="/"+activeProject.git.branches.remoteAlt; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/ace.scss b/packages/node_modules/@node-red/editor-client/src/sass/ace.scss index 9425c356c..c5f85badc 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/ace.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/ace.scss @@ -6,3 +6,16 @@ border-top-right-radius: 4px; border-bottom-right-radius: 4px; } + +#event-log-editor { + .ace_scroller { + background: #444; + color: #dd9; + } + .ace_active-line { + background: #666 !important; + } + .ace_selection { + background: #999 !important; + } +} diff --git a/packages/node_modules/@node-red/registry/lib/installer.js b/packages/node_modules/@node-red/registry/lib/installer.js index 9ec84a2f3..66768a620 100644 --- a/packages/node_modules/@node-red/registry/lib/installer.js +++ b/packages/node_modules/@node-red/registry/lib/installer.js @@ -21,6 +21,7 @@ var fs = require("fs"); var registry = require("./registry"); var library = require("./library"); var log; +var exec; var events; @@ -36,6 +37,7 @@ function init(runtime) { events = runtime.events; settings = runtime.settings; log = runtime.log; + exec = runtime.exec; } var activePromise = Promise.resolve(); @@ -104,50 +106,40 @@ function installModule(module,version) { var installDir = settings.userDir || process.env.NODE_RED_HOME || "."; var args = ['install','--save','--save-prefix="~"','--production',installName]; log.trace(npmCommand + JSON.stringify(args)); - var child = child_process.spawn(npmCommand,args,{ - cwd: installDir, - shell: true - }); - var output = ""; - child.stdout.on('data', (data) => { - output += data; - }); - child.stderr.on('data', (data) => { - output += data; - }); - child.on('close', (code) => { - if (code !== 0) { - var e; - var lookFor404 = new RegExp(" 404 .*"+module,"m"); - var lookForVersionNotFound = new RegExp("version not found: "+module+"@"+version,"m"); - if (lookFor404.test(output)) { - log.warn(log._("server.install.install-failed-not-found",{name:module})); - e = new Error("Module not found"); - e.code = 404; - reject(e); - } else if (isUpgrade && lookForVersionNotFound.test(output)) { - log.warn(log._("server.install.upgrade-failed-not-found",{name:module})); - e = new Error("Module not found"); - e.code = 404; - reject(e); - } else { - log.warn(log._("server.install.install-failed-long",{name:module})); - log.warn("------------------------------------------"); - log.warn(output); - log.warn("------------------------------------------"); - reject(new Error(log._("server.install.install-failed"))); - } + exec.run(npmCommand,args,{ + cwd: installDir + }, true).then(result => { + if (!isUpgrade) { + log.info(log._("server.install.installed",{name:module})); + resolve(require("./index").addModule(module).then(reportAddedModules)); } else { - if (!isUpgrade) { - log.info(log._("server.install.installed",{name:module})); - resolve(require("./index").addModule(module).then(reportAddedModules)); - } else { - log.info(log._("server.install.upgraded",{name:module, version:version})); - events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true}); - resolve(require("./registry").setModulePendingUpdated(module,version)); - } + log.info(log._("server.install.upgraded",{name:module, version:version})); + events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true}); + resolve(require("./registry").setModulePendingUpdated(module,version)); } - }); + }).catch(result => { + var output = result.stderr; + var e; + var lookFor404 = new RegExp(" 404 .*"+module,"m"); + var lookForVersionNotFound = new RegExp("version not found: "+module+"@"+version,"m"); + if (lookFor404.test(output)) { + log.warn(log._("server.install.install-failed-not-found",{name:module})); + e = new Error("Module not found"); + e.code = 404; + reject(e); + } else if (isUpgrade && lookForVersionNotFound.test(output)) { + log.warn(log._("server.install.upgrade-failed-not-found",{name:module})); + e = new Error("Module not found"); + e.code = 404; + reject(e); + } else { + log.warn(log._("server.install.install-failed-long",{name:module})); + log.warn("------------------------------------------"); + log.warn(output); + log.warn("------------------------------------------"); + reject(new Error(log._("server.install.install-failed"))); + } + }) }); }).catch(err => { // In case of error, reset activePromise to be resolvable @@ -208,25 +200,21 @@ function uninstallModule(module) { var args = ['remove','--save',module]; log.trace(npmCommand + JSON.stringify(args)); - var child = child_process.execFile(npmCommand,args, - { - cwd: installDir - }, - function(err, stdin, stdout) { - if (err) { - log.warn(log._("server.install.uninstall-failed-long",{name:module})); - log.warn("------------------------------------------"); - log.warn(err.toString()); - log.warn("------------------------------------------"); - reject(new Error(log._("server.install.uninstall-failed",{name:module}))); - } else { - log.info(log._("server.install.uninstalled",{name:module})); - reportRemovedModules(list); - library.removeExamplesDir(module); - resolve(list); - } - } - ); + exec.run(npmCommand,args,{ + cwd: installDir, + },true).then(result => { + log.info(log._("server.install.uninstalled",{name:module})); + reportRemovedModules(list); + library.removeExamplesDir(module); + resolve(list); + }).catch(result => { + var output = result.stderr; + log.warn(log._("server.install.uninstall-failed-long",{name:module})); + log.warn("------------------------------------------"); + log.warn(output.toString()); + log.warn("------------------------------------------"); + reject(new Error(log._("server.install.uninstall-failed",{name:module}))); + }); }); }).catch(err => { // In case of error, reset activePromise to be resolvable diff --git a/packages/node_modules/@node-red/runtime/lib/api/comms.js b/packages/node_modules/@node-red/runtime/lib/api/comms.js index 3ab361f3d..7713fd3dc 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/comms.js +++ b/packages/node_modules/@node-red/runtime/lib/api/comms.js @@ -42,6 +42,21 @@ function handleRuntimeEvent(event) { runtime.log.trace("runtime event: "+JSON.stringify(event)); publish("notification/"+event.id,event.payload||{},event.retain); } +function handleEventLog(event) { + var type = event.payload.type; + var id = event.id; + if (event.payload.data) { + var data = event.payload.data; + if (data.endsWith('\n')) { + data = data.substring(0,data.length-1); + } + var lines = data.split(/\n/); + lines.forEach(line => { + runtime.log.debug((type?("["+type+"] "):"")+line) + }) + } + publish("event-log/"+event.id,event.payload||{}); +} function publish(topic,data,retain) { if (retain) { @@ -64,6 +79,8 @@ var api = module.exports = { runtime.events.on("runtime-event",handleRuntimeEvent); runtime.events.removeListener("comms",handleCommsEvent); runtime.events.on("comms",handleCommsEvent); + runtime.events.removeListener("event-log",handleEventLog); + runtime.events.on("event-log",handleEventLog); }, /** diff --git a/packages/node_modules/@node-red/runtime/lib/exec.js b/packages/node_modules/@node-red/runtime/lib/exec.js new file mode 100644 index 000000000..0ef3c069c --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/exec.js @@ -0,0 +1,69 @@ +/** + * 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. + **/ + +const child_process = require('child_process'); +const { util } = require('@node-red/util'); + +var events; + +function logLines(id,type,data) { + events.emit("event-log", {id:id,payload:{ts: Date.now(),data:data,type:type}}); +} + +module.exports = { + init: function(_runtime) { + events = _runtime.events; + }, + run: function(command,args,options,emit) { + var invocationId = util.generateId(); + + emit && events.emit("event-log", {ts: Date.now(),id:invocationId,payload:{ts: Date.now(),data:command+" "+args.join(" ")}}); + + return new Promise((resolve, reject) => { + let stdout = ""; + let stderr = ""; + const child = child_process.spawn(command,args,options); + child.stdout.on('data', (data) => { + const str = ""+data; + stdout += str; + emit && logLines(invocationId,"out",str); + }); + child.stderr.on('data', (data) => { + const str = ""+data; + stderr += str; + emit && logLines(invocationId,"err",str); + }); + child.on('error', function(err) { + stderr = err.toString(); + emit && logLines(invocationId,"err",stderr); + }) + child.on('close', (code) => { + let result = { + code: code, + stdout: stdout, + stderr: stderr + } + emit && events.emit("event-log", {id:invocationId,payload:{ts: Date.now(),data:"rc="+code}}); + + if (code === 0) { + resolve(result) + } else { + reject(result); + } + }); + }) + } +} diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index fb4e74b93..fd6e59794 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -23,6 +23,7 @@ var storage = require("./storage"); var library = require("./library"); var events = require("./events"); var settings = require("./settings"); +var exec = require("./exec"); var express = require("express"); var path = require('path'); @@ -71,6 +72,7 @@ function init(userSettings,_redUtil,_adminApi) { redNodes.init(runtime); library.init(runtime); externalAPI.init(runtime); + exec.init(runtime); } var version; @@ -248,6 +250,7 @@ var runtime = { events: events, nodes: redNodes, library: library, + exec: exec, util: require("@node-red/util").util, get adminApi() { return adminApi }, get nodeApp() { return nodeApp }, diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js index b6f3fa61a..d5c23ccb6 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js @@ -15,8 +15,9 @@ **/ var when = require('when'); -var exec = require('child_process').exec; -var spawn = require('child_process').spawn; + +var exec = require("../../../../exec"); + var authResponseServer = require('./authServer').ResponseServer; var sshResponseServer = require('./authServer').ResponseSSHServer; var clone = require('clone'); @@ -26,83 +27,70 @@ var gitCommand = "git"; var gitVersion; var log; -function runGitCommand(args,cwd,env) { +function runGitCommand(args,cwd,env,emit) { log.trace(gitCommand + JSON.stringify(args)); - return when.promise(function(resolve,reject) { - args.unshift("credential.helper=") - args.unshift("-c"); - var child = spawn(gitCommand, args, {cwd:cwd, detached:true, env:env}); - var stdout = ""; - var stderr = ""; - child.stdout.on('data', function(data) { - stdout += data; - }); - child.stderr.on('data', function(data) { - stderr += data; - }); - child.on('error', function(err) { - stderr = err.toString(); - }) - child.on('close', function(code) { - if (code !== 0) { - var err = new Error(stderr); - err.stdout = stdout; - err.stderr = stderr; - if (/Connection refused/i.test(stderr)) { - err.code = "git_connection_failed"; - } else if (/Connection timed out/i.test(stderr)) { - err.code = "git_connection_failed"; - } else if (/fatal: could not read/i.test(stderr)) { - // Username/Password - err.code = "git_auth_failed"; - } else if(/HTTP Basic: Access denied/i.test(stderr)) { - err.code = "git_auth_failed"; - } else if(/Permission denied \(publickey\)/i.test(stderr)) { - err.code = "git_auth_failed"; - } else if(/Host key verification failed/i.test(stderr)) { - // TODO: handle host key verification errors separately - err.code = "git_auth_failed"; - } else if (/commit your changes or stash/i.test(stderr)) { - err.code = "git_local_overwrite"; - } else if (/CONFLICT/.test(err.stdout)) { - err.code = "git_pull_merge_conflict"; - } else if (/not fully merged/i.test(stderr)) { - err.code = "git_delete_branch_unmerged"; - } else if (/remote .* already exists/i.test(stderr)) { - err.code = "git_remote_already_exists"; - } else if (/does not appear to be a git repository/i.test(stderr)) { - err.code = "git_not_a_repository"; - } else if (/Repository not found/i.test(stderr)) { - err.code = "git_repository_not_found"; - } else if (/repository '.*' does not exist/i.test(stderr)) { - err.code = "git_repository_not_found"; - } else if (/refusing to merge unrelated histories/i.test(stderr)) { - err.code = "git_pull_unrelated_history" - } else if (/Please tell me who you are/i.test(stderr)) { - err.code = "git_missing_user"; - } else if (/name consists only of disallowed characters/i.test(stderr)) { - err.code = "git_missing_user"; - } - return reject(err); - } - resolve(stdout); - }); - }); + args.unshift("credential.helper=") + args.unshift("-c"); + return exec.run(gitCommand, args, {cwd:cwd, env:env}, emit).then(result => { + return result.stdout; + }).catch(result => { + var err = new Error(stderr); + var stdout = result.stdout; + var stderr = result.stderr; + err.stdout = stdout; + err.stderr = stderr; + if (/Connection refused/i.test(stderr)) { + err.code = "git_connection_failed"; + } else if (/Connection timed out/i.test(stderr)) { + err.code = "git_connection_failed"; + } else if (/fatal: could not read/i.test(stderr)) { + // Username/Password + err.code = "git_auth_failed"; + } else if(/HTTP Basic: Access denied/i.test(stderr)) { + err.code = "git_auth_failed"; + } else if(/Permission denied \(publickey\)/i.test(stderr)) { + err.code = "git_auth_failed"; + } else if(/Host key verification failed/i.test(stderr)) { + // TODO: handle host key verification errors separately + err.code = "git_auth_failed"; + } else if (/commit your changes or stash/i.test(stderr)) { + err.code = "git_local_overwrite"; + } else if (/CONFLICT/.test(err.stdout)) { + err.code = "git_pull_merge_conflict"; + } else if (/not fully merged/i.test(stderr)) { + err.code = "git_delete_branch_unmerged"; + } else if (/remote .* already exists/i.test(stderr)) { + err.code = "git_remote_already_exists"; + } else if (/does not appear to be a git repository/i.test(stderr)) { + err.code = "git_not_a_repository"; + } else if (/Repository not found/i.test(stderr)) { + err.code = "git_repository_not_found"; + } else if (/repository '.*' does not exist/i.test(stderr)) { + err.code = "git_repository_not_found"; + } else if (/refusing to merge unrelated histories/i.test(stderr)) { + err.code = "git_pull_unrelated_history" + } else if (/Please tell me who you are/i.test(stderr)) { + err.code = "git_missing_user"; + } else if (/name consists only of disallowed characters/i.test(stderr)) { + err.code = "git_missing_user"; + } + throw err; + }) } -function runGitCommandWithAuth(args,cwd,auth) { +function runGitCommandWithAuth(args,cwd,auth,emit) { return authResponseServer(auth).then(function(rs) { var commandEnv = clone(process.env); commandEnv.GIT_ASKPASS = path.join(__dirname,"node-red-ask-pass.sh"); commandEnv.NODE_RED_GIT_NODE_PATH = process.execPath; commandEnv.NODE_RED_GIT_SOCK_PATH = rs.path; commandEnv.NODE_RED_GIT_ASKPASS_PATH = path.join(__dirname,"authWriter.js"); - return runGitCommand(args,cwd,commandEnv).finally(function() { + return runGitCommand(args,cwd,commandEnv,emit).finally(function() { rs.close(); }); }) } -function runGitCommandWithSSHCommand(args,cwd,auth) { +function runGitCommandWithSSHCommand(args,cwd,auth,emit) { return sshResponseServer(auth).then(function(rs) { var commandEnv = clone(process.env); commandEnv.SSH_ASKPASS = path.join(__dirname,"node-red-ask-pass.sh"); @@ -116,7 +104,7 @@ function runGitCommandWithSSHCommand(args,cwd,auth) { // GIT_SSH_COMMAND - added in git 2.3.0 commandEnv.GIT_SSH_COMMAND = "ssh -i " + auth.key_path + " -F /dev/null"; // console.log('commandEnv:', commandEnv); - return runGitCommand(args,cwd,commandEnv).finally(function() { + return runGitCommand(args,cwd,commandEnv,emit).finally(function() { rs.close(); }); }) @@ -448,13 +436,13 @@ module.exports = { var promise; if (auth) { if ( auth.key_path ) { - promise = runGitCommandWithSSHCommand(args,cwd,auth); + promise = runGitCommandWithSSHCommand(args,cwd,auth,true); } else { - promise = runGitCommandWithAuth(args,cwd,auth); + promise = runGitCommandWithAuth(args,cwd,auth,true); } } else { - promise = runGitCommand(args,cwd) + promise = runGitCommand(args,cwd,undefined,true) } return promise; // .catch(function(err) { @@ -485,13 +473,13 @@ module.exports = { var promise; if (auth) { if ( auth.key_path ) { - promise = runGitCommandWithSSHCommand(args,cwd,auth); + promise = runGitCommandWithSSHCommand(args,cwd,auth,true); } else { - promise = runGitCommandWithAuth(args,cwd,auth); + promise = runGitCommandWithAuth(args,cwd,auth,true); } } else { - promise = runGitCommand(args,cwd) + promise = runGitCommand(args,cwd,undefined,true) } return promise.catch(function(err) { if (err.code === 'git_error') { @@ -517,13 +505,13 @@ module.exports = { args.push("."); if (auth) { if ( auth.key_path ) { - return runGitCommandWithSSHCommand(args,cwd,auth); + return runGitCommandWithSSHCommand(args,cwd,auth,true); } else { - return runGitCommandWithAuth(args,cwd,auth); + return runGitCommandWithAuth(args,cwd,auth,true); } } else { - return runGitCommand(args,cwd); + return runGitCommand(args,cwd,undefined,true); } }, getStatus: getStatus, diff --git a/packages/node_modules/@node-red/util/index.js b/packages/node_modules/@node-red/util/index.js index 7fa63fc7d..cc7f502f8 100644 --- a/packages/node_modules/@node-red/util/index.js +++ b/packages/node_modules/@node-red/util/index.js @@ -30,19 +30,22 @@ module.exports = { log.init(settings); i18n.init(); }, + /** * Logging utilities * @see module:@node-red/util.module:log */ log: log, + /** * Internationalization utilities * @see module:@node-red/util.module:i18n */ i18n: i18n, + /** * General utilities * @see module:@node-red/util.module:util */ - util: util + util: util, } diff --git a/packages/node_modules/node-red/lib/red.js b/packages/node_modules/node-red/lib/red.js index b6eed50ac..a25b30fd3 100644 --- a/packages/node_modules/node-red/lib/red.js +++ b/packages/node_modules/node-red/lib/red.js @@ -38,18 +38,6 @@ function checkVersion(userSettings) { } } -function checkBuild() { - console.log("Skipping red.js/checkBuild"); - // var editorFile = path.resolve(path.join(__dirname,"..","..","..","..","public","red","red.min.js")); - // try { - // var stats = fs.statSync(editorFile); - // } catch(err) { - // var e = new Error("Node-RED not built"); - // e.code = "not_built"; - // throw e; - // } -} - module.exports = { init: function(httpServer,userSettings) { if (!userSettings) { @@ -59,7 +47,6 @@ module.exports = { if (!userSettings.SKIP_BUILD_CHECK) { checkVersion(userSettings); - checkBuild(); } if (!userSettings.coreNodesDir) { diff --git a/test/unit/@node-red/registry/lib/installer_spec.js b/test/unit/@node-red/registry/lib/installer_spec.js index 65747d185..f3bae8b3a 100644 --- a/test/unit/@node-red/registry/lib/installer_spec.js +++ b/test/unit/@node-red/registry/lib/installer_spec.js @@ -21,9 +21,6 @@ var path = require("path"); var fs = require('fs'); var EventEmitter = require('events'); -var child_process = require('child_process'); - - var NR_TEST_UTILS = require("nr-test-utils"); var installer = NR_TEST_UTILS.require("@node-red/registry/lib/installer"); @@ -42,16 +39,21 @@ describe('nodes/registry/installer', function() { _: function() { return "abc"} } - before(function() { - installer.init({log:mockLog, settings:{}, events: new EventEmitter()}); + beforeEach(function() { + installer.init({log:mockLog, settings:{}, events: new EventEmitter(), exec: { + run: function() { + return Promise.resolve(""); + } + }}); }); + function initInstaller(execResult) { + installer.init({log:mockLog, settings:{}, events: new EventEmitter(), exec: { + run: function() { + return execResult; + } + }}); + } afterEach(function() { - if (child_process.spawn.restore) { - child_process.spawn.restore(); - } - if (child_process.execFile.restore) { - child_process.execFile.restore(); - } if (registry.addModule.restore) { registry.addModule.restore(); } @@ -76,39 +78,33 @@ describe('nodes/registry/installer', function() { describe("installs module", function() { it("rejects when npm returns a 404", function(done) { - sinon.stub(child_process,"spawn",function(cmd,args,opt) { - var ee = new EventEmitter(); - ee.stdout = new EventEmitter(); - ee.stderr = new EventEmitter(); - setTimeout(function() { - ee.stderr.emit('data'," 404 this_wont_exist"); - ee.emit('close',1); - },10) - return ee; - }); - + var res = { + code: 1, + stdout:"", + stderr:" 404 this_wont_exist" + } + var p = Promise.reject(res); + p.catch((err)=>{}); + initInstaller(p) installer.installModule("this_wont_exist").catch(function(err) { err.should.have.property("code",404); done(); }); }); it("rejects when npm does not find specified version", function(done) { - sinon.stub(child_process,"spawn",function(cmd,args,opt) { - var ee = new EventEmitter(); - ee.stdout = new EventEmitter(); - ee.stderr = new EventEmitter(); - setTimeout(function() { - ee.stderr.emit('data'," version not found: this_wont_exist@0.1.2"); - ee.emit('close',1); - },10) - return ee; - }); + var res = { + code: 1, + stdout:"", + stderr:" version not found: this_wont_exist@0.1.2" + } + var p = Promise.reject(res); + p.catch((err)=>{}); + initInstaller(p) sinon.stub(typeRegistry,"getModuleInfo", function() { return { version: "0.1.1" } }); - installer.installModule("this_wont_exist","0.1.2").catch(function(err) { err.code.should.be.eql(404); done(); @@ -126,17 +122,14 @@ describe('nodes/registry/installer', function() { }); }); it("rejects with generic error", function(done) { - sinon.stub(child_process,"spawn",function(cmd,args,opt,cb) { - var ee = new EventEmitter(); - ee.stdout = new EventEmitter(); - ee.stderr = new EventEmitter(); - setTimeout(function() { - ee.stderr.emit('data'," kaboom!"); - ee.emit('close',1); - },10) - return ee; - }); - + var res = { + code: 1, + stdout:"", + stderr:" kaboom!" + } + var p = Promise.reject(res); + p.catch((err)=>{}); + initInstaller(p) installer.installModule("this_wont_exist").then(function() { done(new Error("Unexpected success")); }).catch(function(err) { @@ -145,15 +138,16 @@ describe('nodes/registry/installer', function() { }); it("succeeds when module is found", function(done) { var nodeInfo = {nodes:{module:"foo",types:["a"]}}; - sinon.stub(child_process,"spawn",function(cmd,args,opt) { - var ee = new EventEmitter(); - ee.stdout = new EventEmitter(); - ee.stderr = new EventEmitter(); - setTimeout(function() { - ee.emit('close',0); - },10) - return ee; - }); + + var res = { + code: 0, + stdout:"", + stderr:"" + } + var p = Promise.resolve(res); + p.catch((err)=>{}); + initInstaller(p) + var addModule = sinon.stub(registry,"addModule",function(md) { return when.resolve(nodeInfo); }); @@ -191,15 +185,15 @@ describe('nodes/registry/installer', function() { return when.resolve(nodeInfo); }); var resourcesDir = path.resolve(path.join(__dirname,"resources","local","TestNodeModule","node_modules","TestNodeModule")); - sinon.stub(child_process,"spawn",function(cmd,args,opt) { - var ee = new EventEmitter(); - ee.stdout = new EventEmitter(); - ee.stderr = new EventEmitter(); - setTimeout(function() { - ee.emit('close',0); - },10) - return ee; - }); + + var res = { + code: 0, + stdout:"", + stderr:"" + } + var p = Promise.resolve(res); + p.catch((err)=>{}); + initInstaller(p) installer.installModule(resourcesDir).then(function(info) { info.should.eql(nodeInfo); done(); @@ -226,9 +220,14 @@ describe('nodes/registry/installer', function() { var removeModule = sinon.stub(registry,"removeModule",function(md) { return when.resolve(nodeInfo); }); - sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { - cb(new Error("test_error"),"",""); - }); + var res = { + code: 1, + stdout:"", + stderr:"error" + } + var p = Promise.reject(res); + p.catch((err)=>{}); + initInstaller(p) installer.uninstallModule("this_wont_exist").then(function() { done(new Error("Unexpected success")); @@ -244,9 +243,14 @@ describe('nodes/registry/installer', function() { var getModuleInfo = sinon.stub(registry,"getModuleInfo",function(md) { return {nodes:[]}; }); - sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { - cb(null,"",""); - }); + var res = { + code: 0, + stdout:"", + stderr:"" + } + var p = Promise.resolve(res); + p.catch((err)=>{}); + initInstaller(p) sinon.stub(fs,"statSync", function(fn) { return {}; }); diff --git a/test/unit/@node-red/runtime/lib/exec_spec.js b/test/unit/@node-red/runtime/lib/exec_spec.js new file mode 100644 index 000000000..e0369e07c --- /dev/null +++ b/test/unit/@node-red/runtime/lib/exec_spec.js @@ -0,0 +1,22 @@ +/** + * 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 should = require("should"); +var sinon = require("sinon"); +var path = require("path"); + +var NR_TEST_UTILS = require("nr-test-utils"); + +var exec = NR_TEST_UTILS.require("@node-red/runtime/lib/exec");