Async downloader, better Proton support (#183)
* feat: async downloader + other fixes * feat: windows command parsing + use library path for install path * feat: better proton support * feat: style fixes and store button now uses in-app * feat: emulator rename + umu emulator fix * feat: bring process creation inline with docs * fix: clippy
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
// Linux-only file
|
||||
|
||||
use std::{
|
||||
fs::{DirEntry, read_dir, read_to_string},
|
||||
io,
|
||||
path::PathBuf,
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use database::{borrow_db_checked, borrow_db_mut_checked};
|
||||
use log::warn;
|
||||
use serde::Serialize;
|
||||
|
||||
static SEARCH_PATHS: LazyLock<Vec<String>> = LazyLock::new(|| {
|
||||
let mut paths = vec!["/usr/share/steam/compatibilitytools.d/".to_owned()];
|
||||
|
||||
if let Some(home_dir) = std::env::home_dir() {
|
||||
paths.push(
|
||||
home_dir
|
||||
.join(".steam/root/compatibilitytools.d/")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
paths
|
||||
});
|
||||
|
||||
pub fn read_proton_path(proton_path: PathBuf) -> Result<Option<ProtonPath>, io::Error> {
|
||||
let read_dir = read_dir(&proton_path)?
|
||||
.flatten()
|
||||
.collect::<Vec<DirEntry>>();
|
||||
let has_proton_path = read_dir
|
||||
.iter()
|
||||
.find(|v| v.file_name().to_string_lossy() == "proton")
|
||||
.is_some();
|
||||
if !has_proton_path {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let compat_vdf = read_dir
|
||||
.iter()
|
||||
.find(|v| v.file_name().to_string_lossy() == "compatibilitytool.vdf");
|
||||
|
||||
let compat_vdf = match compat_vdf {
|
||||
Some(v) => v,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let compat_vdf = read_to_string(compat_vdf.path())?;
|
||||
let compat_vdf = keyvalues_parser::parse(&compat_vdf)
|
||||
.inspect_err(|err| warn!("failed to parse vdf: {:?}", err))
|
||||
.map_err(|err| io::Error::other(err.to_string()))?;
|
||||
|
||||
// Function was made with a lot of trial and error
|
||||
// Not intended to be readable
|
||||
let get_display_name = || -> Option<String> {
|
||||
let compat_tools = compat_vdf.value.unwrap_obj();
|
||||
let compat_tools = compat_tools.values().next()?.iter().next()?;
|
||||
let compat_tools = compat_tools.get_obj().unwrap();
|
||||
let compat_tools = compat_tools.values().next()?.iter().next()?.get_obj()?;
|
||||
let display_name = compat_tools.get("display_name")?.iter().next()?.get_str()?;
|
||||
Some(display_name.to_string())
|
||||
};
|
||||
|
||||
if let Some(display_name) = get_display_name() {
|
||||
return Ok(Some(ProtonPath {
|
||||
path: proton_path.to_string_lossy().to_string(),
|
||||
name: display_name,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn discover_proton_paths() -> Result<Vec<ProtonPath>, io::Error> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for search_path in &*SEARCH_PATHS {
|
||||
if let Ok(potential_dirs) = read_dir(search_path) {
|
||||
for proton_path in potential_dirs {
|
||||
if let Some(proton) = read_proton_path(proton_path?.path())? {
|
||||
results.push(proton);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProtonPath {
|
||||
pub path: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProtonPaths {
|
||||
pub autodiscovered: Vec<ProtonPath>,
|
||||
pub custom: Vec<ProtonPath>,
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_proton_paths() -> Result<ProtonPaths, String> {
|
||||
let autodiscovered = discover_proton_paths().map_err(|v| v.to_string())?;
|
||||
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
let custom = db_lock
|
||||
.applications
|
||||
.additional_proton_paths
|
||||
.iter()
|
||||
.flat_map(|v| read_proton_path(PathBuf::from(v)))
|
||||
.flatten()
|
||||
.collect::<Vec<ProtonPath>>();
|
||||
|
||||
let default = db_lock.applications.default_proton_path.clone();
|
||||
|
||||
Ok(ProtonPaths {
|
||||
autodiscovered,
|
||||
custom,
|
||||
default,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_proton_layer(path: String) -> Result<(), String> {
|
||||
let path = PathBuf::from(path);
|
||||
|
||||
let proton_layer = read_proton_path(path)
|
||||
.map_err(|err| err.to_string())?
|
||||
.ok_or("Unable to detect Proton at selected path.".to_owned())?;
|
||||
|
||||
let mut db = borrow_db_mut_checked();
|
||||
db.applications
|
||||
.additional_proton_paths
|
||||
.push(proton_layer.path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_proton_layer(index: usize) {
|
||||
let mut db = borrow_db_mut_checked();
|
||||
let deleted = db.applications.additional_proton_paths.try_remove(index);
|
||||
if let Some(deleted) = deleted
|
||||
&& let Some(default_path) = &db.applications.default_proton_path
|
||||
&& *default_path == deleted {
|
||||
db.applications.default_proton_path = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_default(path: String) -> Result<(), String> {
|
||||
let proton_paths = fetch_proton_paths().await?;
|
||||
|
||||
let valid = proton_paths
|
||||
.autodiscovered
|
||||
.iter()
|
||||
.find(|v| v.path == path)
|
||||
.or(proton_paths.custom.iter().find(|v| v.path == path))
|
||||
.is_some();
|
||||
|
||||
if !valid {
|
||||
return Err("Invalid default Proton path.".to_string());
|
||||
}
|
||||
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
db_lock.applications.default_proton_path = Some(path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::{fmt::Display, io::{self, Error}, sync::Arc};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{self, Error},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
|
||||
@@ -15,6 +19,7 @@ pub enum ProcessError {
|
||||
OpenerError(Arc<tauri_plugin_opener::Error>),
|
||||
InvalidArguments(String),
|
||||
FailedLaunch(String),
|
||||
NoCompat,
|
||||
}
|
||||
|
||||
impl Display for ProcessError {
|
||||
@@ -38,6 +43,7 @@ impl Display for ProcessError {
|
||||
"Missing a required dependency to launch this game: {} {}",
|
||||
game_id, version_id
|
||||
),
|
||||
ProcessError::NoCompat => "No Proton compatibility layer could be found for this tool. Add an override or set your global default in settings.",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
@@ -47,4 +53,4 @@ impl From<io::Error> for ProcessError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
ProcessError::IOError(Arc::new(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ impl DropFormatArgs {
|
||||
map.insert("abs_exe", absolute_executable_name);
|
||||
|
||||
if let Some(original) = original {
|
||||
map.insert("executor", original);
|
||||
map.insert("rom", original);
|
||||
}
|
||||
|
||||
Self { positional, map }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![feature(nonpoison_mutex)]
|
||||
#![feature(sync_nonpoison)]
|
||||
#![feature(extend_one)]
|
||||
#![feature(vec_try_remove)]
|
||||
|
||||
use std::{
|
||||
ops::Deref,
|
||||
@@ -13,11 +14,13 @@ use crate::process_manager::ProcessManager;
|
||||
|
||||
pub static PROCESS_MANAGER: ProcessManagerWrapper = ProcessManagerWrapper::new();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod compat;
|
||||
pub mod error;
|
||||
pub mod format;
|
||||
mod parser;
|
||||
pub mod process_handlers;
|
||||
pub mod process_manager;
|
||||
mod parser;
|
||||
|
||||
pub struct ProcessManagerWrapper(OnceLock<Mutex<ProcessManager<'static>>>);
|
||||
impl ProcessManagerWrapper {
|
||||
|
||||
@@ -43,8 +43,8 @@ impl ParsedCommand {
|
||||
v.extend(self.env);
|
||||
v.extend_one(self.command);
|
||||
v.extend(self.args);
|
||||
v.join(" ")
|
||||
shell_words::join(v)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LaunchParameters(pub String, pub PathBuf);
|
||||
pub struct LaunchParameters(pub ParsedCommand, pub PathBuf);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::fs::create_dir_all;
|
||||
use std::{fs::create_dir_all, path::PathBuf};
|
||||
|
||||
use client::compat::{COMPAT_INFO, UMU_LAUNCHER_EXECUTABLE};
|
||||
use database::{
|
||||
@@ -15,6 +15,7 @@ impl ProcessHandler for NativeGameLauncher {
|
||||
launch_command: String,
|
||||
_game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
_database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
Ok(format!("\"{}\"", launch_command))
|
||||
}
|
||||
@@ -32,33 +33,55 @@ impl ProcessHandler for UMULauncher {
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
let launch_config = game_version
|
||||
let umu_id_override = game_version
|
||||
.launches
|
||||
.iter()
|
||||
.find(|v| v.platform == meta.target_platform)
|
||||
.ok_or(ProcessError::NotInstalled)?;
|
||||
.and_then(|v| v.umu_id_override.as_ref())
|
||||
.map_or("", |v| v);
|
||||
|
||||
let game_id = match &launch_config.umu_id_override {
|
||||
Some(game_override) => {
|
||||
if game_override.is_empty() {
|
||||
game_version.version_id.clone()
|
||||
} else {
|
||||
game_override.clone()
|
||||
}
|
||||
}
|
||||
None => game_version.version_id.clone(),
|
||||
let game_id = if umu_id_override.is_empty() {
|
||||
&game_version.version_id
|
||||
} else {
|
||||
umu_id_override
|
||||
};
|
||||
|
||||
let pfx_dir = DATA_ROOT_DIR.join("pfx");
|
||||
let pfx_dir = pfx_dir.join(meta.id.clone());
|
||||
create_dir_all(&pfx_dir)?;
|
||||
|
||||
let no_proton = match meta.target_platform {
|
||||
Platform::Linux => Some("UMU_NO_PROTON=1"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let proton_env = if no_proton.is_none() {
|
||||
let proton_path = game_version
|
||||
.user_configuration
|
||||
.override_proton_path
|
||||
.as_ref()
|
||||
.or(database.applications.default_proton_path.as_ref())
|
||||
.ok_or(ProcessError::NoCompat)?;
|
||||
|
||||
let proton_valid = crate::compat::read_proton_path(PathBuf::from(proton_path))
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
if !proton_valid {
|
||||
return Err(ProcessError::NoCompat);
|
||||
}
|
||||
Some(format!("PROTONPATH={}", proton_path))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(format!(
|
||||
"GAMEID={game_id} WINEPREFIX={} {} {umu:?} {launch}",
|
||||
"GAMEID={game_id} {} WINEPREFIX={} {} {umu:?} {launch}",
|
||||
proton_env.unwrap_or(String::new()),
|
||||
pfx_dir.to_string_lossy(),
|
||||
match meta.target_platform {
|
||||
Platform::Linux => "UMU_NO_PROTON=1",
|
||||
_ => "",
|
||||
},
|
||||
no_proton.unwrap_or(""),
|
||||
umu = UMU_LAUNCHER_EXECUTABLE
|
||||
.as_ref()
|
||||
.expect("Failed to get UMU_LAUNCHER_EXECUTABLE as ref"),
|
||||
@@ -82,6 +105,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
database: &Database,
|
||||
) -> Result<String, ProcessError> {
|
||||
let umu_launcher = UMULauncher {};
|
||||
let umu_string = umu_launcher.create_launch_process(
|
||||
@@ -89,6 +113,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
||||
launch_command,
|
||||
game_version,
|
||||
current_dir,
|
||||
database,
|
||||
)?;
|
||||
let mut args_cmd = umu_string
|
||||
.split("umu-run")
|
||||
|
||||
@@ -321,7 +321,7 @@ impl ProcessManager<'_> {
|
||||
|
||||
let process_handler = self.fetch_process_handler(&db_lock, &target_platform)?;
|
||||
|
||||
let (target_command, executor) = match game_status {
|
||||
let (target_command, emulator) = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
@@ -335,7 +335,7 @@ impl ProcessManager<'_> {
|
||||
.ok_or(ProcessError::NotInstalled)?;
|
||||
(
|
||||
launch_config.command.clone(),
|
||||
launch_config.executor.as_ref(),
|
||||
launch_config.emulator.as_ref(),
|
||||
)
|
||||
}
|
||||
GameDownloadStatus::SetupRequired {
|
||||
@@ -353,27 +353,27 @@ impl ProcessManager<'_> {
|
||||
_ => unreachable!("Game registered as 'Partially Installed'"),
|
||||
};
|
||||
|
||||
let target_command = ParsedCommand::parse(target_command)?;
|
||||
let mut target_command = ParsedCommand::parse(target_command)?;
|
||||
|
||||
let launch_parameters = if let Some(executor) = executor {
|
||||
let target_launch_string = if let Some(emulator) = emulator {
|
||||
let err = ProcessError::RequiredDependency(
|
||||
executor.game_id.clone(),
|
||||
executor.version_id.clone(),
|
||||
emulator.game_id.clone(),
|
||||
emulator.version_id.clone(),
|
||||
);
|
||||
|
||||
let executor_metadata = db_lock
|
||||
let emulator_metadata = db_lock
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(&executor.game_id)
|
||||
.get(&emulator.game_id)
|
||||
.ok_or(err.clone())?;
|
||||
|
||||
let executor_game_status = db_lock
|
||||
let emulator_game_status = db_lock
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&executor.game_id)
|
||||
.get(&emulator.game_id)
|
||||
.ok_or(err.clone())?;
|
||||
|
||||
let executor_install_dir = match executor_game_status {
|
||||
let emulator_install_dir = match emulator_game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
@@ -385,86 +385,101 @@ impl ProcessManager<'_> {
|
||||
_ => Err(err.clone()),
|
||||
}?;
|
||||
|
||||
let executor_game_version = db_lock
|
||||
let emulator_game_version = db_lock
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&executor.version_id)
|
||||
.get(&emulator.version_id)
|
||||
.ok_or(err.clone())?;
|
||||
|
||||
let executor_launch_config = executor_game_version
|
||||
let emulator_launch_config = emulator_game_version
|
||||
.launches
|
||||
.iter()
|
||||
.find(|v| v.launch_id == executor.launch_id)
|
||||
.find(|v| v.launch_id == emulator.launch_id)
|
||||
.ok_or(err)?;
|
||||
|
||||
println!("{}", executor_launch_config.command);
|
||||
let mut exe_command = ParsedCommand::parse(executor_launch_config.command.clone())?;
|
||||
println!("{:?}", exe_command);
|
||||
exe_command.env.extend(target_command.env);
|
||||
exe_command.make_absolute(executor_install_dir.into());
|
||||
let mut exe_command = ParsedCommand::parse(emulator_launch_config.command.clone())?;
|
||||
exe_command.env.extend(target_command.env.clone());
|
||||
exe_command.make_absolute(emulator_install_dir.into());
|
||||
|
||||
target_command.make_absolute(PathBuf::from(install_dir.clone()));
|
||||
|
||||
exe_command.args.iter_mut().for_each(|v| {
|
||||
*v = v.replace("{executor}", &target_command.command);
|
||||
*v = v.replace("{rom}", &target_command.command);
|
||||
});
|
||||
|
||||
let executor_launch_string = process_handler.create_launch_process(
|
||||
executor_metadata,
|
||||
exe_command.reconstruct(),
|
||||
executor_game_version,
|
||||
install_dir,
|
||||
)?;
|
||||
|
||||
|
||||
LaunchParameters(executor_launch_string, install_dir.into())
|
||||
process_handler.create_launch_process(
|
||||
emulator_metadata,
|
||||
exe_command.reconstruct(),
|
||||
emulator_game_version,
|
||||
install_dir,
|
||||
&db_lock,
|
||||
)?
|
||||
} else {
|
||||
let target_launch_string = process_handler.create_launch_process(
|
||||
|
||||
|
||||
process_handler.create_launch_process(
|
||||
&meta,
|
||||
target_command.reconstruct(),
|
||||
game_version,
|
||||
install_dir,
|
||||
)?;
|
||||
|
||||
let mut parsed_launch = ParsedCommand::parse(target_launch_string.clone())?;
|
||||
let executable_name = parsed_launch.command.clone();
|
||||
parsed_launch.make_absolute(install_dir.into());
|
||||
|
||||
let format_args = DropFormatArgs::new(
|
||||
target_launch_string,
|
||||
install_dir,
|
||||
&executable_name,
|
||||
parsed_launch.command,
|
||||
None,
|
||||
);
|
||||
|
||||
let target_launch_string = SimpleCurlyFormat
|
||||
.format(&game_version.launch_template, &format_args)
|
||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||
.to_string();
|
||||
|
||||
let target_launch_string = SimpleCurlyFormat
|
||||
.format(&target_launch_string, format_args)
|
||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||
.to_string();
|
||||
|
||||
LaunchParameters(target_launch_string, install_dir.into())
|
||||
&db_lock,
|
||||
)?
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut command = Command::new("cmd");
|
||||
#[cfg(target_os = "windows")]
|
||||
command.raw_arg(format!("/C \"{}\"", &launch_parameters.0));
|
||||
let mut parsed_launch = ParsedCommand::parse(target_launch_string.clone())?;
|
||||
let executable_name = parsed_launch.command.clone();
|
||||
parsed_launch.make_absolute(install_dir.into());
|
||||
|
||||
let format_args = DropFormatArgs::new(
|
||||
target_launch_string,
|
||||
install_dir,
|
||||
&executable_name,
|
||||
parsed_launch.command,
|
||||
None,
|
||||
);
|
||||
|
||||
let target_launch_string = SimpleCurlyFormat
|
||||
.format(
|
||||
&game_version.user_configuration.launch_template,
|
||||
&format_args,
|
||||
)
|
||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||
.to_string();
|
||||
|
||||
let target_launch_string = SimpleCurlyFormat
|
||||
.format(&target_launch_string, format_args)
|
||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||
.to_string();
|
||||
|
||||
let launch_parameters = LaunchParameters(
|
||||
ParsedCommand::parse(target_launch_string)?,
|
||||
install_dir.into(),
|
||||
);
|
||||
|
||||
info!(
|
||||
"launching (in {}): {}",
|
||||
"launching (in {}): {:?}",
|
||||
launch_parameters.1.to_string_lossy(),
|
||||
launch_parameters.0
|
||||
);
|
||||
|
||||
#[cfg(unix)]
|
||||
let mut command: Command = Command::new("sh");
|
||||
#[cfg(unix)]
|
||||
command.args(vec!["-c", &launch_parameters.0]);
|
||||
let mut command = {
|
||||
let mut command = Command::new(launch_parameters.0.command);
|
||||
command.args(launch_parameters.0.args);
|
||||
for parts in launch_parameters
|
||||
.0
|
||||
.env
|
||||
.into_iter()
|
||||
.map(|e| e.split("=").map(|v| v.to_string()).collect::<Vec<String>>())
|
||||
{
|
||||
if let Some(key) = parts.first()
|
||||
&& let Some(value) = parts.get(1) {
|
||||
command.env(key, value);
|
||||
}
|
||||
}
|
||||
command
|
||||
};
|
||||
|
||||
command
|
||||
.stderr(error_file)
|
||||
@@ -474,8 +489,7 @@ impl ProcessManager<'_> {
|
||||
|
||||
let child = command.spawn()?;
|
||||
|
||||
let launch_process_handle =
|
||||
Arc::new(SharedChild::new(child)?);
|
||||
let launch_process_handle = Arc::new(SharedChild::new(child)?);
|
||||
|
||||
db_lock
|
||||
.applications
|
||||
@@ -518,6 +532,7 @@ pub trait ProcessHandler: Send + 'static {
|
||||
launch_command: String,
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
database: &Database,
|
||||
) -> Result<String, ProcessError>;
|
||||
|
||||
fn valid_for_platform(&self, db: &Database, target: &Platform) -> bool;
|
||||
|
||||
Reference in New Issue
Block a user