diff --git a/src/common/serialization.cpp b/src/common/serialization.cpp index cb9662d3..c16b0142 100644 --- a/src/common/serialization.cpp +++ b/src/common/serialization.cpp @@ -108,3 +108,7 @@ AEffect& update_aeffect(AEffect& plugin, const AEffect& updated_plugin) { return plugin; } + +bool PluginParameters::operator==(const PluginParameters& rhs) const { + return plugin_path == rhs.plugin_path && socket_path == rhs.socket_path; +} diff --git a/src/common/serialization.h b/src/common/serialization.h index 2c1fe7f7..b0810dc7 100644 --- a/src/common/serialization.h +++ b/src/common/serialization.h @@ -571,3 +571,30 @@ struct AudioBuffers { s.value4b(sample_frames); } }; + +/** + * An object containing the startup options for hosting a plugin in a plugin + * group process. These are the exact same options that would have been passed + * to `yabridge-host.exe` were the plugin to be hosted individually. + */ +struct PluginParameters { + std::string plugin_path; + std::string socket_path; + + bool operator==(const PluginParameters& rhs) const; + + template + void serialize(S& s) { + s.text1b(plugin_path, 4096); + s.text1b(socket_path, 4096); + } +}; + +template <> +struct std::hash { + std::size_t operator()(PluginParameters const& params) const noexcept { + std::hash hasher{}; + + return hasher(params.plugin_path) ^ (hasher(params.socket_path) << 1); + } +}; diff --git a/src/plugin/plugin-bridge.cpp b/src/plugin/plugin-bridge.cpp index dfcdb272..25414b29 100644 --- a/src/plugin/plugin-bridge.cpp +++ b/src/plugin/plugin-bridge.cpp @@ -121,7 +121,12 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback) std::thread([&]() { using namespace std::literals::chrono_literals; - // TODO: Figure out how this check would work with plugin gruops + // TODO: For plugin groups, we should be polling whether someone is + // still listening on the group socket (and ideally we should be + // able to tell it's the same process). If we can't figure out a + // better way, then we could just return the PID when sending the + // host request to the group process and check whether that + // process is still running. while (true) { if (finished_accepting_sockets) { return; diff --git a/src/wine-host/bridges/group.cpp b/src/wine-host/bridges/group.cpp index 4a547368..ef5aa6ab 100644 --- a/src/wine-host/bridges/group.cpp +++ b/src/wine-host/bridges/group.cpp @@ -20,6 +20,8 @@ #include #include +#include "../../common/communication.h" + // FIXME: `std::filesystem` is broken in wineg++, at least under Wine 5.8. Any // path operation will thrown an encoding related error namespace fs = boost::filesystem; @@ -29,6 +31,13 @@ namespace fs = boost::filesystem; */ std::string create_logger_prefix(const fs::path& socket_path); +// CreateThread() is great and allows you to pass a single value to the +// function, so we'll use this to pass both `this` and the parameters to the +// below thread function so it can do its thing. +using handle_host_plugin_parameters = std::pair; + +uint32_t WINAPI handle_host_plugin_proxy(void* param); + StdIoCapture::StdIoCapture(boost::asio::io_context& io_context, int file_descriptor) : pipe(io_context), @@ -54,10 +63,6 @@ StdIoCapture::~StdIoCapture() { close(pipe_fd[0]); } -bool PluginParameters::operator==(const PluginParameters& other) const { - return removeme == other.removeme; -} - GroupBridge::GroupBridge(boost::filesystem::path group_socket_path) : logger(Logger::create_from_environment( create_logger_prefix(group_socket_path))), @@ -77,18 +82,58 @@ GroupBridge::GroupBridge(boost::filesystem::path group_socket_path) } void GroupBridge::handle_host_plugin(const PluginParameters& parameters) { - // TODO: Start the plugin, + // TODO: Start the plugin // TODO: Allow this process to exit when the last plugin exits. Make sure // that that doesn't cause any race conditions. } void GroupBridge::handle_incoming_connections() { - // TODO: Accept connections here + accept_requests(); logger.log("Now accepting incoming connections"); io_context.run(); } +void GroupBridge::accept_requests() { + group_socket_acceptor.async_accept( + [&](const boost::system::error_code& error, + boost::asio::local::stream_protocol::socket socket) { + // Stop the whole process when the socket gets closed unexpectedly + if (error.failed()) { + logger.log("Error while listening for incoming connections:"); + logger.log(error.message()); + + io_context.stop(); + } + + // Read the parameters, and then host the plugin in this process + // just like if we would be hosting the plugin individually through + // `yabridge-hsot.exe`. Since we're using sockets there's no reason + // to send an acknowledgement back. One potential issue here is that + // it will be hard to tell whether a plugin has crashed before + // initialization, and that the sockets will never be accepted. For + // individually hosted plugins we poll whether the Wine process is + // still active so we can terminate early if it is not, but in this + // case the yabridge instance has to determine that this process is + // still running. + const auto parameters = read_object(socket); + + // Collisions in the generated socket names should be very rare, but + // it could in theory happen + std::lock_guard lock(active_plugins_mutex); + assert(active_plugins.find(parameters) != active_plugins.end()); + + // CreateThread() doesn't support multiple arguments and requires + // manualy memory management. + handle_host_plugin_parameters* thread_params = + new std::pair(this, parameters); + active_plugins[parameters] = + Win32Thread(handle_host_plugin_proxy, &thread_params); + + accept_requests(); + }); +} + void GroupBridge::async_log_pipe_lines( boost::asio::posix::stream_descriptor& pipe, boost::asio::streambuf& buffer, @@ -135,3 +180,16 @@ std::string create_logger_prefix(const fs::path& socket_path) { return "[" + socket_name + "] "; } + +uint32_t WINAPI handle_host_plugin_proxy(void* param) { + // The Win32 API only allows you to pass a void pointer to threads, so we + // need to use manual memory management. + auto thread_params = static_cast(param); + GroupBridge* instance = thread_params->first; + PluginParameters& parameters = thread_params->second; + delete thread_params; + + instance->handle_host_plugin(parameters); + + return 0; +} diff --git a/src/wine-host/bridges/group.h b/src/wine-host/bridges/group.h index c6a5c542..fe38f580 100644 --- a/src/wine-host/bridges/group.h +++ b/src/wine-host/bridges/group.h @@ -24,21 +24,6 @@ #include "vst2.h" -// TODO: Replace this with a proper struct that contains the required arguments -// for creating a PluginBridge instance -struct PluginParameters { - int removeme; - - bool operator==(const PluginParameters& p) const; -}; - -template <> -struct std::hash { - std::size_t operator()(PluginParameters const& params) const noexcept { - return std::hash{}(params.removeme); - } -}; - /** * Encapsulate capturing the STDOUT or STDERR stream by opening a pipe and * reopening the passed file descriptor as one of the ends of the newly opened @@ -123,6 +108,8 @@ class GroupBridge { * where `` is a numerical hash as explained in the * `create_logger_prefix()` function in `./group.cpp`. * + * @throw boost::system::system_error If we can't listen on the socket. + * * @note Creating an `GroupBridge` instance has the side effect that the * STDOUT and STDERR streams of the current process will be redirected to * a pipe so they can be properly written to a log file. @@ -132,10 +119,10 @@ class GroupBridge { /** * Host a new plugin within this process. Called by proxy using * `handle_host_plugin_proxy()` in `./group.cpp` because the Win32 - * `CreateThread` API only allows passing a single pointer to the function. - * Because we don't have access to our own thread handle, the thread that's - * listening on the group socket and that has created this thread will also - * add this thread to the `active_plugins` map. + * `CreateThread` API only allows passing a single pointer to the function + * and does not allow lambdas. Because we don't have access to our own + * thread handle, the thread that's listening on the group socket will have + * already added this thread to the `active_plugins` map. * * Once the plugin has exited, this thread will then remove itself from the * `active_plugins` map. If this causes the vector to become empty, we will @@ -158,6 +145,17 @@ class GroupBridge { void handle_incoming_connections(); private: + /** + * Listen on the group socket for incoming requests to host a new plugin + * within this group process. This will asynchronously listen on the socket, + * and for any connection made it will retrieve a `PluginParameters` object + * containing information about the plugin to host and then spawn a new + * thread to start hosting that plugin. + * + * @see handle_host_plugin + */ + void accept_requests(); + /** * Continuously read from a pipe and write the output to the log file. Used * with the IO streams captured by `stdout_redirect` and `stderr_redirect`. @@ -206,11 +204,11 @@ class GroupBridge { /** * A map of threads that are currently hosting a plugin within this process. * After a plugin has exited or its initialization has failed, the thread - * handling it will remove itself from this map. + * handling it will remove itself from this map. This is to keep track of + * the amount of plugins currently running with their associated thread + * handles. */ - std::unordered_map>> - active_plugins; + std::unordered_map active_plugins; /** * A mutex to prevent two threads from simultaneously accessing the plugins * map, and also to prevent `handle_host_plugin()` from terminating the diff --git a/src/wine-host/group-host.cpp b/src/wine-host/group-host.cpp index 38b8d7f4..4f1b9950 100644 --- a/src/wine-host/group-host.cpp +++ b/src/wine-host/group-host.cpp @@ -56,31 +56,29 @@ int __cdecl main(int argc, char* argv[]) { const std::string group_socket_endpoint_path(argv[1]); - // TODO: Catch exception during initialization when another process is - // already listening on the socket. Make sure to print a more useful - // message instead. - try { - std::cerr << "Initializing yabridge group host version " - << yabridge_git_version + std::cerr << "Initializing yabridge group host version " + << yabridge_git_version #ifdef __i386__ - << " (32-bit compatibility mode)" + << " (32-bit compatibility mode)" #endif - << std::endl; + << std::endl; + try { GroupBridge bridge(group_socket_endpoint_path); // Blocks the main thread until all plugins have exited bridge.handle_incoming_connections(); - } catch (const std::runtime_error& error) { - // Even though the process will likely outlive the yabridge instance - // that spawns it, we can still print any initialization messages and - // errors to STDERR since at this point there will still be a yabridge - // instance capturing this process's output. + } catch (const boost::system::system_error& error) { + // If another process is already listening on the socket, we'll just + // print a message and exit quietly. This could happen if the host + // starts multiple yabridge instances that all use the same plugin group + // at the same time. // TODO: Check if this is printed on the right stream - std::cerr << "Error while initializing the group host process:" + std::cerr << "Another process is already listening on this group's " + "socket, connecting to the existing process:" << std::endl; std::cerr << error.what() << std::endl; - return 1; + return 0; } } diff --git a/src/wine-host/individual-host.cpp b/src/wine-host/individual-host.cpp index 5aa8a314..acd50b7e 100644 --- a/src/wine-host/individual-host.cpp +++ b/src/wine-host/individual-host.cpp @@ -54,6 +54,7 @@ int __cdecl main(int argc, char* argv[]) { << " (32-bit compatibility mode)" #endif << std::endl; + try { Vst2Bridge bridge(plugin_dll_path, socket_endpoint_path); std::cerr << "Finished initializing '" << plugin_dll_path << "'"