mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-08 12:30:12 +02:00
Add a way to disable pipes for the Wine host
For some reason ujam plugins (and other plugins made with the Gorilla Engine, like the LoopCloud plugins) will throw a `JS_EXEC_FAILED` error when trying to load the plugin while either of the STDOUT or STDERR streams is pointing to a pipe. Simply redirecting the output to a file fixes this. By default we'll write the output to `<temporary_directory>/yabridge-plugin-output.log`, but you can also set the new `disable_pipes` option to `"/dev/null"` to completely throw away all output. This addresses #47.
This commit is contained in:
@@ -24,6 +24,8 @@
|
||||
#include <toml++/toml.h>
|
||||
#include <fstream>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
Configuration::Configuration() noexcept {}
|
||||
@@ -84,6 +86,21 @@ Configuration::Configuration(const fs::path& config_path,
|
||||
} else {
|
||||
invalid_options.push_back(key);
|
||||
}
|
||||
} else if (key == "disable_pipes") {
|
||||
// This option can be either enabled or disable with a boolean,
|
||||
// or it can be set to an absolute path
|
||||
if (const auto parsed_value = value.as_boolean()) {
|
||||
if (*parsed_value) {
|
||||
disable_pipes = get_temporary_directory() /
|
||||
"yabridge-plugin-output.log";
|
||||
} else {
|
||||
disable_pipes = std::nullopt;
|
||||
}
|
||||
} else if (const auto parsed_value = value.as_string()) {
|
||||
disable_pipes = parsed_value->get();
|
||||
} else {
|
||||
invalid_options.push_back(key);
|
||||
}
|
||||
} else if (key == "editor_double_embed") {
|
||||
if (const auto parsed_value = value.as_boolean()) {
|
||||
editor_double_embed = parsed_value->get();
|
||||
|
||||
@@ -81,6 +81,19 @@ class Configuration {
|
||||
*/
|
||||
std::optional<std::string> group;
|
||||
|
||||
/**
|
||||
* If enabled, we'll redirect the plugin's STDOUT and STDERR streams to this
|
||||
* file instead of using pipes to intersperse it with yabridge's other
|
||||
* output. This is necessary for _ujam_ plugins to work since they for some
|
||||
* reason will throw `JS_EXEC_FAILED` errors when either STDOUT or STDERR is
|
||||
* a pipe.
|
||||
*
|
||||
* This option can be set to a boolean, in which case we'll set the path to
|
||||
* `<temporary_directory>/yabridge-plugin-output.log`, or it can be set to
|
||||
* an absolute path. (we don't try to expand tildes)
|
||||
*/
|
||||
std::optional<boost::filesystem::path> disable_pipes;
|
||||
|
||||
/**
|
||||
* If this is set to `true`, then the plugin editor should be embedded in
|
||||
* yet another window. This would result in an embedding sequence of
|
||||
@@ -187,6 +200,8 @@ class Configuration {
|
||||
s.ext(group, bitsery::ext::StdOptional(),
|
||||
[](S& s, auto& v) { s.text1b(v, 4096); });
|
||||
|
||||
s.ext(disable_pipes, bitsery::ext::StdOptional(),
|
||||
[](S& s, auto& v) { s.ext(v, bitsery::ext::BoostPath{}); });
|
||||
s.value1b(editor_double_embed);
|
||||
s.value1b(editor_force_dnd);
|
||||
s.value1b(editor_xembed);
|
||||
|
||||
@@ -72,26 +72,27 @@ class PluginBridge {
|
||||
? std::unique_ptr<HostProcess>(std::make_unique<GroupHost>(
|
||||
io_context,
|
||||
generic_logger,
|
||||
config,
|
||||
sockets,
|
||||
info,
|
||||
HostRequest{
|
||||
.plugin_type = plugin_type,
|
||||
.plugin_path = info.windows_plugin_path.string(),
|
||||
.endpoint_base_dir = sockets.base_dir.string(),
|
||||
.parent_pid = getpid()},
|
||||
sockets,
|
||||
*config.group))
|
||||
.parent_pid = getpid()}))
|
||||
: std::unique_ptr<HostProcess>(
|
||||
std::make_unique<IndividualHost>(
|
||||
io_context,
|
||||
generic_logger,
|
||||
config,
|
||||
sockets,
|
||||
info,
|
||||
HostRequest{
|
||||
.plugin_type = plugin_type,
|
||||
.plugin_path =
|
||||
info.windows_plugin_path.string(),
|
||||
.endpoint_base_dir = sockets.base_dir.string(),
|
||||
.parent_pid = getpid()},
|
||||
sockets))),
|
||||
.parent_pid = getpid()}))),
|
||||
has_realtime_priority(has_realtime_priority_promise.get_future()),
|
||||
wine_io_handler([&]() {
|
||||
// We no longer run this thread with realtime scheduling because
|
||||
@@ -177,6 +178,11 @@ class PluginBridge {
|
||||
|
||||
init_msg << "other options: ";
|
||||
std::vector<std::string> other_options;
|
||||
if (config.disable_pipes) {
|
||||
other_options.push_back(
|
||||
"hack: pipes disabled, plugin output will go to \"" +
|
||||
config.disable_pipes->string() + "\"");
|
||||
}
|
||||
if (config.editor_double_embed) {
|
||||
other_options.push_back("editor: double embed");
|
||||
}
|
||||
|
||||
+28
-15
@@ -27,26 +27,39 @@ namespace fs = boost::filesystem;
|
||||
|
||||
HostProcess::HostProcess(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
const Configuration& config,
|
||||
Sockets& sockets)
|
||||
: stdout_pipe(io_context),
|
||||
stderr_pipe(io_context),
|
||||
config(config),
|
||||
sockets(sockets),
|
||||
logger(logger) {
|
||||
// Print the Wine host's STDOUT and STDERR streams to the log file. This
|
||||
// should be done before trying to accept the sockets as otherwise we will
|
||||
// miss all output.
|
||||
logger.async_log_pipe_lines(stdout_pipe, stdout_buffer, "[Wine STDOUT] ");
|
||||
logger.async_log_pipe_lines(stderr_pipe, stderr_buffer, "[Wine STDERR] ");
|
||||
// See the comment above the `on_exec_setup` in `launch_host()`
|
||||
if (config.disable_pipes) {
|
||||
logger.log("");
|
||||
logger.log("WARNING: All Wine output will be written to");
|
||||
logger.log(" '" + config.disable_pipes->string() + "'.");
|
||||
logger.log("");
|
||||
} else {
|
||||
// Print the Wine host's STDOUT and STDERR streams to the log file. This
|
||||
// should be done before trying to accept the sockets as otherwise we
|
||||
// will miss all output.
|
||||
logger.async_log_pipe_lines(stdout_pipe, stdout_buffer,
|
||||
"[Wine STDOUT] ");
|
||||
logger.async_log_pipe_lines(stderr_pipe, stderr_buffer,
|
||||
"[Wine STDERR] ");
|
||||
}
|
||||
}
|
||||
|
||||
HostProcess::~HostProcess() noexcept {}
|
||||
|
||||
IndividualHost::IndividualHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
const Configuration& config,
|
||||
Sockets& sockets,
|
||||
const PluginInfo& plugin_info,
|
||||
const HostRequest& host_request,
|
||||
Sockets& sockets)
|
||||
: HostProcess(io_context, logger, sockets),
|
||||
const HostRequest& host_request)
|
||||
: HostProcess(io_context, logger, config, sockets),
|
||||
plugin_info(plugin_info),
|
||||
host_path(find_vst_host(plugin_info.native_library_path,
|
||||
plugin_info.plugin_arch,
|
||||
@@ -103,11 +116,11 @@ void IndividualHost::terminate() {
|
||||
|
||||
GroupHost::GroupHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
const PluginInfo& plugin_info,
|
||||
const HostRequest& host_request,
|
||||
const Configuration& config,
|
||||
Sockets& sockets,
|
||||
std::string group_name)
|
||||
: HostProcess(io_context, logger, sockets),
|
||||
const PluginInfo& plugin_info,
|
||||
const HostRequest& host_request)
|
||||
: HostProcess(io_context, logger, config, sockets),
|
||||
plugin_info(plugin_info),
|
||||
host_path(find_vst_host(plugin_info.native_library_path,
|
||||
plugin_info.plugin_arch,
|
||||
@@ -128,9 +141,9 @@ GroupHost::GroupHost(boost::asio::io_context& io_context,
|
||||
// will try to connect to the socket once more in the case that another
|
||||
// process is now listening on it.
|
||||
const fs::path endpoint_base_dir = sockets.base_dir;
|
||||
const fs::path group_socket_path =
|
||||
generate_group_endpoint(group_name, plugin_info.normalize_wine_prefix(),
|
||||
plugin_info.plugin_arch);
|
||||
const fs::path group_socket_path = generate_group_endpoint(
|
||||
*config.group, plugin_info.normalize_wine_prefix(),
|
||||
plugin_info.plugin_arch);
|
||||
const auto connect = [&io_context, host_request, endpoint_base_dir,
|
||||
group_socket_path]() {
|
||||
boost::asio::local::stream_protocol::socket group_socket(io_context);
|
||||
|
||||
+40
-12
@@ -90,12 +90,31 @@ class HostProcess {
|
||||
// restarting after an unexpected shutdown. Because of this we
|
||||
// won't use `vfork()`, but instead we'll just manually close
|
||||
// all non-STDIO file descriptors.
|
||||
// HACK: If the `disable_pipes` option is enabled, then we'll
|
||||
// redirect the plugin's output to a file instead of using
|
||||
// pipes to blend it in with the rest of yabridge's output.
|
||||
// This is for some reason necessary for ujam's plugins and
|
||||
// all other plugins made with Gorilla Engine to function.
|
||||
// Otherwise they'll print a nondescriptive `JS_EXEC_FAILED`
|
||||
// error message.
|
||||
boost::process::extend::on_exec_setup =
|
||||
[this](auto& /*executor*/) {
|
||||
const int max_fds = static_cast<int>(sysconf(_SC_OPEN_MAX));
|
||||
for (int fd = STDERR_FILENO + 1; fd < max_fds; fd++) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
// See above
|
||||
if (config.disable_pipes) {
|
||||
const int redirect_fd =
|
||||
open(config.disable_pipes->c_str(),
|
||||
O_CREAT | O_APPEND | O_WRONLY, 0640);
|
||||
|
||||
assert(redirect_fd != -1);
|
||||
dup2(redirect_fd, STDOUT_FILENO);
|
||||
dup2(redirect_fd, STDERR_FILENO);
|
||||
close(redirect_fd);
|
||||
}
|
||||
},
|
||||
std::forward<Args>(args)...);
|
||||
}
|
||||
@@ -114,6 +133,7 @@ class HostProcess {
|
||||
*/
|
||||
HostProcess(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
const Configuration& config,
|
||||
Sockets& sockets);
|
||||
|
||||
/**
|
||||
@@ -125,6 +145,11 @@ class HostProcess {
|
||||
*/
|
||||
patched_async_pipe stderr_pipe;
|
||||
|
||||
/**
|
||||
* The current plugin instance's configuration.
|
||||
*/
|
||||
const Configuration& config;
|
||||
|
||||
/**
|
||||
* The associated sockets for the plugin we're hosting. This is used to
|
||||
* terminate the plugin.
|
||||
@@ -154,23 +179,25 @@ class IndividualHost : public HostProcess {
|
||||
* handled on.
|
||||
* @param logger The `Logger` instance the redirected STDIO streams will be
|
||||
* written to.
|
||||
* @param config The configuration for this plugin instance.
|
||||
* @param sockets The socket endpoints that will be used for communication
|
||||
* with the plugin. When the plugin shuts down, we'll close all of the
|
||||
* sockets used by the plugin.
|
||||
* @param plugin_info Information about the plugin we're going to use. Used
|
||||
* to retrieve the Wine prefix and the plugin's architecture.
|
||||
* @param host_request The information about the plugin we should launch a
|
||||
* host process for. The values in the struct will be used as command line
|
||||
* arguments.
|
||||
* @param sockets The socket endpoints that will be used for communication
|
||||
* with the plugin. When the plugin shuts down, we'll close all of the
|
||||
* sockets used by the plugin.
|
||||
*
|
||||
* @throw std::runtime_error When `plugin_path` does not point to a valid
|
||||
* 32-bit or 64-bit .dll file.
|
||||
*/
|
||||
IndividualHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
const Configuration& config,
|
||||
Sockets& sockets,
|
||||
const PluginInfo& plugin_info,
|
||||
const HostRequest& host_request,
|
||||
Sockets& sockets);
|
||||
const HostRequest& host_request);
|
||||
|
||||
boost::filesystem::path path() override;
|
||||
bool running() noexcept override;
|
||||
@@ -203,21 +230,22 @@ class GroupHost : public HostProcess {
|
||||
* handled on.
|
||||
* @param logger The `Logger` instance the redirected STDIO streams will be
|
||||
* written to.
|
||||
* @param config The configuration for this plugin instance. The group name
|
||||
* will be retrieved from here.
|
||||
* @param sockets The socket endpoints that will be used for communication
|
||||
* with the plugin. When the plugin shuts down, we'll close all of the
|
||||
* sockets used by the plugin.
|
||||
* @param plugin_info Information about the plugin we're going to use. Used
|
||||
* to retrieve the Wine prefix and the plugin's architecture.
|
||||
* @param host_request The information about the plugin we should launch a
|
||||
* host process for. This object will be sent to the group host process.
|
||||
* @param sockets The socket endpoints that will be used for communication
|
||||
* with the plugin. When the plugin shuts down, we'll close all of the
|
||||
* sockets used by the plugin.
|
||||
* @param group_name The name of the plugin group.
|
||||
*/
|
||||
GroupHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
const PluginInfo& plugin_info,
|
||||
const HostRequest& host_request,
|
||||
const Configuration& config,
|
||||
Sockets& sockets,
|
||||
std::string group_name);
|
||||
const PluginInfo& plugin_info,
|
||||
const HostRequest& host_request);
|
||||
|
||||
boost::filesystem::path path() override;
|
||||
bool running() noexcept override;
|
||||
|
||||
Reference in New Issue
Block a user