mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Merge pull request #2936 from node-red/npm-install-hooks
Add pre/postInstall hooks to npm install handling
This commit is contained in:
commit
4133f9c56f
@ -9,6 +9,7 @@ const path = require("path");
|
|||||||
const clone = require("clone");
|
const clone = require("clone");
|
||||||
const exec = require("@node-red/util").exec;
|
const exec = require("@node-red/util").exec;
|
||||||
const log = require("@node-red/util").log;
|
const log = require("@node-red/util").log;
|
||||||
|
const hooks = require("@node-red/util").hooks;
|
||||||
|
|
||||||
const BUILTIN_MODULES = require('module').builtinModules;
|
const BUILTIN_MODULES = require('module').builtinModules;
|
||||||
const EXTERNAL_MODULES_DIR = "externalModules";
|
const EXTERNAL_MODULES_DIR = "externalModules";
|
||||||
@ -189,13 +190,29 @@ async function installModule(moduleDetails) {
|
|||||||
|
|
||||||
await ensureModuleDir();
|
await ensureModuleDir();
|
||||||
|
|
||||||
var args = ["install", installSpec, "--production"];
|
let triggerPayload = {
|
||||||
return exec.run(NPM_COMMAND, args, {
|
"module": moduleDetails.module,
|
||||||
cwd: installDir
|
"version": moduleDetails.version,
|
||||||
},true).then(result => {
|
"dir": installDir,
|
||||||
|
"args": ["--production"]
|
||||||
|
}
|
||||||
|
return hooks.trigger("preInstall", triggerPayload).then((result) => {
|
||||||
|
// preInstall passed
|
||||||
|
// - run install
|
||||||
|
if (result !== false) {
|
||||||
|
let extraArgs = triggerPayload.args || [];
|
||||||
|
let args = ['install', ...extraArgs, installSpec]
|
||||||
|
log.trace(NPM_COMMAND + JSON.stringify(args));
|
||||||
|
return exec.run(NPM_COMMAND, args, { cwd: installDir },true)
|
||||||
|
} else {
|
||||||
|
log.trace("skipping npm install");
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
return hooks.trigger("postInstall", triggerPayload)
|
||||||
|
}).then(() => {
|
||||||
log.info(log._("server.install.installed", { name: installSpec }));
|
log.info(log._("server.install.installed", { name: installSpec }));
|
||||||
}).catch(result => {
|
}).catch(result => {
|
||||||
var output = result.stderr;
|
var output = result.stderr || result.toString();
|
||||||
var e;
|
var e;
|
||||||
if (/E404/.test(output) || /ETARGET/.test(output)) {
|
if (/E404/.test(output) || /ETARGET/.test(output)) {
|
||||||
log.error(log._("server.install.install-failed-not-found",{name:installSpec}));
|
log.error(log._("server.install.install-failed-not-found",{name:installSpec}));
|
||||||
|
@ -23,7 +23,7 @@ const tar = require("tar");
|
|||||||
const registry = require("./registry");
|
const registry = require("./registry");
|
||||||
const registryUtil = require("./util");
|
const registryUtil = require("./util");
|
||||||
const library = require("./library");
|
const library = require("./library");
|
||||||
const {exec,log,events} = require("@node-red/util");
|
const {exec,log,events,hooks} = require("@node-red/util");
|
||||||
const child_process = require('child_process');
|
const child_process = require('child_process');
|
||||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||||
let installerEnabled = false;
|
let installerEnabled = false;
|
||||||
@ -168,11 +168,30 @@ async function installModule(module,version,url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var installDir = settings.userDir || process.env.NODE_RED_HOME || ".";
|
var installDir = settings.userDir || process.env.NODE_RED_HOME || ".";
|
||||||
var args = ['install','--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production',installName];
|
let triggerPayload = {
|
||||||
log.trace(npmCommand + JSON.stringify(args));
|
"module": module,
|
||||||
return exec.run(npmCommand,args,{
|
"version": version,
|
||||||
cwd: installDir
|
"url": url,
|
||||||
}, true).then(result => {
|
"dir": installDir,
|
||||||
|
"isExisting": isExisting,
|
||||||
|
"isUpgrade": isUpgrade,
|
||||||
|
"args": ['--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production']
|
||||||
|
}
|
||||||
|
|
||||||
|
return hooks.trigger("preInstall", triggerPayload).then((result) => {
|
||||||
|
// preInstall passed
|
||||||
|
// - run install
|
||||||
|
if (result !== false) {
|
||||||
|
let extraArgs = triggerPayload.args || [];
|
||||||
|
let args = ['install', ...extraArgs, installName]
|
||||||
|
log.trace(npmCommand + JSON.stringify(args));
|
||||||
|
return exec.run(npmCommand,args,{ cwd: installDir}, true)
|
||||||
|
} else {
|
||||||
|
log.trace("skipping npm install");
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
return hooks.trigger("postInstall", triggerPayload)
|
||||||
|
}).then(() => {
|
||||||
if (isExisting) {
|
if (isExisting) {
|
||||||
// This is a module we already have installed as a non-user module.
|
// This is a module we already have installed as a non-user module.
|
||||||
// That means it was discovered when loading, but was not listed
|
// That means it was discovered when loading, but was not listed
|
||||||
@ -191,29 +210,45 @@ async function installModule(module,version,url) {
|
|||||||
events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true});
|
events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true});
|
||||||
return require("./registry").setModulePendingUpdated(module,version);
|
return require("./registry").setModulePendingUpdated(module,version);
|
||||||
}
|
}
|
||||||
}).catch(result => {
|
}).catch(err => {
|
||||||
var output = result.stderr;
|
let e;
|
||||||
var e;
|
if (err.hook) {
|
||||||
var lookFor404 = new RegExp(" 404 .*"+module,"m");
|
// preInstall failed
|
||||||
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;
|
|
||||||
throw 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;
|
|
||||||
throw e;
|
|
||||||
} else {
|
|
||||||
log.warn(log._("server.install.install-failed-long",{name:module}));
|
log.warn(log._("server.install.install-failed-long",{name:module}));
|
||||||
log.warn("------------------------------------------");
|
log.warn("------------------------------------------");
|
||||||
log.warn(output);
|
log.warn(err.toString());
|
||||||
log.warn("------------------------------------------");
|
log.warn("------------------------------------------");
|
||||||
throw new Error(log._("server.install.install-failed"));
|
e = new Error(log._("server.install.install-failed")+": "+err.toString());
|
||||||
|
if (err.hook === "postInstall") {
|
||||||
|
return exec.run(npmCommand,["remove",module],{ cwd: installDir}, false).finally(() => {
|
||||||
|
throw e;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// npm install failed
|
||||||
|
let output = err.stderr;
|
||||||
|
let lookFor404 = new RegExp(" 404 .*"+module,"m");
|
||||||
|
let 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;
|
||||||
|
} 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;
|
||||||
|
} else {
|
||||||
|
log.warn(log._("server.install.install-failed-long",{name:module}));
|
||||||
|
log.warn("------------------------------------------");
|
||||||
|
log.warn(output);
|
||||||
|
log.warn("------------------------------------------");
|
||||||
|
e = new Error(log._("server.install.install-failed"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
if (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
// In case of error, reset activePromise to be resolvable
|
// In case of error, reset activePromise to be resolvable
|
||||||
activePromise = Promise.resolve();
|
activePromise = Promise.resolve();
|
||||||
@ -412,17 +447,29 @@ function uninstallModule(module) {
|
|||||||
log.info(log._("server.install.uninstalling",{name:module}));
|
log.info(log._("server.install.uninstalling",{name:module}));
|
||||||
|
|
||||||
var args = ['remove','--no-audit','--no-update-notifier','--no-fund','--save',module];
|
var args = ['remove','--no-audit','--no-update-notifier','--no-fund','--save',module];
|
||||||
log.trace(npmCommand + JSON.stringify(args));
|
|
||||||
|
|
||||||
exec.run(npmCommand,args,{
|
let triggerPayload = {
|
||||||
cwd: installDir,
|
"module": module,
|
||||||
},true).then(result => {
|
"dir": installDir,
|
||||||
|
}
|
||||||
|
return hooks.trigger("preUninstall", triggerPayload).then(() => {
|
||||||
|
// preUninstall passed
|
||||||
|
// - run uninstall
|
||||||
|
log.trace(npmCommand + JSON.stringify(args));
|
||||||
|
return exec.run(npmCommand,args,{ cwd: installDir}, true)
|
||||||
|
}).then(() => {
|
||||||
log.info(log._("server.install.uninstalled",{name:module}));
|
log.info(log._("server.install.uninstalled",{name:module}));
|
||||||
reportRemovedModules(list);
|
reportRemovedModules(list);
|
||||||
library.removeExamplesDir(module);
|
library.removeExamplesDir(module);
|
||||||
resolve(list);
|
return hooks.trigger("postUninstall", triggerPayload).catch((err)=>{
|
||||||
|
log.warn("------------------------------------------");
|
||||||
|
log.warn(err.toString());
|
||||||
|
log.warn("------------------------------------------");
|
||||||
|
}).finally(() => {
|
||||||
|
resolve(list);
|
||||||
|
})
|
||||||
}).catch(result => {
|
}).catch(result => {
|
||||||
var output = result.stderr;
|
let output = result.stderr || result;
|
||||||
log.warn(log._("server.install.uninstall-failed-long",{name:module}));
|
log.warn(log._("server.install.uninstall-failed-long",{name:module}));
|
||||||
log.warn("------------------------------------------");
|
log.warn("------------------------------------------");
|
||||||
log.warn(output.toString());
|
log.warn(output.toString());
|
||||||
|
@ -19,7 +19,7 @@ var redUtil = require("@node-red/util").util;
|
|||||||
const events = require("@node-red/util").events;
|
const events = require("@node-red/util").events;
|
||||||
var flowUtil = require("./util");
|
var flowUtil = require("./util");
|
||||||
const context = require('../nodes/context');
|
const context = require('../nodes/context');
|
||||||
const hooks = require("../hooks");
|
const hooks = require("@node-red/util").hooks;
|
||||||
|
|
||||||
var Subflow;
|
var Subflow;
|
||||||
var Log;
|
var Log;
|
||||||
|
@ -20,7 +20,6 @@ var redNodes = require("./nodes");
|
|||||||
var flows = require("./flows");
|
var flows = require("./flows");
|
||||||
var storage = require("./storage");
|
var storage = require("./storage");
|
||||||
var library = require("./library");
|
var library = require("./library");
|
||||||
var hooks = require("./hooks");
|
|
||||||
var plugins = require("./plugins");
|
var plugins = require("./plugins");
|
||||||
var settings = require("./settings");
|
var settings = require("./settings");
|
||||||
|
|
||||||
@ -29,7 +28,7 @@ var path = require('path');
|
|||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
var os = require("os");
|
var os = require("os");
|
||||||
|
|
||||||
const {log,i18n,events,exec,util} = require("@node-red/util");
|
const {log,i18n,events,exec,util,hooks} = require("@node-red/util");
|
||||||
|
|
||||||
var runtimeMetricInterval = null;
|
var runtimeMetricInterval = null;
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ var redUtil = require("@node-red/util").util;
|
|||||||
var Log = require("@node-red/util").log;
|
var Log = require("@node-red/util").log;
|
||||||
var context = require("./context");
|
var context = require("./context");
|
||||||
var flows = require("../flows");
|
var flows = require("../flows");
|
||||||
const hooks = require("../hooks");
|
const hooks = require("@node-red/util").hooks;
|
||||||
|
|
||||||
|
|
||||||
const NOOP_SEND = function() {}
|
const NOOP_SEND = function() {}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"install-failed-not-found": "$t(server.install.install-failed-long) module not found",
|
"install-failed-not-found": "$t(server.install.install-failed-long) module not found",
|
||||||
"install-failed-name": "$t(server.install.install-failed-long) invalid module name: __name__",
|
"install-failed-name": "$t(server.install.install-failed-long) invalid module name: __name__",
|
||||||
"install-failed-url": "$t(server.install.install-failed-long) invalid url: __url__",
|
"install-failed-url": "$t(server.install.install-failed-long) invalid url: __url__",
|
||||||
|
"post-install-error": "Error running 'postInstall' hook:",
|
||||||
"upgrading": "Upgrading module: __name__ to version: __version__",
|
"upgrading": "Upgrading module: __name__ to version: __version__",
|
||||||
"upgraded": "Upgraded module: __name__. Restart Node-RED to use the new version",
|
"upgraded": "Upgraded module: __name__. Restart Node-RED to use the new version",
|
||||||
"upgrade-failed-not-found": "$t(server.install.install-failed-long) version not found",
|
"upgrade-failed-not-found": "$t(server.install.install-failed-long) version not found",
|
||||||
|
10
packages/node_modules/@node-red/util/index.js
vendored
10
packages/node_modules/@node-red/util/index.js
vendored
@ -19,6 +19,7 @@ const i18n = require("./lib/i18n");
|
|||||||
const util = require("./lib/util");
|
const util = require("./lib/util");
|
||||||
const events = require("./lib/events");
|
const events = require("./lib/events");
|
||||||
const exec = require("./lib/exec");
|
const exec = require("./lib/exec");
|
||||||
|
const hooks = require("./lib/hooks");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module provides common utilities for the Node-RED runtime and editor
|
* This module provides common utilities for the Node-RED runtime and editor
|
||||||
@ -69,5 +70,12 @@ module.exports = {
|
|||||||
* @mixes @node-red/util_exec
|
* @mixes @node-red/util_exec
|
||||||
* @memberof @node-red/util
|
* @memberof @node-red/util
|
||||||
*/
|
*/
|
||||||
exec: exec
|
exec: exec,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime hooks
|
||||||
|
* @mixes @node-red/util_hooks
|
||||||
|
* @memberof @node-red/util
|
||||||
|
*/
|
||||||
|
hooks: hooks
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const Log = require("@node-red/util").log;
|
const Log = require("./log.js");
|
||||||
|
|
||||||
const VALID_HOOKS = [
|
const VALID_HOOKS = [
|
||||||
// Message Routing Path
|
// Message Routing Path
|
||||||
@ -8,14 +8,28 @@ const VALID_HOOKS = [
|
|||||||
"postDeliver",
|
"postDeliver",
|
||||||
"onReceive",
|
"onReceive",
|
||||||
"postReceive",
|
"postReceive",
|
||||||
"onComplete"
|
"onComplete",
|
||||||
|
// Module install hooks
|
||||||
|
"preInstall",
|
||||||
|
"postInstall",
|
||||||
|
"preUninstall",
|
||||||
|
"postUninstall"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
// Flags for what hooks have handlers registered
|
// Flags for what hooks have handlers registered
|
||||||
let states = { }
|
let states = { }
|
||||||
|
|
||||||
// Hooks by id
|
// Doubly-LinkedList of hooks by id.
|
||||||
|
// - hooks[id] points to head of list
|
||||||
|
// - each list item looks like:
|
||||||
|
// {
|
||||||
|
// cb: the callback function
|
||||||
|
// location: filename/line of code that added the hook
|
||||||
|
// previousHook: reference to previous hook in list
|
||||||
|
// nextHook: reference to next hook in list
|
||||||
|
// removed: a flag that is set if the item was removed
|
||||||
|
// }
|
||||||
let hooks = { }
|
let hooks = { }
|
||||||
|
|
||||||
// Hooks by label
|
// Hooks by label
|
||||||
@ -35,12 +49,12 @@ let labelledHooks = { }
|
|||||||
* - `postReceive` - passed a `ReceiveEvent` when the message has been given to the node's `input` handler(s)
|
* - `postReceive` - passed a `ReceiveEvent` when the message has been given to the node's `input` handler(s)
|
||||||
* - `onComplete` - passed a `CompleteEvent` when the node has completed with a message or logged an error
|
* - `onComplete` - passed a `CompleteEvent` when the node has completed with a message or logged an error
|
||||||
*
|
*
|
||||||
* @mixin @node-red/runtime_hooks
|
* @mixin @node-red/util_hooks
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a handler to a named hook
|
* Register a handler to a named hook
|
||||||
* @memberof @node-red/runtime_hooks
|
* @memberof @node-red/util_hooks
|
||||||
* @param {String} hookId - the name of the hook to attach to
|
* @param {String} hookId - the name of the hook to attach to
|
||||||
* @param {Function} callback - the callback function for the hook
|
* @param {Function} callback - the callback function for the hook
|
||||||
*/
|
*/
|
||||||
@ -49,26 +63,39 @@ function add(hookId, callback) {
|
|||||||
if (VALID_HOOKS.indexOf(id) === -1) {
|
if (VALID_HOOKS.indexOf(id) === -1) {
|
||||||
throw new Error(`Invalid hook '${id}'`);
|
throw new Error(`Invalid hook '${id}'`);
|
||||||
}
|
}
|
||||||
if (label) {
|
if (label && labelledHooks[label] && labelledHooks[label][id]) {
|
||||||
if (labelledHooks[label] && labelledHooks[label][id]) {
|
throw new Error("Hook "+hookId+" already registered")
|
||||||
throw new Error("Hook "+hookId+" already registered")
|
|
||||||
}
|
|
||||||
labelledHooks[label] = labelledHooks[label]||{};
|
|
||||||
labelledHooks[label][id] = callback;
|
|
||||||
}
|
}
|
||||||
// Get location of calling code
|
// Get location of calling code
|
||||||
const stack = new Error().stack;
|
const stack = new Error().stack;
|
||||||
const callModule = stack.split("\n")[2].split("(")[1].slice(0,-1);
|
const callModule = stack.split("\n")[2].split("(")[1].slice(0,-1);
|
||||||
Log.debug(`Adding hook '${hookId}' from ${callModule}`);
|
Log.debug(`Adding hook '${hookId}' from ${callModule}`);
|
||||||
|
|
||||||
hooks[id] = hooks[id] || [];
|
const hookItem = {cb:callback, location: callModule, previousHook: null, nextHook: null }
|
||||||
hooks[id].push({cb:callback,location:callModule});
|
|
||||||
|
let tailItem = hooks[id];
|
||||||
|
if (tailItem === undefined) {
|
||||||
|
hooks[id] = hookItem;
|
||||||
|
} else {
|
||||||
|
while(tailItem.nextHook !== null) {
|
||||||
|
tailItem = tailItem.nextHook
|
||||||
|
}
|
||||||
|
tailItem.nextHook = hookItem;
|
||||||
|
hookItem.previousHook = tailItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
labelledHooks[label] = labelledHooks[label]||{};
|
||||||
|
labelledHooks[label][id] = hookItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: get rid of this;
|
||||||
states[id] = true;
|
states[id] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a handled from a named hook
|
* Remove a handled from a named hook
|
||||||
* @memberof @node-red/runtime_hooks
|
* @memberof @node-red/util_hooks
|
||||||
* @param {String} hookId - the name of the hook event to remove - must be `name.label`
|
* @param {String} hookId - the name of the hook event to remove - must be `name.label`
|
||||||
*/
|
*/
|
||||||
function remove(hookId) {
|
function remove(hookId) {
|
||||||
@ -95,33 +122,66 @@ function remove(hookId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeHook(id,callback) {
|
function removeHook(id,hookItem) {
|
||||||
let i = hooks[id].findIndex(hook => hook.cb === callback);
|
let previousHook = hookItem.previousHook;
|
||||||
if (i !== -1) {
|
let nextHook = hookItem.nextHook;
|
||||||
hooks[id].splice(i,1);
|
|
||||||
if (hooks[id].length === 0) {
|
if (previousHook) {
|
||||||
delete hooks[id];
|
previousHook.nextHook = nextHook;
|
||||||
delete states[id];
|
} else {
|
||||||
}
|
hooks[id] = nextHook;
|
||||||
|
}
|
||||||
|
if (nextHook) {
|
||||||
|
nextHook.previousHook = previousHook;
|
||||||
|
}
|
||||||
|
hookItem.removed = true;
|
||||||
|
if (!previousHook && !nextHook) {
|
||||||
|
delete hooks[id];
|
||||||
|
delete states[id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function trigger(hookId, payload, done) {
|
function trigger(hookId, payload, done) {
|
||||||
const hookStack = hooks[hookId];
|
let hookItem = hooks[hookId];
|
||||||
if (!hookStack || hookStack.length === 0) {
|
if (!hookItem) {
|
||||||
done();
|
if (done) {
|
||||||
return;
|
done();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let i = 0;
|
if (!done) {
|
||||||
|
return new Promise((resolve,reject) => {
|
||||||
|
invokeStack(hookItem,payload,function(err) {
|
||||||
|
if (err !== undefined && err !== false) {
|
||||||
|
if (!(err instanceof Error)) {
|
||||||
|
err = new Error(err);
|
||||||
|
}
|
||||||
|
err.hook = hookId
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
invokeStack(hookItem,payload,done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function invokeStack(hookItem,payload,done) {
|
||||||
function callNextHook(err) {
|
function callNextHook(err) {
|
||||||
if (i === hookStack.length || err) {
|
if (!hookItem || err) {
|
||||||
done(err);
|
done(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hook = hookStack[i++];
|
if (hookItem.removed) {
|
||||||
const callback = hook.cb;
|
hookItem = hookItem.nextHook;
|
||||||
|
callNextHook();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const callback = hookItem.cb;
|
||||||
if (callback.length === 1) {
|
if (callback.length === 1) {
|
||||||
try {
|
try {
|
||||||
let result = callback(payload);
|
let result = callback(payload);
|
||||||
@ -134,6 +194,7 @@ function trigger(hookId, payload, done) {
|
|||||||
result.then(handleResolve, callNextHook)
|
result.then(handleResolve, callNextHook)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
hookItem = hookItem.nextHook;
|
||||||
callNextHook();
|
callNextHook();
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
done(err);
|
done(err);
|
||||||
@ -148,15 +209,15 @@ function trigger(hookId, payload, done) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callNextHook();
|
|
||||||
|
|
||||||
function handleResolve(result) {
|
function handleResolve(result) {
|
||||||
if (result === undefined) {
|
if (result === undefined) {
|
||||||
|
hookItem = hookItem.nextHook;
|
||||||
callNextHook();
|
callNextHook();
|
||||||
} else {
|
} else {
|
||||||
done(result);
|
done(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
callNextHook();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
@ -14,6 +14,7 @@ const os = require("os");
|
|||||||
const NR_TEST_UTILS = require("nr-test-utils");
|
const NR_TEST_UTILS = require("nr-test-utils");
|
||||||
const externalModules = NR_TEST_UTILS.require("@node-red/registry/lib/externalModules");
|
const externalModules = NR_TEST_UTILS.require("@node-red/registry/lib/externalModules");
|
||||||
const exec = NR_TEST_UTILS.require("@node-red/util/lib/exec");
|
const exec = NR_TEST_UTILS.require("@node-red/util/lib/exec");
|
||||||
|
const hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks");
|
||||||
|
|
||||||
let homeDir;
|
let homeDir;
|
||||||
|
|
||||||
@ -40,19 +41,20 @@ describe("externalModules api", function() {
|
|||||||
await createUserDir()
|
await createUserDir()
|
||||||
})
|
})
|
||||||
afterEach(async function() {
|
afterEach(async function() {
|
||||||
|
hooks.clear();
|
||||||
await fs.remove(homeDir);
|
await fs.remove(homeDir);
|
||||||
})
|
})
|
||||||
describe("checkFlowDependencies", function() {
|
describe("checkFlowDependencies", function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
sinon.stub(exec,"run").callsFake(async function(cmd, args, options) {
|
sinon.stub(exec,"run").callsFake(async function(cmd, args, options) {
|
||||||
let error;
|
let error;
|
||||||
if (args[1] === "moduleNotFound") {
|
if (args[2] === "moduleNotFound") {
|
||||||
error = new Error();
|
error = new Error();
|
||||||
error.stderr = "E404";
|
error.stderr = "E404";
|
||||||
} else if (args[1] === "moduleVersionNotFound") {
|
} else if (args[2] === "moduleVersionNotFound") {
|
||||||
error = new Error();
|
error = new Error();
|
||||||
error.stderr = "ETARGET";
|
error.stderr = "ETARGET";
|
||||||
} else if (args[1] === "moduleFail") {
|
} else if (args[2] === "moduleFail") {
|
||||||
error = new Error();
|
error = new Error();
|
||||||
error.stderr = "Some unexpected install error";
|
error.stderr = "Some unexpected install error";
|
||||||
}
|
}
|
||||||
@ -102,6 +104,45 @@ describe("externalModules api", function() {
|
|||||||
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
|
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it("calls pre/postInstall hooks", async function() {
|
||||||
|
externalModules.init({userDir: homeDir});
|
||||||
|
externalModules.register("function", "libs");
|
||||||
|
let receivedPreEvent,receivedPostEvent;
|
||||||
|
hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; })
|
||||||
|
hooks.add("postInstall", function(event) { receivedPostEvent = event; })
|
||||||
|
|
||||||
|
await externalModules.checkFlowDependencies([
|
||||||
|
{type: "function", libs:[{module: "foo"}]}
|
||||||
|
])
|
||||||
|
exec.run.called.should.be.true();
|
||||||
|
// exec.run.lastCall.args[1].should.eql([ 'install', 'a', 'foo' ]);
|
||||||
|
receivedPreEvent.should.have.property("module","foo")
|
||||||
|
receivedPreEvent.should.have.property("version")
|
||||||
|
receivedPreEvent.should.have.property("dir")
|
||||||
|
receivedPreEvent.should.eql(receivedPostEvent)
|
||||||
|
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
|
||||||
|
})
|
||||||
|
|
||||||
|
it("skips npm install if preInstall returns false", async function() {
|
||||||
|
externalModules.init({userDir: homeDir});
|
||||||
|
externalModules.register("function", "libs");
|
||||||
|
let receivedPreEvent,receivedPostEvent;
|
||||||
|
hooks.add("preInstall", function(event) { receivedPreEvent = event; return false })
|
||||||
|
hooks.add("postInstall", function(event) { receivedPostEvent = event; })
|
||||||
|
|
||||||
|
await externalModules.checkFlowDependencies([
|
||||||
|
{type: "function", libs:[{module: "foo"}]}
|
||||||
|
])
|
||||||
|
exec.run.called.should.be.false();
|
||||||
|
receivedPreEvent.should.have.property("module","foo")
|
||||||
|
receivedPreEvent.should.have.property("version")
|
||||||
|
receivedPreEvent.should.have.property("dir")
|
||||||
|
receivedPreEvent.should.eql(receivedPostEvent)
|
||||||
|
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
it("installs missing modules from inside subflow module", async function() {
|
it("installs missing modules from inside subflow module", async function() {
|
||||||
externalModules.init({userDir: homeDir});
|
externalModules.init({userDir: homeDir});
|
||||||
externalModules.register("function", "libs");
|
externalModules.register("function", "libs");
|
||||||
|
@ -25,7 +25,7 @@ var NR_TEST_UTILS = require("nr-test-utils");
|
|||||||
var installer = NR_TEST_UTILS.require("@node-red/registry/lib/installer");
|
var installer = NR_TEST_UTILS.require("@node-red/registry/lib/installer");
|
||||||
var registry = NR_TEST_UTILS.require("@node-red/registry/lib/index");
|
var registry = NR_TEST_UTILS.require("@node-red/registry/lib/index");
|
||||||
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry/lib/registry");
|
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry/lib/registry");
|
||||||
const { events, exec, log } = NR_TEST_UTILS.require("@node-red/util");
|
const { events, exec, log, hooks } = NR_TEST_UTILS.require("@node-red/util");
|
||||||
|
|
||||||
describe('nodes/registry/installer', function() {
|
describe('nodes/registry/installer', function() {
|
||||||
|
|
||||||
@ -68,6 +68,7 @@ describe('nodes/registry/installer', function() {
|
|||||||
fs.statSync.restore();
|
fs.statSync.restore();
|
||||||
}
|
}
|
||||||
exec.run.restore();
|
exec.run.restore();
|
||||||
|
hooks.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("installs module", function() {
|
describe("installs module", function() {
|
||||||
@ -251,6 +252,70 @@ describe('nodes/registry/installer', function() {
|
|||||||
}).catch(done);
|
}).catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("triggers preInstall and postInstall hooks", function(done) {
|
||||||
|
let receivedPreEvent,receivedPostEvent;
|
||||||
|
hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; })
|
||||||
|
hooks.add("postInstall", function(event) { receivedPostEvent = event; })
|
||||||
|
var nodeInfo = {nodes:{module:"foo",types:["a"]}};
|
||||||
|
var res = {code: 0,stdout:"",stderr:""}
|
||||||
|
var p = Promise.resolve(res);
|
||||||
|
p.catch((err)=>{});
|
||||||
|
execResponse = p;
|
||||||
|
|
||||||
|
var addModule = sinon.stub(registry,"addModule").callsFake(function(md) {
|
||||||
|
return Promise.resolve(nodeInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
installer.installModule("this_wont_exist","1.2.3").then(function(info) {
|
||||||
|
exec.run.called.should.be.true();
|
||||||
|
exec.run.lastCall.args[1].should.eql([ 'install', 'a', 'this_wont_exist@1.2.3' ]);
|
||||||
|
info.should.eql(nodeInfo);
|
||||||
|
should.exist(receivedPreEvent)
|
||||||
|
receivedPreEvent.should.have.property("module","this_wont_exist")
|
||||||
|
receivedPreEvent.should.have.property("version","1.2.3")
|
||||||
|
receivedPreEvent.should.have.property("dir")
|
||||||
|
receivedPreEvent.should.have.property("url")
|
||||||
|
receivedPreEvent.should.have.property("isExisting")
|
||||||
|
receivedPreEvent.should.have.property("isUpgrade")
|
||||||
|
receivedPreEvent.should.eql(receivedPostEvent)
|
||||||
|
done();
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails install if preInstall hook fails", function(done) {
|
||||||
|
let receivedEvent;
|
||||||
|
hooks.add("preInstall", function(event) { throw new Error("preInstall-error"); })
|
||||||
|
var nodeInfo = {nodes:{module:"foo",types:["a"]}};
|
||||||
|
|
||||||
|
installer.installModule("this_wont_exist","1.2.3").catch(function(err) {
|
||||||
|
exec.run.called.should.be.false();
|
||||||
|
done();
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips invoking npm if preInstall returns false", function(done) {
|
||||||
|
let receivedEvent;
|
||||||
|
hooks.add("preInstall", function(event) { return false })
|
||||||
|
hooks.add("postInstall", function(event) { receivedEvent = event; })
|
||||||
|
var nodeInfo = {nodes:{module:"foo",types:["a"]}};
|
||||||
|
|
||||||
|
installer.installModule("this_wont_exist","1.2.3").catch(function(err) {
|
||||||
|
exec.run.called.should.be.false();
|
||||||
|
should.exist(receivedEvent);
|
||||||
|
done();
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rollsback install if postInstall hook fails", function(done) {
|
||||||
|
hooks.add("postInstall", function(event) { throw new Error("fail"); })
|
||||||
|
installer.installModule("this_wont_exist","1.2.3").catch(function(err) {
|
||||||
|
exec.run.calledTwice.should.be.true();
|
||||||
|
exec.run.firstCall.args[1].includes("install").should.be.true();
|
||||||
|
exec.run.secondCall.args[1].includes("remove").should.be.true();
|
||||||
|
done();
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
describe("uninstalls module", function() {
|
describe("uninstalls module", function() {
|
||||||
it("rejects invalid module names", function(done) {
|
it("rejects invalid module names", function(done) {
|
||||||
|
@ -26,7 +26,7 @@ var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/util");
|
|||||||
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
|
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
|
||||||
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
|
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
|
||||||
var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
|
var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
|
||||||
var hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
|
var hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks");
|
||||||
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry");
|
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry");
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ var sinon = require('sinon');
|
|||||||
var NR_TEST_UTILS = require("nr-test-utils");
|
var NR_TEST_UTILS = require("nr-test-utils");
|
||||||
var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
|
var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
|
||||||
var Log = NR_TEST_UTILS.require("@node-red/util").log;
|
var Log = NR_TEST_UTILS.require("@node-red/util").log;
|
||||||
var hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
|
var hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks");
|
||||||
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
|
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
|
||||||
|
|
||||||
describe('Node', function() {
|
describe('Node', function() {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const should = require("should");
|
const should = require("should");
|
||||||
const NR_TEST_UTILS = require("nr-test-utils");
|
const NR_TEST_UTILS = require("nr-test-utils");
|
||||||
|
|
||||||
const hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
|
const hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks");
|
||||||
|
|
||||||
describe("runtime/hooks", function() {
|
describe("util/hooks", function() {
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
hooks.clear();
|
hooks.clear();
|
||||||
})
|
})
|
||||||
@ -121,7 +121,46 @@ describe("runtime/hooks", function() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
it("allows a hook to remove itself whilst being called", function(done) {
|
||||||
|
let data = { order: [] }
|
||||||
|
hooks.add("onSend.A", function(payload) { payload.order.push("A") } )
|
||||||
|
hooks.add("onSend.B", function(payload) {
|
||||||
|
hooks.remove("*.B");
|
||||||
|
})
|
||||||
|
hooks.add("onSend.C", function(payload) { payload.order.push("C") } )
|
||||||
|
hooks.add("onSend.D", function(payload) { payload.order.push("D") } )
|
||||||
|
|
||||||
|
hooks.trigger("onSend", data, err => {
|
||||||
|
try {
|
||||||
|
should.not.exist(err);
|
||||||
|
data.order.should.eql(["A","C","D"])
|
||||||
|
done();
|
||||||
|
} catch(e) {
|
||||||
|
done(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows a hook to remove itself and others whilst being called", function(done) {
|
||||||
|
let data = { order: [] }
|
||||||
|
hooks.add("onSend.A", function(payload) { payload.order.push("A") } )
|
||||||
|
hooks.add("onSend.B", function(payload) {
|
||||||
|
hooks.remove("*.B");
|
||||||
|
hooks.remove("*.C");
|
||||||
|
})
|
||||||
|
hooks.add("onSend.C", function(payload) { payload.order.push("C") } )
|
||||||
|
hooks.add("onSend.D", function(payload) { payload.order.push("D") } )
|
||||||
|
|
||||||
|
hooks.trigger("onSend", data, err => {
|
||||||
|
try {
|
||||||
|
should.not.exist(err);
|
||||||
|
data.order.should.eql(["A","D"])
|
||||||
|
done();
|
||||||
|
} catch(e) {
|
||||||
|
done(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
it("halts execution on return false", function(done) {
|
it("halts execution on return false", function(done) {
|
||||||
hooks.add("onSend.A", function(payload) { payload.order.push("A"); return false } )
|
hooks.add("onSend.A", function(payload) { payload.order.push("A"); return false } )
|
||||||
@ -249,4 +288,51 @@ describe("runtime/hooks", function() {
|
|||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it("handler can use callback function - promise API", function(done) {
|
||||||
|
hooks.add("onSend.A", function(payload, done) {
|
||||||
|
setTimeout(function() {
|
||||||
|
payload.order.push("A")
|
||||||
|
done()
|
||||||
|
},30)
|
||||||
|
})
|
||||||
|
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("onSend",data).then(() => {
|
||||||
|
data.order.should.eql(["A","B"])
|
||||||
|
done()
|
||||||
|
}).catch(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handler can halt execution - promise API", function(done) {
|
||||||
|
hooks.add("onSend.A", function(payload, done) {
|
||||||
|
setTimeout(function() {
|
||||||
|
payload.order.push("A")
|
||||||
|
done(false)
|
||||||
|
},30)
|
||||||
|
})
|
||||||
|
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("onSend",data).then(() => {
|
||||||
|
data.order.should.eql(["A"])
|
||||||
|
done()
|
||||||
|
}).catch(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handler can halt execution on error - promise API", function(done) {
|
||||||
|
hooks.add("onSend.A", function(payload, done) {
|
||||||
|
throw new Error("error");
|
||||||
|
})
|
||||||
|
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("onSend",data).then(() => {
|
||||||
|
done("hooks.trigger resolved unexpectedly")
|
||||||
|
}).catch(err => {
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
})
|
||||||
});
|
});
|
Loading…
Reference in New Issue
Block a user