From 95badeb1bc319dd9b855af68651349cfbc76cee3 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 18 May 2021 17:55:39 +0200 Subject: [PATCH] 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 `/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. --- README.md | 26 +++++++++++------- src/common/configuration.cpp | 17 ++++++++++++ src/common/configuration.h | 15 +++++++++++ src/plugin/bridges/common.h | 16 +++++++---- src/plugin/host-process.cpp | 43 ++++++++++++++++++----------- src/plugin/host-process.h | 52 +++++++++++++++++++++++++++--------- 6 files changed, 128 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index f254edf0..b37727c7 100644 --- a/README.md +++ b/README.md @@ -329,15 +329,16 @@ plugin._ #### Compatibility options -| Option | Values | Description | -| --------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `editor_double_embed` | `{true,false}` | Compatibility option for plugins that rely on the absolute screen coordinates of the window they're embedded in. Since the Wine window gets embedded inside of a window provided by your DAW, these coordinates won't match up and the plugin would end up drawing in the wrong location without this option. Currently the only known plugins that require this option are _PSPaudioware_ plugins with expandable GUIs, such as E27. Defaults to `false`. | -| `editor_force_dnd` | `{true,false}` | This option forcefully enables drag-and-drop support in _REAPER_. Because REAPER's FX window supports drag-and-drop itself, dragging a file onto a plugin editor will cause the drop to be intercepted by the FX window. This makes it impossible to drag files onto plugins in REAPER under normal circumstances. Setting this option to `true` will strip drag-and-drop support from the FX window, thus allowing files to be dragged onto the plugin again. Defaults to `false`. | -| `editor_xembed` | `{true,false}` | Use Wine's XEmbed implementation instead of yabridge's normal window embedding method. Some plugins will have redrawing issues when using XEmbed and editor resizing won't always work properly with it, but it could be useful in certain setups. You may need to use [this Wine patch](https://github.com/psycha0s/airwave/blob/master/fix-xembed-wine-windows.patch) if you're getting blank editor windows. Defaults to `false`. | -| `frame_rate` | `` | The rate at which Win32 events are being handled and usually also the refresh rate of a plugin's editor GUI. When using plugin groups all plugins share the same event handling loop, so in those the last loaded plugin will set the refresh rate. Defaults to `60`. | -| `hide_daw` | `{true,false}` | Don't report the name of the actual DAW to the plugin. See the [known issues](#runtime-dependencies-and-known-issues) section for a list of situations where this may be useful. This affects both VST2 and VST3 plugins. Defaults to `false`. | -| `vst3_no_scaling` | `{true,false}` | Disable HiDPI scaling for VST3 plugins. Wine currently does not have proper fractional HiDPI support, so you might have to enable this option if you're using a HiDPI display. In most cases setting the font DPI in `winecfg`'s graphics tab to 192 will cause plugins to scale correctly at 200% size. Defaults to `false`. | -| `vst3_prefer_32bit` | `{true,false}` | Use the 32-bit version of a VST3 plugin instead the 64-bit version if both are installed and they're in the same VST3 bundle inside of `~/.vst3/yabridge`. You likely won't need this. | +| Option | Values | Description | +| --------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `disable_pipes` | `{true,false,}` | When this option is enabled, yabridge will redirect the Wine plugin host's output streams to a file without any further processing. See the [known issues](#runtime-dependencies-and-known-issues) section for a list of plugins where this may be useful. This can be set to a boolean, in which case the output will be written to `$XDG_RUNTIME_DIR/yabridge-plugin-output.log`, or to an absolute path (with no expansion for tildes or environment variables). Defaults to `false`. _This option is only available on the master branch._ | +| `editor_double_embed` | `{true,false}` | Compatibility option for plugins that rely on the absolute screen coordinates of the window they're embedded in. Since the Wine window gets embedded inside of a window provided by your DAW, these coordinates won't match up and the plugin would end up drawing in the wrong location without this option. Currently the only known plugins that require this option are _PSPaudioware_ plugins with expandable GUIs, such as E27. Defaults to `false`. | +| `editor_force_dnd` | `{true,false}` | This option forcefully enables drag-and-drop support in _REAPER_. Because REAPER's FX window supports drag-and-drop itself, dragging a file onto a plugin editor will cause the drop to be intercepted by the FX window. This makes it impossible to drag files onto plugins in REAPER under normal circumstances. Setting this option to `true` will strip drag-and-drop support from the FX window, thus allowing files to be dragged onto the plugin again. Defaults to `false`. | +| `editor_xembed` | `{true,false}` | Use Wine's XEmbed implementation instead of yabridge's normal window embedding method. Some plugins will have redrawing issues when using XEmbed and editor resizing won't always work properly with it, but it could be useful in certain setups. You may need to use [this Wine patch](https://github.com/psycha0s/airwave/blob/master/fix-xembed-wine-windows.patch) if you're getting blank editor windows. Defaults to `false`. | +| `frame_rate` | `` | The rate at which Win32 events are being handled and usually also the refresh rate of a plugin's editor GUI. When using plugin groups all plugins share the same event handling loop, so in those the last loaded plugin will set the refresh rate. Defaults to `60`. | +| `hide_daw` | `{true,false}` | Don't report the name of the actual DAW to the plugin. See the [known issues](#runtime-dependencies-and-known-issues) section for a list of situations where this may be useful. This affects both VST2 and VST3 plugins. Defaults to `false`. | +| `vst3_no_scaling` | `{true,false}` | Disable HiDPI scaling for VST3 plugins. Wine currently does not have proper fractional HiDPI support, so you might have to enable this option if you're using a HiDPI display. In most cases setting the font DPI in `winecfg`'s graphics tab to 192 will cause plugins to scale correctly at 200% size. Defaults to `false`. | +| `vst3_prefer_32bit` | `{true,false}` | Use the 32-bit version of a VST3 plugin instead the 64-bit version if both are installed and they're in the same VST3 bundle inside of `~/.vst3/yabridge`. You likely won't need this. | These options are workarounds for issues mentioned in the [known issues](#runtime-dependencies-and-known-issues) section. Depending on the hosts @@ -375,6 +376,9 @@ hide_daw = true editor_force_dnd = true frame_rate = 24 +["LoopCloud*"] +disable_pipes = true + # Simple glob patterns can be used to avoid unneeded repetition ["iZotope*/Neutron *"] group = "izotope" @@ -468,6 +472,10 @@ include: then you can try enabling [software rendering](https://forum.scalerplugin.com/t/scaler-2-black-empty-window/3540/8) to fix these issues. +- **ujam** plugins and other plugins made with the Gorilla Engine, such as the + **LoopCloud** plugins, will throw a `JS_EXEC_FAILED` error when trying to load + the plugin. Enabling the `disable_pipes` [compatibility + option](#compatibility-options) for those plugins will fix this. - Plugins by **KiloHearts** have file descriptor leaks when _esync_ is enabled, causing Wine and yabridge to eventually stop working after the system hits the open file limit. To fix this, either unset `WINEESYNC` while using yabridge or diff --git a/src/common/configuration.cpp b/src/common/configuration.cpp index adb0074b..c71998be 100644 --- a/src/common/configuration.cpp +++ b/src/common/configuration.cpp @@ -24,6 +24,8 @@ #include #include +#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(); diff --git a/src/common/configuration.h b/src/common/configuration.h index 92607dfd..c9b91af4 100644 --- a/src/common/configuration.h +++ b/src/common/configuration.h @@ -81,6 +81,19 @@ class Configuration { */ std::optional 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 + * `/yabridge-plugin-output.log`, or it can be set to + * an absolute path. (we don't try to expand tildes) + */ + std::optional 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); diff --git a/src/plugin/bridges/common.h b/src/plugin/bridges/common.h index 02e06558..155fd1c9 100644 --- a/src/plugin/bridges/common.h +++ b/src/plugin/bridges/common.h @@ -72,26 +72,27 @@ class PluginBridge { ? std::unique_ptr(std::make_unique( 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( std::make_unique( 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 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"); } diff --git a/src/plugin/host-process.cpp b/src/plugin/host-process.cpp index 8ef3264b..3ea1b14f 100644 --- a/src/plugin/host-process.cpp +++ b/src/plugin/host-process.cpp @@ -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); diff --git a/src/plugin/host-process.h b/src/plugin/host-process.h index 4e28c562..745ffebc 100644 --- a/src/plugin/host-process.h +++ b/src/plugin/host-process.h @@ -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(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)...); } @@ -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;