Move all plugin group handling boilerplate

This commit is contained in:
Robbert van der Helm
2020-05-19 15:29:48 +02:00
parent daad6f2f00
commit 6d6d928838
4 changed files with 258 additions and 100 deletions
+93
View File
@@ -17,6 +17,17 @@
#include "group.h"
#include <unistd.h>
#include <boost/asio/read_until.hpp>
#include <regex>
// 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-<group_name>-<wine_prefix_id>-<architecture>.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 + "] ";
}
+143
View File
@@ -19,6 +19,25 @@
#include "../boost-fix.h"
#include <boost/asio/posix/stream_descriptor.hpp>
#include <boost/asio/streambuf.hpp>
#include <boost/filesystem.hpp>
#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<PluginParameters> {
std::size_t operator()(PluginParameters const& params) const noexcept {
return std::hash<int>{}(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-<group_name>-<wine_prefix_id>-<architecture>`
* where `<wine_prefix_id>` 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<PluginParameters,
std::pair<Win32Thread, std::unique_ptr<Vst2Bridge>>>
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;
};