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:
Robbert van der Helm
2021-05-18 17:55:39 +02:00
parent 99428ba28e
commit 95badeb1bc
6 changed files with 128 additions and 41 deletions
+17 -9
View File
@@ -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` | `<number>` | 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,<string>}` | 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` | `<number>` | 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
+17
View File
@@ -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();
+15
View File
@@ -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);
+11 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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;