diff --git a/lib/Common.js b/lib/Common.js index 6945f568c..89729447a 100644 --- a/lib/Common.js +++ b/lib/Common.js @@ -368,11 +368,6 @@ Common.sink.determineExecMode = function(app) { }; var resolveNodeInterpreter = function(app) { - if (app.exec_mode && app.exec_mode.indexOf('cluster') > -1) { - Common.printError(cst.PREFIX_MSG_WARNING + chalk.bold.yellow('Choosing the Node.js version in cluster mode is not supported')); - return false; - } - var nvm_path = cst.IS_WINDOWS ? process.env.NVM_HOME : process.env.NVM_DIR; if (!nvm_path) { Common.printError(cst.PREFIX_MSG_ERR + chalk.red('NVM is not available in PATH')); @@ -422,13 +417,92 @@ var resolveNodeInterpreter = function(app) { } }; +function resolveModernNodeInterpreter(app) { + const fnm_path = process.env.FNM_DIR; + + if (!fnm_path) { + Common.printError(cst.PREFIX_MSG_ERR + chalk.red('fnm is not available in PATH')); + Common.printError(cst.PREFIX_MSG_ERR + chalk.red('Fallback to node in PATH')); + return false; + } + + Common.printOut(cst.PREFIX_MSG + 'Using fnm mode (via FNM_DIR)'); + const nodeVersion = app.exec_interpreter.split('@')[1]; + + // We just need the fnm binary - we will determine the node path from the runtime itself + const execName = cst.IS_WINDOWS ? 'fnm.exe' : 'fnm'; + const pathToFnmExec = path.join(fnm_path, execName); + + try { + // See if the executable is real + fs.accessSync(pathToFnmExec); + } catch(e) { + // we cant use the executable so bail + Common.printError(cst.PREFIX_MSG_ERR + chalk.red(`fnm is not available at ${pathToExec} (based on environment)`)); + Common.printError(cst.PREFIX_MSG_ERR + chalk.red('Fallback to node in PATH')); + return false; + } + + // This is the command that will get the full path to the node executable (using fnm). We will invoke this directly + const fnmExecPathCommand = [ + // path + path.normalize(pathToFnmExec), + // fnm argument + `exec --using '${nodeVersion}' node -e`, + // node exec statement + `'process.stdout.write(process.execPath)'` + ].join(' '); + + let pathToNode; + + try { + // See if the node version is available + pathToNode = execSync(fnmExecPathCommand).toString('utf-8'); + } catch(e) { + // Try and fetch the missing version + Common.printOut(cst.PREFIX_MSG + 'Installing Node v%s', node_version); + + const fnmInstallCommand = [ + // fnm binary + path.normalize(pathToFnmExec), + // fnm argument + `install ${nodeVersion}`, + ].join(' '); + + Common.printOut(cst.PREFIX_MSG + 'Executing: %s', fnmInstallCommand); + // Just like the existing behavior, if this fails - it means it's a hard failure + // i.e system isn't in a state to use fnm + execSync(fnmInstallCommand, { + env: process.env, + maxBuffer: 20 * 1024 * 1024 + }); + + // If the install worked, try and get the node path + pathToNode = execSync(fnmExecPathCommand).toString('utf-8'); + } + + if (!pathToNode) { + // Just like the legacy nvm - bail if we didn't get a path + return false; + } + + Common.printOut( + cst.PREFIX_MSG + chalk.green.bold('Setting Node to v%s (path=%s)'), + nodeVersion, + pathToNode + ); + + app.exec_interpreter = pathToNode; +} + /** * Resolve interpreter */ Common.sink.resolveInterpreter = function(app) { - var noInterpreter = !app.exec_interpreter; - var extName = path.extname(app.pm_exec_path); - var betterInterpreter = extItps[extName]; + let noInterpreter = !app.exec_interpreter; + let extName = path.extname(app.pm_exec_path); + let betterInterpreter = extItps[extName]; + let usesDirectBinary = false; // No interpreter defined and correspondance in schema hashmap if (noInterpreter && betterInterpreter) { @@ -437,8 +511,17 @@ Common.sink.resolveInterpreter = function(app) { // Else if no Interpreter detect if process is binary else if (noInterpreter) app.exec_interpreter = isBinary(app.pm_exec_path) ? 'none' : 'node'; - else if (app.exec_interpreter.indexOf('node@') > -1) - resolveNodeInterpreter(app); + else if (app.exec_interpreter.indexOf('node@') > -1) { + if (process.env.PM2_USE_FNM) { + // modern fnm support + resolveModernNodeInterpreter(app); + // flag that we did a selection with fnm (direct path - not using which) + usesDirectBinary = Boolean(app.exec_interpreter) + } else { + // legacy nvm support + resolveNodeInterpreter(app); + } + } if (app.exec_interpreter.indexOf('python') > -1) app.env.PYTHONUNBUFFERED = '1' @@ -458,7 +541,7 @@ Common.sink.resolveInterpreter = function(app) { app.exec_interpreter = path.resolve(__dirname, '../node_modules/.bin/coffee'); } - if (app.exec_interpreter != 'none' && which(app.exec_interpreter) == null) { + if (!usesDirectBinary && app.exec_interpreter != 'none' && which(app.exec_interpreter) == null) { // If node is not present if (app.exec_interpreter == 'node') { Common.warn(`Using builtin node.js version on version ${process.version}`) @@ -622,7 +705,13 @@ Common.mergeEnvironmentVariables = function(app_env, env_name, deploy_conf) { // #2541 force resolution of node interpreter if (app.exec_interpreter && app.exec_interpreter.indexOf('@') > -1) { - resolveNodeInterpreter(app); + if (process.env.PM2_USE_FNM) { + // modern fnm support + resolveModernNodeInterpreter(app); + } else { + // legacy nvm support + resolveNodeInterpreter(app); + } res.current_conf.exec_interpreter = app.exec_interpreter }