Files
yabridge/tools/yabridgectl/src/utils.rs
T
Robbert van der Helm 6bba1a9bf8 Check for Wine and yabridge compatibility on sync
This will probably solve the last common setup issue. Starting Wine can
take a second so this check is only repeated when either Wine or
yabridge get updated.
2020-07-19 16:57:18 +02:00

263 lines
11 KiB
Rust

// yabridge: a Wine VST bridge
// Copyright (C) 2020 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Small helper utilities.
use anyhow::{Context, Result};
use colored::Colorize;
use std::collections::hash_map::DefaultHasher;
use std::env;
use std::fs;
use std::hash::Hasher;
use std::os::unix::fs as unix_fs;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Command, Stdio};
use textwrap::Wrapper;
use crate::config::{Config, KnownConfig};
/// (Part of) the expected output when running `yabridge-host.exe`. Used to verify that everything's
/// working correctly.
const YABRIDGE_HOST_EXPECTED_OUTPUT: &str =
"Usage: yabridge-host.exe <vst_plugin_dll> <unix_domain_socket>";
/// Wrapper around [`std::fs::copy()`](std::fs::copy) with a human readable error message.
pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
fs::copy(&from, &to).with_context(|| {
format!(
"Error copying '{}' to '{}'",
from.as_ref().display(),
to.as_ref().display()
)
})
}
/// Wrapper around [`std::fs::remove_file()`](std::fs::remove_file) with a human readable error
/// message.
pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> {
fs::remove_file(&path)
.with_context(|| format!("Could not remove '{}'", path.as_ref().display()))
}
/// Wrapper around [`std::os::unix::fs::symlink()`](std::os::unix::fs::symlink) with a human
/// readable error message.
pub fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
unix_fs::symlink(&src, &dst).with_context(|| {
format!(
"Error symlinking '{}' to '{}'",
src.as_ref().display(),
dst.as_ref().display()
)
})
}
/// Verify that `yabridge-host.exe` is accessible in a login shell. Returns unit if it is, or if we
/// the login shell is set to an unknown shell. In the last case we'll just print a warning since we
/// don't know how to invoke the shell as a login shell. This is needed when using copies to ensure
/// that yabridge can find the host binaries when the VST host is launched from the desktop
/// enviornment.
///
/// When we could not find `yabridge-host.exe`, we'll return `Err(shell_name)` so we can print a
/// descriptive warning message.
pub fn verify_path_setup() -> Result<(), String> {
match env::var("SHELL") {
Ok(shell_path) => {
// `$SHELL` will often contain a full path, but it doesn't have to
let shell = Path::new(&shell_path)
.file_name()
.and_then(|os_str| os_str.to_str())
.unwrap_or_else(|| shell_path.as_str());
// We're using the `-l` flag present in most shells to start a login shell, but some
// shells don't have this option. According the Bash's man page, another method some
// shells use to determine that they're being run as a login shell is by checking that
// `argv[0]` starts with a hyphen, so we'll also do that.
let mut command = Command::new(&shell_path);
command.arg0(format!("-{}", &shell_path));
let command = match shell {
// All of these shells support the `-l` flag to start a login shell and have a
// POSIX-compatible `command` builtin
"ash" | "bash" | "csh" | "ksh" | "dash" | "fish" | "ion" | "sh" | "tcsh"
| "zsh" => command
.arg("-l")
.arg("-c")
.arg("command -v yabridge-host.exe"),
// These shells either have their own implementation of `which` and don't support
// `command`, or they don't have a seperate login shell flag
"elvish" | "oil" => command.arg("-c").arg("command -v yabridge-host.exe"),
// xonsh's which implementation is broken as of writing this, so I left it out
"pwsh" => command.arg("-l").arg("-c").arg("which yabridge-host.exe"),
"nu" => command.arg("-c").arg("which yabridge-host.exe"),
shell => {
eprintln!(
"\n{}",
wrap(&format!(
"WARNING: Yabridgectl does not know how to handle your login shell \
'{}', skipping PATH environment variable check. Feel free to open a \
feature request in order to get yabridgectl to support your shell.\n\
\n\
https://github.com/robbert-vdh/yabridge/issues",
shell.bright_white(),
))
);
return Ok(());
}
};
// For the login shell we want to a clean environment, but we still have to set `$HOME`
// or else most shells won't know which profile to load
command
.env_clear()
.env("HOME", env::var("HOME").unwrap_or_default());
match command.stdout(Stdio::null()).stderr(Stdio::null()).status() {
Ok(status) if status.success() => Ok(()),
Ok(_) => Err(shell.to_string()),
Err(err) => {
eprintln!(
"\n{}",
wrap(&format!(
"Warning: could not run {} as a login shell, skipping PATH setup check: \
{}",
shell.bright_white(), err
))
);
Ok(())
}
}
}
Err(_) => {
eprintln!("\nWarning: Could not determine login shell, skipping PATH setup check");
Ok(())
}
}
}
/// Verify that the installed versions of Wine and yabridge will work together properly. This check
/// is only performed once per combination of Wine and yabridge, and we'll update the config with
/// the versions we just tested if the check succeeds. Will return `Err` values if either Wine or
/// `yabridge-host.exe` can't be run.
pub fn verify_wine_setup(config: &mut Config) -> Result<()> {
// These winelib scripts respect `$WINELOADER`, so we'll do the same thing
let wine_binary = env::var("WINELOADER").unwrap_or_else(|_| String::from("wine"));
let wine_version_output = Command::new(&wine_binary)
.arg("--version")
.output()
.with_context(|| {
format!(
"Could not run '{}', make sure Wine is installed",
wine_binary
)
})?
.stdout;
// Strip the trailing newline just to make the config file a bit neater
let mut wine_version = String::from_utf8(wine_version_output)?;
wine_version.pop().unwrap();
let yabridge_host_exe_path = config
.yabridge_host_exe()
.context("Could not find 'yabridge-host.exe")?;
// Hash the contents of `yabridge-host.exe.so` since `yabridge-host.exe` is only a Wine
// generated shell script
let yabridge_host_exe_so_path = yabridge_host_exe_path.with_extension("exe.so");
let mut hasher = DefaultHasher::new();
hasher.write(&fs::read(&yabridge_host_exe_so_path).with_context(|| {
format!(
"Could not read contents of '{}'",
yabridge_host_exe_so_path.display()
)
})?);
let yabridge_host_hash = hasher.finish();
// Since these checks can take over a second if wineserver isn't already running we'll only
// perform them when something has changed
let current_config = KnownConfig {
wine_version: wine_version.clone(),
yabridge_host_hash,
};
if config.last_known_config.as_ref() == Some(&current_config) {
return Ok(());
}
// If everything's
let output = Command::new(&yabridge_host_exe_path)
.output()
.with_context(|| format!("Could not run '{}'", yabridge_host_exe_path.display()))?;
let stderr = String::from_utf8(output.stderr)?;
// There are three scenarios here:
// - Either everything is fine and we'll see the usage string being printed
// - Or the used version of Wine is too old and we'll see some line starting with
// `002b:err:module:__wine_process_init`
// - Or the used version of Wine is much newer than what was used to compile yabridge with
//
// I don't know if it's possible to differentiate between the second and the third case, so
// we'll always assume it's Wine that's outdated.
let mut success = false;
let mut last_error: Option<&str> = None;
for line in stderr.lines() {
if line == YABRIDGE_HOST_EXPECTED_OUTPUT {
success = true;
break;
}
// Ignore fixme messages here, since those can be produced by wineserver even after the
// application has errored out
if &line[5..10] != "fixme" {
last_error = Some(line);
}
}
if success {
config.last_known_config = Some(current_config);
config.write()?;
} else {
eprintln!(
"\n{}",
wrap(&format!(
"Warning: Could not run 'yabridge-host.exe'. Wine reported the following error: \n\
\n\
{}\n\
\n\
This can happen when using a version of Wine that is much older than the version \
that has been used to compile yabridge with. Your current Wine version is '{}'. \
See the troubleshooting section of the readme for more information on how to \
upgrade your installation of Wine.\n\
\n\
https://github.com/robbert-vdh/yabridge#troubleshooting-common-issues",
last_error.unwrap_or("<no_output>").bright_white(),
wine_version
.strip_prefix("wine-")
.unwrap_or(&wine_version)
.bright_white(),
))
)
}
Ok(())
}
/// Wrap a long paragraph of text to terminal width, or 80 characters if the width of the terminal
/// can't be determined. Everything after the first line gets indented with four spaces.
pub fn wrap(text: &str) -> String {
let wrapper = Wrapper::with_termwidth().subsequent_indent(" ");
wrapper.fill(text)
}