Skip to content

Commit 125997b

Browse files
watch: strip watch flags from NODE_OPTIONS in child process
Signed-off-by: marcopiraccini <[email protected]>
1 parent bb649d4 commit 125997b

3 files changed

Lines changed: 43 additions & 8 deletions

File tree

lib/internal/main/watch_mode.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const {
77
ArrayPrototypePush,
88
ArrayPrototypePushApply,
99
ArrayPrototypeSlice,
10-
RegExpPrototypeSymbolSplit,
10+
StringPrototypeIncludes,
1111
StringPrototypeStartsWith,
1212
} = primordials;
1313

@@ -19,7 +19,7 @@ const {
1919
triggerUncaughtException,
2020
exitCodes: { kNoFailure },
2121
} = internalBinding('errors');
22-
const { getOptionValue } = require('internal/options');
22+
const { getOptionValue, parseNodeOptionsEnvVar } = require('internal/options');
2323
const { FilesWatcher } = require('internal/watch_mode/files_watcher');
2424
const { green, blue, red, white, clear } = require('internal/util/colors');
2525
const { convertToValidSignal } = require('internal/util');
@@ -85,14 +85,15 @@ for (let i = 0; i < process.execArgv.length; i++) {
8585

8686
ArrayPrototypePushApply(argsWithoutWatchOptions, kCommand);
8787

88+
// Strip watch-related flags from NODE_OPTIONS to prevent infinite loop
89+
// when NODE_OPTIONS contains --watch (see issue #61740).
8890
const kNodeOptions = process.env.NODE_OPTIONS;
8991
let cleanNodeOptions = kNodeOptions;
9092
if (kNodeOptions != null) {
91-
const nodeOptionsArgs = [];
92-
const parts = RegExpPrototypeSymbolSplit(/\s+/, kNodeOptions);
93+
const keep = [];
94+
const parts = parseNodeOptionsEnvVar(kNodeOptions);
9395
for (let i = 0; i < parts.length; i++) {
9496
const part = parts[i];
95-
if (part === '') continue;
9697
if (part === '--watch' ||
9798
part === '--watch-preserve-output' ||
9899
StringPrototypeStartsWith(part, '--watch=') ||
@@ -102,13 +103,17 @@ if (kNodeOptions != null) {
102103
continue;
103104
}
104105
if (part === '--watch-path' || part === '--watch-kill-signal') {
105-
// These flags take a separate value argument
106+
// Skip the flag and its separate value argument
106107
i++;
107108
continue;
108109
}
109-
ArrayPrototypePush(nodeOptionsArgs, part);
110+
// The C++ tokenizer strips quotes during parsing, so values that
111+
// originally contained spaces (e.g. --require "./path with spaces/f.js")
112+
// need to be re-quoted before rejoining into a single string, otherwise
113+
// the child's C++ parser would split them into separate tokens.
114+
ArrayPrototypePush(keep, StringPrototypeIncludes(part, ' ') ? `"${part}"` : part);
110115
}
111-
cleanNodeOptions = ArrayPrototypeJoin(nodeOptionsArgs, ' ');
116+
cleanNodeOptions = ArrayPrototypeJoin(keep, ' ');
112117
}
113118

114119
const watcher = new FilesWatcher({ debounce: 200, mode: kShouldFilterModules ? 'filter' : 'all' });

lib/internal/options.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
getEmbedderOptions: getEmbedderOptionsFromBinding,
1717
getEnvOptionsInputType,
1818
getNamespaceOptionsInputType,
19+
parseNodeOptionsEnvVar,
1920
} = internalBinding('options');
2021

2122
let warnOnAllowUnauthorized = true;
@@ -172,5 +173,6 @@ module.exports = {
172173
getAllowUnauthorized,
173174
getEmbedderOptions,
174175
generateConfigJsonSchema,
176+
parseNodeOptionsEnvVar,
175177
refreshOptions,
176178
};

src/node_options.cc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2096,6 +2096,29 @@ void GetOptionsAsFlags(const FunctionCallbackInfo<Value>& args) {
20962096
args.GetReturnValue().Set(result);
20972097
}
20982098

2099+
void ParseNodeOptionsEnvVarBinding(
2100+
const FunctionCallbackInfo<Value>& args) {
2101+
Isolate* isolate = args.GetIsolate();
2102+
Local<Context> context = isolate->GetCurrentContext();
2103+
2104+
Utf8Value node_options(isolate, args[0]);
2105+
std::string options_str(*node_options, node_options.length());
2106+
2107+
std::vector<std::string> errors;
2108+
std::vector<std::string> result =
2109+
ParseNodeOptionsEnvVar(options_str, &errors);
2110+
2111+
if (!errors.empty()) {
2112+
Environment* env = Environment::GetCurrent(context);
2113+
env->ThrowError(errors[0].c_str());
2114+
return;
2115+
}
2116+
2117+
Local<Value> v8_result;
2118+
CHECK(ToV8Value(context, result).ToLocal(&v8_result));
2119+
args.GetReturnValue().Set(v8_result);
2120+
}
2121+
20992122
void Initialize(Local<Object> target,
21002123
Local<Value> unused,
21012124
Local<Context> context,
@@ -2116,6 +2139,10 @@ void Initialize(Local<Object> target,
21162139
target,
21172140
"getNamespaceOptionsInputType",
21182141
GetNamespaceOptionsInputType);
2142+
SetMethodNoSideEffect(context,
2143+
target,
2144+
"parseNodeOptionsEnvVar",
2145+
ParseNodeOptionsEnvVarBinding);
21192146
Local<Object> env_settings = Object::New(isolate);
21202147
NODE_DEFINE_CONSTANT(env_settings, kAllowedInEnvvar);
21212148
NODE_DEFINE_CONSTANT(env_settings, kDisallowedInEnvvar);
@@ -2144,6 +2171,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
21442171
registry->Register(GetEmbedderOptions);
21452172
registry->Register(GetEnvOptionsInputType);
21462173
registry->Register(GetNamespaceOptionsInputType);
2174+
registry->Register(ParseNodeOptionsEnvVarBinding);
21472175
}
21482176
} // namespace options_parser
21492177

0 commit comments

Comments
 (0)