mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Add pre/postInstall hooks to module install path
This commit is contained in:
parent
22df59e229
commit
8140057bea
@ -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";
|
||||||
@ -190,12 +191,22 @@ async function installModule(moduleDetails) {
|
|||||||
await ensureModuleDir();
|
await ensureModuleDir();
|
||||||
|
|
||||||
var args = ["install", installSpec, "--production"];
|
var args = ["install", installSpec, "--production"];
|
||||||
return exec.run(NPM_COMMAND, args, {
|
let triggerPayload = {
|
||||||
cwd: installDir
|
"module": moduleDetails.module,
|
||||||
},true).then(result => {
|
"version": moduleDetails.version,
|
||||||
|
"dir": installDir,
|
||||||
|
}
|
||||||
|
return hooks.trigger("preInstall", triggerPayload).then(() => {
|
||||||
|
// preInstall passed
|
||||||
|
// - run install
|
||||||
|
log.trace(NPM_COMMAND + JSON.stringify(args));
|
||||||
|
return exec.run(NPM_COMMAND, args, { cwd: installDir },true)
|
||||||
|
}).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;
|
||||||
@ -169,10 +169,23 @@ 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];
|
var args = ['install','--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production',installName];
|
||||||
|
let triggerPayload = {
|
||||||
|
"module": module,
|
||||||
|
"version": version,
|
||||||
|
"url": url,
|
||||||
|
"dir": installDir,
|
||||||
|
"isExisting": isExisting,
|
||||||
|
"isUpgrade": isUpgrade
|
||||||
|
}
|
||||||
|
|
||||||
|
return hooks.trigger("preInstall", triggerPayload).then(() => {
|
||||||
|
// preInstall passed
|
||||||
|
// - run install
|
||||||
log.trace(npmCommand + JSON.stringify(args));
|
log.trace(npmCommand + JSON.stringify(args));
|
||||||
return exec.run(npmCommand,args,{
|
return exec.run(npmCommand,args,{ cwd: installDir}, true)
|
||||||
cwd: installDir
|
}).then(() => {
|
||||||
}, true).then(result => {
|
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 +204,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");
|
log.warn(log._("server.install.install-failed-long",{name:module}));
|
||||||
|
log.warn("------------------------------------------");
|
||||||
|
log.warn(err.toString());
|
||||||
|
log.warn("------------------------------------------");
|
||||||
|
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)) {
|
if (lookFor404.test(output)) {
|
||||||
log.warn(log._("server.install.install-failed-not-found",{name:module}));
|
log.warn(log._("server.install.install-failed-not-found",{name:module}));
|
||||||
e = new Error("Module not found");
|
e = new Error("Module not found");
|
||||||
e.code = 404;
|
e.code = 404;
|
||||||
throw e;
|
|
||||||
} else if (isUpgrade && lookForVersionNotFound.test(output)) {
|
} else if (isUpgrade && lookForVersionNotFound.test(output)) {
|
||||||
log.warn(log._("server.install.upgrade-failed-not-found",{name:module}));
|
log.warn(log._("server.install.upgrade-failed-not-found",{name:module}));
|
||||||
e = new Error("Module not found");
|
e = new Error("Module not found");
|
||||||
e.code = 404;
|
e.code = 404;
|
||||||
throw e;
|
|
||||||
} else {
|
} 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(output);
|
||||||
log.warn("------------------------------------------");
|
log.warn("------------------------------------------");
|
||||||
throw new Error(log._("server.install.install-failed"));
|
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 +441,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);
|
||||||
|
return hooks.trigger("postUninstall", triggerPayload).catch((err)=>{
|
||||||
|
log.warn("------------------------------------------");
|
||||||
|
log.warn(err.toString());
|
||||||
|
log.warn("------------------------------------------");
|
||||||
|
}).finally(() => {
|
||||||
resolve(list);
|
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());
|
||||||
|
@ -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",
|
||||||
|
@ -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,6 +41,7 @@ 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() {
|
||||||
@ -102,6 +104,25 @@ 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) { receivedPreEvent = event; })
|
||||||
|
hooks.add("postInstall", function(event) { receivedPostEvent = event; })
|
||||||
|
|
||||||
|
await externalModules.checkFlowDependencies([
|
||||||
|
{type: "function", libs:[{module: "foo"}]}
|
||||||
|
])
|
||||||
|
exec.run.called.should.be.true();
|
||||||
|
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,65 @@ 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) { 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",function(md) {
|
||||||
|
return Promise.resolve(nodeInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
installer.installModule("this_wont_exist","1.2.3").then(function(info) {
|
||||||
|
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("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("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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user