mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Add pre/postInstall hooks to module install path
This commit is contained in:
		| @@ -9,6 +9,7 @@ const path = require("path"); | ||||
| const clone = require("clone"); | ||||
| const exec = require("@node-red/util").exec; | ||||
| const log = require("@node-red/util").log; | ||||
| const hooks = require("@node-red/util").hooks; | ||||
|  | ||||
| const BUILTIN_MODULES = require('module').builtinModules; | ||||
| const EXTERNAL_MODULES_DIR = "externalModules"; | ||||
| @@ -190,12 +191,22 @@ async function installModule(moduleDetails) { | ||||
|     await ensureModuleDir(); | ||||
|  | ||||
|     var args = ["install", installSpec, "--production"]; | ||||
|     return exec.run(NPM_COMMAND, args, { | ||||
|         cwd: installDir | ||||
|     },true).then(result => { | ||||
|     let triggerPayload = { | ||||
|         "module": moduleDetails.module, | ||||
|         "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 })); | ||||
|     }).catch(result => { | ||||
|         var output = result.stderr; | ||||
|         var output = result.stderr || result.toString(); | ||||
|         var e; | ||||
|         if (/E404/.test(output) || /ETARGET/.test(output)) { | ||||
|             log.error(log._("server.install.install-failed-not-found",{name:installSpec})); | ||||
|   | ||||
| @@ -23,7 +23,7 @@ const tar = require("tar"); | ||||
| const registry = require("./registry"); | ||||
| const registryUtil = require("./util"); | ||||
| 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 npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; | ||||
| let installerEnabled = false; | ||||
| @@ -169,10 +169,23 @@ async function installModule(module,version,url) { | ||||
|  | ||||
|         var installDir = settings.userDir || process.env.NODE_RED_HOME || "."; | ||||
|         var args = ['install','--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production',installName]; | ||||
|         log.trace(npmCommand + JSON.stringify(args)); | ||||
|         return exec.run(npmCommand,args,{ | ||||
|             cwd: installDir | ||||
|         }, true).then(result => { | ||||
|         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)); | ||||
|             return exec.run(npmCommand,args,{ cwd: installDir}, true) | ||||
|         }).then(() => { | ||||
|             return hooks.trigger("postInstall", triggerPayload) | ||||
|         }).then(() => { | ||||
|             if (isExisting) { | ||||
|                 // This is a module we already have installed as a non-user module. | ||||
|                 // 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}); | ||||
|                 return 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; | ||||
|                 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 { | ||||
|         }).catch(err => { | ||||
|             let e; | ||||
|             if (err.hook) { | ||||
|                 // preInstall failed | ||||
|                 log.warn(log._("server.install.install-failed-long",{name:module})); | ||||
|                 log.warn("------------------------------------------"); | ||||
|                 log.warn(output); | ||||
|                 log.warn(err.toString()); | ||||
|                 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 => { | ||||
|         // In case of error, reset activePromise to be resolvable | ||||
|         activePromise = Promise.resolve(); | ||||
| @@ -412,17 +441,29 @@ function uninstallModule(module) { | ||||
|             log.info(log._("server.install.uninstalling",{name:module})); | ||||
|  | ||||
|             var args = ['remove','--no-audit','--no-update-notifier','--no-fund','--save',module]; | ||||
|             log.trace(npmCommand + JSON.stringify(args)); | ||||
|  | ||||
|             exec.run(npmCommand,args,{ | ||||
|                 cwd: installDir, | ||||
|             },true).then(result => { | ||||
|             let triggerPayload = { | ||||
|                 "module": module, | ||||
|                 "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})); | ||||
|                 reportRemovedModules(list); | ||||
|                 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 => { | ||||
|                 var output = result.stderr; | ||||
|                 let output = result.stderr || result; | ||||
|                 log.warn(log._("server.install.uninstall-failed-long",{name:module})); | ||||
|                 log.warn("------------------------------------------"); | ||||
|                 log.warn(output.toString()); | ||||
|   | ||||
| @@ -34,6 +34,7 @@ | ||||
|             "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-url": "$t(server.install.install-failed-long) invalid url: __url__", | ||||
|             "post-install-error": "Error running 'postInstall' hook:", | ||||
|             "upgrading": "Upgrading module: __name__ to version: __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", | ||||
|   | ||||
| @@ -14,6 +14,7 @@ const os = require("os"); | ||||
| const NR_TEST_UTILS = require("nr-test-utils"); | ||||
| const externalModules = NR_TEST_UTILS.require("@node-red/registry/lib/externalModules"); | ||||
| const exec = NR_TEST_UTILS.require("@node-red/util/lib/exec"); | ||||
| const hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks"); | ||||
|  | ||||
| let homeDir; | ||||
|  | ||||
| @@ -40,6 +41,7 @@ describe("externalModules api", function() { | ||||
|         await createUserDir() | ||||
|     }) | ||||
|     afterEach(async function() { | ||||
|         hooks.clear(); | ||||
|         await fs.remove(homeDir); | ||||
|     }) | ||||
|     describe("checkFlowDependencies", function() { | ||||
| @@ -102,6 +104,25 @@ describe("externalModules api", function() { | ||||
|             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() { | ||||
|             externalModules.init({userDir: homeDir}); | ||||
|             externalModules.register("function", "libs"); | ||||
| @@ -299,4 +320,4 @@ describe("externalModules api", function() { | ||||
|             } | ||||
|         }) | ||||
|     }) | ||||
| }); | ||||
| }); | ||||
|   | ||||
| @@ -25,7 +25,7 @@ var NR_TEST_UTILS = require("nr-test-utils"); | ||||
| var installer = NR_TEST_UTILS.require("@node-red/registry/lib/installer"); | ||||
| var registry = NR_TEST_UTILS.require("@node-red/registry/lib/index"); | ||||
| 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() { | ||||
|  | ||||
| @@ -68,6 +68,7 @@ describe('nodes/registry/installer', function() { | ||||
|             fs.statSync.restore(); | ||||
|         } | ||||
|         exec.run.restore(); | ||||
|         hooks.clear(); | ||||
|     }); | ||||
|  | ||||
|     describe("installs module", function() { | ||||
| @@ -251,6 +252,65 @@ describe('nodes/registry/installer', function() { | ||||
|             }).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() { | ||||
|         it("rejects invalid module names", function(done) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user