diff --git a/lib/God/ClusterMode.js b/lib/God/ClusterMode.js index 10619b0fe..cfe3bdf9f 100644 --- a/lib/God/ClusterMode.js +++ b/lib/God/ClusterMode.js @@ -11,6 +11,7 @@ * @project PM2 */ var cluster = require('cluster'); +var path = require('path'); var Utility = require('../Utility.js'); var pkg = require('../../package.json'); @@ -34,9 +35,12 @@ module.exports = function ClusterMode(God) { var clu = null; console.log(`App [${env_copy.name}:${env_copy.pm_id}] starting in -cluster mode-`) + var initEnvScript = path.resolve(__dirname, '..', 'ProcessContainerClusterInitEnv.js'); + var execArgv = ['--require', initEnvScript]; if (env_copy.node_args && Array.isArray(env_copy.node_args)) { - cluster.settings.execArgv = env_copy.node_args; + execArgv = execArgv.concat(env_copy.node_args); } + cluster.settings.execArgv = execArgv; env_copy._pm2_version = pkg.version; diff --git a/lib/ProcessContainerClusterInitEnv.js b/lib/ProcessContainerClusterInitEnv.js new file mode 100644 index 000000000..b74a1a4b9 --- /dev/null +++ b/lib/ProcessContainerClusterInitEnv.js @@ -0,0 +1,16 @@ +/** + * Copyright 2013-present the PM2 project authors. All rights reserved. + * Use of this source code is governed by a license that + * can be found in the LICENSE file. + * + * Expand the pm2_env JSON string into individual process.env entries. + * This must run before any user --require modules so they can access + * the configured environment variables. + */ + +if (process.env.pm2_env) { + var pm2_env = JSON.parse(process.env.pm2_env); + for (var k in pm2_env) { + process.env[k] = pm2_env[k]; + } +} diff --git a/test/fixtures/require-env-capture.js b/test/fixtures/require-env-capture.js new file mode 100644 index 000000000..4875865a4 --- /dev/null +++ b/test/fixtures/require-env-capture.js @@ -0,0 +1,12 @@ +var fs = require('fs'); + +// Capture the env var at --require time and write it to a result file. +// PM2_REQUIRE_RESULT_FILE tells us where to write, so cluster and fork +// tests can share this fixture without colliding. +var resultFile = process.env.PM2_REQUIRE_RESULT_FILE; +if (resultFile) { + fs.writeFileSync( + resultFile, + JSON.stringify({ value: process.env.PM2_TEST_REQUIRE_VAR || null }) + ); +} diff --git a/test/programmatic/require_cluster_env.mocha.js b/test/programmatic/require_cluster_env.mocha.js new file mode 100644 index 000000000..18ad74b6c --- /dev/null +++ b/test/programmatic/require_cluster_env.mocha.js @@ -0,0 +1,87 @@ + +process.chdir(__dirname) + +var fs = require('fs') +var path = require('path') +var PM2 = require('../..') +var should = require('should') + +var CLUSTER_RESULT = path.join(__dirname, '..', 'require-env-cluster-result.json') +var FORK_RESULT = path.join(__dirname, '..', 'require-env-fork-result.json') +var REQUIRE_SCRIPT = path.resolve(__dirname, '../fixtures/require-env-capture.js') + +// In cluster mode PM2 serialises env vars into a pm2_env JSON string and the +// entry wrapper (ProcessContainer.js) unpacks it. Node.js processes execArgv +// (including --require flags) *before* that wrapper runs, so without the fix a +// library loaded via --require would see the raw JSON blob rather than the +// individual env vars. ProcessContainerClusterInitEnv.js is prepended as the +// first --require so the env is expanded before any user --require runs. + +describe('--require script env var visibility', function () { + this.timeout(15000) + + before(function (done) { + try { fs.unlinkSync(CLUSTER_RESULT) } catch (e) {} + try { fs.unlinkSync(FORK_RESULT) } catch (e) {} + PM2.delete('all', function () { done() }) + }) + + after(function (done) { + try { fs.unlinkSync(CLUSTER_RESULT) } catch (e) {} + try { fs.unlinkSync(FORK_RESULT) } catch (e) {} + PM2.kill(done) + }) + + afterEach(function (done) { + PM2.delete('all', done) + }) + + it('should expose PM2 env vars to --require scripts in cluster mode', function (done) { + PM2.start({ + script: './../fixtures/empty.js', + name: 'test-require-env-cluster', + exec_mode: 'cluster', + instances: 1, + node_args: ['--require', REQUIRE_SCRIPT], + env: { + PM2_TEST_REQUIRE_VAR: 'cluster-env-value', + PM2_REQUIRE_RESULT_FILE: CLUSTER_RESULT + } + }, function (err) { + should(err).be.null() + + setTimeout(function () { + var data = JSON.parse(fs.readFileSync(CLUSTER_RESULT, 'utf8')) + data.value.should.eql( + 'cluster-env-value', + 'PM2_TEST_REQUIRE_VAR should be visible in --require script running in cluster mode' + ) + done() + }, 3000) + }) + }) + + it('should expose PM2 env vars to --require scripts in fork mode', function (done) { + PM2.start({ + script: './../fixtures/empty.js', + name: 'test-require-env-fork', + exec_mode: 'fork', + node_args: ['--require', REQUIRE_SCRIPT], + env: { + PM2_TEST_REQUIRE_VAR: 'fork-env-value', + PM2_REQUIRE_RESULT_FILE: FORK_RESULT + } + }, function (err) { + should(err).be.null() + + setTimeout(function () { + var data = JSON.parse(fs.readFileSync(FORK_RESULT, 'utf8')) + data.value.should.eql( + 'fork-env-value', + 'PM2_TEST_REQUIRE_VAR should be visible in --require script running in fork mode' + ) + done() + }, 3000) + }) + }) +}) diff --git a/test/unit.sh b/test/unit.sh index 0dfb53324..32c201f90 100644 --- a/test/unit.sh +++ b/test/unit.sh @@ -100,6 +100,7 @@ runUnitTest $D/issue_5990_bun_substring_match.mocha.js runUnitTest $D/issue_6089_prototype_pollution.mocha.js runUnitTest $D/issue_6075_redos.mocha.js runUnitTest $D/issue_6073_object_env.mocha.js +runUnitTest $D/require_cluster_env.mocha.js runUnitTest $D/treekill.mocha.js runUnitTest $D/http_interface.mocha.js