From 4b533425144fe21daba159f01f34565d3bfaec45 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 25 Oct 2020 16:50:58 +0100 Subject: [PATCH] :boom: Encapsulate and rework all socket logic This is a pretty huge change that will be important for being able to handle nested or mutually recursive `dispatch()` and `audioMaster()` calls. This sadly all had to be done in a single commit, so here's a summary: - `src/common/sockets.h:Sockets` contains all sockets on both the plugin and the Wine host side, and is used to both listen on and connect to the sockets. - Sockets and other temporary files respect `$XDG_RUNTIME_DIR` instead of being dumped in `/tmp`. - All sockets now have a unique endpoint in `/run/user//yabridge--/`. This is important for when we want to have multiple socket connections for handling `dispatch()` and `audioMaster()`. - Because of the above, we no longer clean up the socket endpoint files after the connection gets established during initialization. Instead we'll remove the socket base directory when shutting down. --- meson.build | 2 + src/common/communication.cpp | 127 ++++++++++++++++++++++++++ src/common/communication.h | 147 ++++++++++++++++++++++++++++++ src/common/serialization.cpp | 3 +- src/common/serialization.h | 7 +- src/common/utils.cpp | 13 +++ src/common/utils.h | 11 +++ src/plugin/host-process.cpp | 37 ++++---- src/plugin/host-process.h | 24 +++-- src/plugin/plugin-bridge.cpp | 91 ++++++++---------- src/plugin/plugin-bridge.h | 41 +-------- src/plugin/utils.cpp | 68 +++----------- src/plugin/utils.h | 31 +++---- src/wine-host/bridges/group.cpp | 6 +- src/wine-host/bridges/vst2.cpp | 55 +++++------ src/wine-host/bridges/vst2.h | 51 ++--------- src/wine-host/individual-host.cpp | 10 +- 17 files changed, 439 insertions(+), 285 deletions(-) create mode 100644 src/common/communication.cpp diff --git a/meson.build b/meson.build index 5942e132..bade536b 100644 --- a/meson.build +++ b/meson.build @@ -92,6 +92,7 @@ shared_library( 'src/common/configuration.cpp', 'src/common/logging.cpp', 'src/common/serialization.cpp', + 'src/common/communication.cpp', 'src/common/utils.cpp', 'src/plugin/host-process.cpp', 'src/plugin/plugin.cpp', @@ -116,6 +117,7 @@ host_sources = [ 'src/common/configuration.cpp', 'src/common/logging.cpp', 'src/common/serialization.cpp', + 'src/common/communication.cpp', 'src/common/utils.cpp', 'src/wine-host/bridges/vst2.cpp', 'src/wine-host/editor.cpp', diff --git a/src/common/communication.cpp b/src/common/communication.cpp new file mode 100644 index 00000000..391eb709 --- /dev/null +++ b/src/common/communication.cpp @@ -0,0 +1,127 @@ +// 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 "communication.h" + +#include + +#include "utils.h" + +namespace fs = boost::filesystem; + +/** + * Used for generating random identifiers. + */ +constexpr char alphanumeric_characters[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +Sockets::Sockets(boost::asio::io_context& io_context, + const boost::filesystem::path& endpoint_base_dir, + bool listen) + : base_dir(endpoint_base_dir), + io_context(io_context), + host_vst_dispatch(io_context), + host_vst_dispatch_midi_events(io_context), + vst_host_callback(io_context), + host_vst_parameters(io_context), + host_vst_process_replacing(io_context), + host_vst_control(io_context), + host_vst_dispatch_endpoint( + (base_dir / "host_vst_dispatch.sock").string()), + host_vst_dispatch_midi_events_endpoint( + (base_dir / "host_vst_dispatch_midi_events.sock").string()), + vst_host_callback_endpoint( + (base_dir / "vst_host_callback.sock").string()), + host_vst_parameters_endpoint( + (base_dir / "host_vst_parameters.sock").string()), + host_vst_process_replacing_endpoint( + (base_dir / "host_vst_process_replacing.sock").string()), + host_vst_control_endpoint((base_dir / "host_vst_control.sock").string()) { + if (listen) { + fs::create_directory(base_dir); + + acceptors = Acceptors{ + .host_vst_dispatch{io_context, host_vst_dispatch_endpoint}, + .host_vst_dispatch_midi_events{ + io_context, host_vst_dispatch_midi_events_endpoint}, + .vst_host_callback{io_context, vst_host_callback_endpoint}, + .host_vst_parameters{io_context, host_vst_parameters_endpoint}, + .host_vst_process_replacing{io_context, + host_vst_process_replacing_endpoint}, + .host_vst_control{io_context, host_vst_control_endpoint}, + }; + } +} + +Sockets::~Sockets() { + // Only clean if we're the ones who have created these files, although it + // should not cause any harm to also do this on the Wine side + if (acceptors) { + try { + fs::remove_all(base_dir); + } catch (const fs::filesystem_error&) { + // There should not be any filesystem errors since only one side + // removes the files, but if we somehow can't delete the file then + // we can just silently ignore this + } + } +} + +void Sockets::connect() { + if (acceptors) { + acceptors->host_vst_dispatch.accept(host_vst_dispatch); + acceptors->host_vst_dispatch_midi_events.accept( + host_vst_dispatch_midi_events); + acceptors->vst_host_callback.accept(vst_host_callback); + acceptors->host_vst_parameters.accept(host_vst_parameters); + acceptors->host_vst_process_replacing.accept( + host_vst_process_replacing); + acceptors->host_vst_control.accept(host_vst_control); + } else { + host_vst_dispatch.connect(host_vst_dispatch_endpoint); + host_vst_dispatch_midi_events.connect( + host_vst_dispatch_midi_events_endpoint); + vst_host_callback.connect(vst_host_callback_endpoint); + host_vst_parameters.connect(host_vst_parameters_endpoint); + host_vst_process_replacing.connect(host_vst_process_replacing_endpoint); + host_vst_control.connect(host_vst_control_endpoint); + } +} + +boost::filesystem::path generate_endpoint_base(const std::string& plugin_name) { + fs::path temp_directory = get_temporary_directory(); + + std::random_device random_device; + std::mt19937 rng(random_device()); + fs::path candidate_endpoint; + do { + std::string random_id; + std::sample( + alphanumeric_characters, + alphanumeric_characters + strlen(alphanumeric_characters) - 1, + std::back_inserter(random_id), 8, rng); + + // We'll get rid of the file descriptors immediately after accepting the + // sockets, so putting them inside of a subdirectory would only leave + // behind an empty directory + std::ostringstream socket_name; + socket_name << "yabridge-" << plugin_name << "-" << random_id; + + candidate_endpoint = temp_directory / socket_name.str(); + } while (fs::exists(candidate_endpoint)); + + return candidate_endpoint; +} diff --git a/src/common/communication.h b/src/common/communication.h index 22ac3516..21e5ee47 100644 --- a/src/common/communication.h +++ b/src/common/communication.h @@ -22,9 +22,11 @@ #ifdef __WINE__ #include "../wine-host/boost-fix.h" #endif +#include #include #include #include +#include template using OutputAdapter = bitsery::OutputBufferAdapter; @@ -32,6 +34,151 @@ using OutputAdapter = bitsery::OutputBufferAdapter; template using InputAdapter = bitsery::InputBufferAdapter; +/** + * Manages all the sockets used for communicating between the plugin and the + * Wine host. Every plugin will get its own directory (the socket endpoint base + * directory), and all socket endpoints are created within this directory. This + * is usually `/run/user//yabridge--/`. + * + * On the plugin side this class should be initialized with `listen` set to + * `true` before launching the Wine VST host. This will start listening on the + * sockets, and the call to `connect()` will then accept any incoming + * connections. + */ +class Sockets { + public: + /** + * Sets up the sockets using the specified base directory. The sockets won't + * be active until `connect()` gets called. + * + * @param io_context The IO context the sockets should be bound to. Relevant + * when doing asynchronous operations. + * @param endpoint_base_dir The base directory that will be used for the + * Unix domain sockets. + * @param listen If `true`, start listening on the sockets. Incoming + * connections will be accepted when `connect()` gets called. This should + * be set to `true` on the plugin side, and `false` on the Wine host side. + * + * @see Sockets::connect + */ + Sockets(boost::asio::io_context& io_context, + const boost::filesystem::path& endpoint_base_dir, + bool listen); + + /** + * Cleans up the directory containing the socket endpoints when yabridge + * shuts down if it still exists. + */ + ~Sockets(); + + /** + * Depending on the value of the `listen` argument passed to the + * constructor, either accept connections made to the sockets on the Linux + * side or connect to the sockets on the Wine side + */ + void connect(); + + /** + * The base directory for our socket endpoints. All `*_endpoint` variables + * below are files within this directory. + */ + const boost::filesystem::path base_dir; + + /** + * The IO context that the sockets will be created on. This is only relevant + * for asynchronous operations. + */ + boost::asio::io_context& io_context; + + // The naming convention for these sockets is `__`. For + // instance the socket named `host_vst_dispatch` forwards + // `AEffect.dispatch()` calls from the native VST host to the Windows VST + // plugin (through the Wine VST host). + + /** + * The socket that forwards all `dispatcher()` calls from the VST host to + * the plugin. + */ + boost::asio::local::stream_protocol::socket host_vst_dispatch; + /** + * Used specifically for the `effProcessEvents` opcode. This is needed + * because the Win32 API is designed to block during certain GUI + * interactions such as resizing a window or opening a dropdown. Without + * this MIDI input would just stop working at times. + */ + boost::asio::local::stream_protocol::socket host_vst_dispatch_midi_events; + /** + * The socket that forwards all `audioMaster()` calls from the Windows VST + * plugin to the host. + */ + boost::asio::local::stream_protocol::socket vst_host_callback; + /** + * Used for both `getParameter` and `setParameter` since they mostly + * overlap. + */ + boost::asio::local::stream_protocol::socket host_vst_parameters; + /** + * Used for processing audio usign the `process()`, `processReplacing()` and + * `processDoubleReplacing()` functions. + */ + boost::asio::local::stream_protocol::socket host_vst_process_replacing; + /** + * A control socket that sends data that is not suitable for the other + * sockets. At the moment this is only used to, on startup, send the Windows + * VST plugin's `AEffect` object to the native VST plugin, and to then send + * the configuration (from `config`) back to the Wine host. + */ + boost::asio::local::stream_protocol::socket host_vst_control; + + private: + const boost::asio::local::stream_protocol::endpoint + host_vst_dispatch_endpoint; + const boost::asio::local::stream_protocol::endpoint + host_vst_dispatch_midi_events_endpoint; + const boost::asio::local::stream_protocol::endpoint + vst_host_callback_endpoint; + const boost::asio::local::stream_protocol::endpoint + host_vst_parameters_endpoint; + const boost::asio::local::stream_protocol::endpoint + host_vst_process_replacing_endpoint; + const boost::asio::local::stream_protocol::endpoint + host_vst_control_endpoint; + + /** + * All of our socket acceptors. We have to create these before launching the + * Wine process. + */ + struct Acceptors { + boost::asio::local::stream_protocol::acceptor host_vst_dispatch; + boost::asio::local::stream_protocol::acceptor + host_vst_dispatch_midi_events; + boost::asio::local::stream_protocol::acceptor vst_host_callback; + boost::asio::local::stream_protocol::acceptor host_vst_parameters; + boost::asio::local::stream_protocol::acceptor + host_vst_process_replacing; + boost::asio::local::stream_protocol::acceptor host_vst_control; + }; + + /** + * If the `listen` constructor argument was set to `true`, when we'll + * prepare a set of socket acceptors that listen on the socket endpoints. + */ + std::optional acceptors; +}; + +/** + * Generate a unique base directory that can be used as a prefix for all Unix + * domain socket endpoints used in `PluginBridge`/`Vst2Bridge`. This will + * usually return `/run/user//yabridge--/`. + * + * Sockets for group hosts are handled separately. See + * `../plugin/utils.h:generate_group_endpoint` for more information on those. + * + * @param plugin_name The name of the plugin we're generating endpoints for. + * Used as a visual indication of what plugin is using this endpoint. + */ +boost::filesystem::path generate_endpoint_base(const std::string& plugin_name); + /** * Serialize an object using bitsery and write it to a socket. This will write * both the size of the serialized object and the object itself over the socket. diff --git a/src/common/serialization.cpp b/src/common/serialization.cpp index 4a5e14d4..d49d480d 100644 --- a/src/common/serialization.cpp +++ b/src/common/serialization.cpp @@ -110,5 +110,6 @@ AEffect& update_aeffect(AEffect& plugin, const AEffect& updated_plugin) { } bool GroupRequest::operator==(const GroupRequest& rhs) const { - return plugin_path == rhs.plugin_path && socket_path == rhs.socket_path; + return plugin_path == rhs.plugin_path && + endpoint_base_dir == rhs.endpoint_base_dir; } diff --git a/src/common/serialization.h b/src/common/serialization.h index 3679d528..c53612a5 100644 --- a/src/common/serialization.h +++ b/src/common/serialization.h @@ -598,14 +598,14 @@ struct AudioBuffers { */ struct GroupRequest { std::string plugin_path; - std::string socket_path; + std::string endpoint_base_dir; bool operator==(const GroupRequest& rhs) const; template void serialize(S& s) { s.text1b(plugin_path, 4096); - s.text1b(socket_path, 4096); + s.text1b(endpoint_base_dir, 4096); } }; @@ -614,7 +614,8 @@ struct std::hash { std::size_t operator()(GroupRequest const& params) const noexcept { std::hash hasher{}; - return hasher(params.plugin_path) ^ (hasher(params.socket_path) << 1); + return hasher(params.plugin_path) ^ + (hasher(params.endpoint_base_dir) << 1); } }; diff --git a/src/common/utils.cpp b/src/common/utils.cpp index e62b5a0a..1b2d0e64 100644 --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -17,6 +17,19 @@ #include "utils.h" #include +#include + +namespace bp = boost::process; +namespace fs = boost::filesystem; + +fs::path get_temporary_directory() { + bp::environment env = boost::this_process::environment(); + if (!env["XDG_RUNTIME_DIR"].empty()) { + return env["XDG_RUNTIME_DIR"].to_string(); + } else { + return fs::temp_directory_path(); + } +} bool set_realtime_priority() { sched_param params{.sched_priority = 5}; diff --git a/src/common/utils.h b/src/common/utils.h index 7543294e..a21fb0dd 100644 --- a/src/common/utils.h +++ b/src/common/utils.h @@ -16,6 +16,17 @@ #pragma once +#ifdef __WINE__ +#include "../wine-host/boost-fix.h" +#endif +#include + +/** + * Return the path to the directory for story temporary files. This will be + * `$XDG_RUNTIME_DIR` if set, and `/tmp` otherwise. + */ +boost::filesystem::path get_temporary_directory(); + /** * Set the scheduling policy to `SCHED_FIFO` with priority 10 for this process. * We explicitly don't do this for wineserver itself since from my testing that diff --git a/src/plugin/host-process.cpp b/src/plugin/host-process.cpp index 83d78c23..0473abb3 100644 --- a/src/plugin/host-process.cpp +++ b/src/plugin/host-process.cpp @@ -83,7 +83,7 @@ void HostProcess::async_log_pipe_lines(patched_async_pipe& pipe, IndividualHost::IndividualHost(boost::asio::io_context& io_context, Logger& logger, fs::path plugin_path, - fs::path socket_endpoint) + const Sockets& sockets) : HostProcess(io_context, logger), plugin_arch(find_vst_architecture(plugin_path)), host_path(find_vst_host(plugin_arch, false)), @@ -93,7 +93,7 @@ IndividualHost::IndividualHost(boost::asio::io_context& io_context, #else plugin_path, #endif - socket_endpoint, + sockets.base_dir, bp::env = set_wineprefix(), bp::std_out = stdout_pipe, bp::std_err = stderr_pipe @@ -127,17 +127,15 @@ void IndividualHost::terminate() { host.wait(); } -GroupHost::GroupHost( - boost::asio::io_context& io_context, - Logger& logger, - fs::path plugin_path, - fs::path socket_endpoint, - std::string group_name, - boost::asio::local::stream_protocol::socket& host_vst_dispatch) +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)), - host_vst_dispatch(host_vst_dispatch) { + sockets(sockets) { #ifdef WITH_WINEDBG if (plugin_path.string().find(' ') != std::string::npos) { logger.log("Warning: winedbg does not support paths containing spaces"); @@ -167,6 +165,7 @@ GroupHost::GroupHost( 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); try { @@ -175,9 +174,10 @@ GroupHost::GroupHost( 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(), - .socket_path = socket_endpoint.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); host_pid = response.pid; @@ -199,7 +199,7 @@ GroupHost::GroupHost( // meantime. group_host_connect_handler = std::jthread([&, group_socket_path, plugin_path, - socket_endpoint]() { + endpoint_base_dir]() { using namespace std::literals::chrono_literals; // TODO: Replace this polling with inotify @@ -214,8 +214,9 @@ GroupHost::GroupHost( write_object( group_socket, - GroupRequest{.plugin_path = plugin_path.string(), - .socket_path = socket_endpoint.string()}); + GroupRequest{ + .plugin_path = plugin_path.string(), + .endpoint_base_dir = endpoint_base_dir.string()}); const auto response = read_object(group_socket); @@ -262,7 +263,7 @@ 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 dispatch socket will cause the associated plugin to exit. - host_vst_dispatch.shutdown( + sockets.host_vst_dispatch.shutdown( boost::asio::local::stream_protocol::socket::shutdown_both); - host_vst_dispatch.close(); + sockets.host_vst_dispatch.close(); } diff --git a/src/plugin/host-process.h b/src/plugin/host-process.h index 5330adf2..840b3a74 100644 --- a/src/plugin/host-process.h +++ b/src/plugin/host-process.h @@ -26,6 +26,7 @@ #include #include "../common/logging.h" +#include "../common/communication.h" #include "utils.h" /** @@ -117,7 +118,7 @@ class IndividualHost : public HostProcess { * handled on. * @param logger The `Logger` instance the redirected STDIO streams will be * written to. - * @param socket_endpoint The endpoint that should be used to communicate + * @param sockets The socket endpoints that will be used for communication * with the plugin. * * @throw std::runtime_error When `plugin_path` does not point to a valid @@ -126,7 +127,7 @@ class IndividualHost : public HostProcess { IndividualHost(boost::asio::io_context& io_context, Logger& logger, boost::filesystem::path plugin_path, - boost::filesystem::path socket_endpoint); + const Sockets& sockets); PluginArchitecture architecture() override; boost::filesystem::path path() override; @@ -160,19 +161,16 @@ class GroupHost : public HostProcess { * handled on. * @param logger The `Logger` instance the redirected STDIO streams will be * written to. - * @param socket_endpoint The endpoint that should be used to communicate - * with the plugin. + * @param sockets The socket endpoints that will be used for communication + * with the plugin. When the plugin shuts down, we'll terminate the + * dispatch socket contained in this object. * @param group_name The name of the plugin group. - * @param host_vst_dispatch The socket used to communicate - * `AEffect::dispatcher()` events with this plugin. Will be closed as to - * shut down the plugin. */ GroupHost(boost::asio::io_context& io_context, Logger& logger, boost::filesystem::path plugin_path, - boost::filesystem::path socket_endpoint, - std::string group_name, - boost::asio::local::stream_protocol::socket& host_vst_dispatch); + Sockets& socket_endpoint, + std::string group_name); PluginArchitecture architecture() override; boost::filesystem::path path() override; @@ -191,10 +189,10 @@ class GroupHost : public HostProcess { pid_t host_pid; /** - * The associated dispatch socket for the plugin we're hosting. This is used - * to terminate the plugin. + * The associated sockets for the plugin we're hosting. This is used to + * terminate the plugin. */ - boost::asio::local::stream_protocol::socket& host_vst_dispatch; + Sockets& sockets; /** * A thread that waits for the group host to have started and then ask it to diff --git a/src/plugin/plugin-bridge.cpp b/src/plugin/plugin-bridge.cpp index 886abeec..0c0c3003 100644 --- a/src/plugin/plugin-bridge.cpp +++ b/src/plugin/plugin-bridge.cpp @@ -54,32 +54,26 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback) // bridge will crash otherwise plugin(), io_context(), - socket_endpoint(generate_plugin_endpoint().string()), - socket_acceptor(io_context, socket_endpoint), - host_vst_dispatch(io_context), - host_vst_dispatch_midi_events(io_context), - vst_host_callback(io_context), - host_vst_parameters(io_context), - host_vst_process_replacing(io_context), - host_vst_control(io_context), + sockets(io_context, + generate_endpoint_base( + vst_plugin_path.filename().replace_extension("").string()), + true), host_callback_function(host_callback), logger(Logger::create_from_environment( - create_logger_prefix(socket_endpoint.path()))), + create_logger_prefix(sockets.base_dir))), wine_version(get_wine_version()), - vst_host( - config.group - ? std::unique_ptr( - std::make_unique(io_context, - logger, - vst_plugin_path, - socket_endpoint.path(), - *config.group, - host_vst_dispatch)) - : std::unique_ptr( - std::make_unique(io_context, + vst_host(config.group + ? std::unique_ptr( + std::make_unique(io_context, logger, vst_plugin_path, - socket_endpoint.path()))), + sockets, + *config.group)) + : std::unique_ptr( + std::make_unique(io_context, + logger, + vst_plugin_path, + sockets))), has_realtime_priority(set_realtime_priority()), wine_io_handler([&]() { io_context.run(); }) { log_init_message(); @@ -107,24 +101,13 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback) }); #endif - // It's very important that these sockets are connected to in the same - // order in the Wine VST host - socket_acceptor.accept(host_vst_dispatch); - socket_acceptor.accept(host_vst_dispatch_midi_events); - socket_acceptor.accept(vst_host_callback); - socket_acceptor.accept(host_vst_parameters); - socket_acceptor.accept(host_vst_process_replacing); - socket_acceptor.accept(host_vst_control); - + // This will block until all sockets have been connected to by the Wine VST + // host + sockets.connect(); #ifndef WITH_WINEDBG host_guard_handler.request_stop(); #endif - // There's no need to keep the socket endpoint file around after accepting - // all the sockets, and RAII won't clean these files up for us - socket_acceptor.close(); - fs::remove(socket_endpoint.path()); - // Set up all pointers for our `AEffect` struct. We will fill this with data // from the VST plugin loaded in Wine at the end of this constructor. plugin.ptr3 = this; @@ -144,8 +127,8 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback) // TODO: Think of a nicer way to structure this and the similar // handler in `Vst2Bridge::handle_dispatch_midi_events` receive_event( - vst_host_callback, std::pair(logger, false), - [&](Event& event) { + sockets.vst_host_callback, + std::pair(logger, false), [&](Event& event) { // MIDI events sent from the plugin back to the host are // a special case here. They have to sent during the // `processReplacing()` function or else the host will @@ -181,13 +164,14 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback) // call these during its initialization. Any further updates will be sent // over the `dispatcher()` socket. This would happen whenever the plugin // calls `audioMasterIOChanged()` and after the host calls `effOpen()`. - const auto initialization_data = read_object(host_vst_control); + const auto initialization_data = + read_object(sockets.host_vst_control); const auto initialized_plugin = std::get(initialization_data.payload); // After receiving the `AEffect` values we'll want to send the configuration // back to complete the startup process - write_object(host_vst_control, config); + write_object(sockets.host_vst_control, config); update_aeffect(plugin, initialized_plugin); } @@ -452,10 +436,10 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/, intptr_t return_value = 0; try { // TODO: Add some kind of timeout? - return_value = - send_event(host_vst_dispatch, dispatch_mutex, converter, - std::pair(logger, true), opcode, - index, value, data, option); + return_value = send_event( + sockets.host_vst_dispatch, dispatch_mutex, converter, + std::pair(logger, true), opcode, index, + value, data, option); } catch (const boost::system::system_error& a) { // Thrown when the socket gets closed because the VST plugin // loaded into the Wine process crashed during shutdown @@ -478,7 +462,7 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/, // thread and socket to pass MIDI events. Otherwise plugins will // stop receiving MIDI data when they have an open dropdowns or // message box. - return send_event(host_vst_dispatch_midi_events, + return send_event(sockets.host_vst_dispatch_midi_events, dispatch_midi_events_mutex, converter, std::pair(logger, true), opcode, index, value, data, option); @@ -538,7 +522,7 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/, // and loading plugin state it's much better to have bitsery or our // receiving function temporarily allocate a large enough buffer rather than // to have a bunch of allocated memory sitting around doing nothing. - return send_event(host_vst_dispatch, dispatch_mutex, converter, + return send_event(sockets.host_vst_dispatch, dispatch_mutex, converter, std::pair(logger, true), opcode, index, value, data, option); } @@ -555,11 +539,11 @@ void PluginBridge::do_process(T** inputs, T** outputs, int sample_frames) { } const AudioBuffers request{input_buffers, sample_frames}; - write_object(host_vst_process_replacing, request, process_buffer); + write_object(sockets.host_vst_process_replacing, request, process_buffer); // Write the results back to the `outputs` arrays - const auto response = - read_object(host_vst_process_replacing, process_buffer); + const auto response = read_object( + sockets.host_vst_process_replacing, process_buffer); const auto& response_buffers = std::get>>(response.buffers); @@ -608,8 +592,8 @@ float PluginBridge::get_parameter(AEffect* /*plugin*/, int index) { // called at the same time since they share the same socket { std::lock_guard lock(parameters_mutex); - write_object(host_vst_parameters, request); - response = read_object(host_vst_parameters); + write_object(sockets.host_vst_parameters, request); + response = read_object(sockets.host_vst_parameters); } logger.log_get_parameter_response(*response.value); @@ -625,9 +609,9 @@ void PluginBridge::set_parameter(AEffect* /*plugin*/, int index, float value) { { std::lock_guard lock(parameters_mutex); - write_object(host_vst_parameters, request); + write_object(sockets.host_vst_parameters, request); - response = read_object(host_vst_parameters); + response = read_object(sockets.host_vst_parameters); } logger.log_set_parameter_response(); @@ -647,7 +631,8 @@ void PluginBridge::log_init_message() { << std::endl; init_msg << "realtime: '" << (has_realtime_priority ? "yes" : "no") << "'" << std::endl; - init_msg << "socket: '" << socket_endpoint.path() << "'" << std::endl; + init_msg << "sockets: '" << sockets.base_dir.string() << "'" + << std::endl; init_msg << "wine prefix: '"; // If the Wine prefix is manually overridden, then this should be made diff --git a/src/plugin/plugin-bridge.h b/src/plugin/plugin-bridge.h index 6b3fc57e..ad0d62a2 100644 --- a/src/plugin/plugin-bridge.h +++ b/src/plugin/plugin-bridge.h @@ -25,6 +25,7 @@ #include "../common/configuration.h" #include "../common/logging.h" +#include "../common/communication.h" #include "host-process.h" /** @@ -123,45 +124,7 @@ class PluginBridge { void log_init_message(); boost::asio::io_context io_context; - boost::asio::local::stream_protocol::endpoint socket_endpoint; - boost::asio::local::stream_protocol::acceptor socket_acceptor; - - // The naming convention for these sockets is `__`. For - // instance the socket named `host_vst_dispatch` forwards - // `AEffect.dispatch()` calls from the native VST host to the Windows VST - // plugin (through the Wine VST host). - - /** - * The socket that forwards all `dispatcher()` calls from the VST host to - * the plugin. - */ - boost::asio::local::stream_protocol::socket host_vst_dispatch; - /** - * Used specifically for the `effProcessEvents` opcode. This is needed - * because the Win32 API is designed to block during certain GUI - * interactions such as resizing a window or opening a dropdown. Without - * this MIDI input would just stop working at times. - */ - boost::asio::local::stream_protocol::socket host_vst_dispatch_midi_events; - /** - * The socket that forwards all `audioMaster()` calls from the Windows VST - * plugin to the host. - */ - boost::asio::local::stream_protocol::socket vst_host_callback; - /** - * Used for both `getParameter` and `setParameter` since they mostly - * overlap. - */ - boost::asio::local::stream_protocol::socket host_vst_parameters; - boost::asio::local::stream_protocol::socket host_vst_process_replacing; - - /** - * A control socket that sends data that is not suitable for the other - * sockets. At the moment this is only used to, on startup, send the Windows - * VST plugin's `AEffect` object to the native VST plugin, and to then send - * the configuration (from `config`) back to the Wine host. - */ - boost::asio::local::stream_protocol::socket host_vst_control; + Sockets sockets; /** * The thread that handles host callbacks. diff --git a/src/plugin/utils.cpp b/src/plugin/utils.cpp index 651bbaa5..86ca0e82 100644 --- a/src/plugin/utils.cpp +++ b/src/plugin/utils.cpp @@ -22,37 +22,27 @@ #include #include #include -#include #include // Generated inside of the build directory #include #include "../common/configuration.h" +#include "../common/utils.h" namespace bp = boost::process; namespace fs = boost::filesystem; -/** - * Used for generating random identifiers. - */ -constexpr char alphanumeric_characters[] = - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - -std::string create_logger_prefix(const fs::path& socket_path) { - // Use the socket filename as the logger prefix, but strip the `yabridge-` - // part since that's redundant - std::string socket_name = - socket_path.filename().replace_extension().string(); +std::string create_logger_prefix(const fs::path& endpoint_base_dir) { + // Use the name of the base directory used for our sockets as the logger + // prefix, but strip the `yabridge-` part since that's redundant + std::string endpoint_name = endpoint_base_dir.filename().string(); constexpr std::string_view socket_prefix("yabridge-"); - assert(socket_name.starts_with(socket_prefix)); - socket_name = socket_name.substr(socket_prefix.size()); + assert(endpoint_name.starts_with(socket_prefix)); + endpoint_name = endpoint_name.substr(socket_prefix.size()); - std::ostringstream prefix; - prefix << "[" << socket_name << "] "; - - return prefix.str(); + return "[" + endpoint_name + "] "; } std::optional find_wineprefix() { @@ -183,39 +173,7 @@ boost::filesystem::path generate_group_endpoint( } socket_name << ".sock"; - return fs::temp_directory_path() / socket_name.str(); -} - -fs::path generate_plugin_endpoint() { - const auto plugin_name = - find_vst_plugin().filename().replace_extension("").string(); - - std::random_device random_device; - std::mt19937 rng(random_device()); - fs::path candidate_endpoint; - do { - std::string random_id; - std::sample( - alphanumeric_characters, - alphanumeric_characters + strlen(alphanumeric_characters) - 1, - std::back_inserter(random_id), 8, rng); - - // We'll get rid of the file descriptors immediately after accepting the - // sockets, so putting them inside of a subdirectory would only leave - // behind an empty directory - std::ostringstream socket_name; - socket_name << "yabridge-" << plugin_name << "-" << random_id - << ".sock"; - - candidate_endpoint = fs::temp_directory_path() / socket_name.str(); - } while (fs::exists(candidate_endpoint)); - - // TODO: Should probably try creating the endpoint right here and catch any - // exceptions since this could technically result in a race condition - // when two instances of yabridge decide to use the same endpoint name - // at the same time - - return candidate_endpoint; + return get_temporary_directory() / socket_name.str(); } fs::path get_this_file_location() { @@ -243,7 +201,7 @@ std::string get_wine_version() { bp::environment env = boost::this_process::environment(); if (!env["WINELOADER"].empty()) { - wine_command = env.get("WINELOADER"); + wine_command = env["WINELOADER"].to_string(); } bp::ipstream output; @@ -271,13 +229,13 @@ std::string get_wine_version() { std::string join_quoted_strings(std::vector& strings) { bool is_first = true; - std::ostringstream joined_strigns{}; + std::ostringstream joined_strings{}; for (const auto& option : strings) { - joined_strigns << (is_first ? "'" : ", '") << option << "'"; + joined_strings << (is_first ? "'" : ", '") << option << "'"; is_first = false; } - return joined_strigns.str(); + return joined_strings.str(); } Configuration load_config_for(const fs::path& yabridge_path) { diff --git a/src/plugin/utils.h b/src/plugin/utils.h index a9d3fb92..8e2d6fbb 100644 --- a/src/plugin/utils.h +++ b/src/plugin/utils.h @@ -46,15 +46,17 @@ class patched_async_pipe : public boost::process::async_pipe { enum class PluginArchitecture { vst_32, vst_64 }; /** - * Create a logger prefix based on the unique socket path for easy - * identification. The socket path contains both the plugin's name and a unique - * identifier. + * Create a logger prefix based on the endpoint base directory used for the + * sockets for easy identification. This will result in a prefix of the form + * `[-] `. * - * @param socket_path The path to the socket endpoint in use. + * @param endpoint_base_dir A directory name generated by + * `generate_endpoint_base()`. * * @return A prefix string for log messages. */ -std::string create_logger_prefix(const boost::filesystem::path& socket_path); +std::string create_logger_prefix( + const boost::filesystem::path& endpoint_base_dir); /** * Determine the architecture of a VST plugin (or rather, a .dll file) based on @@ -117,10 +119,11 @@ std::optional find_wineprefix(); /** * Generate the group socket endpoint name used based on the name of the group, * the Wine prefix in use and the plugin architecture. The resulting format is - * `/tmp/yabridge-group---.sock`. In - * this socket name the `wine_prefix_id` is a numerical hash based on the Wine - * prefix in use. This way the same group name can be used for multiple Wine - * prefixes and for both 32 and 64 bit plugins without clashes. + * in the form + * `/run/user//yabridge-group---.sock`. + * In this socket name the `wine_prefix_id` is a numerical hash based on the + * Wine prefix in use. This way the same group name can be used for multiple + * Wine prefixes and for both 32 and 64 bit plugins without clashes. * * @param group_name The name of the plugin group. * @param wine_prefix The name of the Wine prefix in use. This should be @@ -140,16 +143,6 @@ boost::filesystem::path generate_group_endpoint( const boost::filesystem::path& wine_prefix, const PluginArchitecture architecture); -/** - * Generate a unique name for the Unix domain socket endpoint based on the VST - * plugin's name. This will also generate the parent directory if it does not - * yet exist since we're using this in the constructor's initializer list. - * - * @return A path to a not yet existing Unix domain socket endpoint. - * @throw std::runtime_error If no matching .dll file could be found. - */ -boost::filesystem::path generate_plugin_endpoint(); - /** * Return a path to this `.so` file. This can be used to find out from where * this link to or copy of `libyabridge.so` was loaded. diff --git a/src/wine-host/bridges/group.cpp b/src/wine-host/bridges/group.cpp index bb01b6bb..b50fdbe4 100644 --- a/src/wine-host/bridges/group.cpp +++ b/src/wine-host/bridges/group.cpp @@ -209,10 +209,12 @@ void GroupBridge::accept_requests() { // this has to be done on the same thread that's handling messages, // and all window messages have to be handled from the same thread. logger.log("Received request to host '" + request.plugin_path + - "' using socket '" + request.socket_path + "'"); + "' using socket endpoint base directory '" + + request.endpoint_base_dir + "'"); try { auto bridge = std::make_unique( - plugin_context, request.plugin_path, request.socket_path); + plugin_context, request.plugin_path, + request.endpoint_base_dir); logger.log("Finished initializing '" + request.plugin_path + "'"); diff --git a/src/wine-host/bridges/vst2.cpp b/src/wine-host/bridges/vst2.cpp index e2647cb3..7d5ad91f 100644 --- a/src/wine-host/bridges/vst2.cpp +++ b/src/wine-host/bridges/vst2.cpp @@ -67,16 +67,10 @@ Vst2Bridge& get_bridge_instance(const AEffect* plugin) { Vst2Bridge::Vst2Bridge(boost::asio::io_context& main_context, std::string plugin_dll_path, - std::string socket_endpoint_path) + std::string endpoint_base_dir) : io_context(main_context), plugin_handle(LoadLibrary(plugin_dll_path.c_str()), FreeLibrary), - socket_endpoint(socket_endpoint_path), - host_vst_dispatch(io_context), - host_vst_dispatch_midi_events(io_context), - vst_host_callback(io_context), - host_vst_parameters(io_context), - host_vst_process_replacing(io_context), - host_vst_control(io_context) { + sockets(io_context, endpoint_base_dir, false) { // Got to love these C APIs if (!plugin_handle) { throw std::runtime_error("Could not load the Windows .dll file at '" + @@ -101,14 +95,7 @@ Vst2Bridge::Vst2Bridge(boost::asio::io_context& main_context, "'."); } - // It's very important that these sockets are accepted to in the same order - // in the Linux plugin - host_vst_dispatch.connect(socket_endpoint); - host_vst_dispatch_midi_events.connect(socket_endpoint); - vst_host_callback.connect(socket_endpoint); - host_vst_parameters.connect(socket_endpoint); - host_vst_process_replacing.connect(socket_endpoint); - host_vst_control.connect(socket_endpoint); + sockets.connect(); // Initialize after communication has been set up // We'll try to do the same `get_bridge_isntance` trick as in @@ -132,13 +119,14 @@ Vst2Bridge::Vst2Bridge(boost::asio::io_context& main_context, // of this object will be sent over the `dispatcher()` socket. This would be // done after the host calls `effOpen()`, and when the plugin calls // `audioMasterIOChanged()`. - write_object(host_vst_control, EventResult{.return_value = 0, - .payload = *plugin, - .value_payload = std::nullopt}); + write_object(sockets.host_vst_control, + EventResult{.return_value = 0, + .payload = *plugin, + .value_payload = std::nullopt}); // After sending the AEffect struct we'll receive this instance's // configuration as a response - config = read_object(host_vst_control); + config = read_object(sockets.host_vst_control); // This works functionally identically to the `handle_dispatch()` function, // but this socket will only handle MIDI events and it will handle them @@ -160,7 +148,7 @@ void Vst2Bridge::handle_dispatch() { while (true) { try { receive_event( - host_vst_dispatch, std::nullopt, + sockets.host_vst_dispatch, std::nullopt, passthrough_event( plugin, [&](AEffect* plugin, int opcode, int index, intptr_t value, @@ -194,7 +182,8 @@ void Vst2Bridge::handle_dispatch_midi_events() { while (true) { try { receive_event( - host_vst_dispatch_midi_events, std::nullopt, [&](Event& event) { + sockets.host_vst_dispatch_midi_events, std::nullopt, + [&](Event& event) { if (BOOST_LIKELY(event.opcode == effProcessEvents)) { // For 99% of the plugins we can just call // `effProcessReplacing()` and be done with it, but a @@ -255,19 +244,19 @@ void Vst2Bridge::handle_parameters() { // through on this socket since they have a lot of overlap. The // presence of the `value` field tells us which one we're dealing // with. - auto request = read_object(host_vst_parameters); + auto request = read_object(sockets.host_vst_parameters); if (request.value) { // `setParameter` plugin->setParameter(plugin, request.index, *request.value); ParameterResult response{std::nullopt}; - write_object(host_vst_parameters, response); + write_object(sockets.host_vst_parameters, response); } else { // `getParameter` float value = plugin->getParameter(plugin, request.index); ParameterResult response{value}; - write_object(host_vst_parameters, response); + write_object(sockets.host_vst_parameters, response); } } catch (const boost::system::system_error&) { // The plugin has cut off communications, so we can shut down this @@ -288,8 +277,8 @@ void Vst2Bridge::handle_process_replacing() { while (true) { try { - auto request = read_object(host_vst_process_replacing, - process_buffer); + auto request = read_object( + sockets.host_vst_process_replacing, process_buffer); // Let the plugin process the MIDI events that were received since // the last buffer, and then clean up those events. This approach // should not be needed but Kontakt only stores pointers to rather @@ -348,8 +337,8 @@ void Vst2Bridge::handle_process_replacing() { AudioBuffers response{output_buffers_single_precision, request.sample_frames}; - write_object(host_vst_process_replacing, response, - process_buffer); + write_object(sockets.host_vst_process_replacing, + response, process_buffer); }, [&](std::vector>& input_buffers) { // Exactly the same as the above, but for double @@ -373,8 +362,8 @@ void Vst2Bridge::handle_process_replacing() { AudioBuffers response{output_buffers_double_precision, request.sample_frames}; - write_object(host_vst_process_replacing, response, - process_buffer); + write_object(sockets.host_vst_process_replacing, + response, process_buffer); }}, request.buffers); @@ -419,7 +408,7 @@ intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin, // When hosting multiple plugins in a group process, all plugins // should get a unique window class const std::string window_class = - "yabridge plugin " + socket_endpoint.path(); + "yabridge plugin " + sockets.base_dir.string(); Editor& editor_instance = editor.emplace( config, window_class, x11_handle, plugin); @@ -581,7 +570,7 @@ intptr_t Vst2Bridge::host_callback(AEffect* effect, } HostCallbackDataConverter converter(effect, time_info); - return send_event(vst_host_callback, host_callback_mutex, converter, + return send_event(sockets.vst_host_callback, host_callback_mutex, converter, std::nullopt, opcode, index, value, data, option); } diff --git a/src/wine-host/bridges/vst2.h b/src/wine-host/bridges/vst2.h index 3d0815af..162907e4 100644 --- a/src/wine-host/bridges/vst2.h +++ b/src/wine-host/bridges/vst2.h @@ -31,6 +31,7 @@ #include "../../common/configuration.h" #include "../../common/logging.h" +#include "../../common/communication.h" #include "../editor.h" #include "../utils.h" @@ -46,7 +47,7 @@ struct EditorOpening {}; * plugin and provides host callback function for the plugin to talk back. * * @remark Because of Win32 API limitations, all window handling has to be done - * from the same thread. Most plugins won't have any issues when using + * from a single thread. Most plugins won't have any issues when using * multiple message loops, but the Melda plugins for instance will only update * their GUIs from the message loop of the thread that created the first * instance. This is why we pass an IO context to this class so everything @@ -64,8 +65,8 @@ class Vst2Bridge { * also be run from this context. * @param plugin_dll_path A (Unix style) path to the VST plugin .dll file to * load. - * @param socket_endpoint_path A (Unix style) path to the Unix socket - * endpoint the native VST plugin created to communicate over. + * @param endpoint_base_dir The base directory used for the socket + * endpoints. See `Sockets` for more information. * * @note The object has to be constructed from the same thread that calls * `main_context.run()`. @@ -75,7 +76,7 @@ class Vst2Bridge { */ Vst2Bridge(boost::asio::io_context& main_context, std::string plugin_dll_path, - std::string socket_endpoint_path); + std::string endpoint_base_dir); /** * Returns true if the message loop should be skipped. This happens when the @@ -189,47 +190,9 @@ class Vst2Bridge { AEffect* plugin; /** - * The UNIX domain socket endpoint used for communicating to this specific - * bridged plugin. + * All sockets used for communicating with this specific plugin. */ - boost::asio::local::stream_protocol::endpoint socket_endpoint; - - // The naming convention for these sockets is `__`. For - // instance the socket named `host_vst_dispatch` forwards - // `AEffect.dispatch()` calls from the native VST host to the Windows VST - // plugin (through the Wine VST host). - - /** - * The socket that forwards all `dispatcher()` calls from the VST host to - * the plugin. - */ - boost::asio::local::stream_protocol::socket host_vst_dispatch; - /** - * Used specifically for the `effProcessEvents` opcode. This is needed - * because the Win32 API is designed to block during certain GUI - * interactions such as resizing a window or opening a dropdown. Without - * this MIDI input would just stop working at times. - */ - boost::asio::local::stream_protocol::socket host_vst_dispatch_midi_events; - /** - * The socket that forwards all `audioMaster()` calls from the Windows VST - * plugin to the host. - */ - boost::asio::local::stream_protocol::socket vst_host_callback; - /** - * Used for both `getParameter` and `setParameter` since they mostly - * overlap. - */ - boost::asio::local::stream_protocol::socket host_vst_parameters; - boost::asio::local::stream_protocol::socket host_vst_process_replacing; - - /** - * A control socket that sends data that is not suitable for the other - * sockets. At the moment this is only used to, on startup, send the Windows - * VST plugin's `AEffect` object to the native VST plugin, and to then send - * the configuration (from `config`) back to the Wine host. - */ - boost::asio::local::stream_protocol::socket host_vst_control; + Sockets sockets; /** * The thread that specifically handles `effProcessEvents` opcodes so the diff --git a/src/wine-host/individual-host.cpp b/src/wine-host/individual-host.cpp index 48e4f847..3f93e46b 100644 --- a/src/wine-host/individual-host.cpp +++ b/src/wine-host/individual-host.cpp @@ -39,7 +39,7 @@ void async_handle_events(boost::asio::steady_timer& timer, Vst2Bridge& bridge); /** * This is the default VST host application. It will load the specified VST2 - * plugin, and then connect back to the `libyabridge.so` instace that spawned + * plugin, and then connect back to the `libyabridge.so` instance that spawned * this over the socket. * * The explicit calling convention is needed to work around a bug introduced in @@ -48,9 +48,9 @@ void async_handle_events(boost::asio::steady_timer& timer, Vst2Bridge& bridge); int __cdecl main(int argc, char* argv[]) { set_realtime_priority(); - // We pass the name of the VST plugin .dll file to load and the Unix domain - // socket to connect to in plugin/bridge.cpp as the first two arguments of - // this process. + // We pass the name of the VST plugin .dll file to load and the base + // directory for the Unix domain socket endpoints to connect to as the first + // two arguments of this process in plugin/bridge.cpp. if (argc < 3) { std::cerr << "Usage: " #ifdef __i386__ @@ -58,7 +58,7 @@ int __cdecl main(int argc, char* argv[]) { #else << yabridge_individual_host_name #endif - << " " << std::endl; + << " " << std::endl; return 1; }