mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-10 04:22:13 +10:00
Process manager templating & game importing (#96)
* feat: add new template options, asahi support, and refactoring * feat: install dir scanning, validation fixes, progress fixes, download manager refactor This kind of ballooned out of scope, but I implemented some much needed fixes for the download manager. First off, I cleanup the Downloadable trait, there was some duplication of function. Second, I refactored the "validate" into the GameDownloadAgent, which calls a 'validate_chunk_logic' yada, same structure as downloading. Third, I fixed the progress and validation issues. Fourth, I added game scanning * feat: out of box support for Asahi Linux * fix: clippy * fix: don't break database
This commit is contained in:
13
src-tauri/Cargo.lock
generated
13
src-tauri/Cargo.lock
generated
@ -1284,7 +1284,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "drop-app"
|
||||
version = "0.3.1-mac"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"atomic-instant-full",
|
||||
"bitcode",
|
||||
@ -1306,6 +1306,7 @@ dependencies = [
|
||||
"log4rs",
|
||||
"md5",
|
||||
"native_model",
|
||||
"page_size",
|
||||
"parking_lot 0.12.3",
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
@ -3523,6 +3524,16 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.3.1-mac"
|
||||
version = "0.3.1"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2024"
|
||||
@ -70,6 +70,7 @@ tauri-plugin-opener = "2.4.0"
|
||||
bitcode = "0.6.6"
|
||||
reqwest-websocket = "0.5.0"
|
||||
futures-lite = "2.6.0"
|
||||
page_size = "0.6.0"
|
||||
# tailscale = { path = "./tailscale" }
|
||||
|
||||
[dependencies.dynfmt]
|
||||
|
||||
@ -7,7 +7,7 @@ use std::{
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
|
||||
database::{db::borrow_db_mut_checked, scan::scan_install_dirs}, error::download_manager_error::DownloadManagerError,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@ -59,6 +59,8 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
|
||||
lock.applications.install_dirs.push(new_dir);
|
||||
drop(lock);
|
||||
|
||||
scan_install_dirs();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ use chrono::Utc;
|
||||
use log::{debug, error, info, warn};
|
||||
use native_model::{Decode, Encode};
|
||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use url::Url;
|
||||
|
||||
use crate::DB;
|
||||
@ -67,17 +67,18 @@ impl DatabaseImpls for DatabaseInterface {
|
||||
|
||||
let exists = fs::exists(db_path.clone()).unwrap();
|
||||
|
||||
if exists { match PathDatabase::load_from_path(db_path.clone()) {
|
||||
Ok(db) => db,
|
||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
|
||||
} } else {
|
||||
if exists {
|
||||
match PathDatabase::load_from_path(db_path.clone()) {
|
||||
Ok(db) => db,
|
||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
|
||||
}
|
||||
} else {
|
||||
let default = Database::new(games_base_dir, None, cache_dir);
|
||||
debug!(
|
||||
"Creating database at path {}",
|
||||
db_path.as_os_str().to_str().unwrap()
|
||||
);
|
||||
PathDatabase::create_at_path(db_path, default)
|
||||
.expect("Database could not be created")
|
||||
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,24 +122,24 @@ fn handle_invalid_database(
|
||||
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
|
||||
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
|
||||
impl<'a> Deref for DBWrite<'a> {
|
||||
type Target = RwLockWriteGuard<'a, Database>;
|
||||
type Target = Database;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<'a> DerefMut for DBWrite<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl<'a> Deref for DBRead<'a> {
|
||||
type Target = RwLockReadGuard<'a, Database>;
|
||||
type Target = Database;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl DerefMut for DBWrite<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl Drop for DBWrite<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
@ -154,6 +155,7 @@ impl Drop for DBWrite<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
|
||||
match DB.borrow_data() {
|
||||
Ok(data) => DBRead(data),
|
||||
|
||||
@ -2,3 +2,4 @@ pub mod commands;
|
||||
pub mod db;
|
||||
pub mod debug;
|
||||
pub mod models;
|
||||
pub mod scan;
|
||||
@ -1,8 +1,14 @@
|
||||
use crate::database::models::data::Database;
|
||||
|
||||
/**
|
||||
* NEXT BREAKING CHANGE
|
||||
*
|
||||
* UPDATE DATABASE TO USE RPMSERDENAMED
|
||||
*
|
||||
* WE CAN'T DELETE ANY FIELDS
|
||||
*/
|
||||
pub mod data {
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
use native_model::native_model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -18,16 +24,14 @@ pub mod data {
|
||||
pub type DatabaseApplications = v2::DatabaseApplications;
|
||||
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
||||
|
||||
use std::{collections::HashMap, process::Command};
|
||||
|
||||
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub mod v1 {
|
||||
use crate::process::process_manager::Platform;
|
||||
use serde_with::serde_as;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use super::{Serialize, Deserialize, native_model};
|
||||
use super::{Deserialize, Serialize, native_model};
|
||||
|
||||
fn default_template() -> String {
|
||||
"{}".to_owned()
|
||||
@ -115,6 +119,7 @@ pub mod data {
|
||||
Downloading { version_name: String },
|
||||
Uninstalling {},
|
||||
Updating { version_name: String },
|
||||
Validating { version_name: String },
|
||||
Running {},
|
||||
}
|
||||
|
||||
@ -174,7 +179,10 @@ pub mod data {
|
||||
|
||||
use serde_with::serde_as;
|
||||
|
||||
use super::{Serialize, Deserialize, native_model, Settings, DatabaseAuth, v1, GameVersion, DownloadableMetadata, ApplicationTransientStatus};
|
||||
use super::{
|
||||
ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata,
|
||||
GameVersion, Serialize, Settings, native_model, v1,
|
||||
};
|
||||
|
||||
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
@ -206,7 +214,7 @@ pub mod data {
|
||||
applications: value.applications,
|
||||
prev_database: value.prev_database,
|
||||
cache_dir: value.cache_dir,
|
||||
compat_info: crate::database::models::Database::create_new_compat_info(),
|
||||
compat_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -283,7 +291,10 @@ pub mod data {
|
||||
mod v3 {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{Serialize, Deserialize, native_model, Settings, DatabaseAuth, DatabaseApplications, DatabaseCompatInfo, v2};
|
||||
use super::{
|
||||
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize,
|
||||
Settings, native_model, v2,
|
||||
};
|
||||
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub struct Database {
|
||||
@ -297,6 +308,7 @@ pub mod data {
|
||||
pub cache_dir: PathBuf,
|
||||
pub compat_info: Option<DatabaseCompatInfo>,
|
||||
}
|
||||
|
||||
impl From<v2::Database> for Database {
|
||||
fn from(value: v2::Database) -> Self {
|
||||
Self {
|
||||
@ -306,22 +318,13 @@ pub mod data {
|
||||
applications: value.applications.into(),
|
||||
prev_database: value.prev_database,
|
||||
cache_dir: value.cache_dir,
|
||||
compat_info: Database::create_new_compat_info(),
|
||||
compat_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
|
||||
#[cfg(target_os = "windows")]
|
||||
return None;
|
||||
|
||||
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
|
||||
Some(DatabaseCompatInfo {
|
||||
umu_installed: has_umu_installed,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new<T: Into<PathBuf>>(
|
||||
games_base_dir: T,
|
||||
prev_database: Option<PathBuf>,
|
||||
@ -340,7 +343,7 @@ pub mod data {
|
||||
auth: None,
|
||||
settings: Settings::default(),
|
||||
cache_dir,
|
||||
compat_info: Database::create_new_compat_info(),
|
||||
compat_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
52
src-tauri/src/database/scan.rs
Normal file
52
src-tauri/src/database/scan.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use std::fs;
|
||||
|
||||
use log::warn;
|
||||
|
||||
use crate::{
|
||||
database::{
|
||||
db::borrow_db_mut_checked,
|
||||
models::data::v1::{DownloadType, DownloadableMetadata},
|
||||
},
|
||||
games::{
|
||||
downloads::drop_data::{v1::DropData, DROP_DATA_PATH},
|
||||
library::set_partially_installed_db,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn scan_install_dirs() {
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
for install_dir in db_lock.applications.install_dirs.clone() {
|
||||
let Ok(files) = fs::read_dir(install_dir) else {
|
||||
continue;
|
||||
};
|
||||
for game in files.into_iter().flatten() {
|
||||
let drop_data_file = game.path().join(DROP_DATA_PATH);
|
||||
if !drop_data_file.exists() {
|
||||
continue;
|
||||
}
|
||||
let game_id = game.file_name().into_string().unwrap();
|
||||
let Ok(drop_data) = DropData::read(&game.path()) else {
|
||||
warn!(
|
||||
".dropdata exists for {}, but couldn't read it. is it corrupted?",
|
||||
game.file_name().into_string().unwrap()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if db_lock.applications.game_statuses.contains_key(&game_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let metadata = DownloadableMetadata::new(
|
||||
drop_data.game_id,
|
||||
Some(drop_data.game_version),
|
||||
DownloadType::Game,
|
||||
);
|
||||
set_partially_installed_db(
|
||||
&mut db_lock,
|
||||
&metadata,
|
||||
drop_data.base_path.to_str().unwrap().to_string(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -254,13 +254,13 @@ impl DownloadManagerBuilder {
|
||||
}
|
||||
};
|
||||
|
||||
// If the download gets cancel
|
||||
// If the download gets cancelled
|
||||
// immediately return, on_cancelled gets called for us earlier
|
||||
if !download_result {
|
||||
download_agent.on_incomplete(&app_handle);
|
||||
return;
|
||||
}
|
||||
|
||||
let validate_result = match download_agent.validate() {
|
||||
let validate_result = match download_agent.validate(&app_handle) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(
|
||||
|
||||
@ -14,14 +14,14 @@ use super::{
|
||||
|
||||
pub trait Downloadable: Send + Sync {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
||||
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
||||
|
||||
fn progress(&self) -> Arc<ProgressObject>;
|
||||
fn control_flag(&self) -> DownloadThreadControl;
|
||||
fn validate(&self) -> Result<bool, ApplicationDownloadError>;
|
||||
fn status(&self) -> DownloadStatus;
|
||||
fn metadata(&self) -> DownloadableMetadata;
|
||||
fn on_initialised(&self, app_handle: &AppHandle);
|
||||
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
|
||||
fn on_complete(&self, app_handle: &AppHandle);
|
||||
fn on_incomplete(&self, app_handle: &AppHandle);
|
||||
fn on_cancelled(&self, app_handle: &AppHandle);
|
||||
}
|
||||
|
||||
@ -38,9 +38,9 @@ impl DownloadThreadControl {
|
||||
}
|
||||
}
|
||||
pub fn get(&self) -> DownloadThreadControlFlag {
|
||||
self.inner.load(Ordering::Relaxed).into()
|
||||
self.inner.load(Ordering::Acquire).into()
|
||||
}
|
||||
pub fn set(&self, flag: DownloadThreadControlFlag) {
|
||||
self.inner.store(flag.into(), Ordering::Relaxed);
|
||||
self.inner.store(flag.into(), Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,20 +39,20 @@ impl ProgressHandle {
|
||||
}
|
||||
}
|
||||
pub fn set(&self, amount: usize) {
|
||||
self.progress.store(amount, Ordering::Relaxed);
|
||||
self.progress.store(amount, Ordering::Release);
|
||||
}
|
||||
pub fn add(&self, amount: usize) {
|
||||
self.progress
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::AcqRel);
|
||||
calculate_update(&self.progress_object);
|
||||
}
|
||||
pub fn skip(&self, amount: usize) {
|
||||
self.progress
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::Acquire);
|
||||
// Offset the bytes at last offset by this amount
|
||||
self.progress_object
|
||||
.bytes_last_update
|
||||
.fetch_add(amount, Ordering::Relaxed);
|
||||
.fetch_add(amount, Ordering::Acquire);
|
||||
// Dont' fire update
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,6 @@ impl ProgressHandle {
|
||||
impl ProgressObject {
|
||||
pub fn new(max: usize, length: usize, sender: Sender<DownloadManagerSignal>) -> Self {
|
||||
let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect());
|
||||
// TODO: consolidate this calculation with the set_max function below
|
||||
Self {
|
||||
max: Arc::new(Mutex::new(max)),
|
||||
progress_instances: Arc::new(arr),
|
||||
@ -81,19 +80,18 @@ impl ProgressObject {
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|instance| instance.load(Ordering::Relaxed))
|
||||
.map(|instance| instance.load(Ordering::Acquire))
|
||||
.sum()
|
||||
}
|
||||
pub fn reset(&self, size: usize) {
|
||||
pub fn reset(&self) {
|
||||
self.set_time_now();
|
||||
self.set_size(size);
|
||||
self.bytes_last_update.store(0, Ordering::Release);
|
||||
self.rolling.reset();
|
||||
self.progress_instances
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.for_each(|x| x.store(0, Ordering::Release));
|
||||
.for_each(|x| x.store(0, Ordering::SeqCst));
|
||||
}
|
||||
pub fn get_max(&self) -> usize {
|
||||
*self.max.lock().unwrap()
|
||||
@ -127,7 +125,7 @@ pub fn calculate_update(progress: &ProgressObject) {
|
||||
let max = progress.get_max();
|
||||
let bytes_at_last_update = progress
|
||||
.bytes_last_update
|
||||
.swap(current_bytes_downloaded, Ordering::Relaxed);
|
||||
.swap(current_bytes_downloaded, Ordering::Acquire);
|
||||
|
||||
let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ impl<const S: usize> RollingProgressWindow<S> {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| i < ¤t)
|
||||
.map(|(_, x)| x.load(Ordering::Relaxed))
|
||||
.map(|(_, x)| x.load(Ordering::Acquire))
|
||||
.sum::<usize>()
|
||||
/ S
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ use serde_with::SerializeDisplay;
|
||||
pub enum ProcessError {
|
||||
NotInstalled,
|
||||
AlreadyRunning,
|
||||
NotDownloaded,
|
||||
InvalidID,
|
||||
InvalidVersion,
|
||||
IOError(Error),
|
||||
@ -20,7 +19,6 @@ impl Display for ProcessError {
|
||||
let s = match self {
|
||||
ProcessError::NotInstalled => "Game not installed",
|
||||
ProcessError::AlreadyRunning => "Game already running",
|
||||
ProcessError::NotDownloaded => "Game not downloaded",
|
||||
ProcessError::InvalidID => "Invalid game ID",
|
||||
ProcessError::InvalidVersion => "Invalid game version",
|
||||
ProcessError::IOError(error) => &error.to_string(),
|
||||
|
||||
@ -3,19 +3,23 @@ use std::sync::Mutex;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::{
|
||||
database::models::data::GameVersion,
|
||||
AppState,
|
||||
database::{
|
||||
db::borrow_db_checked,
|
||||
models::data::GameVersion,
|
||||
},
|
||||
error::{library_error::LibraryError, remote_access_error::RemoteAccessError},
|
||||
games::library::{
|
||||
fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta,
|
||||
uninstall_game_logic,
|
||||
},
|
||||
offline, AppState,
|
||||
offline,
|
||||
};
|
||||
|
||||
use super::{
|
||||
library::{
|
||||
fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct,
|
||||
Game,
|
||||
FetchGameStruct, Game, fetch_game_logic, fetch_game_verion_options_logic,
|
||||
fetch_library_logic,
|
||||
},
|
||||
state::{GameStatusManager, GameStatusWithTransient},
|
||||
};
|
||||
@ -48,7 +52,8 @@ pub fn fetch_game(
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
|
||||
GameStatusManager::fetch_state(&id)
|
||||
let db_handle = borrow_db_checked();
|
||||
GameStatusManager::fetch_state(&id, &db_handle)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@ -12,8 +12,11 @@ use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObj
|
||||
use crate::error::application_download_error::ApplicationDownloadError;
|
||||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||
use crate::games::downloads::validate::game_validate_logic;
|
||||
use crate::games::library::{on_game_complete, on_game_incomplete, push_game_update};
|
||||
use crate::games::downloads::validate::validate_game_chunk;
|
||||
use crate::games::library::{
|
||||
on_game_complete, push_game_update, set_partially_installed,
|
||||
};
|
||||
use crate::games::state::GameStatusManager;
|
||||
use crate::remote::requests::make_request;
|
||||
use crate::remote::utils::DROP_CLIENT_SYNC;
|
||||
use log::{debug, error, info};
|
||||
@ -41,7 +44,7 @@ pub struct GameDownloadAgent {
|
||||
pub manifest: Mutex<Option<DropManifest>>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
pub stored_manifest: DropData,
|
||||
pub dropdata: DropData,
|
||||
status: Mutex<DownloadStatus>,
|
||||
}
|
||||
|
||||
@ -82,38 +85,43 @@ impl GameDownloadAgent {
|
||||
context_map: Mutex::new(HashMap::new()),
|
||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||
sender,
|
||||
stored_manifest,
|
||||
dropdata: stored_manifest,
|
||||
status: Mutex::new(DownloadStatus::Queued),
|
||||
}
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
|
||||
pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> {
|
||||
self.ensure_manifest_exists()?;
|
||||
|
||||
self.ensure_contexts()?;
|
||||
|
||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
db_lock.applications.transient_statuses.insert(
|
||||
self.metadata(),
|
||||
ApplicationTransientStatus::Downloading {
|
||||
version_name: self.version.clone(),
|
||||
},
|
||||
);
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&self.metadata().id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&self.metadata().id, &db_lock),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
self.setup_download()?;
|
||||
self.set_progress_object_params();
|
||||
self.setup_download(app_handle)?;
|
||||
let timer = Instant::now();
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&self.metadata().id,
|
||||
None,
|
||||
(
|
||||
None,
|
||||
Some(ApplicationTransientStatus::Downloading {
|
||||
version_name: self.version.clone(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
info!("beginning download for {}...", self.metadata().id);
|
||||
|
||||
let res = self
|
||||
.run()
|
||||
.map_err(|()| ApplicationDownloadError::DownloadError);
|
||||
@ -166,12 +174,8 @@ impl GameDownloadAgent {
|
||||
Err(ApplicationDownloadError::Lock)
|
||||
}
|
||||
|
||||
fn set_progress_object_params(&self) {
|
||||
// Avoid re-setting it
|
||||
if self.progress.get_max() != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sets it up for both download and validate
|
||||
fn setup_progress(&self) {
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
|
||||
let length = contexts.len();
|
||||
@ -180,7 +184,7 @@ impl GameDownloadAgent {
|
||||
|
||||
self.progress.set_max(chunk_count);
|
||||
self.progress.set_size(length);
|
||||
self.progress.set_time_now();
|
||||
self.progress.reset();
|
||||
}
|
||||
|
||||
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||
@ -188,7 +192,7 @@ impl GameDownloadAgent {
|
||||
self.generate_contexts()?;
|
||||
}
|
||||
|
||||
*self.context_map.lock().unwrap() = self.stored_manifest.get_contexts();
|
||||
*self.context_map.lock().unwrap() = self.dropdata.get_contexts();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -198,7 +202,7 @@ impl GameDownloadAgent {
|
||||
let game_id = self.id.clone();
|
||||
|
||||
let mut contexts = Vec::new();
|
||||
let base_path = Path::new(&self.stored_manifest.base_path);
|
||||
let base_path = Path::new(&self.dropdata.base_path);
|
||||
create_dir_all(base_path).unwrap();
|
||||
|
||||
for (raw_path, chunk) in manifest {
|
||||
@ -207,11 +211,12 @@ impl GameDownloadAgent {
|
||||
let container = path.parent().unwrap();
|
||||
create_dir_all(container).unwrap();
|
||||
|
||||
let already_exists = path.exists();
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(path.clone())
|
||||
.unwrap();
|
||||
let mut running_offset = 0;
|
||||
@ -232,12 +237,12 @@ impl GameDownloadAgent {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if running_offset > 0 {
|
||||
if running_offset > 0 && !already_exists {
|
||||
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
|
||||
}
|
||||
}
|
||||
let existing_contexts = self.stored_manifest.get_completed_contexts();
|
||||
self.stored_manifest.set_contexts(
|
||||
let existing_contexts = self.dropdata.get_completed_contexts();
|
||||
self.dropdata.set_contexts(
|
||||
&contexts
|
||||
.iter()
|
||||
.map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum)))
|
||||
@ -249,8 +254,8 @@ impl GameDownloadAgent {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Change return value on Err
|
||||
pub fn run(&self) -> Result<bool, ()> {
|
||||
fn run(&self) -> Result<bool, ()> {
|
||||
self.setup_progress();
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
|
||||
debug!(
|
||||
@ -266,7 +271,6 @@ impl GameDownloadAgent {
|
||||
let completed_indexes_loop_arc = completed_contexts.clone();
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
debug!("{contexts:#?}");
|
||||
pool.scope(|scope| {
|
||||
let client = &DROP_CLIENT_SYNC.clone();
|
||||
let context_map = self.context_map.lock().unwrap();
|
||||
@ -278,7 +282,10 @@ impl GameDownloadAgent {
|
||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||
|
||||
// If we've done this one already, skip it
|
||||
if Some(&true) == context_map.get(&context.checksum) {
|
||||
// Note to future DecDuck, DropData gets loaded into context_map
|
||||
if let Some(v) = context_map.get(&context.checksum)
|
||||
&& *v
|
||||
{
|
||||
progress_handle.skip(context.length);
|
||||
continue;
|
||||
}
|
||||
@ -345,8 +352,8 @@ impl GameDownloadAgent {
|
||||
.collect::<Vec<(String, bool)>>();
|
||||
drop(context_map_lock);
|
||||
|
||||
self.stored_manifest.set_contexts(&contexts);
|
||||
self.stored_manifest.write();
|
||||
self.dropdata.set_contexts(&contexts);
|
||||
self.dropdata.write();
|
||||
|
||||
// If there are any contexts left which are false
|
||||
if !contexts.iter().all(|x| x.1) {
|
||||
@ -361,6 +368,93 @@ impl GameDownloadAgent {
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn setup_validate(&self, app_handle: &AppHandle) {
|
||||
self.setup_progress();
|
||||
|
||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
db_lock.applications.transient_statuses.insert(
|
||||
self.metadata(),
|
||||
ApplicationTransientStatus::Validating {
|
||||
version_name: self.version.clone(),
|
||||
},
|
||||
);
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&self.metadata().id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&self.metadata().id, &db_lock),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
self.setup_validate(app_handle);
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
|
||||
debug!(
|
||||
"validating game: {} with {} threads",
|
||||
self.dropdata.game_id, max_download_threads
|
||||
);
|
||||
let pool = ThreadPoolBuilder::new()
|
||||
.num_threads(max_download_threads)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let invalid_chunks = Arc::new(boxcar::Vec::new());
|
||||
pool.scope(|scope| {
|
||||
for (index, context) in contexts.iter().enumerate() {
|
||||
let current_progress = self.progress.get(index);
|
||||
let progress_handle = ProgressHandle::new(current_progress, self.progress.clone());
|
||||
let invalid_chunks_scoped = invalid_chunks.clone();
|
||||
let sender = self.sender.clone();
|
||||
|
||||
scope.spawn(move |_| {
|
||||
match validate_game_chunk(context, &self.control_flag, progress_handle) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
invalid_chunks_scoped.push(context.checksum.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If there are any contexts left which are false
|
||||
if !invalid_chunks.is_empty() {
|
||||
info!("validation of game id {} failed", self.id);
|
||||
|
||||
for context in invalid_chunks.iter() {
|
||||
self.dropdata.set_context(context.1.clone(), false);
|
||||
}
|
||||
|
||||
self.dropdata.write();
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn cancel(&self, app_handle: &AppHandle) {
|
||||
// See docs on usage
|
||||
set_partially_installed(
|
||||
&self.metadata(),
|
||||
self.dropdata.base_path.to_str().unwrap().to_string(),
|
||||
Some(app_handle),
|
||||
);
|
||||
|
||||
self.dropdata.write();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl Downloadable for GameDownloadAgent {
|
||||
@ -369,6 +463,11 @@ impl Downloadable for GameDownloadAgent {
|
||||
self.download(app_handle)
|
||||
}
|
||||
|
||||
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Validating;
|
||||
self.validate(app_handle)
|
||||
}
|
||||
|
||||
fn progress(&self) -> Arc<ProgressObject> {
|
||||
self.progress.clone()
|
||||
}
|
||||
@ -407,37 +506,25 @@ impl Downloadable for GameDownloadAgent {
|
||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
on_game_complete(
|
||||
&self.metadata(),
|
||||
self.stored_manifest.base_path.to_string_lossy().to_string(),
|
||||
self.dropdata.base_path.to_string_lossy().to_string(),
|
||||
app_handle,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
|
||||
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
|
||||
on_game_incomplete(
|
||||
&self.metadata(),
|
||||
self.stored_manifest.base_path.to_string_lossy().to_string(),
|
||||
app_handle,
|
||||
)
|
||||
.unwrap();
|
||||
println!("Attempting to redownload");
|
||||
fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
|
||||
self.cancel(app_handle);
|
||||
/*
|
||||
on_game_incomplete(
|
||||
&self.metadata(),
|
||||
self.dropdata.base_path.to_string_lossy().to_string(),
|
||||
app_handle,
|
||||
)
|
||||
.unwrap();
|
||||
*/
|
||||
}
|
||||
|
||||
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
|
||||
|
||||
fn status(&self) -> DownloadStatus {
|
||||
self.status.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<bool, ApplicationDownloadError> {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Validating;
|
||||
game_validate_logic(
|
||||
&self.stored_manifest,
|
||||
self.contexts.lock().unwrap().clone(),
|
||||
self.progress.clone(),
|
||||
self.sender.clone(),
|
||||
&self.control_flag,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
use std::{
|
||||
collections::HashMap, fs::File, io::{Read, Write}, path::PathBuf
|
||||
collections::HashMap, fs::File, io::{self, Read, Write}, path::{Path, PathBuf}
|
||||
};
|
||||
|
||||
use log::{debug, error, info, warn};
|
||||
use log::error;
|
||||
use native_model::{Decode, Encode};
|
||||
|
||||
pub type DropData = v1::DropData;
|
||||
|
||||
static DROP_DATA_PATH: &str = ".dropdata";
|
||||
pub static DROP_DATA_PATH: &str = ".dropdata";
|
||||
|
||||
pub mod v1 {
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
|
||||
@ -38,27 +38,18 @@ pub mod v1 {
|
||||
|
||||
impl DropData {
|
||||
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
||||
let mut file = if let Ok(file) = File::open(base_path.join(DROP_DATA_PATH)) { file } else {
|
||||
debug!("Generating new dropdata for game {game_id}");
|
||||
return DropData::new(game_id, game_version, base_path);
|
||||
};
|
||||
match DropData::read(&base_path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => DropData::new(game_id, game_version, base_path),
|
||||
}
|
||||
}
|
||||
pub fn read(base_path: &Path) -> Result<Self, io::Error> {
|
||||
let mut file = File::open(base_path.join(DROP_DATA_PATH))?;
|
||||
|
||||
let mut s = Vec::new();
|
||||
match file.read_to_end(&mut s) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
return DropData::new(game_id, game_version, base_path);
|
||||
}
|
||||
}
|
||||
file.read_to_end(&mut s)?;
|
||||
|
||||
match native_model::rmp_serde_1_3::RmpSerde::decode(s) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(e) => {
|
||||
warn!("{e}");
|
||||
DropData::new(game_id, game_version, base_path)
|
||||
}
|
||||
}
|
||||
Ok(native_model::rmp_serde_1_3::RmpSerde::decode(s).unwrap())
|
||||
}
|
||||
pub fn write(&self) {
|
||||
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
|
||||
@ -94,10 +85,6 @@ impl DropData {
|
||||
.collect()
|
||||
}
|
||||
pub fn get_contexts(&self) -> HashMap<String, bool> {
|
||||
info!(
|
||||
"Any contexts which are complete? {}",
|
||||
self.contexts.lock().unwrap().iter().any(|x| *x.1)
|
||||
);
|
||||
self.contexts.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
pub mod commands;
|
||||
pub mod download_agent;
|
||||
mod download_logic;
|
||||
mod drop_data;
|
||||
pub mod drop_data;
|
||||
mod manifest;
|
||||
pub mod validate;
|
||||
|
||||
@ -1,99 +1,22 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
|
||||
sync::{Arc, mpsc::Sender},
|
||||
};
|
||||
|
||||
use log::{debug, error, info};
|
||||
use log::debug;
|
||||
use md5::Context;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
|
||||
use crate::{
|
||||
database::db::borrow_db_checked,
|
||||
download_manager::{
|
||||
download_manager_frontend::DownloadManagerSignal,
|
||||
download_manager::
|
||||
util::{
|
||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||
progress_object::{ProgressHandle, ProgressObject},
|
||||
},
|
||||
},
|
||||
progress_object::ProgressHandle,
|
||||
}
|
||||
,
|
||||
error::application_download_error::ApplicationDownloadError,
|
||||
games::downloads::{drop_data::DropData, manifest::DropDownloadContext},
|
||||
games::downloads::manifest::DropDownloadContext,
|
||||
};
|
||||
|
||||
pub fn game_validate_logic(
|
||||
dropdata: &DropData,
|
||||
contexts: Vec<DropDownloadContext>,
|
||||
progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
control_flag: &DownloadThreadControl,
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
progress.reset(contexts.len());
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
|
||||
debug!(
|
||||
"validating game: {} with {} threads",
|
||||
dropdata.game_id, max_download_threads
|
||||
);
|
||||
let pool = ThreadPoolBuilder::new()
|
||||
.num_threads(max_download_threads)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
debug!("{contexts:#?}");
|
||||
let invalid_chunks = Arc::new(boxcar::Vec::new());
|
||||
pool.scope(|scope| {
|
||||
for (index, context) in contexts.iter().enumerate() {
|
||||
let current_progress = progress.get(index);
|
||||
let progress_handle = ProgressHandle::new(current_progress, progress.clone());
|
||||
let invalid_chunks_scoped = invalid_chunks.clone();
|
||||
let sender = sender.clone();
|
||||
|
||||
scope.spawn(move |_| {
|
||||
match validate_game_chunk(context, control_flag, progress_handle) {
|
||||
Ok(true) => {
|
||||
debug!(
|
||||
"Finished context #{} with checksum {}",
|
||||
index, context.checksum
|
||||
);
|
||||
}
|
||||
Ok(false) => {
|
||||
debug!(
|
||||
"Didn't finish context #{} with checksum {}",
|
||||
index, &context.checksum
|
||||
);
|
||||
invalid_chunks_scoped.push(context.checksum.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// If there are any contexts left which are false
|
||||
if !invalid_chunks.is_empty() {
|
||||
info!(
|
||||
"validation of game id {} failed for chunks {:?}",
|
||||
dropdata.game_id.clone(),
|
||||
invalid_chunks
|
||||
);
|
||||
|
||||
for context in invalid_chunks.iter() {
|
||||
dropdata.set_context(context.1.clone(), false);
|
||||
}
|
||||
|
||||
dropdata.write();
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn validate_game_chunk(
|
||||
ctx: &DropDownloadContext,
|
||||
control_flag: &DownloadThreadControl,
|
||||
@ -129,10 +52,6 @@ pub fn validate_game_chunk(
|
||||
|
||||
let res = hex::encode(hasher.compute().0);
|
||||
if res != ctx.checksum {
|
||||
println!(
|
||||
"Checksum failed. Correct: {}, actual: {}",
|
||||
&ctx.checksum, &res
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use tauri::Emitter;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||
use crate::database::models::data::Database;
|
||||
use crate::database::models::data::{
|
||||
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||
};
|
||||
@ -16,11 +18,11 @@ use crate::error::library_error::LibraryError;
|
||||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||
use crate::remote::auth::generate_authorization_header;
|
||||
use crate::remote::cache::cache_object_db;
|
||||
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
|
||||
use crate::remote::requests::make_request;
|
||||
use crate::remote::utils::DROP_CLIENT_SYNC;
|
||||
use crate::AppState;
|
||||
use bitcode::{Encode, Decode};
|
||||
use bitcode::{Decode, Encode};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct FetchGameStruct {
|
||||
@ -146,27 +148,22 @@ pub fn fetch_game_logic(
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
let mut state_handle = state.lock().unwrap();
|
||||
|
||||
let handle = borrow_db_checked();
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
let metadata_option = handle.applications.installed_game_version.get(&id);
|
||||
let metadata_option = db_lock.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
None => None,
|
||||
Some(metadata) => Some(
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
.unwrap()
|
||||
.get(metadata.version.as_ref().unwrap())
|
||||
.unwrap()
|
||||
.clone(),
|
||||
),
|
||||
Some(metadata) => db_lock
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
|
||||
.cloned(),
|
||||
};
|
||||
drop(handle);
|
||||
|
||||
let game = state_handle.games.get(&id);
|
||||
if let Some(game) = game {
|
||||
let status = GameStatusManager::fetch_state(&id);
|
||||
let status = GameStatusManager::fetch_state(&id, &db_lock);
|
||||
|
||||
let data = FetchGameStruct {
|
||||
game: game.clone(),
|
||||
@ -174,10 +171,12 @@ pub fn fetch_game_logic(
|
||||
version,
|
||||
};
|
||||
|
||||
cache_object(&id, game)?;
|
||||
cache_object_db(&id, game, &db_lock)?;
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
drop(db_lock);
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| {
|
||||
r.header("Authorization", generate_authorization_header())
|
||||
@ -203,9 +202,10 @@ pub fn fetch_game_logic(
|
||||
.game_statuses
|
||||
.entry(id.clone())
|
||||
.or_insert(GameDownloadStatus::Remote {});
|
||||
drop(db_handle);
|
||||
|
||||
let status = GameStatusManager::fetch_state(&id);
|
||||
let status = GameStatusManager::fetch_state(&id, &db_handle);
|
||||
|
||||
drop(db_handle);
|
||||
|
||||
let data = FetchGameStruct {
|
||||
game: game.clone(),
|
||||
@ -222,12 +222,12 @@ pub fn fetch_game_logic_offline(
|
||||
id: String,
|
||||
_state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
let handle = borrow_db_checked();
|
||||
let metadata_option = handle.applications.installed_game_version.get(&id);
|
||||
let db_handle = borrow_db_checked();
|
||||
let metadata_option = db_handle.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
None => None,
|
||||
Some(metadata) => Some(
|
||||
handle
|
||||
db_handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
@ -237,11 +237,12 @@ pub fn fetch_game_logic_offline(
|
||||
.clone(),
|
||||
),
|
||||
};
|
||||
drop(handle);
|
||||
|
||||
let status = GameStatusManager::fetch_state(&id);
|
||||
let status = GameStatusManager::fetch_state(&id, &db_handle);
|
||||
let game = get_cached_object::<Game>(&id)?;
|
||||
|
||||
drop(db_handle);
|
||||
|
||||
Ok(FetchGameStruct {
|
||||
game,
|
||||
status,
|
||||
@ -275,7 +276,11 @@ pub fn fetch_game_verion_options_logic(
|
||||
let process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
let data: Vec<GameVersion> = data
|
||||
.into_iter()
|
||||
.filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
|
||||
.filter(|v| {
|
||||
process_manager_lock
|
||||
.valid_platform(&v.platform, &state_lock)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
drop(process_manager_lock);
|
||||
drop(state_lock);
|
||||
@ -283,6 +288,49 @@ pub fn fetch_game_verion_options_logic(
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by:
|
||||
* - on_cancel, when cancelled, for obvious reasons
|
||||
* - when downloading, so if drop unexpectedly quits, we can resume the download. hidden by the "Downloading..." transient state, though
|
||||
* - when scanning, to import the game
|
||||
*/
|
||||
pub fn set_partially_installed(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: Option<&AppHandle>,
|
||||
) {
|
||||
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle);
|
||||
}
|
||||
|
||||
pub fn set_partially_installed_db(
|
||||
db_lock: &mut Database,
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: Option<&AppHandle>,
|
||||
) {
|
||||
db_lock.applications.transient_statuses.remove(meta);
|
||||
db_lock.applications.game_statuses.insert(
|
||||
meta.id.clone(),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name: meta.version.as_ref().unwrap().clone(),
|
||||
install_dir,
|
||||
},
|
||||
);
|
||||
db_lock
|
||||
.applications
|
||||
.installed_game_version
|
||||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
if let Some(app_handle) = app_handle {
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&meta.id, db_lock),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||
debug!("triggered uninstall for agent");
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
@ -296,7 +344,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
(None, Some(ApplicationTransientStatus::Uninstalling {})),
|
||||
GameStatusManager::fetch_state(&meta.id, &db_handle),
|
||||
);
|
||||
|
||||
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
||||
@ -330,31 +378,35 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
drop(db_handle);
|
||||
|
||||
let app_handle = app_handle.clone();
|
||||
spawn(move || if let Err(e) = remove_dir_all(install_dir) {
|
||||
error!("{e}");
|
||||
} else {
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.remove(&meta.id);
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.entry(meta.id.clone())
|
||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||
drop(db_handle);
|
||||
spawn(move || {
|
||||
if let Err(e) = remove_dir_all(install_dir) {
|
||||
error!("{e}");
|
||||
} else {
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.remove(&meta.id);
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.entry(meta.id.clone())
|
||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||
let _ = db_handle.applications.transient_statuses.remove(&meta);
|
||||
|
||||
debug!("uninstalled game id {}", &meta.id);
|
||||
app_handle.emit("update_library", ()).unwrap();
|
||||
push_game_update(
|
||||
&app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&meta.id, &db_handle),
|
||||
);
|
||||
|
||||
push_game_update(
|
||||
&app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
(Some(GameDownloadStatus::Remote {}), None),
|
||||
);
|
||||
debug!("uninstalled game id {}", &meta.id);
|
||||
app_handle.emit("update_library", ()).unwrap();
|
||||
|
||||
drop(db_handle);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
warn!("invalid previous state for uninstall, failing silently.");
|
||||
@ -369,66 +421,6 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn on_game_incomplete(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
// Fetch game version information from remote
|
||||
if meta.version.is_none() {
|
||||
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
||||
}
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/game/version"],
|
||||
&[
|
||||
("id", &meta.id),
|
||||
("version", meta.version.as_ref().unwrap()),
|
||||
],
|
||||
|f| f.header("Authorization", generate_authorization_header()),
|
||||
)?
|
||||
.send()?;
|
||||
|
||||
let game_version: GameVersion = response.json()?;
|
||||
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
.entry(meta.id.clone())
|
||||
.or_default()
|
||||
.insert(meta.version.clone().unwrap(), game_version.clone());
|
||||
handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
let status = GameDownloadStatus::PartiallyInstalled {
|
||||
version_name: meta.version.clone().unwrap(),
|
||||
install_dir,
|
||||
};
|
||||
|
||||
handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
drop(handle);
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
GameUpdateEvent {
|
||||
game_id: meta.id.clone(),
|
||||
status: (Some(status), None),
|
||||
version: Some(game_version),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_game_complete(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
use crate::database::{
|
||||
db::borrow_db_checked,
|
||||
models::data::{ApplicationTransientStatus, GameDownloadStatus},
|
||||
};
|
||||
use crate::database::models::data::{ApplicationTransientStatus, Database, GameDownloadStatus};
|
||||
|
||||
pub type GameStatusWithTransient = (
|
||||
Option<GameDownloadStatus>,
|
||||
@ -10,14 +7,12 @@ pub type GameStatusWithTransient = (
|
||||
pub struct GameStatusManager {}
|
||||
|
||||
impl GameStatusManager {
|
||||
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
|
||||
let db_lock = borrow_db_checked();
|
||||
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
|
||||
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
|
||||
pub fn fetch_state(game_id: &String, database: &Database) -> GameStatusWithTransient {
|
||||
let online_state = match database.applications.installed_game_version.get(game_id) {
|
||||
Some(meta) => database.applications.transient_statuses.get(meta).cloned(),
|
||||
None => None,
|
||||
};
|
||||
let offline_state = db_lock.applications.game_statuses.get(game_id).cloned();
|
||||
drop(db_lock);
|
||||
let offline_state = database.applications.game_statuses.get(game_id).cloned();
|
||||
|
||||
if online_state.is_some() {
|
||||
return (None, online_state);
|
||||
|
||||
@ -11,7 +11,9 @@ mod error;
|
||||
mod process;
|
||||
mod remote;
|
||||
|
||||
use crate::database::scan::scan_install_dirs;
|
||||
use crate::process::commands::open_process_logs;
|
||||
use crate::process::process_handlers::UMU_LAUNCHER_EXECUTABLE;
|
||||
use crate::remote::commands::auth_initiate_code;
|
||||
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
|
||||
use bitcode::{Decode, Encode};
|
||||
@ -60,6 +62,7 @@ use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::panic::PanicHookInfo;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
@ -95,6 +98,27 @@ pub struct User {
|
||||
profile_picture_object_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CompatInfo {
|
||||
umu_installed: bool,
|
||||
}
|
||||
|
||||
fn create_new_compat_info() -> Option<CompatInfo> {
|
||||
#[cfg(target_os = "windows")]
|
||||
return None;
|
||||
|
||||
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE)
|
||||
.stdout(Stdio::null())
|
||||
.spawn();
|
||||
if let Err(umu_error) = &has_umu_installed {
|
||||
warn!("disabling windows support with error: {umu_error}");
|
||||
}
|
||||
let has_umu_installed = has_umu_installed.is_ok();
|
||||
Some(CompatInfo {
|
||||
umu_installed: has_umu_installed,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppState<'a> {
|
||||
@ -106,6 +130,8 @@ pub struct AppState<'a> {
|
||||
download_manager: Arc<DownloadManager>,
|
||||
#[serde(skip_serializing)]
|
||||
process_manager: Arc<Mutex<ProcessManager<'a>>>,
|
||||
#[serde(skip_serializing)]
|
||||
compat_info: Option<CompatInfo>,
|
||||
}
|
||||
|
||||
fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
@ -142,9 +168,13 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
let games = HashMap::new();
|
||||
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
|
||||
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
|
||||
let compat_info = create_new_compat_info();
|
||||
|
||||
debug!("checking if database is set up");
|
||||
let is_set_up = DB.database_is_set_up();
|
||||
|
||||
scan_install_dirs();
|
||||
|
||||
if !is_set_up {
|
||||
return AppState {
|
||||
status: AppStatus::NotConfigured,
|
||||
@ -152,6 +182,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
games,
|
||||
download_manager,
|
||||
process_manager,
|
||||
compat_info,
|
||||
};
|
||||
}
|
||||
|
||||
@ -164,6 +195,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
let mut missing_games = Vec::new();
|
||||
let statuses = db_handle.applications.game_statuses.clone();
|
||||
drop(db_handle);
|
||||
|
||||
for (game_id, status) in statuses {
|
||||
match status {
|
||||
GameDownloadStatus::Remote {} => {}
|
||||
@ -215,6 +247,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
games,
|
||||
download_manager,
|
||||
process_manager,
|
||||
compat_info,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ pub fn launch_game(
|
||||
// download_type: DownloadType::Game,
|
||||
//};
|
||||
|
||||
match process_manager_lock.launch_process(id) {
|
||||
match process_manager_lock.launch_process(id, &state_lock) {
|
||||
Ok(()) => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
33
src-tauri/src/process/format.rs
Normal file
33
src-tauri/src/process/format.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dynfmt::{Argument, FormatArgs};
|
||||
|
||||
pub struct DropFormatArgs {
|
||||
positional: Vec<String>,
|
||||
map: HashMap<&'static str, String>,
|
||||
}
|
||||
|
||||
impl DropFormatArgs {
|
||||
pub fn new(launch_string: String, working_dir: &String, executable_name: &String, absolute_executable_name: String) -> Self {
|
||||
let mut positional = Vec::new();
|
||||
let mut map: HashMap<&'static str, String> = HashMap::new();
|
||||
|
||||
positional.push(launch_string);
|
||||
|
||||
map.insert("dir", working_dir.to_string());
|
||||
map.insert("exe", executable_name.to_string());
|
||||
map.insert("abs_exe", absolute_executable_name);
|
||||
|
||||
Self { positional, map }
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatArgs for DropFormatArgs {
|
||||
fn get_index(&self, index: usize) -> Result<Option<dynfmt::Argument<'_>>, ()> {
|
||||
Ok(self.positional.get(index).map(|arg| arg as Argument<'_>))
|
||||
}
|
||||
|
||||
fn get_key(&self, key: &str) -> Result<Option<dynfmt::Argument<'_>>, ()> {
|
||||
Ok(self.map.get(key).map(|arg| arg as Argument<'_>))
|
||||
}
|
||||
}
|
||||
@ -2,3 +2,5 @@ pub mod commands;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod compat;
|
||||
pub mod process_manager;
|
||||
pub mod process_handlers;
|
||||
pub mod format;
|
||||
109
src-tauri/src/process/process_handlers.rs
Normal file
109
src-tauri/src/process/process_handlers.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use log::debug;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
database::models::data::{Database, DownloadableMetadata, GameVersion},
|
||||
process::process_manager::{Platform, ProcessHandler},
|
||||
};
|
||||
|
||||
pub struct NativeGameLauncher;
|
||||
impl ProcessHandler for NativeGameLauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
args: Vec<String>,
|
||||
_game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
) -> String {
|
||||
format!("\"{}\" {}", launch_command, args.join(" "))
|
||||
}
|
||||
|
||||
fn valid_for_platform(&self, _db: &Database, _state: &AppState, _target: &Platform) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
|
||||
pub struct UMULauncher;
|
||||
impl ProcessHandler for UMULauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
args: Vec<String>,
|
||||
game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
) -> String {
|
||||
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
|
||||
let game_id = match &game_version.umu_id_override {
|
||||
Some(game_override) => {
|
||||
if game_override.is_empty() {
|
||||
game_version.game_id.clone()
|
||||
} else {
|
||||
game_override.clone()
|
||||
}
|
||||
}
|
||||
None => game_version.game_id.clone(),
|
||||
};
|
||||
format!(
|
||||
"GAMEID={game_id} {umu} \"{launch}\" {args}",
|
||||
umu = UMU_LAUNCHER_EXECUTABLE,
|
||||
launch = launch_command,
|
||||
args = args.join(" ")
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
|
||||
let Some(ref compat_info) = state.compat_info else {
|
||||
return false;
|
||||
};
|
||||
compat_info.umu_installed
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AsahiMuvmLauncher;
|
||||
impl ProcessHandler for AsahiMuvmLauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
args: Vec<String>,
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
) -> String {
|
||||
let umu_launcher = UMULauncher {};
|
||||
let umu_string = umu_launcher.create_launch_process(
|
||||
meta,
|
||||
launch_command,
|
||||
args,
|
||||
game_version,
|
||||
current_dir,
|
||||
);
|
||||
let mut args_cmd = umu_string.split("umu-run").collect::<Vec<&str>>().into_iter();
|
||||
let args = args_cmd.next().unwrap().trim();
|
||||
let cmd = format!("umu-run{}", args_cmd.next().unwrap());
|
||||
|
||||
format!("{args} muvm -- {cmd}")
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return false;
|
||||
|
||||
#[cfg(not(target_arch = "aarch64"))]
|
||||
return false;
|
||||
|
||||
let page_size = page_size::get();
|
||||
if page_size != 16384 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(ref compat_info) = state.compat_info else {
|
||||
return false;
|
||||
};
|
||||
|
||||
compat_info.umu_installed
|
||||
}
|
||||
}
|
||||
@ -21,14 +21,18 @@ use tauri_plugin_opener::OpenerExt;
|
||||
use crate::{
|
||||
AppState, DB,
|
||||
database::{
|
||||
db::{DATA_ROOT_DIR, borrow_db_mut_checked},
|
||||
db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked},
|
||||
models::data::{
|
||||
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
|
||||
GameVersion,
|
||||
ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata,
|
||||
GameDownloadStatus, GameVersion,
|
||||
},
|
||||
},
|
||||
error::process_error::ProcessError,
|
||||
games::{library::push_game_update, state::GameStatusManager},
|
||||
process::{
|
||||
format::DropFormatArgs,
|
||||
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
|
||||
},
|
||||
};
|
||||
|
||||
pub struct RunningProcess {
|
||||
@ -42,7 +46,10 @@ pub struct ProcessManager<'a> {
|
||||
log_output_dir: PathBuf,
|
||||
processes: HashMap<String, RunningProcess>,
|
||||
app_handle: AppHandle,
|
||||
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
|
||||
game_launchers: Vec<(
|
||||
(Platform, Platform),
|
||||
&'a (dyn ProcessHandler + Sync + Send + 'static),
|
||||
)>,
|
||||
}
|
||||
|
||||
impl ProcessManager<'_> {
|
||||
@ -62,7 +69,7 @@ impl ProcessManager<'_> {
|
||||
app_handle,
|
||||
processes: HashMap::new(),
|
||||
log_output_dir,
|
||||
game_launchers: HashMap::from([
|
||||
game_launchers: vec![
|
||||
// Current platform to target platform
|
||||
(
|
||||
(Platform::Windows, Platform::Windows),
|
||||
@ -76,11 +83,15 @@ impl ProcessManager<'_> {
|
||||
(Platform::MacOs, Platform::MacOs),
|
||||
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Linux, Platform::Windows),
|
||||
&AsahiMuvmLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
(
|
||||
(Platform::Linux, Platform::Windows),
|
||||
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||
),
|
||||
]),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,8 +110,12 @@ impl ProcessManager<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_log_dir(&self, game_id: String) -> PathBuf {
|
||||
self.log_output_dir.join(game_id)
|
||||
}
|
||||
|
||||
pub fn open_process_logs(&mut self, game_id: String) -> Result<(), ProcessError> {
|
||||
let dir = self.log_output_dir.join(game_id);
|
||||
let dir = self.get_log_dir(game_id);
|
||||
self.app_handle
|
||||
.opener()
|
||||
.open_path(dir.to_str().unwrap(), None::<&str>)
|
||||
@ -145,7 +160,6 @@ impl ProcessManager<'_> {
|
||||
},
|
||||
);
|
||||
}
|
||||
drop(db_handle);
|
||||
|
||||
let elapsed = process.start.elapsed().unwrap_or(Duration::ZERO);
|
||||
// If we started and ended really quickly, something might've gone wrong
|
||||
@ -158,16 +172,42 @@ impl ProcessManager<'_> {
|
||||
let _ = self.app_handle.emit("launch_external_error", &game_id);
|
||||
}
|
||||
|
||||
let status = GameStatusManager::fetch_state(&game_id);
|
||||
let status = GameStatusManager::fetch_state(&game_id, &db_handle);
|
||||
drop(db_handle);
|
||||
|
||||
push_game_update(&self.app_handle, &game_id, None, status);
|
||||
}
|
||||
|
||||
pub fn valid_platform(&self, platform: &Platform) -> Result<bool, String> {
|
||||
let current = &self.current_platform;
|
||||
Ok(self.game_launchers.contains_key(&(*current, *platform)))
|
||||
fn fetch_process_handler(
|
||||
&self,
|
||||
db_lock: &Database,
|
||||
state: &AppState,
|
||||
target_platform: &Platform,
|
||||
) -> Result<&(dyn ProcessHandler + Send + Sync), ProcessError> {
|
||||
Ok(self
|
||||
.game_launchers
|
||||
.iter()
|
||||
.find(|e| {
|
||||
let (e_current, e_target) = e.0;
|
||||
e_current == self.current_platform
|
||||
&& e_target == *target_platform
|
||||
&& e.1.valid_for_platform(db_lock, state, target_platform)
|
||||
})
|
||||
.ok_or(ProcessError::InvalidPlatform)?
|
||||
.1)
|
||||
}
|
||||
|
||||
pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
|
||||
pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> Result<bool, String> {
|
||||
let db_lock = borrow_db_checked();
|
||||
let process_handler = self.fetch_process_handler(&db_lock, state, platform);
|
||||
Ok(process_handler.is_ok())
|
||||
}
|
||||
|
||||
pub fn launch_process(
|
||||
&mut self,
|
||||
game_id: String,
|
||||
state: &AppState,
|
||||
) -> Result<(), ProcessError> {
|
||||
if self.processes.contains_key(&game_id) {
|
||||
return Err(ProcessError::AlreadyRunning);
|
||||
}
|
||||
@ -191,10 +231,6 @@ impl ProcessManager<'_> {
|
||||
};
|
||||
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
debug!(
|
||||
"Launching process {:?} with games {:?}",
|
||||
&game_id, db_lock.applications.game_versions
|
||||
);
|
||||
|
||||
let game_status = db_lock
|
||||
.applications
|
||||
@ -211,13 +247,15 @@ impl ProcessManager<'_> {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
_ => return Err(ProcessError::NotDownloaded),
|
||||
_ => return Err(ProcessError::NotInstalled),
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Launching process {:?} with version {:?}",
|
||||
&game_id,
|
||||
db_lock.applications.game_versions.get(&game_id).unwrap()
|
||||
);
|
||||
|
||||
let game_version = db_lock
|
||||
.applications
|
||||
.game_versions
|
||||
@ -227,7 +265,7 @@ impl ProcessManager<'_> {
|
||||
.ok_or(ProcessError::InvalidVersion)?;
|
||||
|
||||
// TODO: refactor this path with open_process_logs
|
||||
let game_log_folder = &self.log_output_dir.join(game_id);
|
||||
let game_log_folder = &self.get_log_dir(game_id);
|
||||
create_dir_all(game_log_folder).map_err(ProcessError::IOError)?;
|
||||
|
||||
let current_time = chrono::offset::Local::now();
|
||||
@ -251,13 +289,9 @@ impl ProcessManager<'_> {
|
||||
)))
|
||||
.map_err(ProcessError::IOError)?;
|
||||
|
||||
let current_platform = self.current_platform;
|
||||
let target_platform = game_version.platform;
|
||||
|
||||
let game_launcher = self
|
||||
.game_launchers
|
||||
.get(&(current_platform, target_platform))
|
||||
.ok_or(ProcessError::InvalidPlatform)?;
|
||||
let process_handler = self.fetch_process_handler(&db_lock, state, &target_platform)?;
|
||||
|
||||
let (launch, args) = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
@ -278,7 +312,7 @@ impl ProcessManager<'_> {
|
||||
let launch = PathBuf::from_str(install_dir).unwrap().join(launch);
|
||||
let launch = launch.to_str().unwrap();
|
||||
|
||||
let launch_string = game_launcher.create_launch_process(
|
||||
let launch_string = process_handler.create_launch_process(
|
||||
&meta,
|
||||
launch.to_string(),
|
||||
args.clone(),
|
||||
@ -286,8 +320,15 @@ impl ProcessManager<'_> {
|
||||
install_dir,
|
||||
);
|
||||
|
||||
let format_args = DropFormatArgs::new(
|
||||
launch_string,
|
||||
install_dir,
|
||||
&game_version.launch_command,
|
||||
launch.to_string(),
|
||||
);
|
||||
|
||||
let launch_string = SimpleCurlyFormat
|
||||
.format(&game_version.launch_command_template, &[launch_string])
|
||||
.format(&game_version.launch_command_template, format_args)
|
||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||
.to_string();
|
||||
|
||||
@ -306,9 +347,12 @@ impl ProcessManager<'_> {
|
||||
#[cfg(unix)]
|
||||
command.args(vec!["-c", &launch_string]);
|
||||
|
||||
debug!("final launch string:\n\n{launch_string}\n");
|
||||
|
||||
command
|
||||
.stderr(error_file)
|
||||
.stdout(log_file)
|
||||
.env_remove("RUST_LOG")
|
||||
.current_dir(install_dir);
|
||||
|
||||
let child = command.spawn().map_err(ProcessError::IOError)?;
|
||||
@ -413,49 +457,6 @@ pub trait ProcessHandler: Send + 'static {
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
) -> String;
|
||||
}
|
||||
|
||||
struct NativeGameLauncher;
|
||||
impl ProcessHandler for NativeGameLauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
args: Vec<String>,
|
||||
_game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
) -> String {
|
||||
format!("\"{}\" {}", launch_command, args.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
|
||||
struct UMULauncher;
|
||||
impl ProcessHandler for UMULauncher {
|
||||
fn create_launch_process(
|
||||
&self,
|
||||
_meta: &DownloadableMetadata,
|
||||
launch_command: String,
|
||||
args: Vec<String>,
|
||||
game_version: &GameVersion,
|
||||
_current_dir: &str,
|
||||
) -> String {
|
||||
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
|
||||
let game_id = match &game_version.umu_id_override {
|
||||
Some(game_override) => {
|
||||
if game_override.is_empty() {
|
||||
game_version.game_id.clone()
|
||||
} else {
|
||||
game_override.clone()
|
||||
}
|
||||
}
|
||||
None => game_version.game_id.clone(),
|
||||
};
|
||||
format!(
|
||||
"GAMEID={game_id} {umu} \"{launch}\" {args}",
|
||||
umu = UMU_LAUNCHER_EXECUTABLE,
|
||||
launch = launch_command,
|
||||
args = args.join(" ")
|
||||
)
|
||||
}
|
||||
fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool;
|
||||
}
|
||||
|
||||
@ -51,8 +51,15 @@ fn read_sync(base: &Path, key: &str) -> io::Result<Vec<u8>> {
|
||||
}
|
||||
|
||||
pub fn cache_object<D: Encode>(key: &str, data: &D) -> Result<(), RemoteAccessError> {
|
||||
cache_object_db(key, data, &borrow_db_checked())
|
||||
}
|
||||
pub fn cache_object_db<D: Encode>(
|
||||
key: &str,
|
||||
data: &D,
|
||||
database: &Database,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
let bytes = bitcode::encode(data);
|
||||
write_sync(&borrow_db_checked().cache_dir, key, bytes).map_err(RemoteAccessError::Cache)
|
||||
write_sync(&database.cache_dir, key, bytes).map_err(RemoteAccessError::Cache)
|
||||
}
|
||||
pub fn get_cached_object<D: Encode + DecodeOwned>(key: &str) -> Result<D, RemoteAccessError> {
|
||||
get_cached_object_db::<D>(key, &borrow_db_checked())
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||
"productName": "Drop Desktop Client",
|
||||
"version": "0.3.1-mac",
|
||||
"version": "0.3.1",
|
||||
"identifier": "dev.drop.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "yarn dev --port 1432",
|
||||
|
||||
Reference in New Issue
Block a user