// yabridge: a Wine VST bridge
// Copyright (C) 2020 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
#include
#include
namespace bp = boost::process;
namespace fs = boost::filesystem;
/**
* Check whether a process with the given PID is still active (and not a
* zombie).
*/
bool pid_running(pid_t pid);
/**
* Simple helper function around `boost::process::child` that launches the host
* application (`*.exe`) wrapped in winedbg if compiling with
* `-Dwith-winedbg=true`. Keep in mind that winedbg does not handle arguments
* containing spaces, so most Windows paths will be split up into multiple
* arugments.
*/
template
bp::child launch_host(fs::path host_path, Args&&... args) {
return bp::child(
#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.
"/usr/bin/kstart5", "konsole", "--", "-e", "winedbg", "--gdb",
host_path.string() + ".so",
#else
host_path,
#endif
// We'll use vfork() instead of fork to avoid potential issues with
// inheriting file descriptors
// https://github.com/robbert-vdh/yabridge/issues/45
bp::posix::use_vfork, std::forward(args)...);
}
HostProcess::HostProcess(boost::asio::io_context& io_context, Logger& logger)
: stdout_pipe(io_context), stderr_pipe(io_context), logger(logger) {
// 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.
async_log_pipe_lines(stdout_pipe, stdout_buffer, "[Wine STDOUT] ");
async_log_pipe_lines(stderr_pipe, stderr_buffer, "[Wine STDERR] ");
}
void HostProcess::async_log_pipe_lines(patched_async_pipe& pipe,
boost::asio::streambuf& buffer,
std::string prefix) {
boost::asio::async_read_until(
pipe, buffer, '\n',
[&, prefix](const boost::system::error_code& error, size_t) {
// When we get an error code then that likely means that the pipe
// has been clsoed and we have reached the end of the file
if (error.failed()) {
return;
}
std::string line;
std::getline(std::istream(&buffer), line);
logger.log(prefix + line);
async_log_pipe_lines(pipe, buffer, prefix);
});
}
IndividualHost::IndividualHost(boost::asio::io_context& io_context,
Logger& logger,
fs::path plugin_path,
const Sockets& sockets)
: HostProcess(io_context, logger),
plugin_arch(find_vst_architecture(plugin_path)),
host_path(find_vst_host(plugin_arch, false)),
host(launch_host(host_path,
#ifdef WITH_WINEDBG
plugin_path.filename(),
#else
plugin_path,
#endif
sockets.base_dir,
bp::env = set_wineprefix(),
bp::std_out = stdout_pipe,
bp::std_err = stderr_pipe
#ifdef WITH_WINEDBG
, // winedbg has no reliable way to escape spaces, so
// we'll start the process in the plugin's directory
bp::start_dir = plugin_path.parent_path()
#endif
)) {
#ifdef WITH_WINEDBG
if (plugin_path.filename().string().find(' ') != std::string::npos) {
logger.log("Warning: winedbg does not support paths containing spaces");
}
#endif
}
PluginArchitecture IndividualHost::architecture() {
return plugin_arch;
}
fs::path IndividualHost::path() {
return host_path;
}
bool IndividualHost::running() {
return host.running();
}
void IndividualHost::terminate() {
host.terminate();
host.wait();
}
GroupHost::GroupHost(boost::asio::io_context& io_context,
Logger& logger,
fs::path plugin_path,
Sockets& sockets,
std::string group_name)
: HostProcess(io_context, logger),
plugin_arch(find_vst_architecture(plugin_path)),
host_path(find_vst_host(plugin_arch, true)),
sockets(sockets) {
#ifdef WITH_WINEDBG
if (plugin_path.string().find(' ') != std::string::npos) {
logger.log("Warning: winedbg does not support paths containing spaces");
}
#endif
// 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 bp::environment host_env = set_wineprefix();
fs::path wine_prefix;
if (auto wine_prefix_envvar = host_env.find("WINEPREFIX");
wine_prefix_envvar != host_env.end()) {
// This is a bit ugly, but Boost.Process's environment does not have a
// graceful way to check for empty environment variables in const
// qualified environments
wine_prefix = wine_prefix_envvar->to_string();
} else {
// Fall back to `~/.wine` if this has not been set or detected. This
// would happen if the plugin's .dll file is not inside of a Wine
// prefix. If this happens, then the Wine instance will be launched in
// the default Wine prefix, so we should reflect that here.
wine_prefix = fs::path(host_env.at("HOME").to_string()) / ".wine";
}
const fs::path endpoint_base_dir = sockets.base_dir;
const fs::path group_socket_path =
generate_group_endpoint(group_name, wine_prefix, plugin_arch);
const auto connect = [&io_context, plugin_path, endpoint_base_dir,
group_socket_path]() {
boost::asio::local::stream_protocol::socket group_socket(io_context);
group_socket.connect(group_socket_path.string());
write_object(
group_socket,
GroupRequest{.plugin_path = plugin_path.string(),
.endpoint_base_dir = endpoint_base_dir.string()});
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 boost::system::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.
bp::child group_host =
launch_host(host_path, group_socket_path, bp::env = host_env,
bp::std_out = stdout_pipe, bp::std_err = stderr_pipe);
group_host.detach();
const pid_t group_host_pid = group_host.id();
group_host_connect_handler =
std::jthread([this, connect, group_socket_path, plugin_path,
endpoint_base_dir, group_host_pid]() {
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 (pid_running(group_host_pid)) {
std::this_thread::sleep_for(20ms);
try {
connect();
return;
} catch (const boost::system::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 boost::system::system_error&) {
startup_failed = true;
}
});
}
}
PluginArchitecture GroupHost::architecture() {
return plugin_arch;
}
fs::path GroupHost::path() {
return host_path;
}
bool GroupHost::running() {
// 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();
}
bool pid_running(pid_t pid) {
// With regular individually hosted plugins we can simply check whether the
// process is still running, however Boost.Process does not allow you to do
// the same thing for a process that's not a direct child if this process.
// When using plugin groups we'll have to manually check whether the PID
// returned by the group host process is still active. We sadly can't use
// `kill()` for this as that provides no way to distinguish between active
// processes and zombies, and a terminated group host process will always be
// left as a zombie process. If the process is active, then
// `/proc//{cwd,exe,root}` will be valid symlinks.
try {
fs::canonical("/proc/" + std::to_string(pid) + "/exe");
return true;
} catch (const fs::filesystem_error&) {
return false;
}
}