worker: enable passing command line flags
This PR adds the ability to provide Workers with their own execArgv flags in replacement of the main thread's execArgv. Only per-Isolate/per-Environment options are allowed. Per-Process options and V8 flags are not allowed. Passing an empty execArgv array will reset per-Isolate and per-Environment options of the Worker to their defaults. If execArgv option is not passed, the Worker will get the same flags as the main thread. Usage example: ``` const worker = new Worker(__filename, { execArgv: ['--trace-warnings'], }); ``` PR-URL: https://github.com/nodejs/node/pull/25467 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
parent
74562356db
commit
01cd21973b
@ -1909,6 +1909,12 @@ The fulfilled value of a linking promise is not a `vm.SourceTextModule` object.
|
|||||||
The current module's status does not allow for this operation. The specific
|
The current module's status does not allow for this operation. The specific
|
||||||
meaning of the error depends on the specific function.
|
meaning of the error depends on the specific function.
|
||||||
|
|
||||||
|
<a id="ERR_WORKER_INVALID_EXEC_ARGV"></a>
|
||||||
|
### ERR_WORKER_INVALID_EXEC_ARGV
|
||||||
|
|
||||||
|
The `execArgv` option passed to the `Worker` constructor contains
|
||||||
|
invalid flags.
|
||||||
|
|
||||||
<a id="ERR_WORKER_PATH"></a>
|
<a id="ERR_WORKER_PATH"></a>
|
||||||
### ERR_WORKER_PATH
|
### ERR_WORKER_PATH
|
||||||
|
|
||||||
|
@ -316,13 +316,16 @@ if (isMainThread) {
|
|||||||
occur as described in the [HTML structured clone algorithm][], and an error
|
occur as described in the [HTML structured clone algorithm][], and an error
|
||||||
will be thrown if the object cannot be cloned (e.g. because it contains
|
will be thrown if the object cannot be cloned (e.g. because it contains
|
||||||
`function`s).
|
`function`s).
|
||||||
* stdin {boolean} If this is set to `true`, then `worker.stdin` will
|
* `stdin` {boolean} If this is set to `true`, then `worker.stdin` will
|
||||||
provide a writable stream whose contents will appear as `process.stdin`
|
provide a writable stream whose contents will appear as `process.stdin`
|
||||||
inside the Worker. By default, no data is provided.
|
inside the Worker. By default, no data is provided.
|
||||||
* stdout {boolean} If this is set to `true`, then `worker.stdout` will
|
* `stdout` {boolean} If this is set to `true`, then `worker.stdout` will
|
||||||
not automatically be piped through to `process.stdout` in the parent.
|
not automatically be piped through to `process.stdout` in the parent.
|
||||||
* stderr {boolean} If this is set to `true`, then `worker.stderr` will
|
* `stderr` {boolean} If this is set to `true`, then `worker.stderr` will
|
||||||
not automatically be piped through to `process.stderr` in the parent.
|
not automatically be piped through to `process.stderr` in the parent.
|
||||||
|
* `execArgv` {string[]} List of node CLI options passed to the worker.
|
||||||
|
V8 options (such as `--max-old-space-size`) and options that affect the
|
||||||
|
process (such as `--title`) are not supported.
|
||||||
|
|
||||||
### Event: 'error'
|
### Event: 'error'
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
|
@ -992,6 +992,9 @@ E('ERR_VM_MODULE_NOT_LINKED',
|
|||||||
E('ERR_VM_MODULE_NOT_MODULE',
|
E('ERR_VM_MODULE_NOT_MODULE',
|
||||||
'Provided module is not an instance of Module', Error);
|
'Provided module is not an instance of Module', Error);
|
||||||
E('ERR_VM_MODULE_STATUS', 'Module status %s', Error);
|
E('ERR_VM_MODULE_STATUS', 'Module status %s', Error);
|
||||||
|
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors) =>
|
||||||
|
`Initiated Worker with invalid execArgv flags: ${errors.join(', ')}`,
|
||||||
|
Error);
|
||||||
E('ERR_WORKER_PATH',
|
E('ERR_WORKER_PATH',
|
||||||
'The worker script filename must be an absolute path or a relative ' +
|
'The worker script filename must be an absolute path or a relative ' +
|
||||||
'path starting with \'./\' or \'../\'. Received "%s"',
|
'path starting with \'./\' or \'../\'. Received "%s"',
|
||||||
|
@ -8,6 +8,8 @@ const {
|
|||||||
ERR_WORKER_PATH,
|
ERR_WORKER_PATH,
|
||||||
ERR_WORKER_UNSERIALIZABLE_ERROR,
|
ERR_WORKER_UNSERIALIZABLE_ERROR,
|
||||||
ERR_WORKER_UNSUPPORTED_EXTENSION,
|
ERR_WORKER_UNSUPPORTED_EXTENSION,
|
||||||
|
ERR_WORKER_INVALID_EXEC_ARGV,
|
||||||
|
ERR_INVALID_ARG_TYPE,
|
||||||
} = require('internal/errors').codes;
|
} = require('internal/errors').codes;
|
||||||
const { validateString } = require('internal/validators');
|
const { validateString } = require('internal/validators');
|
||||||
|
|
||||||
@ -49,7 +51,11 @@ class Worker extends EventEmitter {
|
|||||||
super();
|
super();
|
||||||
debug(`[${threadId}] create new worker`, filename, options);
|
debug(`[${threadId}] create new worker`, filename, options);
|
||||||
validateString(filename, 'filename');
|
validateString(filename, 'filename');
|
||||||
|
if (options.execArgv && !Array.isArray(options.execArgv)) {
|
||||||
|
throw new ERR_INVALID_ARG_TYPE('options.execArgv',
|
||||||
|
'array',
|
||||||
|
options.execArgv);
|
||||||
|
}
|
||||||
if (!options.eval) {
|
if (!options.eval) {
|
||||||
if (!path.isAbsolute(filename) &&
|
if (!path.isAbsolute(filename) &&
|
||||||
!filename.startsWith('./') &&
|
!filename.startsWith('./') &&
|
||||||
@ -68,7 +74,10 @@ class Worker extends EventEmitter {
|
|||||||
|
|
||||||
const url = options.eval ? null : pathToFileURL(filename);
|
const url = options.eval ? null : pathToFileURL(filename);
|
||||||
// Set up the C++ handle for the worker, as well as some internal wiring.
|
// Set up the C++ handle for the worker, as well as some internal wiring.
|
||||||
this[kHandle] = new WorkerImpl(url);
|
this[kHandle] = new WorkerImpl(url, options.execArgv);
|
||||||
|
if (this[kHandle].invalidExecArgv) {
|
||||||
|
throw new ERR_WORKER_INVALID_EXEC_ARGV(this[kHandle].invalidExecArgv);
|
||||||
|
}
|
||||||
this[kHandle].onexit = (code) => this[kOnExit](code);
|
this[kHandle].onexit = (code) => this[kOnExit](code);
|
||||||
this[kPort] = this[kHandle].messagePort;
|
this[kPort] = this[kHandle].messagePort;
|
||||||
this[kPort].on('message', (data) => this[kOnMessage](data));
|
this[kPort].on('message', (data) => this[kOnMessage](data));
|
||||||
|
@ -595,6 +595,11 @@ inline std::shared_ptr<PerIsolateOptions> IsolateData::options() {
|
|||||||
return options_;
|
return options_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void IsolateData::set_options(
|
||||||
|
std::shared_ptr<PerIsolateOptions> options) {
|
||||||
|
options_ = options;
|
||||||
|
}
|
||||||
|
|
||||||
void Environment::CreateImmediate(native_immediate_callback cb,
|
void Environment::CreateImmediate(native_immediate_callback cb,
|
||||||
void* data,
|
void* data,
|
||||||
v8::Local<v8::Object> obj,
|
v8::Local<v8::Object> obj,
|
||||||
|
@ -394,6 +394,7 @@ class IsolateData {
|
|||||||
inline uint32_t* zero_fill_field() const;
|
inline uint32_t* zero_fill_field() const;
|
||||||
inline MultiIsolatePlatform* platform() const;
|
inline MultiIsolatePlatform* platform() const;
|
||||||
inline std::shared_ptr<PerIsolateOptions> options();
|
inline std::shared_ptr<PerIsolateOptions> options();
|
||||||
|
inline void set_options(std::shared_ptr<PerIsolateOptions> options);
|
||||||
|
|
||||||
#define VP(PropertyName, StringValue) V(v8::Private, PropertyName)
|
#define VP(PropertyName, StringValue) V(v8::Private, PropertyName)
|
||||||
#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName)
|
#define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName)
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
#include "async_wrap-inl.h"
|
#include "async_wrap-inl.h"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using node::options_parser::kDisallowedInEnvironment;
|
||||||
using v8::ArrayBuffer;
|
using v8::ArrayBuffer;
|
||||||
using v8::Context;
|
using v8::Context;
|
||||||
using v8::Function;
|
using v8::Function;
|
||||||
@ -68,7 +70,10 @@ void WaitForWorkerInspectorToStop(Environment* child) {}
|
|||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
Worker::Worker(Environment* env, Local<Object> wrap, const std::string& url)
|
Worker::Worker(Environment* env,
|
||||||
|
Local<Object> wrap,
|
||||||
|
const std::string& url,
|
||||||
|
std::shared_ptr<PerIsolateOptions> per_isolate_opts)
|
||||||
: AsyncWrap(env, wrap, AsyncWrap::PROVIDER_WORKER), url_(url) {
|
: AsyncWrap(env, wrap, AsyncWrap::PROVIDER_WORKER), url_(url) {
|
||||||
// Generate a new thread id.
|
// Generate a new thread id.
|
||||||
{
|
{
|
||||||
@ -113,6 +118,9 @@ Worker::Worker(Environment* env, Local<Object> wrap, const std::string& url)
|
|||||||
&loop_,
|
&loop_,
|
||||||
env->isolate_data()->platform(),
|
env->isolate_data()->platform(),
|
||||||
array_buffer_allocator_.get()));
|
array_buffer_allocator_.get()));
|
||||||
|
if (per_isolate_opts != nullptr) {
|
||||||
|
isolate_data_->set_options(per_isolate_opts);
|
||||||
|
}
|
||||||
CHECK(isolate_data_);
|
CHECK(isolate_data_);
|
||||||
|
|
||||||
Local<Context> context = NewContext(isolate_);
|
Local<Context> context = NewContext(isolate_);
|
||||||
@ -391,14 +399,67 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string url;
|
std::string url;
|
||||||
|
std::shared_ptr<PerIsolateOptions> per_isolate_opts = nullptr;
|
||||||
|
|
||||||
// Argument might be a string or URL
|
// Argument might be a string or URL
|
||||||
if (args.Length() == 1 && !args[0]->IsNullOrUndefined()) {
|
if (args.Length() > 0 && !args[0]->IsNullOrUndefined()) {
|
||||||
Utf8Value value(
|
Utf8Value value(
|
||||||
args.GetIsolate(),
|
args.GetIsolate(),
|
||||||
args[0]->ToString(env->context()).FromMaybe(v8::Local<v8::String>()));
|
args[0]->ToString(env->context()).FromMaybe(v8::Local<v8::String>()));
|
||||||
url.append(value.out(), value.length());
|
url.append(value.out(), value.length());
|
||||||
|
|
||||||
|
if (args.Length() > 1 && args[1]->IsArray()) {
|
||||||
|
v8::Local<v8::Array> array = args[1].As<v8::Array>();
|
||||||
|
// The first argument is reserved for program name, but we don't need it
|
||||||
|
// in workers.
|
||||||
|
std::vector<std::string> exec_argv = {""};
|
||||||
|
uint32_t length = array->Length();
|
||||||
|
for (uint32_t i = 0; i < length; i++) {
|
||||||
|
v8::Local<v8::Value> arg;
|
||||||
|
if (!array->Get(env->context(), i).ToLocal(&arg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
v8::MaybeLocal<v8::String> arg_v8_string =
|
||||||
|
arg->ToString(env->context());
|
||||||
|
if (arg_v8_string.IsEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Utf8Value arg_utf8_value(
|
||||||
|
args.GetIsolate(),
|
||||||
|
arg_v8_string.FromMaybe(v8::Local<v8::String>()));
|
||||||
|
std::string arg_string(arg_utf8_value.out(), arg_utf8_value.length());
|
||||||
|
exec_argv.push_back(arg_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> invalid_args{};
|
||||||
|
std::vector<std::string> errors{};
|
||||||
|
per_isolate_opts.reset(new PerIsolateOptions());
|
||||||
|
|
||||||
|
// Using invalid_args as the v8_args argument as it stores unknown
|
||||||
|
// options for the per isolate parser.
|
||||||
|
options_parser::PerIsolateOptionsParser::instance.Parse(
|
||||||
|
&exec_argv,
|
||||||
|
nullptr,
|
||||||
|
&invalid_args,
|
||||||
|
per_isolate_opts.get(),
|
||||||
|
kDisallowedInEnvironment,
|
||||||
|
&errors);
|
||||||
|
|
||||||
|
// The first argument is program name.
|
||||||
|
invalid_args.erase(invalid_args.begin());
|
||||||
|
if (errors.size() > 0 || invalid_args.size() > 0) {
|
||||||
|
v8::Local<v8::Value> value =
|
||||||
|
ToV8Value(env->context(),
|
||||||
|
errors.size() > 0 ? errors : invalid_args)
|
||||||
|
.ToLocalChecked();
|
||||||
|
Local<String> key =
|
||||||
|
FIXED_ONE_BYTE_STRING(env->isolate(), "invalidExecArgv");
|
||||||
|
args.This()->Set(env->context(), key, value).FromJust();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
new Worker(env, args.This(), url);
|
new Worker(env, args.This(), url, per_isolate_opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
|
void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
|
||||||
|
@ -12,7 +12,10 @@ namespace worker {
|
|||||||
// A worker thread, as represented in its parent thread.
|
// A worker thread, as represented in its parent thread.
|
||||||
class Worker : public AsyncWrap {
|
class Worker : public AsyncWrap {
|
||||||
public:
|
public:
|
||||||
Worker(Environment* env, v8::Local<v8::Object> wrap, const std::string& url);
|
Worker(Environment* env,
|
||||||
|
v8::Local<v8::Object> wrap,
|
||||||
|
const std::string& url,
|
||||||
|
std::shared_ptr<PerIsolateOptions> per_isolate_opts);
|
||||||
~Worker();
|
~Worker();
|
||||||
|
|
||||||
// Run the worker. This is only called from the worker thread.
|
// Run the worker. This is only called from the worker thread.
|
||||||
|
@ -269,3 +269,13 @@ assert.strictEqual(
|
|||||||
|
|
||||||
restoreStdout();
|
restoreStdout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const error = new errors.codes.ERR_WORKER_INVALID_EXEC_ARGV(
|
||||||
|
['--foo, --bar']
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
error.message,
|
||||||
|
'Initiated Worker with invalid execArgv flags: --foo, --bar'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
35
test/parallel/test-worker-execargv-invalid.js
Normal file
35
test/parallel/test-worker-execargv-invalid.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const { Worker } = require('worker_threads');
|
||||||
|
|
||||||
|
{
|
||||||
|
const expectedErr = common.expectsError({
|
||||||
|
code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
type: TypeError
|
||||||
|
}, 2);
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
new Worker(__filename, { execArgv: 'hello' });
|
||||||
|
}, expectedErr);
|
||||||
|
assert.throws(() => {
|
||||||
|
new Worker(__filename, { execArgv: 6 });
|
||||||
|
}, expectedErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const expectedErr = common.expectsError({
|
||||||
|
code: 'ERR_WORKER_INVALID_EXEC_ARGV',
|
||||||
|
type: Error
|
||||||
|
}, 3);
|
||||||
|
assert.throws(() => {
|
||||||
|
new Worker(__filename, { execArgv: ['--foo'] });
|
||||||
|
}, expectedErr);
|
||||||
|
assert.throws(() => {
|
||||||
|
new Worker(__filename, { execArgv: ['--title=blah'] });
|
||||||
|
}, expectedErr);
|
||||||
|
assert.throws(() => {
|
||||||
|
new Worker(__filename, { execArgv: ['--redirect-warnings'] });
|
||||||
|
}, expectedErr);
|
||||||
|
}
|
22
test/parallel/test-worker-execargv.js
Normal file
22
test/parallel/test-worker-execargv.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
// This test ensures that Workers have the ability to get
|
||||||
|
// their own command line flags.
|
||||||
|
|
||||||
|
const { Worker, isMainThread } = require('worker_threads');
|
||||||
|
const { StringDecoder } = require('string_decoder');
|
||||||
|
const decoder = new StringDecoder('utf8');
|
||||||
|
|
||||||
|
if (isMainThread) {
|
||||||
|
const w = new Worker(__filename, { execArgv: ['--trace-warnings'] });
|
||||||
|
w.stderr.on('data', common.mustCall((chunk) => {
|
||||||
|
const error = decoder.write(chunk);
|
||||||
|
assert.ok(
|
||||||
|
/Warning: some warning[\s\S]*at Object\.<anonymous>/.test(error)
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
process.emitWarning('some warning');
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user