-
Notifications
You must be signed in to change notification settings - Fork 29.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
src: allow CLI args in env with NODE_OPTIONS #12028
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3626,6 +3626,9 @@ static void PrintHelp() { | |
#endif | ||
#endif | ||
"NODE_NO_WARNINGS set to 1 to silence process warnings\n" | ||
#if !defined(NODE_WITHOUT_NODE_OPTIONS) | ||
"NODE_OPTIONS set CLI options in the environment\n" | ||
#endif | ||
#ifdef _WIN32 | ||
"NODE_PATH ';'-separated list of directories\n" | ||
#else | ||
|
@@ -3642,6 +3645,51 @@ static void PrintHelp() { | |
} | ||
|
||
|
||
static void CheckIfAllowedInEnv(const char* exe, bool is_env, | ||
const char* arg) { | ||
if (!is_env) | ||
return; | ||
|
||
// Find the arg prefix when its --some_arg=val | ||
const char* eq = strchr(arg, '='); | ||
size_t arglen = eq ? eq - arg : strlen(arg); | ||
|
||
static const char* whitelist[] = { | ||
// Node options | ||
"-r", "--require", | ||
"--no-deprecation", | ||
"--no-warnings", | ||
"--trace-warnings", | ||
"--redirect-warnings", | ||
"--trace-deprecation", | ||
"--trace-sync-io", | ||
"--trace-events-enabled", | ||
"--track-heap-objects", | ||
"--throw-deprecation", | ||
"--zero-fill-buffers", | ||
"--v8-pool-size", | ||
"--use-openssl-ca", | ||
"--use-bundled-ca", | ||
"--enable-fips", | ||
"--force-fips", | ||
"--openssl-config", | ||
"--icu-data-dir", | ||
|
||
// V8 options | ||
"--max_old_space_size", | ||
}; | ||
|
||
for (unsigned i = 0; i < arraysize(whitelist); i++) { | ||
const char* allowed = whitelist[i]; | ||
if (strlen(allowed) == arglen && strncmp(allowed, arg, arglen) == 0) | ||
return; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I like the simplicity of this, it's not the most performant approach. Would like to see this optimized more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any suggestions? Note:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure what to do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A couple of options I can think of include:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, the code ends up being a bit more complicated but the amount of time it takes to go through the various options is much shorter. |
||
|
||
fprintf(stderr, "%s: %s is not allowed in NODE_OPTIONS\n", exe, arg); | ||
exit(9); | ||
} | ||
|
||
|
||
// Parse command line arguments. | ||
// | ||
// argv is modified in place. exec_argv and v8_argv are out arguments that | ||
|
@@ -3658,7 +3706,8 @@ static void ParseArgs(int* argc, | |
int* exec_argc, | ||
const char*** exec_argv, | ||
int* v8_argc, | ||
const char*** v8_argv) { | ||
const char*** v8_argv, | ||
bool is_env) { | ||
const unsigned int nargs = static_cast<unsigned int>(*argc); | ||
const char** new_exec_argv = new const char*[nargs]; | ||
const char** new_v8_argv = new const char*[nargs]; | ||
|
@@ -3687,6 +3736,8 @@ static void ParseArgs(int* argc, | |
const char* const arg = argv[index]; | ||
unsigned int args_consumed = 1; | ||
|
||
CheckIfAllowedInEnv(argv[0], is_env, arg); | ||
|
||
if (debug_options.ParseOption(arg)) { | ||
// Done, consumed by DebugOptions::ParseOption(). | ||
} else if (strcmp(arg, "--version") == 0 || strcmp(arg, "-v") == 0) { | ||
|
@@ -3839,6 +3890,13 @@ static void ParseArgs(int* argc, | |
|
||
// Copy remaining arguments. | ||
const unsigned int args_left = nargs - index; | ||
|
||
if (is_env && args_left) { | ||
fprintf(stderr, "%s: %s is not supported in NODE_OPTIONS\n", | ||
argv[0], argv[index]); | ||
exit(9); | ||
} | ||
|
||
memcpy(new_argv + new_argc, argv + index, args_left * sizeof(*argv)); | ||
new_argc += args_left; | ||
|
||
|
@@ -4161,6 +4219,54 @@ inline void PlatformInit() { | |
} | ||
|
||
|
||
void ProcessArgv(int* argc, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This just makes me happy... |
||
const char** argv, | ||
int* exec_argc, | ||
const char*** exec_argv, | ||
bool is_env = false) { | ||
// Parse a few arguments which are specific to Node. | ||
int v8_argc; | ||
const char** v8_argv; | ||
ParseArgs(argc, argv, exec_argc, exec_argv, &v8_argc, &v8_argv, is_env); | ||
|
||
// TODO(bnoordhuis) Intercept --prof arguments and start the CPU profiler | ||
// manually? That would give us a little more control over its runtime | ||
// behavior but it could also interfere with the user's intentions in ways | ||
// we fail to anticipate. Dillema. | ||
for (int i = 1; i < v8_argc; ++i) { | ||
if (strncmp(v8_argv[i], "--prof", sizeof("--prof") - 1) == 0) { | ||
v8_is_profiling = true; | ||
break; | ||
} | ||
} | ||
|
||
#ifdef __POSIX__ | ||
// Block SIGPROF signals when sleeping in epoll_wait/kevent/etc. Avoids the | ||
// performance penalty of frequent EINTR wakeups when the profiler is running. | ||
// Only do this for v8.log profiling, as it breaks v8::CpuProfiler users. | ||
if (v8_is_profiling) { | ||
uv_loop_configure(uv_default_loop(), UV_LOOP_BLOCK_SIGNAL, SIGPROF); | ||
} | ||
#endif | ||
|
||
// The const_cast doesn't violate conceptual const-ness. V8 doesn't modify | ||
// the argv array or the elements it points to. | ||
if (v8_argc > 1) | ||
V8::SetFlagsFromCommandLine(&v8_argc, const_cast<char**>(v8_argv), true); | ||
|
||
// Anything that's still in v8_argv is not a V8 or a node option. | ||
for (int i = 1; i < v8_argc; i++) { | ||
fprintf(stderr, "%s: bad option: %s\n", argv[0], v8_argv[i]); | ||
} | ||
delete[] v8_argv; | ||
v8_argv = nullptr; | ||
|
||
if (v8_argc > 1) { | ||
exit(9); | ||
} | ||
} | ||
|
||
|
||
void Init(int* argc, | ||
const char** argv, | ||
int* exec_argc, | ||
|
@@ -4214,31 +4320,36 @@ void Init(int* argc, | |
SafeGetenv("OPENSSL_CONF", &openssl_config); | ||
#endif | ||
|
||
// Parse a few arguments which are specific to Node. | ||
int v8_argc; | ||
const char** v8_argv; | ||
ParseArgs(argc, argv, exec_argc, exec_argv, &v8_argc, &v8_argv); | ||
|
||
// TODO(bnoordhuis) Intercept --prof arguments and start the CPU profiler | ||
// manually? That would give us a little more control over its runtime | ||
// behavior but it could also interfere with the user's intentions in ways | ||
// we fail to anticipate. Dillema. | ||
for (int i = 1; i < v8_argc; ++i) { | ||
if (strncmp(v8_argv[i], "--prof", sizeof("--prof") - 1) == 0) { | ||
v8_is_profiling = true; | ||
break; | ||
#if !defined(NODE_WITHOUT_NODE_OPTIONS) | ||
std::string node_options; | ||
if (SafeGetenv("NODE_OPTIONS", &node_options)) { | ||
// Smallest tokens are 2-chars (a not space and a space), plus 2 extra | ||
// pointers, for the prepended executable name, and appended NULL pointer. | ||
size_t max_len = 2 + (node_options.length() + 1) / 2; | ||
const char** argv_from_env = new const char*[max_len]; | ||
int argc_from_env = 0; | ||
// [0] is expected to be the program name, fill it in from the real argv. | ||
argv_from_env[argc_from_env++] = argv[0]; | ||
|
||
char* cstr = strdup(node_options.c_str()); | ||
char* initptr = cstr; | ||
char* token; | ||
while ((token = strtok(initptr, " "))) { // NOLINT(runtime/threadsafe_fn) | ||
initptr = nullptr; | ||
argv_from_env[argc_from_env++] = token; | ||
} | ||
} | ||
|
||
#ifdef __POSIX__ | ||
// Block SIGPROF signals when sleeping in epoll_wait/kevent/etc. Avoids the | ||
// performance penalty of frequent EINTR wakeups when the profiler is running. | ||
// Only do this for v8.log profiling, as it breaks v8::CpuProfiler users. | ||
if (v8_is_profiling) { | ||
uv_loop_configure(uv_default_loop(), UV_LOOP_BLOCK_SIGNAL, SIGPROF); | ||
argv_from_env[argc_from_env] = nullptr; | ||
int exec_argc_; | ||
const char** exec_argv_ = nullptr; | ||
ProcessArgv(&argc_from_env, argv_from_env, &exec_argc_, &exec_argv_, true); | ||
delete[] exec_argv_; | ||
delete[] argv_from_env; | ||
free(cstr); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Personally, I'd rather that C++ std lib classes are used to avoid all the manual allocations. And you shouldn't have to worry about if the C++ std lib is available on a certain platform. And you don't need to rely on the non-reentrant legacy c std::vector<std::string> env_args;
std::string::size_type pos = 0, pos_next = 0;
while (pos_next < nodeopt.size())
{
pos_next = nodeopt.find(" ", pos);
if (pos_next == std::string::npos)
{
pos_next = nodeopt.size();
}
if (pos_next > pos)
{
env_args.push_back(nodeopt.substr(pos, pos_next - pos));
}
pos = pos_next + 1;
}
std::vector<const char *> argv_from_env;
argv_from_env.reserve(args.size() + 2);
argv_from_env.push_back(argv[0]);
for (size_t i = 0; i < args.size(); i++)
{
argv_from_env.push_back(env_args[i].c_str());
}
argv_from_env.push_back(nullptr); and now you can call ProcessArgv(&argc_from_env, &argv_from_env[0], &exec_argc_, &exec_argv_, true ); |
||
} | ||
#endif | ||
|
||
ProcessArgv(argc, argv, exec_argc, exec_argv); | ||
|
||
#if defined(NODE_HAVE_I18N_SUPPORT) | ||
// If the parameter isn't given, use the env variable. | ||
if (icu_data_dir.empty()) | ||
|
@@ -4250,21 +4361,6 @@ void Init(int* argc, | |
"(check NODE_ICU_DATA or --icu-data-dir parameters)"); | ||
} | ||
#endif | ||
// The const_cast doesn't violate conceptual const-ness. V8 doesn't modify | ||
// the argv array or the elements it points to. | ||
if (v8_argc > 1) | ||
V8::SetFlagsFromCommandLine(&v8_argc, const_cast<char**>(v8_argv), true); | ||
|
||
// Anything that's still in v8_argv is not a V8 or a node option. | ||
for (int i = 1; i < v8_argc; i++) { | ||
fprintf(stderr, "%s: bad option: %s\n", argv[0], v8_argv[i]); | ||
} | ||
delete[] v8_argv; | ||
v8_argv = nullptr; | ||
|
||
if (v8_argc > 1) { | ||
exit(9); | ||
} | ||
|
||
// Unconditionally force typed arrays to allocate outside the v8 heap. This | ||
// is to prevent memory pointers from being moved around that are returned by | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
'use strict'; | ||
const common = require('../common'); | ||
if (process.config.variables.node_without_node_options) | ||
return common.skip('missing NODE_OPTIONS support'); | ||
|
||
// Test options specified by env variable. | ||
|
||
const assert = require('assert'); | ||
const exec = require('child_process').execFile; | ||
|
||
disallow('--version'); | ||
disallow('-v'); | ||
disallow('--help'); | ||
disallow('-h'); | ||
disallow('--eval'); | ||
disallow('-e'); | ||
disallow('--print'); | ||
disallow('-p'); | ||
disallow('-pe'); | ||
disallow('--check'); | ||
disallow('-c'); | ||
disallow('--interactive'); | ||
disallow('-i'); | ||
disallow('--v8-options'); | ||
disallow('--'); | ||
|
||
function disallow(opt) { | ||
const options = {env: {NODE_OPTIONS: opt}}; | ||
exec(process.execPath, options, common.mustCall(function(err) { | ||
const message = err.message.split(/\r?\n/)[1]; | ||
const expect = process.execPath + ': ' + opt + | ||
' is not allowed in NODE_OPTIONS'; | ||
|
||
assert.strictEqual(err.code, 9); | ||
assert.strictEqual(message, expect); | ||
})); | ||
} | ||
|
||
const printA = require.resolve('../fixtures/printA.js'); | ||
|
||
expect('-r ' + printA, 'A\nB\n'); | ||
expect('--no-deprecation', 'B\n'); | ||
expect('--no-warnings', 'B\n'); | ||
expect('--trace-warnings', 'B\n'); | ||
expect('--redirect-warnings=_', 'B\n'); | ||
expect('--trace-deprecation', 'B\n'); | ||
expect('--trace-sync-io', 'B\n'); | ||
expect('--trace-events-enabled', 'B\n'); | ||
expect('--track-heap-objects', 'B\n'); | ||
expect('--throw-deprecation', 'B\n'); | ||
expect('--zero-fill-buffers', 'B\n'); | ||
expect('--v8-pool-size=10', 'B\n'); | ||
expect('--use-openssl-ca', 'B\n'); | ||
expect('--use-bundled-ca', 'B\n'); | ||
expect('--openssl-config=_ossl_cfg', 'B\n'); | ||
expect('--icu-data-dir=_d', 'B\n'); | ||
|
||
// V8 options | ||
expect('--max_old_space_size=0', 'B\n'); | ||
|
||
function expect(opt, want) { | ||
const printB = require.resolve('../fixtures/printB.js'); | ||
const argv = [printB]; | ||
const opts = { | ||
env: {NODE_OPTIONS: opt}, | ||
maxBuffer: 1000000000, | ||
}; | ||
exec(process.execPath, argv, opts, common.mustCall(function(err, stdout) { | ||
assert.ifError(err); | ||
if (!RegExp(want).test(stdout)) { | ||
console.error('For %j, failed to find %j in: <\n%s\n>', | ||
opt, expect, stdout); | ||
assert(false, 'Expected ' + expect); | ||
} | ||
})); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Until I finish #12425 we'll need this in
vcbuild.bat
, sorry for the hassle.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You want this implemented for Windows? If so, you'll have to give me a hint as to how. Also, I'm not sure it has to be added. Lots of other
configure
options aren't implemented in vcbuild.bat, I'm not sure this one is so useful it has to exist for Windows.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I don't have an opinion about this specific option. But I don't the disparity to widen.