Files
node/src/node_task_runner.cc
Nathan Baulch b5fb2ff81e src: fix typos
PR-URL: https://github.com/nodejs/node/pull/55064
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
2024-09-27 15:34:48 +00:00

370 lines
13 KiB
C++

#include "node_task_runner.h"
#include "util-inl.h"
#include <regex> // NOLINT(build/c++11)
namespace node::task_runner {
#ifdef _WIN32
static constexpr const char* env_var_separator = ";";
#else
static constexpr const char* env_var_separator = ":";
#endif // _WIN32
ProcessRunner::ProcessRunner(std::shared_ptr<InitializationResultImpl> result,
const std::filesystem::path& package_json_path,
std::string_view script_name,
std::string_view command,
std::string_view path_env_var,
const PositionalArgs& positional_args)
: init_result(std::move(result)),
package_json_path_(package_json_path),
script_name_(script_name),
path_env_var_(path_env_var) {
memset(&options_, 0, sizeof(uv_process_options_t));
// 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
// 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;
SetEnvironmentVariables();
std::string command_str(command);
// Use the stored reference on the instance.
options_.file = file_.c_str();
// Add positional arguments to the command string.
// Note that each argument needs to be escaped.
if (!positional_args.empty()) {
for (const auto& arg : positional_args) {
command_str += " " + EscapeShell(arg);
}
}
#ifdef _WIN32
if (file_.ends_with("cmd.exe")) {
// 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 {
// If the file is not cmd.exe, and it is unclear which shell is being used,
// so assume -c is the correct syntax (Unix-like shells use -c for this
// purpose).
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;
}
void ProcessRunner::SetEnvironmentVariables() {
uv_env_item_t* env_items;
int env_count;
CHECK_EQ(0, uv_os_environ(&env_items, &env_count));
// 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 (StringEqualNoCase(name.c_str(), "comspec")) {
file_ = value;
}
#endif // _WIN32
if (StringEqualNoCase(name.c_str(), "path")) {
// Add path env variable to the beginning of the PATH
value = path_env_var_ + value;
}
env_vars_.push_back(name + "=" + value);
}
uv_os_free_environ(env_items, env_count);
// Add NODE_RUN_SCRIPT_NAME environment variable to the environment
// to indicate which script is being run.
env_vars_.push_back("NODE_RUN_SCRIPT_NAME=" + script_name_);
// Add NODE_RUN_PACKAGE_JSON_PATH environment variable to the environment to
// indicate which package.json is being processed.
env_vars_.push_back("NODE_RUN_PACKAGE_JSON_PATH=" +
package_json_path_.string());
env = std::unique_ptr<char*[]>(new char*[env_vars_.size() + 1]);
options_.env = env.get();
for (size_t i = 0; i < env_vars_.size(); i++) {
options_.env[i] = const_cast<char*>(env_vars_[i].c_str());
}
options_.env[env_vars_.size()] = nullptr;
}
// EscapeShell escapes a string to be used as a command line argument.
// Under Windows, we follow:
// https://daviddeley.com/autohotkey/parameters/parameters.htm
// Elsewhere:
// 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_view input) {
// If the input is an empty string, return a pair of quotes
if (input.empty()) {
#ifdef _WIN32
return "\"\"";
#else
return "''";
#endif
}
static constexpr 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 std::string(input);
}
static const std::regex leadingQuotePairs("^(?:'')+(?!$)");
#ifdef _WIN32
// Replace double quotes with single quotes and surround the string
// with double quotes for Windows.
std::string escaped =
std::regex_replace(std::string(input), std::regex("\""), "\"\"");
escaped = "\"" + escaped + "\"";
// Remove excessive quote pairs and handle edge cases
static const std::regex tripleSingleQuote("\\\\\"\"\"");
escaped = std::regex_replace(escaped, leadingQuotePairs, "");
escaped = std::regex_replace(escaped, tripleSingleQuote, "\\\"");
#else
// Replace single quotes("'") with "\\'" and wrap the result
// in single quotes.
std::string escaped =
std::regex_replace(std::string(input), std::regex("'"), "\\'");
escaped = "'" + escaped + "'";
// Remove excessive quote pairs and handle edge cases
static const std::regex tripleSingleQuote("\\\\'''");
escaped = std::regex_replace(escaped, leadingQuotePairs, "");
escaped = std::regex_replace(escaped, tripleSingleQuote, "\\'");
#endif // _WIN32
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) {
const auto self = static_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() {
// keeps the string alive until destructor
cwd = package_json_path_.parent_path().string();
options_.cwd = cwd.c_str();
if (int r = uv_spawn(loop_, &process_, &options_)) {
fprintf(stderr, "Error: %s\n", uv_strerror(r));
}
uv_run(loop_, UV_RUN_DEFAULT);
}
std::optional<std::tuple<std::filesystem::path, std::string, std::string>>
FindPackageJson(const std::filesystem::path& cwd) {
auto package_json_path = cwd / "package.json";
std::string raw_content;
std::string path_env_var;
auto root_path = cwd.root_path();
for (auto directory_path = cwd;
!std::filesystem::equivalent(root_path, directory_path);
directory_path = directory_path.parent_path()) {
// Append "path/node_modules/.bin" to the env var, if it is a directory.
auto node_modules_bin = directory_path / "node_modules" / ".bin";
if (std::filesystem::is_directory(node_modules_bin)) {
path_env_var += node_modules_bin.string() + env_var_separator;
}
if (raw_content.empty()) {
package_json_path = directory_path / "package.json";
// This is required for Windows because std::filesystem::path::c_str()
// returns wchar_t* on Windows, and char* on other platforms.
std::string contents = package_json_path.string();
USE(ReadFileSync(&raw_content, contents.c_str()) > 0);
}
}
// This means that there is no package.json until the root directory.
// In this case, we just return nullopt, which will terminate the process..
if (raw_content.empty()) {
return std::nullopt;
}
return {{package_json_path, raw_content, path_env_var}};
}
void RunTask(const std::shared_ptr<InitializationResultImpl>& result,
std::string_view command_id,
const std::vector<std::string_view>& positional_args) {
auto cwd = std::filesystem::current_path();
auto package_json = FindPackageJson(cwd);
if (!package_json.has_value()) {
fprintf(stderr,
"Can't find package.json for directory %s\n",
cwd.string().c_str());
result->exit_code_ = ExitCode::kGenericUserError;
return;
}
// - path: Path to the package.json file.
// - raw_json: Raw content of the package.json file.
// - path_env_var: This represents the `PATH` environment variable.
// It always ends with ";" or ":" depending on the platform.
auto [path, raw_json, path_env_var] = *package_json;
simdjson::ondemand::parser json_parser;
simdjson::ondemand::document document;
simdjson::ondemand::object main_object;
if (json_parser.iterate(raw_json).get(document)) {
fprintf(stderr, "Can't parse %s\n", path.string().c_str());
result->exit_code_ = ExitCode::kGenericUserError;
return;
}
// If document is not an object, throw an error.
if (auto root_error = document.get_object().get(main_object)) {
if (root_error == simdjson::error_code::INCORRECT_TYPE) {
fprintf(stderr,
"Root value unexpected not an object for %s\n\n",
path.string().c_str());
} else {
fprintf(stderr, "Can't parse %s\n", path.string().c_str());
}
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 %s\n", path.string().c_str());
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 (auto command_error =
scripts_object[command_id].get_string().get(command)) {
if (command_error == simdjson::error_code::INCORRECT_TYPE) {
fprintf(stderr,
"Script \"%.*s\" is unexpectedly not a string for %s\n\n",
static_cast<int>(command_id.size()),
command_id.data(),
path.string().c_str());
} else {
fprintf(stderr,
"Missing script: \"%.*s\" for %s\n\n",
static_cast<int>(command_id.size()),
command_id.data(),
path.string().c_str());
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, path, command_id, command, path_env_var, 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".
PositionalArgs 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()) {
PositionalArgs positional_args{};
positional_args.reserve(args.size() - (dash_dash - args.begin()));
for (auto it = dash_dash + 1; it != args.end(); ++it) {
// SAFETY: The following code is safe because the lifetime of the
// arguments is guaranteed to be valid until the end of the task runner.
positional_args.emplace_back(it->c_str(), it->size());
}
return positional_args;
}
return {};
}
} // namespace node::task_runner