// 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 "host-process.h" #include #include "../common/utils.h" namespace fs = ghc::filesystem; HostProcess::HostProcess(asio::io_context& io_context, Sockets& sockets) : sockets_(sockets), stdout_pipe_(io_context), stderr_pipe_(io_context) {} Process::Handle HostProcess::launch_host( const ghc::filesystem::path& host_path, std::initializer_list args, Logger& logger, const Configuration& config, const PluginInfo& plugin_info) { #ifdef WITH_WINEDBG // This is set up for KDE Plasma. Other desktop environments and window // managers require some slight modifications to spawn a detached terminal // emulator. Alternatively, you can spawn `winedbg` with the `--no-start` // option to launch a gdb server and then connect to it from another // terminal. Process child("kstart5"); child.arg("konsole").arg("--").arg("-e").arg("winedbg").arg("--gdb"); #ifdef WINEDBG_LEGACY_ARGUMENT_QUOTING // Note the double quoting here. Old versions of winedbg didn't // respect `argv` and instead expected a pre-quoted Win32 command // line as its arguments. child.arg("\"" + host_path.string() + ".so\""), #else child.arg(host_path.string() + ".so"); #endif // WINEDBG_LEGACY_ARGUMENT_QUOTING #else Process child(host_path); #endif // WITH_WINEDBG // What's up with this indentation for (const auto& arg : args) { child.arg(arg); } child.environment(plugin_info.create_host_env()); Process::Handle child_handle = std::visit( overload{ [](Process::Handle handle) -> Process::Handle { return handle; }, [&host_path](const Process::CommandNotFound&) -> Process::Handle { throw std::runtime_error("Could not launch '" + host_path.string() + "', command not found"); }, [](const std::error_code& err) -> Process::Handle { throw std::runtime_error("Error spawning Wine process: " + err.message()); }, }, // HACK: If the `disable_pipes` option is enabled, then we'll redirect // the plugin's output to a file instead of using pipes to blend // it in with the rest of yabridge's output. This is for some // reason necessary for ujam's plugins and all other plugins made // with Gorilla Engine to function. Otherwise they'll print a // nondescriptive `JS_EXEC_FAILED` error message. config.disable_pipes ? child.spawn_child_redirected(*config.disable_pipes) : child.spawn_child_piped(stdout_pipe_, stderr_pipe_)); // See the above comment if (config.disable_pipes) { logger.log(""); logger.log("WARNING: All Wine output will be written to"); logger.log(" '" + config.disable_pipes->string() + "'."); logger.log(""); } else { // Print the Wine host's STDOUT and STDERR streams to the log file. This // should be done before trying to accept the sockets as otherwise we // will miss all output. logger.async_log_pipe_lines(stdout_pipe_, stdout_buffer_, "[Wine STDOUT] "); logger.async_log_pipe_lines(stderr_pipe_, stderr_buffer_, "[Wine STDERR] "); } return child_handle; } IndividualHost::IndividualHost(asio::io_context& io_context, Logger& logger, const Configuration& config, Sockets& sockets, const PluginInfo& plugin_info, const HostRequest& host_request) : HostProcess(io_context, sockets), plugin_info_(plugin_info), host_path_(find_vst_host(plugin_info.native_library_path_, plugin_info.plugin_arch_)), handle_(launch_host( host_path_, { plugin_type_to_string(host_request.plugin_type), #if defined(WITH_WINEDBG) && defined(WINEDBG_LEGACY_ARGUMENT_QUOTING) // Old versions of winedbg flattened all command line // arguments to a single space separated Win32 command line, // so we had to do our own quoting "\"" + plugin_info.windows_plugin_path + "\"", #else host_request.plugin_path, #endif host_request.endpoint_base_dir, // We pass this process' process ID as an argument so we can // run a watchdog on the Wine plugin host process that shuts // down the sockets after this process shuts down std::to_string(getpid()) }, logger, config, plugin_info)) { #ifdef WITH_WINEDBG if (plugin_info.windows_plugin_path_.string().find('"') != std::string::npos) { logger.log( "Warning: plugin paths containing double quotes won't be properly " "escaped"); } #endif } fs::path IndividualHost::path() { return host_path_; } bool IndividualHost::running() { return handle_.running(); } void IndividualHost::terminate() { // NOTE: This technically shouldn't be needed, but in Wine 6.5 sending // SIGKILL to a Wine process no longer terminates the threads spawned // by that process, so if we don't manually close the sockets there // will still be threads listening on those sockets which in turn also // prevents us from joining our `std::jthread`s on the plugin side. sockets_.close(); // This will also reap the terminated process handle_.terminate(); } GroupHost::GroupHost(asio::io_context& io_context, Logger& logger, const Configuration& config, Sockets& sockets, const PluginInfo& plugin_info, const HostRequest& host_request) : HostProcess(io_context, sockets), plugin_info_(plugin_info), host_path_(find_vst_host(plugin_info.native_library_path_, plugin_info.plugin_arch_)) { // When using plugin groups, we'll first try to connect to an existing group // host process and ask it to host our plugin. If no such process exists, // then we'll start a new process. In the event that multiple yabridge // instances simultaneously try to start a new group process for the same // group, then the first process to listen on the socket will win and all // other processes will exit. When a plugin's host process has exited, it // will try to connect to the socket once more in the case that another // process is now listening on it. const fs::path endpoint_base_dir = sockets.base_dir_; const fs::path group_socket_path = generate_group_endpoint( *config.group, plugin_info.normalize_wine_prefix(), plugin_info.plugin_arch_); const auto connect = [&io_context, host_request, endpoint_base_dir, group_socket_path]() { asio::local::stream_protocol::socket group_socket(io_context); group_socket.connect(group_socket_path.string()); write_object(group_socket, host_request); const auto response = read_object(group_socket); assert(response.pid > 0); }; try { // Request an existing group host process to host our plugin connect(); } catch (const std::system_error&) { // In case we could not connect to the socket, then we'll start a // new group host process. This process is detached immediately // because it should run independently of this yabridge instance as // it will likely outlive it. Process::Handle group_host = launch_host(host_path_, {"group", group_socket_path.string()}, logger, config, plugin_info); group_host.detach(); group_host_connect_handler_ = std::jthread([this, connect, group_host = std::move(group_host)]() { set_realtime_priority(true); pthread_setname_np(pthread_self(), "group-connect"); using namespace std::literals::chrono_literals; // We'll first try to connect to the group host we just spawned // TODO: Replace this polling with inotify while (group_host.running()) { std::this_thread::sleep_for(20ms); try { connect(); return; } catch (const std::system_error&) { // Keep trying to connect until either connection gets // accepted or the group host crashes } } // When the group host exits before we can connect to it this // either means that there has been some kind of error (for // instance related to Wine), or that another process was able // to listen on the socket first. For the last case we'll try to // connect once more, before concluding that we failed. try { connect(); } catch (const std::system_error&) { startup_failed_ = true; } }); } } fs::path GroupHost::path() { return host_path_; } bool GroupHost::running() noexcept { // When we are unable to connect to a new or existing group host process, // then we'll consider the startup failed and we'll allow the initialization // process to terminate. return !startup_failed_; } void GroupHost::terminate() { // There's no need to manually terminate group host processes as they will // shut down automatically after all plugins have exited. Manually closing // the sockets will cause the associated plugin to exit. sockets_.close(); }