Encapsulate individual/group handling differences

This cleans up the PluginBridge significantly by getting rid of all
fields and handling that was only needed for connecting to plugin
groups. This was also the last thing I wanted to refactor before
releasing the plugin groups feature with yabridge 1.2.
This commit is contained in:
Robbert van der Helm
2020-05-29 18:08:44 +02:00
parent d462421490
commit b379708b21
6 changed files with 493 additions and 274 deletions
+24 -199
View File
@@ -16,12 +16,6 @@
#include "plugin-bridge.h"
#include <boost/asio/read_until.hpp>
#include <boost/process/env.hpp>
#include <boost/process/io.hpp>
#include <boost/process/start_dir.hpp>
#include <iostream>
// Generated inside of build directory
#include <src/common/config/config.h>
#include <src/common/config/version.h>
@@ -29,7 +23,6 @@
#include "../common/communication.h"
#include "../common/events.h"
namespace bp = boost::process;
// I'd rather use std::filesystem instead, but Boost.Process depends on
// boost::filesystem
namespace fs = boost::filesystem;
@@ -49,15 +42,9 @@ PluginBridge& get_bridge_instance(const AEffect& plugin) {
return *static_cast<PluginBridge*>(plugin.ptr3);
}
// TODO: It would be nice to have a better way to encapsulate the small
// differences in behavior when using plugin groups, i.e. everywhere where
// we check for `config.group.has_value()`
PluginBridge::PluginBridge(audioMasterCallback host_callback)
: config(Configuration::load_for(get_this_file_location())),
vst_plugin_path(find_vst_plugin()),
vst_plugin_arch(find_vst_architecture(vst_plugin_path)),
vst_host_path(find_vst_host(vst_plugin_arch, config.group.has_value())),
// All the fields should be zero initialized because
// `Vst2PluginInstance::vstAudioMasterCallback` from Bitwig's plugin
// bridge will crash otherwise
@@ -74,17 +61,22 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
logger(Logger::create_from_environment(
create_logger_prefix(socket_endpoint.path()))),
wine_version(get_wine_version()),
wine_stdout(io_context),
wine_stderr(io_context) {
vst_host(
config.group.has_value()
? std::unique_ptr<HostProcess>(
std::make_unique<GroupHost>(io_context,
logger,
vst_plugin_path,
socket_endpoint.path(),
config.group.value(),
host_vst_dispatch))
: std::unique_ptr<HostProcess>(
std::make_unique<IndividualHost>(io_context,
logger,
vst_plugin_path,
socket_endpoint.path()))),
wine_io_handler([&]() { io_context.run(); }) {
log_init_message();
launch_vst_host();
// 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(wine_stdout, wine_stdout_buffer, "[Wine STDOUT] ");
async_log_pipe_lines(wine_stderr, wine_stderr_buffer, "[Wine STDERR] ");
wine_io_handler = std::thread([&]() { io_context.run(); });
#ifndef USE_WINEDBG
// If the Wine process fails to start, then nothing will connect to the
@@ -100,27 +92,11 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
if (finished_accepting_sockets) {
return;
}
// When using regular individually hosted plugins we can simply
// check whether the process is still running, but Boost.Process
// does not allow you to do the same thing for a process that's not
// a 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.
if (config.group.has_value()) {
if (kill(vst_host_pid, 0) != 0) {
logger.log(
"The group host process has exited unexpectedly. Check "
"the output above for more information.");
std::terminate();
}
} else {
if (!vst_host.running()) {
logger.log(
"The Wine process failed to start. Check the output "
"above for more information.");
std::terminate();
}
if (!vst_host->running()) {
logger.log(
"The Wine host process has exited unexpectedly. Check the "
"output above for more information.");
std::terminate();
}
std::this_thread::sleep_for(1s);
@@ -467,15 +443,7 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
logger.log("The plugin crashed during shutdown, ignoring");
}
// Don't terminate group host processes. They will shut down
// automatically after all plugins have exited.
if (!config.group.has_value()) {
vst_host.terminate();
} else {
// Manually the dispatch socket will cause the host process to
// terminate
host_vst_dispatch.close();
}
vst_host->terminate();
// The `stop()` method will cause the IO context to just drop all of
// its work immediately and not throw any exceptions that would have
@@ -484,10 +452,6 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
// These threads should now be finished because we've forcefully
// terminated the Wine process, interupting their socket operations
if (group_host_connect_handler.joinable()) {
// This thread is only used when using plugin groups
group_host_connect_handler.join();
}
host_callback_handler.join();
wine_io_handler.join();
@@ -614,152 +578,13 @@ void PluginBridge::set_parameter(AEffect* /*plugin*/, int index, float value) {
assert(!response.value.has_value());
}
void PluginBridge::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);
});
}
void PluginBridge::launch_vst_host() {
const bp::environment host_env = set_wineprefix();
#ifndef USE_WINEDBG
const std::vector<std::string> host_command{vst_host_path.string()};
#else
// This is set up for KDE Plasma. Other desktop environments and window
// managers require some slight modifications to spawn a detached terminal
// emulator.
const std::vector<std::string> host_command{"/usr/bin/kstart5",
"konsole",
"--",
"-e",
"winedbg",
"--gdb",
vst_host_path.string() + ".so"};
#endif
#ifndef USE_WINEDBG
const fs::path plugin_path = vst_plugin_path;
const fs::path starting_dir = fs::current_path();
#else
// winedbg has no reliable way to escape spaces, so we'll start the process
// in the plugin's directory
const fs::path plugin_path = vst_plugin_path.filename();
const fs::path starting_dir = vst_plugin_path.parent_path();
if (plugin_path.string().find(' ') != std::string::npos) {
logger.log("Warning: winedbg does not support paths containing spaces");
}
#endif
const fs::path socket_path = socket_endpoint.path();
if (!config.group.has_value()) {
vst_host =
bp::child(host_command, plugin_path, socket_path,
bp::env = host_env, bp::std_out = wine_stdout,
bp::std_err = wine_stderr, bp::start_dir = starting_dir);
return;
}
// 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 two yabridge instances
// simultaneously try to start a new group process for the same group, then
// the last process to connect to the socket will terminate gracefully and
// the first process will handle the connections for both yabridge
// instances.
fs::path wine_prefix = host_env.at("WINEPREFIX").to_string();
if (host_env.at("WINEPREFIX").empty()) {
// 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 group_socket_path = generate_group_endpoint(
config.group.value(), wine_prefix, vst_plugin_arch);
try {
// Request the existing group host process to host our plugin, and store
// the PID of that process so we'll know if it has crashed
boost::asio::local::stream_protocol::socket group_socket(io_context);
group_socket.connect(group_socket_path.string());
write_object(group_socket,
GroupRequest{plugin_path.string(), socket_path.string()});
const auto response = read_object<GroupResponse>(group_socket);
vst_host_pid = response.pid;
} 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.
vst_host =
bp::child(host_command, group_socket_path, bp::env = host_env,
bp::std_out = wine_stdout, bp::std_err = wine_stderr,
bp::start_dir = starting_dir);
vst_host_pid = vst_host.id();
vst_host.detach();
// We now want to connect to the socket the in the exact same way as
// above. The only problem is that it may take some time for the
// process to start depending on Wine's current state. We'll defer
// this to a thread so we can finish the rest of the startup in the
// meantime.
group_host_connect_handler = std::thread([&, group_socket_path,
plugin_path, socket_path]() {
using namespace std::literals::chrono_literals;
// TODO: Replace this polling with inotify when encapsulating
// the different host launch behaviors
while (vst_host.running()) {
std::this_thread::sleep_for(20ms);
try {
// This is the exact same connection sequence as above
boost::asio::local::stream_protocol::socket group_socket(
io_context);
group_socket.connect(group_socket_path.string());
write_object(group_socket,
GroupRequest{plugin_path.string(),
socket_path.string()});
const auto response =
read_object<GroupResponse>(group_socket);
// If two group processes started at the same time, than the
// first one will be the one to respond to the host request
vst_host_pid = response.pid;
return;
} catch (const boost::system::system_error&) {
}
}
});
}
}
void PluginBridge::log_init_message() {
std::stringstream init_msg;
init_msg << "Initializing yabridge version " << yabridge_git_version
<< std::endl;
init_msg << "host: '" << vst_host_path.string() << "'" << std::endl;
init_msg << "host: '" << vst_host->path().string() << "'"
<< std::endl;
init_msg << "plugin: '" << vst_plugin_path.string() << "'"
<< std::endl;
init_msg << "socket: '" << socket_endpoint.path() << "'" << std::endl;
@@ -782,7 +607,7 @@ void PluginBridge::log_init_message() {
} else {
init_msg << "individually";
}
if (vst_plugin_arch == PluginArchitecture::vst_32) {
if (vst_host->architecture() == PluginArchitecture::vst_32) {
init_msg << ", 32-bit";
} else {
init_msg << ", 64-bit";