Read the Wine version without Boost.Process

By spawning the process using posix_spawn and manually reading from a
pipe.
This commit is contained in:
Robbert van der Helm
2022-04-11 14:33:35 +02:00
parent 60e4619ef1
commit 006cc6f52a
4 changed files with 251 additions and 43 deletions
+86 -1
View File
@@ -18,7 +18,8 @@
#include <cassert>
#include <ghc/filesystem.hpp>
#include <spawn.h>
#include <sys/wait.h>
namespace fs = ghc::filesystem;
@@ -91,3 +92,87 @@ char* const* ProcessEnvironment::make_environ() const {
return const_cast<char* const*>(recreated_environ_.data());
}
bool Process::Handle::running() const noexcept {
return pid_running(pid);
}
void Process::Handle::terminate() const noexcept {
kill(pid, SIGINT);
wait();
}
std::optional<int> Process::Handle::wait() const noexcept {
int status = 0;
assert(waitpid(pid, &status, 0) > 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return std::nullopt;
}
}
Process::Process(std::string command) : command_(command) {}
Process::StringResult Process::spawn_get_stdout_line() {
/// We'll read the results from a pipe. The child writes to the second pipe,
/// we'll read from the first one.
int output_pipe[2];
::pipe(output_pipe);
const auto argv = build_argv();
const auto envp = env_ ? env_->make_environ() : environ;
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
posix_spawn_file_actions_adddup2(&actions, output_pipe[1], STDOUT_FILENO);
posix_spawn_file_actions_addclose(&actions, output_pipe[0]);
posix_spawn_file_actions_addclose(&actions, output_pipe[1]);
pid_t child_pid = 0;
const auto result = posix_spawnp(&child_pid, command_.c_str(), &actions,
nullptr, argv, envp);
close(output_pipe[1]);
if (result == 2) {
close(output_pipe[0]);
return Process::CommandNotFound{};
} else if (result != 0) {
close(output_pipe[0]);
return std::error_code(result, std::system_category());
}
// Try to read the first line out the output until the line feed
std::array<char, 1024> output{0};
FILE* output_pipe_stream = fdopen(output_pipe[0], "r");
assert(output_pipe_stream);
fgets(output.data(), output.size(), output_pipe_stream);
fclose(output_pipe_stream);
int status = 0;
assert(waitpid(child_pid, &status, 0) > 0);
if (!WIFEXITED(status) || WEXITSTATUS(status) == 127) {
return Process::CommandNotFound{};
} else {
// `fgets()` returns the line feed, so we'll get rid of that
std::string output_str(output.data());
if (output_str.back() == '\n') {
output_str.pop_back();
}
return output_str;
}
}
char* const* Process::build_argv() const {
argv_.clear();
argv_.push_back(command_.c_str());
for (const auto& arg : args_) {
argv_.push_back(arg.c_str());
}
argv_.push_back(nullptr);
return const_cast<char* const*>(argv_.data());
}
+97
View File
@@ -18,8 +18,18 @@
#include <optional>
#include <string>
#include <system_error>
#include <variant>
#include <vector>
#ifdef __WINE__
#include "../wine-host/asio-fix.h"
#endif
#include <unistd.h>
#include <asio/posix/stream_descriptor.hpp>
#include <ghc/filesystem.hpp>
// A minimal API akin to Boost.Process for launching and managing processes
// using plain Linux APIs. Needed so we can implement our chainloader without
// pulling in Boost.Process' Boost.Filesystem dependency (which would defeat the
@@ -81,3 +91,90 @@ class ProcessEnvironment {
*/
mutable std::vector<char const*> recreated_environ_;
};
/**
* A child process whose output can be captured. Simple wrapper around the Posix
* APIs.
*/
class Process {
public:
/**
* Marker to indicate that the program was not found.
*/
struct CommandNotFound {};
/**
* A handle to a running process.
*/
class Handle {
public:
/**
* The process' ID.
*/
const pid_t pid;
/**
* Whether the process is still running **and not a zombie**.
*/
bool running() const noexcept;
/**
* Forcefully terminate the process by sending `SIGKILL`. Will reap the
* process zombie after sending the signal.
*/
void terminate() const noexcept;
/**
* Wait for the process to exit, returning the exit code if it exited
* successfully. Returns a nullopt otherwise.
*/
std::optional<int> wait() const noexcept;
};
using StringResult =
std::variant<std::string, CommandNotFound, std::error_code>;
using StatusResult = std::variant<int, CommandNotFound, std::error_code>;
using HandleResult = std::variant<Handle, CommandNotFound, std::error_code>;
/**
* Build a process. Use the other functions to add arguments or to
* launch the process.
*
* @param command The name of the command. `$PATH` will be searched for
* this command if it is not absolute.
*/
Process(std::string command);
/**
* Add an argument to the command invocation.
*/
inline void arg(std::string arg) { args_.emplace_back(std::move(arg)); }
/**
* Use the specified environment for this command.
*
* @see environment
*/
inline void environment(ProcessEnvironment env) { env_ = std::move(env); }
/**
* Spawn the process, leave STDIN and STDERR alone, and return the first
* line (without the trailing linefeed) of STDOUT. The first output line
* will still be returned even if the process exits with a non-zero exit
* code. Uses `posix_spawn()`, leaves file descriptors in tact.
*/
StringResult spawn_get_stdout_line();
private:
/**
* Create the `argv` array from the command and the arguments. Only valid as
* long as the pointers in `args_` at the time of calling stay valid.
*/
char* const* build_argv() const;
std::string command_;
std::vector<std::string> args_;
std::optional<ProcessEnvironment> env_;
mutable std::vector<char const*> argv_;
};
+65 -42
View File
@@ -83,14 +83,35 @@ bp::environment PluginInfo::create_host_env() const {
return env;
}
ProcessEnvironment PluginInfo::create_host_env_2() const {
ProcessEnvironment env(environ);
// Only set the prefix when could auto detect it and it's not being
// overridden (this entire `std::visit` instead of `std::has_alternative` is
// just for clarity's sake)
std::visit(overload{
[](const OverridenWinePrefix&) {},
[&](const ghc::filesystem::path& prefix) {
env.insert("WINEPREFIX", prefix.string());
},
[](const DefaultWinePrefix&) {},
},
wine_prefix_);
return env;
}
ghc::filesystem::path PluginInfo::normalize_wine_prefix() const {
return std::visit(
overload{
[](const OverridenWinePrefix& prefix) { return prefix.value; },
[](const ghc::filesystem::path& prefix) { return prefix; },
[](const DefaultWinePrefix&) {
const bp::environment env = boost::this_process::environment();
return fs::path(env.at("HOME").to_string()) / ".wine";
// NOLINTNEXTLINE(concurrency-mt-unsafe)
const char* home_dir = getenv("HOME");
assert(home_dir);
return fs::path(home_dir) / ".wine";
},
},
wine_prefix_);
@@ -99,38 +120,40 @@ ghc::filesystem::path PluginInfo::normalize_wine_prefix() const {
std::string PluginInfo::wine_version() const {
// The '*.exe' scripts generated by winegcc allow you to override the binary
// used to run Wine, so will will handle this in the same way for our Wine
// version detection
// FIXME: Replace Boost.Filesystem usage
boost::filesystem::path wine_path;
bp::environment env = create_host_env();
if (const std::string wineloader_path = env["WINELOADER"].to_string();
access(wineloader_path.c_str(), X_OK) == 0) {
// version detection. We'll be using `execvpe`
std::string wine_path = "wine";
// NOLINTNEXTLINE(concurrency-mt-unsafe)
if (const char* wineloader_path = getenv("WINELOADER");
wineloader_path && access(wineloader_path, X_OK) == 0) {
wine_path = wineloader_path;
} else {
wine_path = bp::search_path("wine").string();
}
bp::ipstream output;
try {
bp::system(wine_path, "--version", bp::std_out = output, bp::env = env,
bp::posix::use_vfork);
} catch (const std::system_error&) {
return "<NOT FOUND>";
}
Process process(wine_path);
process.arg("--version");
process.environment(create_host_env_2());
// `wine --version` might contain additional output in certain custom Wine
// builds, so we only want to look at the first line
std::string version_string;
std::getline(output, version_string);
auto result = process.spawn_get_stdout_line();
return std::visit(
overload{
[](std::string version_string) -> std::string {
// Strip the `wine-` prefix from the output, could potentially
// be absent in custom Wine builds
constexpr std::string_view version_prefix("wine-");
if (version_string.starts_with(version_prefix)) {
version_string =
version_string.substr(version_prefix.size());
}
// Strip the `wine-` prefix from the output, could potentially be absent in
// custom Wine builds
constexpr std::string_view version_prefix("wine-");
if (version_string.starts_with(version_prefix)) {
version_string = version_string.substr(version_prefix.size());
}
return version_string;
return version_string;
},
[](const Process::CommandNotFound&) -> std::string {
return "<NOT FOUND>";
},
[](const std::error_code& err) -> std::string {
return "<ERROR SPAWNING WINE: " + err.message() + " >";
},
},
result);
}
fs::path find_plugin_library(const fs::path& this_plugin_path,
@@ -256,9 +279,9 @@ fs::path normalize_plugin_path(const fs::path& windows_library_path,
std::variant<OverridenWinePrefix, fs::path, DefaultWinePrefix> find_wine_prefix(
fs::path windows_plugin_path) {
const bp::environment env = boost::this_process::environment();
if (const auto prefix = env.find("WINEPREFIX"); prefix != env.end()) {
return OverridenWinePrefix{prefix->to_string()};
// NOLINTNEXTLINE(concurrency-mt-unsafe)
if (const auto prefix = getenv("WINEPREFIX")) {
return OverridenWinePrefix{prefix};
}
const std::optional<fs::path> dosdevices_dir = find_dominating_file(
@@ -411,16 +434,14 @@ std::vector<boost::filesystem::path> get_augmented_search_path() {
std::vector<boost::filesystem::path> search_path =
boost::this_process::path();
const bp::environment environment = boost::this_process::environment();
if (auto xdg_data_home = environment.find("XDG_DATA_HOME");
xdg_data_home != environment.end()) {
search_path.push_back(
boost::filesystem::path(xdg_data_home->to_string()) / "yabridge");
} else if (auto home_directory = environment.find("HOME");
home_directory != environment.end()) {
search_path.push_back(
boost::filesystem::path(home_directory->to_string()) / ".local" /
"share" / "yabridge");
// NOLINTNEXTLINE(concurrency-mt-unsafe)
if (const char* xdg_data_home = getenv("XDG_DATA_HOME")) {
search_path.push_back(boost::filesystem::path(xdg_data_home) /
"yabridge");
// NOLINTNEXTLINE(concurrency-mt-unsafe)
} else if (const char* home_directory = getenv("HOME")) {
search_path.push_back(boost::filesystem::path(home_directory) /
".local" / "share" / "yabridge");
}
return search_path;
@@ -472,6 +493,8 @@ bool send_notification(const std::string& title,
}
}
// TODO: Also use a custom process here, and wrap the above code in a
// class
try {
return bp::system(notify_send_path, "--urgency=normal",
"--expire-time=15000", "--app-name=yabridge", title,
+3
View File
@@ -22,6 +22,7 @@
#include "../common/configuration.h"
#include "../common/plugins.h"
#include "../common/process.h"
#include "../common/utils.h"
/**
@@ -76,6 +77,8 @@ struct PluginInfo {
* unset if we could not detect a prefix.
*/
boost::process::environment create_host_env() const;
// FIXME: Replace create_host_env with this one
ProcessEnvironment create_host_env_2() const;
/**
* Return the path to the actual Wine prefix in use, taking into account