// yabridge: a Wine plugin bridge // Copyright (C) 2020-2022 Robbert van der Helm // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . #include "process.h" #include #include #include #include namespace fs = ghc::filesystem; bool pid_running(pid_t pid) { // In theory you could `kill(0)` a process to check if it's still active, // but that doesn't distinguish between actually running processes and // unreaped zombies, and terminated group host processes will always be left // as zombies since there may not be anything left to reap them. Instead // we'll check whether one of `/proc//{cwd,exe,root}` are valid // symlinks. std::error_code err; fs::canonical("/proc/" + std::to_string(pid) + "/exe", err); // NOTE: We can get a `EACCES` here if we don't have permissions to read // this process's memory. This does mean that the process is still // running. return !err || err.value() == EACCES; } std::vector get_augmented_search_path() { // HACK: `std::locale("")` would return the current locale, but this // overload is implementation specific, and libstdc++ returns an error // when this happens and one of the locale variables (or `LANG`) is // set to a locale that doesn't exist. Because of that, you should use // the default constructor instead which does fall back gracefully // when using an invalid locale. Boost.Process sadly doesn't seem to // do this, so some intervention is required. We can remove this once // the PR linked below is merged into Boost proper and included in // most distro's copy of Boost (which will probably take a while): // // https://svn.boost.org/trac10/changeset/72855 // // https://github.com/boostorg/process/pull/179 // FIXME: As mentioned above, we did this in the past to work around a // Boost.Process bug. Since we no longer use Boost.Process, we can // technically get rid of this, but we could also leave it in place // since this may still cause other crashes for the user if we don't // do it. try { std::locale(""); } catch (const std::runtime_error&) { // We normally avoid modifying the current process' environment and // instead use `boost::process::environment` to only modify the // environment of launched child processes, but in this case we do need // to fix this // TODO: We don't have access to the logger here, so we cannot yet // properly print the message inform the user that their locale is // broken when this happens std::cerr << std::endl; std::cerr << "WARNING: Your locale is broken. Yabridge was kind enough " "to monkey patch it for you in this DAW session, but you " "should probably take a look at it ;)" << std::endl; std::cerr << std::endl; setenv("LC_ALL", "C", true); // NOLINT(concurrency-mt-unsafe) } // NOLINTNEXTLINE(concurrency-mt-unsafe) const char* path_env = getenv("PATH"); assert(path_env); std::vector search_path = split_path(path_env); // NOLINTNEXTLINE(concurrency-mt-unsafe) if (const char* xdg_data_home = getenv("XDG_DATA_HOME")) { search_path.push_back(fs::path(xdg_data_home) / "yabridge"); // NOLINTNEXTLINE(concurrency-mt-unsafe) } else if (const char* home_directory = getenv("HOME")) { search_path.push_back(fs::path(home_directory) / ".local" / "share" / "yabridge"); } return search_path; } std::vector split_path( const std::string_view& path_env) { // C++ has these great split range adapters. That are completely usless. std::vector search_path; size_t segment_begin = 0; while (segment_begin != path_env.size()) { const size_t segment_end = path_env.find(':', segment_begin); if (segment_end == std::string_view::npos) { search_path.push_back(path_env.substr( segment_begin, path_env.size() - segment_begin)); break; } else { search_path.push_back( path_env.substr(segment_begin, segment_end - segment_begin)); // Restart after the colon segment_begin = segment_end + 1; } } return search_path; } std::optional search_in_path( const std::vector& path, const std::string_view& target) { for (const auto& dir : path) { ghc::filesystem::path candidate = dir / target; if (access(candidate.c_str(), X_OK) == 0) { return candidate; } } return std::nullopt; } ProcessEnvironment::ProcessEnvironment(char** initial_env) { // We'll need to read all strings from `initial_env`. They _should_ all be // zero-terminated strings, with a null pointer to indicate the end of the // array. assert(initial_env); while (*initial_env) { variables_.push_back(*initial_env); initial_env++; } } bool ProcessEnvironment::contains(const std::string_view& key) const { for (const auto& variable : variables_) { if (variable.starts_with(key) && variable.size() > key.size() && variable[key.size()] == '=') { return true; } } return false; } std::optional ProcessEnvironment::get( const std::string_view& key) const { for (const auto& variable : variables_) { if (variable.starts_with(key) && variable.size() > key.size() && variable[key.size()] == '=') { return std::string_view(variable).substr(key.size() + 1); } } return std::nullopt; } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) void ProcessEnvironment::insert(const std::string& key, const std::string& value) { variables_.push_back(key + "=" + value); } char* const* ProcessEnvironment::make_environ() const { recreated_environ_.clear(); for (const auto& variable : variables_) { recreated_environ_.push_back(variable.c_str()); } recreated_environ_.push_back(nullptr); return const_cast(recreated_environ_.data()); } Process::Handle::Handle(pid_t pid) : pid_(pid) {} Process::Handle::~Handle() { if (!detached_) { // If this function has already been called then that's okay terminate(); } } Process::Handle::Handle(Handle&& o) noexcept : pid_(o.pid_) { o.detached_ = true; } Process::Handle& Process::Handle::operator=(Handle&& o) noexcept { o.detached_ = true; pid_ = o.pid_; return *this; } pid_t Process::Handle::pid() const noexcept { return pid_; } bool Process::Handle::running() const noexcept { return pid_running(pid_); } void Process::Handle::detach() noexcept { detached_ = true; } void Process::Handle::terminate() const noexcept { kill(pid_, SIGINT); wait(); } std::optional Process::Handle::wait() const noexcept { // This may fail if we've already reaped the process and terminate gets // called another time, so we won't check the result here int status = 0; waitpid(pid_, &status, 0); if (WIFEXITED(status)) { return WEXITSTATUS(status); } else { return std::nullopt; } } Process::Process(std::string command) : command_(command) {} Process::StringResult Process::spawn_get_stdout_line() const { // We'll read the results from a pipe. The child writes to the second pipe, // we'll read from the first one. int stdout_pipe_fds[2]; assert(pipe(stdout_pipe_fds) == 0); const auto argv = build_argv(); const auto envp = env_ ? env_->make_environ() : environ; posix_spawn_file_actions_t actions; posix_spawn_file_actions_init(&actions); posix_spawn_file_actions_adddup2(&actions, stdout_pipe_fds[1], STDOUT_FILENO); posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null", O_WRONLY | O_APPEND, 0); posix_spawn_file_actions_addclose(&actions, stdout_pipe_fds[0]); posix_spawn_file_actions_addclose(&actions, stdout_pipe_fds[1]); pid_t child_pid = 0; const auto result = posix_spawnp(&child_pid, command_.c_str(), &actions, nullptr, argv, envp); close(stdout_pipe_fds[1]); if (result == 2) { close(stdout_pipe_fds[0]); return Process::CommandNotFound{}; } else if (result != 0) { close(stdout_pipe_fds[0]); return std::error_code(result, std::system_category()); } // Try to read the first line out the output until the line feed std::array output{0}; FILE* output_pipe_stream = fdopen(stdout_pipe_fds[0], "r"); assert(output_pipe_stream); [[maybe_unused]] char* not_relevant = fgets(output.data(), output.size(), output_pipe_stream); fclose(output_pipe_stream); int status = 0; assert(waitpid(child_pid, &status, 0) > 0); if (!WIFEXITED(status) || WEXITSTATUS(status) == 127) { return Process::CommandNotFound{}; } else { // `fgets()` returns the line feed, so we'll get rid of that std::string output_str(output.data()); if (output_str.back() == '\n') { output_str.pop_back(); } return output_str; } } Process::StatusResult Process::spawn_get_status() const { const auto argv = build_argv(); const auto envp = env_ ? env_->make_environ() : environ; pid_t child_pid = 0; const auto result = posix_spawnp(&child_pid, command_.c_str(), nullptr, nullptr, argv, envp); if (result == 2) { return Process::CommandNotFound{}; } else if (result != 0) { return std::error_code(result, std::system_category()); } int status = 0; assert(waitpid(child_pid, &status, 0) > 0); if (!WIFEXITED(status) || WEXITSTATUS(status) == 127) { return Process::CommandNotFound{}; } else { return WEXITSTATUS(status); } } #ifndef WITHOUT_ASIO Process::HandleResult Process::spawn_child_piped( // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) asio::posix::stream_descriptor& stdout_pipe, asio::posix::stream_descriptor& stderr_pipe) const { // We'll reopen the child process' STDOUT and STDERR stream from a pipe, and // we'll assign the other ends of those pipes to the stream descriptors // passed to this function so they can be read from asynchronously in an // Asio IO context loop. We'll read from the first elements of these pipes, // and the child process will write to the second elements. int stdout_pipe_fds[2]; int stderr_pipe_fds[2]; assert(pipe(stdout_pipe_fds) == 0); assert(pipe(stderr_pipe_fds) == 0); const auto argv = build_argv(); const auto envp = env_ ? env_->make_environ() : environ; posix_spawn_file_actions_t actions; posix_spawn_file_actions_init(&actions); posix_spawn_file_actions_adddup2(&actions, stdout_pipe_fds[1], STDOUT_FILENO); posix_spawn_file_actions_adddup2(&actions, stderr_pipe_fds[1], STDERR_FILENO); // We'll close the four pipe fds along with the rest of the file descriptors // NOTE: If the Wine process outlives the host, then it may cause issues if // our process is still keeping the host's file descriptors alive // that. This can prevent Ardour from restarting after an unexpected // shutdown. Because of this we won't use `vfork()`, but instead we'll // just manually close all non-STDIO file descriptors. #if (__GLIBC__ > 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 34) posix_spawn_file_actions_addclosefrom_np(&actions, STDERR_FILENO + 1); #else const int max_fds = static_cast(sysconf(_SC_OPEN_MAX)); for (int fd = STDERR_FILENO + 1; fd < max_fds; fd++) { posix_spawn_file_actions_addclose(&actions, fd); } #endif pid_t child_pid = 0; const auto result = posix_spawnp(&child_pid, command_.c_str(), &actions, nullptr, argv, envp); // We'll assign the read ends of the pipes to the Asio stream descriptors // passed to this function, even if launching the process failed. // `asio::posix::stream_descriptor::assign()` will take ownership of the FD // and close it when the object gets dropped. stdout_pipe.assign(stdout_pipe_fds[0]); stderr_pipe.assign(stderr_pipe_fds[0]); close(stdout_pipe_fds[1]); close(stderr_pipe_fds[1]); if (result == 2) { return Process::CommandNotFound{}; } else if (result != 0) { return std::error_code(result, std::system_category()); } // With glibc `posix_spawn*()` will return 2/`ENOENT` when the file does not // exist, but the specification says that it should return a PID that exits // with status 127 instead. I have no idea how we'd check for that without // waiting here though, so this check may not work int status = 0; assert(waitpid(child_pid, &status, WNOHANG) >= 0); if (WIFEXITED(status) && WEXITSTATUS(status) == 127) { return Process::CommandNotFound{}; } else { return Handle(child_pid); } } #endif // WITHOUT_ASIO Process::HandleResult Process::spawn_child_redirected( const ghc::filesystem::path& filename) const { const auto argv = build_argv(); const auto envp = env_ ? env_->make_environ() : environ; posix_spawn_file_actions_t actions; posix_spawn_file_actions_init(&actions); posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0640); posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0640); // See the note in the other function #if (__GLIBC__ > 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 34) posix_spawn_file_actions_addclosefrom_np(&actions, STDERR_FILENO + 1); #else const int max_fds = static_cast(sysconf(_SC_OPEN_MAX)); for (int fd = STDERR_FILENO + 1; fd < max_fds; fd++) { posix_spawn_file_actions_addclose(&actions, fd); } #endif pid_t child_pid = 0; const auto result = posix_spawnp(&child_pid, command_.c_str(), &actions, nullptr, argv, envp); if (result == 2) { return Process::CommandNotFound{}; } else if (result != 0) { return std::error_code(result, std::system_category()); } int status = 0; assert(waitpid(child_pid, &status, WNOHANG) >= 0); if (WIFEXITED(status) && WEXITSTATUS(status) == 127) { return Process::CommandNotFound{}; } else { return Handle(child_pid); } } char* const* Process::build_argv() const { argv_.clear(); argv_.push_back(command_.c_str()); for (const auto& arg : args_) { argv_.push_back(arg.c_str()); } argv_.push_back(nullptr); return const_cast(argv_.data()); }