diff --git a/src/plugin/plugin-bridge.cpp b/src/plugin/plugin-bridge.cpp index 5a7b1eb6..dfcdb272 100644 --- a/src/plugin/plugin-bridge.cpp +++ b/src/plugin/plugin-bridge.cpp @@ -121,6 +121,7 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback) std::thread([&]() { using namespace std::literals::chrono_literals; + // TODO: Figure out how this check would work with plugin gruops while (true) { if (finished_accepting_sockets) { return; diff --git a/src/wine-host/bridges/group.cpp b/src/wine-host/bridges/group.cpp index 64b1599e..4a547368 100644 --- a/src/wine-host/bridges/group.cpp +++ b/src/wine-host/bridges/group.cpp @@ -17,6 +17,17 @@ #include "group.h" #include +#include +#include + +// 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; + +/** + * Create a logger prefix containing the group name based on the socket path. + */ +std::string create_logger_prefix(const fs::path& socket_path); StdIoCapture::StdIoCapture(boost::asio::io_context& io_context, int file_descriptor) @@ -42,3 +53,85 @@ StdIoCapture::~StdIoCapture() { close(original_fd_copy); 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))), + io_context(), + stdout_redirect(io_context, STDOUT_FILENO), + stderr_redirect(io_context, STDERR_FILENO), + group_socket_endpoint(group_socket_path.string()), + group_socket_acceptor(io_context, group_socket_endpoint) { + // TODO: After initializing, listen for connections and spawn plugins + // the exact same way as what happens in `individual-host.cpp` + // TODO: Allow this process to exit when the last plugin exits. Make sure + // that that doesn't cause any race conditions. + + // Write this process's original STDOUT and STDERR streams to the logger + async_log_pipe_lines(stdout_redirect.pipe, stdout_buffer, "[STDOUT] "); + async_log_pipe_lines(stderr_redirect.pipe, stderr_buffer, "[STDERR] "); +} + +void GroupBridge::handle_host_plugin(const PluginParameters& parameters) { + // 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 + + logger.log("Now accepting incoming connections"); + io_context.run(); +} + +void GroupBridge::async_log_pipe_lines( + boost::asio::posix::stream_descriptor& 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); + }); +} + +std::string create_logger_prefix(const fs::path& socket_path) { + // The group socket filename will be in the format + // '/tmp/yabridge-group---.sock', + // where Wine prefix ID is just Wine prefix ran through `std::hash` to + // prevent collisions without needing complicated filenames. We want to + // extract the group name. + std::string socket_name = + socket_path.filename().replace_extension().string(); + + std::smatch group_match; + std::regex group_regexp("^yabridge-group-(.*)-[^-]+-[^-]+$", + std::regex::ECMAScript); + if (std::regex_match(socket_name, group_match, group_regexp)) { + socket_name = group_match[1].str(); + +#ifdef __i386__ + // Mark 32-bit versions to avoid potential confusion caused by 32-bit + // and regular 64-bit group processes with the same name running + // alongside eachother + socket_name += "-x32"; +#endif + } + + return "[" + socket_name + "] "; +} diff --git a/src/wine-host/bridges/group.h b/src/wine-host/bridges/group.h index 89c6c866..c6a5c542 100644 --- a/src/wine-host/bridges/group.h +++ b/src/wine-host/bridges/group.h @@ -19,6 +19,25 @@ #include "../boost-fix.h" #include +#include +#include + +#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 @@ -76,3 +95,127 @@ class StdIoCapture { */ int pipe_fd[2]; }; + +/** + * A 'plugin group' that listens on a _group socket_ for plugins to host in this + * process. Once the plugin gets loaded into a new thread the actual bridging + * process is identical to individually hosted plugins. + * + * An important detail worth mentioning here is that while this plugin group can + * throw in the constructor when another process is already listening on the + * socket, this should not be treated as an error. When using plugins groups, + * yabridge will try to connect to the group socket on initialization and it + * will launch a new group host process if it can't. If this is done for + * multiple yabridge instances at the same time, then multiple group host + * processes will be launched. Instead of using complicated inter-process + * synchronization, we'll simply allow the processes to fail when another + * process is already listening on the socket. + */ +class GroupBridge { + public: + /** + * Create a plugin group by listening on the provided socket for incoming + * plugin host requests. + * + * @param gruop_socket_path The path to the group socket endpoint. This path + * should be in the form of + * `/tmp/yabridge-group---` + * where `` is a numerical hash as explained in the + * `create_logger_prefix()` function in `./group.cpp`. + * + * @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. + */ + GroupBridge(boost::filesystem::path group_socket_path); + + /** + * 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. + * + * 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 + * terminate this process. + * + * @param parameters Information about the plugin to launch, i.e. the path + * to the plugin and the path of the socket endpoint that will be used for + * communication. + * + * @note In the case that the process starts but no plugin gets initiated, + * then the process will never exit on its own. This should not happen + * though. + */ + void handle_host_plugin(const PluginParameters& parameters); + + /** + * Listen for new requests to spawn plugins within this process and handle + * them accordingly. Will terminate once all plugins have exited. + */ + void handle_incoming_connections(); + + private: + /** + * 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`. + * + * TODO: Merge this with `PluginBridge::async_log_pipe_lines` + * + * @param pipe The pipe to read from. + * @param buffer The stream buffer to write to. + * @param prefix Text to prepend to the line before writing to the log. + */ + void async_log_pipe_lines(boost::asio::posix::stream_descriptor& pipe, + boost::asio::streambuf& buffer, + std::string prefix); + + /** + * The logging facility used for this group host process. Since we can't + * identify which plugin is generating (debug) output, every line will only + * be prefixed with the name of the group. + */ + Logger logger; + + boost::asio::io_context io_context; + + boost::asio::streambuf stdout_buffer; + boost::asio::streambuf stderr_buffer; + /** + * Contains a pipe used for capturing this process's STDOUT stream. Needed + * to be able to process the output generated by Wine and plugins and to be + * able write it write it to an external log file. + */ + StdIoCapture stdout_redirect; + /** + * Contains a pipe used for capturing this process's STDERR stream. Needed + * to be able to process the output generated by Wine and plugins and to be + * able write it write it to an external log file. + */ + StdIoCapture stderr_redirect; + + boost::asio::local::stream_protocol::endpoint group_socket_endpoint; + /** + * The UNIX domain socket acceptor that will be used to listen for incoming + * connections to spawn new plugins within this process. + */ + boost::asio::local::stream_protocol::acceptor group_socket_acceptor; + + /** + * 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. + */ + 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 + * process because it thinks there are no active plugins left just as a new + * plugin is being spawned. + */ + std::mutex active_plugins_mutex; +}; diff --git a/src/wine-host/group-host.cpp b/src/wine-host/group-host.cpp index ff846183..38b8d7f4 100644 --- a/src/wine-host/group-host.cpp +++ b/src/wine-host/group-host.cpp @@ -16,12 +16,7 @@ #include "boost-fix.h" -#include -#include -#include #include -#include -#include // Generated inside of build directory #include @@ -30,27 +25,6 @@ #include "bridges/group.h" #include "bridges/vst2.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; - -// TODO: Move most plumbing to another file - -/** - * Create a logger prefix containing the group name based on the socket path. - */ -std::string create_logger_prefix(const fs::path& socket_path); - -/** - * Continuously Read from a stream and write the lines to a logger instance. - * - * TODO: Merge this with the other similar function in `PluginBridge` - */ -void log_lines(Logger& logger, - boost::asio::posix::stream_descriptor& pipe, - boost::asio::streambuf& buffer, - std::string prefix); - /** * This works very similar to the host application defined in * `individual-host.cpp`, but instead of just loading a single plugin this will @@ -82,84 +56,31 @@ int __cdecl main(int argc, char* argv[]) { const std::string group_socket_endpoint_path(argv[1]); - // TODO: Before doing anything, try listening on the socket and fail - // silently (or log a message?) if another application is already - // listening on the socket. This way we don't need any complicated - // inter-process synchronization to ensure that there is a single - // active group host listening for this group. - - // Has to be initialized before redirecting the STDIO streams - Logger logger = Logger::create_from_environment( - create_logger_prefix(group_socket_endpoint_path)); - - // Redirect this process's STDOUT and STDERR streams to a pipe so we can - // process the output and redirect it to a logger. Needed to capture Wine - // debug output, since this process will likely outlive the yabridge - // instance that originally spawned it. - boost::asio::io_context io_context; - boost::asio::streambuf stdout_buffer; - boost::asio::streambuf stderr_buffer; - StdIoCapture stdout_redirect(io_context, STDOUT_FILENO); - StdIoCapture stderr_redirect(io_context, STDERR_FILENO); - log_lines(logger, stdout_redirect.pipe, stdout_buffer, "[STDOUT] "); - log_lines(logger, stderr_redirect.pipe, stderr_buffer, "[STDERR] "); - - std::thread io_handler([&]() { io_context.run(); }); - - logger.log("Initializing yabridge group host version " + - std::string(yabridge_git_version) + // 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 #ifdef __i386__ - + " (32-bit compatibility mode)" + << " (32-bit compatibility mode)" #endif - ); + << std::endl; - // TODO: Remove debug prints - printf("This should be caught now!\n"); - std::cerr << "This too!" << std::endl; + GroupBridge bridge(group_socket_endpoint_path); - // TODO: After initializing, listen for connections and spawn plugins - // the exact same way as what happens in `individual-host.cpp` - // TODO: Allow this process to exit when the last plugin exits. Make sure - // that that doesn't cause any race conditions. + // 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. + // TODO: Check if this is printed on the right stream + std::cerr << "Error while initializing the group host process:" + << std::endl; + std::cerr << error.what() << std::endl; - // TODO: This usleep() is just to ensure that the second print to stderr - // also gets processed before stopping the IO context since we're - // immediately stopping it after starting. This would not needed in - // normal use. - usleep(1000); - io_context.stop(); - io_handler.join(); -} - -void log_lines(Logger& logger, - boost::asio::posix::stream_descriptor& pipe, - boost::asio::streambuf& buffer, - std::string prefix) { - boost::asio::async_read_until(pipe, buffer, '\n', - [&, prefix](const auto&, size_t) { - std::string line; - std::getline(std::istream(&buffer), line); - logger.log(prefix + line); - - log_lines(logger, pipe, buffer, prefix); - }); -} - -std::string create_logger_prefix(const fs::path& socket_path) { - // The group socket filename will be in the format - // '/tmp/yabridge-group---.sock', - // where Wine prefix ID is just Wine prefix ran through `std::hash` to - // prevent collisions without needing complicated filenames. We want to - // extract the group name. - std::string socket_name = - socket_path.filename().replace_extension().string(); - - std::smatch group_match; - std::regex group_regexp("^yabridge-group-(.*)-[^-]+-[^-]+$", - std::regex::ECMAScript); - if (std::regex_match(socket_name, group_match, group_regexp)) { - socket_name = group_match[1].str(); + return 1; } - - return "[" + socket_name + "] "; }