mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-06-20 19:03:56 +02:00
Move all plugin group handling boilerplate
This commit is contained in:
@@ -121,6 +121,7 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
|
|||||||
std::thread([&]() {
|
std::thread([&]() {
|
||||||
using namespace std::literals::chrono_literals;
|
using namespace std::literals::chrono_literals;
|
||||||
|
|
||||||
|
// TODO: Figure out how this check would work with plugin gruops
|
||||||
while (true) {
|
while (true) {
|
||||||
if (finished_accepting_sockets) {
|
if (finished_accepting_sockets) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -17,6 +17,17 @@
|
|||||||
#include "group.h"
|
#include "group.h"
|
||||||
|
|
||||||
#include <unistd.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,
|
StdIoCapture::StdIoCapture(boost::asio::io_context& io_context,
|
||||||
int file_descriptor)
|
int file_descriptor)
|
||||||
@@ -42,3 +53,85 @@ StdIoCapture::~StdIoCapture() {
|
|||||||
close(original_fd_copy);
|
close(original_fd_copy);
|
||||||
close(pipe_fd[0]);
|
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 + "] ";
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,25 @@
|
|||||||
#include "../boost-fix.h"
|
#include "../boost-fix.h"
|
||||||
|
|
||||||
#include <boost/asio/posix/stream_descriptor.hpp>
|
#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
|
* Encapsulate capturing the STDOUT or STDERR stream by opening a pipe and
|
||||||
@@ -76,3 +95,127 @@ class StdIoCapture {
|
|||||||
*/
|
*/
|
||||||
int pipe_fd[2];
|
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;
|
||||||
|
};
|
||||||
|
|||||||
+21
-100
@@ -16,12 +16,7 @@
|
|||||||
|
|
||||||
#include "boost-fix.h"
|
#include "boost-fix.h"
|
||||||
|
|
||||||
#include <boost/asio/read_until.hpp>
|
|
||||||
#include <boost/asio/streambuf.hpp>
|
|
||||||
#include <boost/filesystem.hpp>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <regex>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
// Generated inside of build directory
|
// Generated inside of build directory
|
||||||
#include <src/common/config/config.h>
|
#include <src/common/config/config.h>
|
||||||
@@ -30,27 +25,6 @@
|
|||||||
#include "bridges/group.h"
|
#include "bridges/group.h"
|
||||||
#include "bridges/vst2.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
|
* This works very similar to the host application defined in
|
||||||
* `individual-host.cpp`, but instead of just loading a single plugin this will
|
* `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]);
|
const std::string group_socket_endpoint_path(argv[1]);
|
||||||
|
|
||||||
// TODO: Before doing anything, try listening on the socket and fail
|
// TODO: Catch exception during initialization when another process is
|
||||||
// silently (or log a message?) if another application is already
|
// already listening on the socket. Make sure to print a more useful
|
||||||
// listening on the socket. This way we don't need any complicated
|
// message instead.
|
||||||
// inter-process synchronization to ensure that there is a single
|
try {
|
||||||
// active group host listening for this group.
|
std::cerr << "Initializing yabridge group host version "
|
||||||
|
<< yabridge_git_version
|
||||||
// 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)
|
|
||||||
#ifdef __i386__
|
#ifdef __i386__
|
||||||
+ " (32-bit compatibility mode)"
|
<< " (32-bit compatibility mode)"
|
||||||
#endif
|
#endif
|
||||||
);
|
<< std::endl;
|
||||||
|
|
||||||
// TODO: Remove debug prints
|
GroupBridge bridge(group_socket_endpoint_path);
|
||||||
printf("This should be caught now!\n");
|
|
||||||
std::cerr << "This too!" << std::endl;
|
|
||||||
|
|
||||||
// TODO: After initializing, listen for connections and spawn plugins
|
// Blocks the main thread until all plugins have exited
|
||||||
// the exact same way as what happens in `individual-host.cpp`
|
bridge.handle_incoming_connections();
|
||||||
// TODO: Allow this process to exit when the last plugin exits. Make sure
|
} catch (const std::runtime_error& error) {
|
||||||
// that that doesn't cause any race conditions.
|
// 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
|
return 1;
|
||||||
// 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-<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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "[" + socket_name + "] ";
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user