mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
src: rewrite task runner in c++
PR-URL: https://github.com/nodejs/node/pull/52609 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Daniel Lemire <daniel@lemire.me> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
parent
905ab3e9ec
commit
c5cfdd4849
15 changed files with 404 additions and 189 deletions
|
@ -1,74 +0,0 @@
|
||||||
'use strict';
|
|
||||||
/* eslint-disable node-core/prefer-primordials */
|
|
||||||
|
|
||||||
// There is no need to add primordials to this file.
|
|
||||||
// `run.js` is a script only executed when `node --run <script>` is called.
|
|
||||||
const {
|
|
||||||
prepareMainThreadExecution,
|
|
||||||
markBootstrapComplete,
|
|
||||||
} = require('internal/process/pre_execution');
|
|
||||||
const { getPackageJSONScripts } = internalBinding('modules');
|
|
||||||
const { execSync } = require('child_process');
|
|
||||||
const { resolve, delimiter } = require('path');
|
|
||||||
const { escapeShell } = require('internal/shell');
|
|
||||||
const { getOptionValue } = require('internal/options');
|
|
||||||
const { emitExperimentalWarning } = require('internal/util');
|
|
||||||
|
|
||||||
prepareMainThreadExecution(false, false);
|
|
||||||
markBootstrapComplete();
|
|
||||||
emitExperimentalWarning('Task runner');
|
|
||||||
|
|
||||||
// TODO(@anonrig): Search for all package.json's until root folder.
|
|
||||||
const json_string = getPackageJSONScripts();
|
|
||||||
|
|
||||||
// Check if package.json exists and is parseable
|
|
||||||
if (json_string === undefined) {
|
|
||||||
process.exitCode = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const scripts = JSON.parse(json_string);
|
|
||||||
// Remove the first argument, which are the node binary.
|
|
||||||
const args = process.argv.slice(1);
|
|
||||||
const id = getOptionValue('--run');
|
|
||||||
let command = scripts[id];
|
|
||||||
|
|
||||||
if (!command) {
|
|
||||||
const { error } = require('internal/console/global');
|
|
||||||
|
|
||||||
error(`Missing script: "${id}"\n`);
|
|
||||||
|
|
||||||
const keys = Object.keys(scripts);
|
|
||||||
if (keys.length === 0) {
|
|
||||||
error('There are no scripts available in package.json');
|
|
||||||
} else {
|
|
||||||
error('Available scripts are:');
|
|
||||||
for (const script of keys) {
|
|
||||||
error(` ${script}: ${scripts[script]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const env = process.env;
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const binPath = resolve(cwd, 'node_modules/.bin');
|
|
||||||
|
|
||||||
// Filter all environment variables that contain the word "path"
|
|
||||||
const keys = Object.keys(env).filter((key) => /^path$/i.test(key));
|
|
||||||
const PATH = keys.map((key) => env[key]);
|
|
||||||
|
|
||||||
// Append only the current folder bin path to the PATH variable.
|
|
||||||
// TODO(@anonrig): Prepend the bin path of all parent folders.
|
|
||||||
const paths = [binPath, PATH].join(delimiter);
|
|
||||||
for (const key of keys) {
|
|
||||||
env[key] = paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are any remaining arguments left, append them to the command.
|
|
||||||
// This is useful if you want to pass arguments to the script, such as
|
|
||||||
// `node --run linter -- --help` which runs `biome --check . --help`
|
|
||||||
if (args.length > 0) {
|
|
||||||
command += ' ' + escapeShell(args.map((arg) => arg.trim()).join(' '));
|
|
||||||
}
|
|
||||||
execSync(command, { stdio: 'inherit', env, shell: true });
|
|
|
@ -1,37 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
|
|
||||||
// There is no need to add primordials to this file.
|
|
||||||
// `shell.js` is a script only executed when `node run <script>` is called.
|
|
||||||
|
|
||||||
const forbiddenCharacters = /[\t\n\r "#$&'()*;<>?\\`|~]/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escapes a string to be used as a shell argument.
|
|
||||||
*
|
|
||||||
* Adapted from `promise-spawn` module available under ISC license.
|
|
||||||
* Ref: https://github.com/npm/promise-spawn/blob/16b36410f9b721dbe190141136432a418869734f/lib/escape.js
|
|
||||||
* @param {string} input
|
|
||||||
*/
|
|
||||||
function escapeShell(input) {
|
|
||||||
// If the input is an empty string, return a pair of quotes
|
|
||||||
if (!input.length) {
|
|
||||||
return '\'\'';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if input contains any forbidden characters
|
|
||||||
// If it doesn't, return the input as is.
|
|
||||||
if (!forbiddenCharacters.test(input)) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace single quotes with '\'' and wrap the whole result in a fresh set of quotes
|
|
||||||
return `'${input.replace(/'/g, '\'\\\'\'')}'`
|
|
||||||
// If the input string already had single quotes around it, clean those up
|
|
||||||
.replace(/^(?:'')+(?!$)/, '')
|
|
||||||
.replace(/\\'''/g, '\\\'');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
escapeShell,
|
|
||||||
};
|
|
2
node.gyp
2
node.gyp
|
@ -136,6 +136,7 @@
|
||||||
'src/node_stat_watcher.cc',
|
'src/node_stat_watcher.cc',
|
||||||
'src/node_symbols.cc',
|
'src/node_symbols.cc',
|
||||||
'src/node_task_queue.cc',
|
'src/node_task_queue.cc',
|
||||||
|
'src/node_task_runner.cc',
|
||||||
'src/node_trace_events.cc',
|
'src/node_trace_events.cc',
|
||||||
'src/node_types.cc',
|
'src/node_types.cc',
|
||||||
'src/node_url.cc',
|
'src/node_url.cc',
|
||||||
|
@ -397,6 +398,7 @@
|
||||||
'test/cctest/test_base_object_ptr.cc',
|
'test/cctest/test_base_object_ptr.cc',
|
||||||
'test/cctest/test_cppgc.cc',
|
'test/cctest/test_cppgc.cc',
|
||||||
'test/cctest/test_node_postmortem_metadata.cc',
|
'test/cctest/test_node_postmortem_metadata.cc',
|
||||||
|
'test/cctest/test_node_task_runner.cc',
|
||||||
'test/cctest/test_environment.cc',
|
'test/cctest/test_environment.cc',
|
||||||
'test/cctest/test_linked_binding.cc',
|
'test/cctest/test_linked_binding.cc',
|
||||||
'test/cctest/test_node_api.cc',
|
'test/cctest/test_node_api.cc',
|
||||||
|
|
29
src/node.cc
29
src/node.cc
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
#include "node.h"
|
#include "node.h"
|
||||||
#include "node_dotenv.h"
|
#include "node_dotenv.h"
|
||||||
|
#include "node_task_runner.h"
|
||||||
|
|
||||||
// ========== local headers ==========
|
// ========== local headers ==========
|
||||||
|
|
||||||
|
@ -360,10 +361,6 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
|
||||||
return StartExecution(env, "internal/main/watch_mode");
|
return StartExecution(env, "internal/main/watch_mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!env->options()->run.empty()) {
|
|
||||||
return StartExecution(env, "internal/main/run");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!first_argv.empty() && first_argv != "-") {
|
if (!first_argv.empty() && first_argv != "-") {
|
||||||
return StartExecution(env, "internal/main/run_main_module");
|
return StartExecution(env, "internal/main/run_main_module");
|
||||||
}
|
}
|
||||||
|
@ -976,11 +973,11 @@ int InitializeNodeWithArgs(std::vector<std::string>* argv,
|
||||||
InitializeNodeWithArgsInternal(argv, exec_argv, errors, flags));
|
InitializeNodeWithArgsInternal(argv, exec_argv, errors, flags));
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::unique_ptr<InitializationResultImpl>
|
static std::shared_ptr<InitializationResultImpl>
|
||||||
InitializeOncePerProcessInternal(const std::vector<std::string>& args,
|
InitializeOncePerProcessInternal(const std::vector<std::string>& args,
|
||||||
ProcessInitializationFlags::Flags flags =
|
ProcessInitializationFlags::Flags flags =
|
||||||
ProcessInitializationFlags::kNoFlags) {
|
ProcessInitializationFlags::kNoFlags) {
|
||||||
auto result = std::make_unique<InitializationResultImpl>();
|
auto result = std::make_shared<InitializationResultImpl>();
|
||||||
result->args_ = args;
|
result->args_ = args;
|
||||||
|
|
||||||
if (!(flags & ProcessInitializationFlags::kNoParseGlobalDebugVariables)) {
|
if (!(flags & ProcessInitializationFlags::kNoParseGlobalDebugVariables)) {
|
||||||
|
@ -1010,6 +1007,22 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!per_process::cli_options->run.empty()) {
|
||||||
|
// TODO(@anonrig): Handle NODE_NO_WARNINGS, NODE_REDIRECT_WARNINGS,
|
||||||
|
// --disable-warning and --redirect-warnings.
|
||||||
|
if (per_process::cli_options->per_isolate->per_env->warnings) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"ExperimentalWarning: Task runner is an experimental feature and "
|
||||||
|
"might change at any time\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto positional_args = task_runner::GetPositionalArgs(args);
|
||||||
|
result->early_return_ = true;
|
||||||
|
task_runner::RunTask(
|
||||||
|
result, per_process::cli_options->run, positional_args);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(flags & ProcessInitializationFlags::kNoPrintHelpOrVersionOutput)) {
|
if (!(flags & ProcessInitializationFlags::kNoPrintHelpOrVersionOutput)) {
|
||||||
if (per_process::cli_options->print_version) {
|
if (per_process::cli_options->print_version) {
|
||||||
printf("%s\n", NODE_VERSION);
|
printf("%s\n", NODE_VERSION);
|
||||||
|
@ -1169,7 +1182,7 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<InitializationResult> InitializeOncePerProcess(
|
std::shared_ptr<InitializationResult> InitializeOncePerProcess(
|
||||||
const std::vector<std::string>& args,
|
const std::vector<std::string>& args,
|
||||||
ProcessInitializationFlags::Flags flags) {
|
ProcessInitializationFlags::Flags flags) {
|
||||||
return InitializeOncePerProcessInternal(args, flags);
|
return InitializeOncePerProcessInternal(args, flags);
|
||||||
|
@ -1375,7 +1388,7 @@ static ExitCode StartInternal(int argc, char** argv) {
|
||||||
// Hack around with the argv pointer. Used for process.title = "blah".
|
// Hack around with the argv pointer. Used for process.title = "blah".
|
||||||
argv = uv_setup_args(argc, argv);
|
argv = uv_setup_args(argc, argv);
|
||||||
|
|
||||||
std::unique_ptr<InitializationResultImpl> result =
|
std::shared_ptr<InitializationResultImpl> result =
|
||||||
InitializeOncePerProcessInternal(
|
InitializeOncePerProcessInternal(
|
||||||
std::vector<std::string>(argv, argv + argc));
|
std::vector<std::string>(argv, argv + argc));
|
||||||
for (const std::string& error : result->errors()) {
|
for (const std::string& error : result->errors()) {
|
||||||
|
|
|
@ -349,7 +349,7 @@ NODE_DEPRECATED("Use InitializeOncePerProcess() instead",
|
||||||
// including the arguments split into argv/exec_argv, a list of potential
|
// including the arguments split into argv/exec_argv, a list of potential
|
||||||
// errors encountered during initialization, and a potential suggested
|
// errors encountered during initialization, and a potential suggested
|
||||||
// exit code.
|
// exit code.
|
||||||
NODE_EXTERN std::unique_ptr<InitializationResult> InitializeOncePerProcess(
|
NODE_EXTERN std::shared_ptr<InitializationResult> InitializeOncePerProcess(
|
||||||
const std::vector<std::string>& args,
|
const std::vector<std::string>& args,
|
||||||
ProcessInitializationFlags::Flags flags =
|
ProcessInitializationFlags::Flags flags =
|
||||||
ProcessInitializationFlags::kNoFlags);
|
ProcessInitializationFlags::kNoFlags);
|
||||||
|
@ -358,7 +358,7 @@ NODE_EXTERN std::unique_ptr<InitializationResult> InitializeOncePerProcess(
|
||||||
NODE_EXTERN void TearDownOncePerProcess();
|
NODE_EXTERN void TearDownOncePerProcess();
|
||||||
// Convenience overload for specifying multiple flags without having
|
// Convenience overload for specifying multiple flags without having
|
||||||
// to worry about casts.
|
// to worry about casts.
|
||||||
inline std::unique_ptr<InitializationResult> InitializeOncePerProcess(
|
inline std::shared_ptr<InitializationResult> InitializeOncePerProcess(
|
||||||
const std::vector<std::string>& args,
|
const std::vector<std::string>& args,
|
||||||
std::initializer_list<ProcessInitializationFlags::Flags> list) {
|
std::initializer_list<ProcessInitializationFlags::Flags> list) {
|
||||||
uint64_t flags_accum = ProcessInitializationFlags::kNoFlags;
|
uint64_t flags_accum = ProcessInitializationFlags::kNoFlags;
|
||||||
|
|
|
@ -367,28 +367,6 @@ void BindingData::GetNearestParentPackageJSONType(
|
||||||
args.GetReturnValue().Set(Array::New(realm->isolate(), values, 3));
|
args.GetReturnValue().Set(Array::New(realm->isolate(), values, 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindingData::GetPackageJSONScripts(
|
|
||||||
const FunctionCallbackInfo<Value>& args) {
|
|
||||||
Realm* realm = Realm::GetCurrent(args);
|
|
||||||
std::string_view path = "package.json";
|
|
||||||
|
|
||||||
THROW_IF_INSUFFICIENT_PERMISSIONS(
|
|
||||||
realm->env(), permission::PermissionScope::kFileSystemRead, path);
|
|
||||||
|
|
||||||
auto package_json = GetPackageJSON(realm, path);
|
|
||||||
if (package_json == nullptr) {
|
|
||||||
printf("Can't read package.json\n");
|
|
||||||
return;
|
|
||||||
} else if (!package_json->scripts.has_value()) {
|
|
||||||
printf("Can't read package.json \"scripts\" object\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
args.GetReturnValue().Set(
|
|
||||||
ToV8Value(realm->context(), package_json->scripts.value())
|
|
||||||
.ToLocalChecked());
|
|
||||||
}
|
|
||||||
|
|
||||||
void BindingData::GetPackageScopeConfig(
|
void BindingData::GetPackageScopeConfig(
|
||||||
const FunctionCallbackInfo<Value>& args) {
|
const FunctionCallbackInfo<Value>& args) {
|
||||||
CHECK_GE(args.Length(), 1);
|
CHECK_GE(args.Length(), 1);
|
||||||
|
@ -469,7 +447,6 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
|
||||||
"getNearestParentPackageJSON",
|
"getNearestParentPackageJSON",
|
||||||
GetNearestParentPackageJSON);
|
GetNearestParentPackageJSON);
|
||||||
SetMethod(isolate, target, "getPackageScopeConfig", GetPackageScopeConfig);
|
SetMethod(isolate, target, "getPackageScopeConfig", GetPackageScopeConfig);
|
||||||
SetMethod(isolate, target, "getPackageJSONScripts", GetPackageJSONScripts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindingData::CreatePerContextProperties(Local<Object> target,
|
void BindingData::CreatePerContextProperties(Local<Object> target,
|
||||||
|
@ -486,7 +463,6 @@ void BindingData::RegisterExternalReferences(
|
||||||
registry->Register(GetNearestParentPackageJSONType);
|
registry->Register(GetNearestParentPackageJSONType);
|
||||||
registry->Register(GetNearestParentPackageJSON);
|
registry->Register(GetNearestParentPackageJSON);
|
||||||
registry->Register(GetPackageScopeConfig);
|
registry->Register(GetPackageScopeConfig);
|
||||||
registry->Register(GetPackageJSONScripts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace modules
|
} // namespace modules
|
||||||
|
|
|
@ -566,10 +566,7 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
||||||
"process V8 profiler output generated using --prof",
|
"process V8 profiler output generated using --prof",
|
||||||
&EnvironmentOptions::prof_process);
|
&EnvironmentOptions::prof_process);
|
||||||
// Options after --prof-process are passed through to the prof processor.
|
// Options after --prof-process are passed through to the prof processor.
|
||||||
AddAlias("--prof-process", { "--prof-process", "--" });
|
AddAlias("--prof-process", {"--prof-process", "--"});
|
||||||
AddOption("--run",
|
|
||||||
"Run a script specified in package.json",
|
|
||||||
&EnvironmentOptions::run);
|
|
||||||
#if HAVE_INSPECTOR
|
#if HAVE_INSPECTOR
|
||||||
AddOption("--cpu-prof",
|
AddOption("--cpu-prof",
|
||||||
"Start the V8 CPU profiler on start up, and write the CPU profile "
|
"Start the V8 CPU profiler on start up, and write the CPU profile "
|
||||||
|
@ -1069,6 +1066,10 @@ PerProcessOptionsParser::PerProcessOptionsParser(
|
||||||
"Generate a blob that can be embedded into the single executable "
|
"Generate a blob that can be embedded into the single executable "
|
||||||
"application",
|
"application",
|
||||||
&PerProcessOptions::experimental_sea_config);
|
&PerProcessOptions::experimental_sea_config);
|
||||||
|
|
||||||
|
AddOption("--run",
|
||||||
|
"Run a script specified in package.json",
|
||||||
|
&PerProcessOptions::run);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline std::string RemoveBrackets(const std::string& host) {
|
inline std::string RemoveBrackets(const std::string& host) {
|
||||||
|
|
|
@ -161,7 +161,6 @@ class EnvironmentOptions : public Options {
|
||||||
bool heap_prof = false;
|
bool heap_prof = false;
|
||||||
#endif // HAVE_INSPECTOR
|
#endif // HAVE_INSPECTOR
|
||||||
std::string redirect_warnings;
|
std::string redirect_warnings;
|
||||||
std::string run;
|
|
||||||
std::string diagnostic_dir;
|
std::string diagnostic_dir;
|
||||||
std::string env_file;
|
std::string env_file;
|
||||||
bool has_env_file_string = false;
|
bool has_env_file_string = false;
|
||||||
|
@ -280,6 +279,7 @@ class PerProcessOptions : public Options {
|
||||||
bool print_v8_help = false;
|
bool print_v8_help = false;
|
||||||
bool print_version = false;
|
bool print_version = false;
|
||||||
std::string experimental_sea_config;
|
std::string experimental_sea_config;
|
||||||
|
std::string run;
|
||||||
|
|
||||||
#ifdef NODE_HAVE_I18N_SUPPORT
|
#ifdef NODE_HAVE_I18N_SUPPORT
|
||||||
std::string icu_data_dir;
|
std::string icu_data_dir;
|
||||||
|
|
275
src/node_task_runner.cc
Normal file
275
src/node_task_runner.cc
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
#include "node_task_runner.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
#include <regex> // NOLINT(build/c++11)
|
||||||
|
|
||||||
|
namespace node::task_runner {
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
static constexpr char bin_path[] = "\\node_modules\\.bin";
|
||||||
|
#else
|
||||||
|
static constexpr char bin_path[] = "/node_modules/.bin";
|
||||||
|
#endif // _WIN32
|
||||||
|
|
||||||
|
ProcessRunner::ProcessRunner(
|
||||||
|
std::shared_ptr<InitializationResultImpl> result,
|
||||||
|
std::string_view command,
|
||||||
|
const std::optional<std::string>& positional_args) {
|
||||||
|
memset(&options_, 0, sizeof(uv_process_options_t));
|
||||||
|
|
||||||
|
// Get the current working directory.
|
||||||
|
char cwd[PATH_MAX_BYTES];
|
||||||
|
size_t cwd_size = PATH_MAX_BYTES;
|
||||||
|
CHECK_EQ(uv_cwd(cwd, &cwd_size), 0);
|
||||||
|
CHECK_GT(cwd_size, 0);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::string current_bin_path = cwd + std::string(bin_path) + ";";
|
||||||
|
#else
|
||||||
|
std::string current_bin_path = cwd + std::string(bin_path) + ":";
|
||||||
|
#endif // _WIN32
|
||||||
|
|
||||||
|
// Inherit stdin, stdout, and stderr from the parent process.
|
||||||
|
options_.stdio_count = 3;
|
||||||
|
child_stdio[0].flags = UV_INHERIT_FD;
|
||||||
|
child_stdio[0].data.fd = 0;
|
||||||
|
child_stdio[1].flags = UV_INHERIT_FD;
|
||||||
|
child_stdio[1].data.fd = 1;
|
||||||
|
child_stdio[2].flags = UV_INHERIT_FD;
|
||||||
|
child_stdio[2].data.fd = 2;
|
||||||
|
options_.stdio = child_stdio;
|
||||||
|
options_.exit_cb = ExitCallback;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
options_.flags |= UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
init_result = std::move(result);
|
||||||
|
|
||||||
|
// Set the process handle data to this class instance.
|
||||||
|
// This is used to access the class instance from the OnExit callback.
|
||||||
|
// It is required because libuv doesn't allow passing lambda functions as a
|
||||||
|
// callback.
|
||||||
|
process_.data = this;
|
||||||
|
|
||||||
|
std::string command_str(command);
|
||||||
|
|
||||||
|
if (positional_args.has_value()) {
|
||||||
|
command_str += " " + EscapeShell(positional_args.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set environment variables
|
||||||
|
uv_env_item_t* env_items;
|
||||||
|
int env_count;
|
||||||
|
CHECK_EQ(0, uv_os_environ(&env_items, &env_count));
|
||||||
|
env = std::unique_ptr<char*[]>(new char*[env_count + 1]);
|
||||||
|
options_.env = env.get();
|
||||||
|
|
||||||
|
// Iterate over environment variables once to store them in the current
|
||||||
|
// ProcessRunner instance.
|
||||||
|
for (int i = 0; i < env_count; i++) {
|
||||||
|
std::string name = env_items[i].name;
|
||||||
|
std::string value = env_items[i].value;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// We use comspec environment variable to find cmd.exe path on Windows
|
||||||
|
// Example: 'C:\\Windows\\system32\\cmd.exe'
|
||||||
|
// If we don't find it, we fallback to 'cmd.exe' for Windows
|
||||||
|
if (name.size() == 7 && StringEqualNoCaseN(name.c_str(), "comspec", 7)) {
|
||||||
|
file_ = value;
|
||||||
|
}
|
||||||
|
#endif // _WIN32
|
||||||
|
|
||||||
|
// Check if environment variable key is matching case-insensitive "path"
|
||||||
|
if (name.size() == 4 && StringEqualNoCaseN(name.c_str(), "path", 4)) {
|
||||||
|
value.insert(0, current_bin_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variables should be in "KEY=value" format
|
||||||
|
value.insert(0, name + "=");
|
||||||
|
env_vars_.push_back(value);
|
||||||
|
}
|
||||||
|
uv_os_free_environ(env_items, env_count);
|
||||||
|
|
||||||
|
// Use the stored reference on the instance.
|
||||||
|
options_.file = file_.c_str();
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (file_.find("cmd.exe") != std::string::npos) {
|
||||||
|
// If the file is cmd.exe, use the following command line arguments:
|
||||||
|
// "/c" Carries out the command and exit.
|
||||||
|
// "/d" Disables execution of AutoRun commands.
|
||||||
|
// "/s" Strip the first and last quotes (") around the <string> but leaves
|
||||||
|
// the rest of the command unchanged.
|
||||||
|
command_args_ = {
|
||||||
|
options_.file, "/d", "/s", "/c", "\"" + command_str + "\""};
|
||||||
|
} else {
|
||||||
|
command_args_ = {options_.file, "-c", command_str};
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
command_args_ = {options_.file, "-c", command_str};
|
||||||
|
#endif // _WIN32
|
||||||
|
|
||||||
|
auto argc = command_args_.size();
|
||||||
|
CHECK_GE(argc, 1);
|
||||||
|
arg = std::unique_ptr<char*[]>(new char*[argc + 1]);
|
||||||
|
options_.args = arg.get();
|
||||||
|
for (size_t i = 0; i < argc; ++i) {
|
||||||
|
options_.args[i] = const_cast<char*>(command_args_[i].c_str());
|
||||||
|
}
|
||||||
|
options_.args[argc] = nullptr;
|
||||||
|
|
||||||
|
for (int i = 0; i < env_count; i++) {
|
||||||
|
options_.env[i] = const_cast<char*>(env_vars_[i].c_str());
|
||||||
|
}
|
||||||
|
options_.env[env_count] = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapeShell escapes a string to be used as a command line argument.
|
||||||
|
// It replaces single quotes with "\\'" and double quotes with "\\\"".
|
||||||
|
// It also removes excessive quote pairs and handles edge cases.
|
||||||
|
std::string EscapeShell(const std::string& input) {
|
||||||
|
// If the input is an empty string, return a pair of quotes
|
||||||
|
if (input.empty()) {
|
||||||
|
return "''";
|
||||||
|
}
|
||||||
|
|
||||||
|
static const std::string_view forbidden_characters =
|
||||||
|
"[\t\n\r \"#$&'()*;<>?\\\\`|~]";
|
||||||
|
|
||||||
|
// Check if input contains any forbidden characters
|
||||||
|
// If it doesn't, return the input as is.
|
||||||
|
if (input.find_first_of(forbidden_characters) == std::string::npos) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace single quotes("'") with "\\'"
|
||||||
|
std::string escaped = std::regex_replace(input, std::regex("'"), "\\'");
|
||||||
|
|
||||||
|
// Wrap the result in single quotes
|
||||||
|
escaped = "'" + escaped + "'";
|
||||||
|
|
||||||
|
// Remove excessive quote pairs and handle edge cases
|
||||||
|
static const std::regex leadingQuotePairs("^(?:'')+(?!$)");
|
||||||
|
static const std::regex tripleSingleQuote("\\\\'''");
|
||||||
|
|
||||||
|
escaped = std::regex_replace(escaped, leadingQuotePairs, "");
|
||||||
|
escaped = std::regex_replace(escaped, tripleSingleQuote, "\\'");
|
||||||
|
|
||||||
|
return escaped;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExitCallback is the callback function that is called when the process exits.
|
||||||
|
// It closes the process handle and calls the OnExit function.
|
||||||
|
// It is defined as a static function due to the limitations of libuv.
|
||||||
|
void ProcessRunner::ExitCallback(uv_process_t* handle,
|
||||||
|
int64_t exit_status,
|
||||||
|
int term_signal) {
|
||||||
|
auto self = reinterpret_cast<ProcessRunner*>(handle->data);
|
||||||
|
uv_close(reinterpret_cast<uv_handle_t*>(handle), nullptr);
|
||||||
|
self->OnExit(exit_status, term_signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessRunner::OnExit(int64_t exit_status, int term_signal) {
|
||||||
|
if (exit_status > 0) {
|
||||||
|
init_result->exit_code_ = ExitCode::kGenericUserError;
|
||||||
|
} else {
|
||||||
|
init_result->exit_code_ = ExitCode::kNoFailure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessRunner::Run() {
|
||||||
|
if (int r = uv_spawn(loop_, &process_, &options_)) {
|
||||||
|
fprintf(stderr, "Error: %s\n", uv_strerror(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
uv_run(loop_, UV_RUN_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
||||||
|
std::string_view command_id,
|
||||||
|
const std::optional<std::string>& positional_args) {
|
||||||
|
std::string_view path = "package.json";
|
||||||
|
std::string raw_json;
|
||||||
|
|
||||||
|
// No need to exclude BOM since simdjson will skip it.
|
||||||
|
if (ReadFileSync(&raw_json, path.data()) < 0) {
|
||||||
|
fprintf(stderr, "Can't read package.json\n");
|
||||||
|
result->exit_code_ = ExitCode::kGenericUserError;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
simdjson::ondemand::parser json_parser;
|
||||||
|
simdjson::ondemand::document document;
|
||||||
|
simdjson::ondemand::object main_object;
|
||||||
|
simdjson::error_code error = json_parser.iterate(raw_json).get(document);
|
||||||
|
|
||||||
|
// If document is not an object, throw an error.
|
||||||
|
if (error || document.get_object().get(main_object)) {
|
||||||
|
fprintf(stderr, "Can't parse package.json\n");
|
||||||
|
result->exit_code_ = ExitCode::kGenericUserError;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If package_json object doesn't have "scripts" field, throw an error.
|
||||||
|
simdjson::ondemand::object scripts_object;
|
||||||
|
if (main_object["scripts"].get_object().get(scripts_object)) {
|
||||||
|
fprintf(stderr, "Can't find \"scripts\" field in package.json\n");
|
||||||
|
result->exit_code_ = ExitCode::kGenericUserError;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the command_id is not found in the scripts object, throw an error.
|
||||||
|
std::string_view command;
|
||||||
|
if (scripts_object[command_id].get_string().get(command)) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Missing script: \"%.*s\"\n\n",
|
||||||
|
static_cast<int>(command_id.size()),
|
||||||
|
command_id.data());
|
||||||
|
fprintf(stderr, "Available scripts are:\n");
|
||||||
|
|
||||||
|
// Reset the object to iterate over it again
|
||||||
|
scripts_object.reset();
|
||||||
|
simdjson::ondemand::value value;
|
||||||
|
for (auto field : scripts_object) {
|
||||||
|
std::string_view key_str;
|
||||||
|
std::string_view value_str;
|
||||||
|
if (!field.unescaped_key().get(key_str) && !field.value().get(value) &&
|
||||||
|
!value.get_string().get(value_str)) {
|
||||||
|
fprintf(stderr,
|
||||||
|
" %.*s: %.*s\n",
|
||||||
|
static_cast<int>(key_str.size()),
|
||||||
|
key_str.data(),
|
||||||
|
static_cast<int>(value_str.size()),
|
||||||
|
value_str.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result->exit_code_ = ExitCode::kGenericUserError;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto runner = ProcessRunner(result, command, positional_args);
|
||||||
|
runner.Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPositionalArgs returns the positional arguments from the command line.
|
||||||
|
// If the "--" flag is not found, it returns an empty optional.
|
||||||
|
// Otherwise, it returns the positional arguments as a single string.
|
||||||
|
// Example: "node -- script.js arg1 arg2" returns "arg1 arg2".
|
||||||
|
std::optional<std::string> GetPositionalArgs(
|
||||||
|
const std::vector<std::string>& args) {
|
||||||
|
// If the "--" flag is not found, return an empty optional
|
||||||
|
// Otherwise, return the positional arguments as a single string
|
||||||
|
if (auto dash_dash = std::find(args.begin(), args.end(), "--");
|
||||||
|
dash_dash != args.end()) {
|
||||||
|
std::string positional_args;
|
||||||
|
for (auto it = dash_dash + 1; it != args.end(); ++it) {
|
||||||
|
positional_args += it->c_str();
|
||||||
|
}
|
||||||
|
return positional_args;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace node::task_runner
|
64
src/node_task_runner.h
Normal file
64
src/node_task_runner.h
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
#ifndef SRC_NODE_TASK_RUNNER_H_
|
||||||
|
#define SRC_NODE_TASK_RUNNER_H_
|
||||||
|
|
||||||
|
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||||
|
|
||||||
|
#include "node_internals.h"
|
||||||
|
#include "simdjson.h"
|
||||||
|
#include "spawn_sync.h"
|
||||||
|
#include "uv.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace node {
|
||||||
|
namespace task_runner {
|
||||||
|
|
||||||
|
// ProcessRunner is the class responsible for running a process.
|
||||||
|
// A class instance is created for each process to be run.
|
||||||
|
// The class is responsible for spawning the process and handling its exit.
|
||||||
|
// The class also handles the environment variables and arguments.
|
||||||
|
class ProcessRunner {
|
||||||
|
public:
|
||||||
|
ProcessRunner(std::shared_ptr<InitializationResultImpl> result,
|
||||||
|
std::string_view command_id,
|
||||||
|
const std::optional<std::string>& positional_args);
|
||||||
|
void Run();
|
||||||
|
static void ExitCallback(uv_process_t* req,
|
||||||
|
int64_t exit_status,
|
||||||
|
int term_signal);
|
||||||
|
|
||||||
|
private:
|
||||||
|
uv_loop_t* loop_ = uv_default_loop();
|
||||||
|
uv_process_t process_{};
|
||||||
|
uv_process_options_t options_{};
|
||||||
|
uv_stdio_container_t child_stdio[3]{};
|
||||||
|
std::shared_ptr<InitializationResultImpl> init_result;
|
||||||
|
std::vector<std::string> command_args_{};
|
||||||
|
std::vector<std::string> env_vars_{};
|
||||||
|
std::unique_ptr<char* []> env {}; // memory for options_.env
|
||||||
|
std::unique_ptr<char* []> arg {}; // memory for options_.args
|
||||||
|
|
||||||
|
// OnExit is the callback function that is called when the process exits.
|
||||||
|
void OnExit(int64_t exit_status, int term_signal);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::string file_ = "cmd.exe";
|
||||||
|
#else
|
||||||
|
std::string file_ = "/bin/sh";
|
||||||
|
#endif // _WIN32
|
||||||
|
};
|
||||||
|
|
||||||
|
void RunTask(std::shared_ptr<InitializationResultImpl> result,
|
||||||
|
std::string_view command_id,
|
||||||
|
const std::optional<std::string>& positional_args);
|
||||||
|
std::optional<std::string> GetPositionalArgs(
|
||||||
|
const std::vector<std::string>& args);
|
||||||
|
std::string EscapeShell(const std::string& command);
|
||||||
|
|
||||||
|
} // namespace task_runner
|
||||||
|
} // namespace node
|
||||||
|
|
||||||
|
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||||
|
|
||||||
|
#endif // SRC_NODE_TASK_RUNNER_H_
|
27
test/cctest/test_node_task_runner.cc
Normal file
27
test/cctest/test_node_task_runner.cc
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#include "gtest/gtest.h"
|
||||||
|
#include "node_task_runner.h"
|
||||||
|
#include "node_test_fixture.h"
|
||||||
|
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class TaskRunnerTest : public EnvironmentTestFixture {};
|
||||||
|
|
||||||
|
TEST_F(TaskRunnerTest, EscapeShell) {
|
||||||
|
std::vector<std::pair<std::string, std::string>> expectations = {
|
||||||
|
{"", "''"},
|
||||||
|
{"test", "test"},
|
||||||
|
{"test words", "'test words'"},
|
||||||
|
{"$1", "'$1'"},
|
||||||
|
{"\"$1\"", "'\"$1\"'"},
|
||||||
|
{"'$1'", "'\\'$1\\''"},
|
||||||
|
{"\\$1", "'\\$1'"},
|
||||||
|
{"--arg=\"$1\"", "'--arg=\"$1\"'"},
|
||||||
|
{"--arg=node exec -c \"$1\"", "'--arg=node exec -c \"$1\"'"},
|
||||||
|
{"--arg=node exec -c '$1'", "'--arg=node exec -c \\'$1\\''"},
|
||||||
|
{"'--arg=node exec -c \"$1\"'", "'\\'--arg=node exec -c \"$1\"\\''"}};
|
||||||
|
|
||||||
|
for (const auto& [input, expected] : expectations) {
|
||||||
|
EXPECT_EQ(node::task_runner::EscapeShell(input), expected);
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ static int RunNodeInstance(MultiIsolatePlatform* platform,
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
argv = uv_setup_args(argc, argv);
|
argv = uv_setup_args(argc, argv);
|
||||||
std::vector<std::string> args(argv, argv + argc);
|
std::vector<std::string> args(argv, argv + argc);
|
||||||
std::unique_ptr<node::InitializationResult> result =
|
std::shared_ptr<node::InitializationResult> result =
|
||||||
node::InitializeOncePerProcess(
|
node::InitializeOncePerProcess(
|
||||||
args,
|
args,
|
||||||
{node::ProcessInitializationFlags::kNoInitializeV8,
|
{node::ProcessInitializationFlags::kNoInitializeV8,
|
||||||
|
|
|
@ -7,7 +7,7 @@ const fixtures = require('../common/fixtures');
|
||||||
|
|
||||||
const child = childProcess.spawnSync(
|
const child = childProcess.spawnSync(
|
||||||
process.execPath,
|
process.execPath,
|
||||||
[ '--run', 'non-existent-command'],
|
[ '--no-warnings', '--run', 'non-existent-command'],
|
||||||
{ cwd: fixtures.path('run-script'), encoding: 'utf8' },
|
{ cwd: fixtures.path('run-script'), encoding: 'utf8' },
|
||||||
);
|
);
|
||||||
assert.strictEqual(child.status, 1);
|
assert.strictEqual(child.status, 1);
|
||||||
|
|
|
@ -16,7 +16,8 @@ describe('node run [command]', () => {
|
||||||
{ cwd: __dirname },
|
{ cwd: __dirname },
|
||||||
);
|
);
|
||||||
assert.match(child.stderr, /ExperimentalWarning: Task runner is an experimental feature and might change at any time/);
|
assert.match(child.stderr, /ExperimentalWarning: Task runner is an experimental feature and might change at any time/);
|
||||||
assert.match(child.stdout, /Can't read package\.json/);
|
assert.match(child.stderr, /Can't read package\.json/);
|
||||||
|
assert.strictEqual(child.stdout, '');
|
||||||
assert.strictEqual(child.code, 1);
|
assert.strictEqual(child.code, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,8 +27,8 @@ describe('node run [command]', () => {
|
||||||
[ '--no-warnings', '--run', 'test'],
|
[ '--no-warnings', '--run', 'test'],
|
||||||
{ cwd: __dirname },
|
{ cwd: __dirname },
|
||||||
);
|
);
|
||||||
assert.match(child.stdout, /Can't read package\.json/);
|
assert.match(child.stderr, /Can't read package\.json/);
|
||||||
assert.strictEqual(child.stderr, '');
|
assert.strictEqual(child.stdout, '');
|
||||||
assert.strictEqual(child.code, 1);
|
assert.strictEqual(child.code, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,37 +64,4 @@ describe('node run [command]', () => {
|
||||||
assert.strictEqual(child.stderr, '');
|
assert.strictEqual(child.stderr, '');
|
||||||
assert.strictEqual(child.code, 0);
|
assert.strictEqual(child.code, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support having --env-file cli flag', async () => {
|
|
||||||
const child = await common.spawnPromisified(
|
|
||||||
process.execPath,
|
|
||||||
[ '--no-warnings', `--env-file=${fixtures.path('run-script/.env')}`, '--run', `custom-env${envSuffix}`],
|
|
||||||
{ cwd: fixtures.path('run-script') },
|
|
||||||
);
|
|
||||||
assert.match(child.stdout, /hello world/);
|
|
||||||
assert.strictEqual(child.stderr, '');
|
|
||||||
assert.strictEqual(child.code, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should properly escape shell', async () => {
|
|
||||||
const { escapeShell } = require('internal/shell');
|
|
||||||
|
|
||||||
const expectations = [
|
|
||||||
['', '\'\''],
|
|
||||||
['test', 'test'],
|
|
||||||
['test words', '\'test words\''],
|
|
||||||
['$1', '\'$1\''],
|
|
||||||
['"$1"', '\'"$1"\''],
|
|
||||||
['\'$1\'', '\\\'\'$1\'\\\''],
|
|
||||||
['\\$1', '\'\\$1\''],
|
|
||||||
['--arg="$1"', '\'--arg="$1"\''],
|
|
||||||
['--arg=node exec -c "$1"', '\'--arg=node exec -c "$1"\''],
|
|
||||||
['--arg=node exec -c \'$1\'', '\'--arg=node exec -c \'\\\'\'$1\'\\\''],
|
|
||||||
['\'--arg=node exec -c "$1"\'', '\\\'\'--arg=node exec -c "$1"\'\\\''],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [input, expectation] of expectations) {
|
|
||||||
assert.strictEqual(escapeShell(input), expectation);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -61,7 +61,7 @@ int BuildSnapshot(int argc, char* argv[]) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<node::InitializationResult> result =
|
std::shared_ptr<node::InitializationResult> result =
|
||||||
node::InitializeOncePerProcess(
|
node::InitializeOncePerProcess(
|
||||||
std::vector<std::string>(argv, argv + argc),
|
std::vector<std::string>(argv, argv + argc),
|
||||||
node::ProcessInitializationFlags::kGeneratePredictableSnapshot);
|
node::ProcessInitializationFlags::kGeneratePredictableSnapshot);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue