Implement better error system and segregate errors and commands (#23)

* chore: Progress on amend_settings command

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): Progress on better error handling with segragation of files

* chore: Progress on amend_settings command

Signed-off-by: quexeky <git@quexeky.dev>

* chore(commands): Separated commands under each subdirectory into respective commands.rs files

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): Almost all errors and commands have been segregated

* chore(errors): Added drop server error

Signed-off-by: quexeky <git@quexeky.dev>

* feat(core): Update to using nightly compiler

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): More progress on error handling

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): Implementing Try and FromResidual for UserValue

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(errors): Segregated errors and commands from code, and made commands return UserValue struct

Signed-off-by: quexeky <git@quexeky.dev>

* fix(errors): Added missing files

* chore(errors): Convert match statement to map_err

* feat(settings): Implemented settings editing from UI

* feat(errors): Clarified return values from retry_connect command

* chore(errors): Moved autostart commands to autostart.rs

* chore(process manager): Converted launch_process function for games to use game_id

---------

Signed-off-by: quexeky <git@quexeky.dev>
This commit is contained in:
quexeky
2025-01-13 21:44:57 +11:00
committed by GitHub
parent 245a84d20b
commit 604d5b5884
45 changed files with 822 additions and 600 deletions

View File

@ -280,5 +280,6 @@ async function deleteDirectory(index: number) {
async function saveDownloadThreads() {
//Would save download threads downloadThreads.value);
await invoke("amend_settings", { newSettings: { max_download_threads: downloadThreads.value } })
}
</script>

23
src-tauri/Cargo.lock generated
View File

@ -1042,6 +1042,7 @@ dependencies = [
"log",
"log4rs",
"md5",
"merge-struct",
"openssl",
"parking_lot 0.12.3",
"rayon",
@ -1051,6 +1052,7 @@ dependencies = [
"serde",
"serde-binary",
"serde_json",
"serde_merge",
"serde_with",
"shared_child",
"slice-deque",
@ -2453,6 +2455,16 @@ dependencies = [
"autocfg",
]
[[package]]
name = "merge-struct"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d82012d21e24135b839b6b9bebd622b7ff0cb40071498bc2d066d3a6d04dd4a"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -3901,6 +3913,17 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_merge"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "606e91878516232ac3b16c12e063d4468d762f16d77e7aef14a1f2326c5f409b"
dependencies = [
"serde",
"serde_json",
"thiserror 1.0.69",
]
[[package]]
name = "serde_repr"
version = "0.1.19"

View File

@ -50,6 +50,8 @@ slice-deque = "0.3.0"
derive_builder = "0.20.2"
throttle_my_fn = "0.2.6"
parking_lot = "0.12.3"
merge-struct = "0.1.0"
serde_merge = "0.1.3"
[dependencies.tauri]
version = "2.1.1"

View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@ -3,8 +3,7 @@ use log::debug;
use tauri::AppHandle;
use tauri_plugin_autostart::ManagerExt;
#[tauri::command]
pub async fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
let manager = app.autolaunch();
if enabled {
manager.enable().map_err(|e| e.to_string())?;
@ -23,23 +22,22 @@ pub async fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), Strin
Ok(())
}
#[tauri::command]
pub async fn get_autostart_enabled(app: AppHandle) -> Result<bool, String> {
pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
// First check DB state
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
let db_handle = DB.borrow_data().unwrap();
let db_state = db_handle.settings.autostart;
drop(db_handle);
// Get actual system state
let manager = app.autolaunch();
let system_state = manager.is_enabled().map_err(|e| e.to_string())?;
let system_state = manager.is_enabled()?;
// If they don't match, sync to DB state
if db_state != system_state {
if db_state {
manager.enable().map_err(|e| e.to_string())?;
manager.enable()?;
} else {
manager.disable().map_err(|e| e.to_string())?;
manager.disable()?;
}
}
@ -67,3 +65,12 @@ pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
Ok(())
}
#[tauri::command]
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
toggle_autostart_logic(app, enabled)
}
#[tauri::command]
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
get_autostart_enabled_logic(app)
}

16
src-tauri/src/commands.rs Normal file
View File

@ -0,0 +1,16 @@
use tauri::AppHandle;
use crate::{
autostart::{get_autostart_enabled_logic, toggle_autostart_logic},
AppState,
};
#[tauri::command]
pub fn fetch_state(
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
) -> Result<String, String> {
let guard = state.lock().unwrap();
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
drop(guard);
Ok(cloned_state)
}

View File

@ -0,0 +1,83 @@
use std::{
fs::create_dir_all,
io::{Error, ErrorKind},
path::{Path, PathBuf},
};
use serde_json::Value;
use crate::{database::settings::Settings, error::user_error::UserValue, DB};
use super::{db::DATA_ROOT_DIR, debug::SystemData};
// Will, in future, return disk/remaining size
// Just returns the directories that have been set up
#[tauri::command]
pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
let lock = DB.borrow_data().unwrap();
lock.applications.install_dirs.clone()
}
#[tauri::command]
pub fn delete_download_dir(index: usize) {
let mut lock = DB.borrow_data_mut().unwrap();
lock.applications.install_dirs.remove(index);
drop(lock);
DB.save().unwrap();
}
#[tauri::command]
pub fn add_download_dir(new_dir: PathBuf) -> UserValue<(), Error> {
// Check the new directory is all good
let new_dir_path = Path::new(&new_dir);
if new_dir_path.exists() {
let dir_contents = new_dir_path.read_dir()?;
if dir_contents.count() != 0 {
return UserValue::Err(Error::new(
ErrorKind::DirectoryNotEmpty,
"Selected directory cannot contain any existing files",
));
}
} else {
create_dir_all(new_dir_path)?;
}
// Add it to the dictionary
let mut lock = DB.borrow_data_mut().unwrap();
if lock.applications.install_dirs.contains(&new_dir) {
return UserValue::Err(Error::new(
ErrorKind::AlreadyExists,
"Selected directory already exists in database",
));
}
lock.applications.install_dirs.push(new_dir);
drop(lock);
DB.save().unwrap();
UserValue::Ok(())
}
#[tauri::command]
pub fn update_settings(new_settings: Value) {
println!("{}", new_settings);
let mut db_lock = DB.borrow_data_mut().unwrap();
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
for (key, value) in new_settings.as_object().unwrap() {
current_settings[key] = value.clone();
}
println!("New settings unset: {}", &current_settings);
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
db_lock.settings = new_settings;
println!("New Settings: {:?}", db_lock.settings);
}
#[tauri::command]
pub fn fetch_system_data() -> SystemData {
let db_handle = DB.borrow_data().unwrap();
SystemData::new(
db_handle.auth.as_ref().unwrap().client_id.clone(),
db_handle.base_url.clone(),
DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
)
}

View File

@ -15,10 +15,10 @@ use tauri::AppHandle;
use url::Url;
use crate::{
database::settings::Settings,
download_manager::downloadable_metadata::DownloadableMetadata,
games::{library::push_game_update, state::GameStatusManager},
process::process_manager::Platform,
settings::Settings,
DB,
};
@ -164,61 +164,6 @@ impl DatabaseImpls for DatabaseInterface {
}
}
#[tauri::command]
pub fn add_download_dir(new_dir: PathBuf) -> Result<(), String> {
// Check the new directory is all good
let new_dir_path = Path::new(&new_dir);
if new_dir_path.exists() {
let metadata = new_dir_path
.metadata()
.map_err(|e| format!("Unable to access file or directory: {}", e))?;
if !metadata.is_dir() {
return Err("Invalid path: not a directory".to_string());
}
let dir_contents = new_dir_path
.read_dir()
.map_err(|e| format!("Unable to check directory contents: {}", e))?;
if dir_contents.count() != 0 {
return Err("Directory is not empty".to_string());
}
} else {
create_dir_all(new_dir_path)
.map_err(|e| format!("Unable to create directories to path: {}", e))?;
}
// Add it to the dictionary
let mut lock = DB.borrow_data_mut().unwrap();
if lock.applications.install_dirs.contains(&new_dir) {
return Err("Download directory already used".to_string());
}
lock.applications.install_dirs.push(new_dir);
drop(lock);
DB.save().unwrap();
Ok(())
}
#[tauri::command]
pub fn delete_download_dir(index: usize) -> Result<(), String> {
let mut lock = DB.borrow_data_mut().unwrap();
lock.applications.install_dirs.remove(index);
drop(lock);
DB.save().unwrap();
Ok(())
}
// Will, in future, return disk/remaining size
// Just returns the directories that have been set up
#[tauri::command]
pub fn fetch_download_dir_stats() -> Result<Vec<PathBuf>, String> {
let lock = DB.borrow_data().unwrap();
let directories = lock.applications.install_dirs.clone();
drop(lock);
Ok(directories)
}
pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &DownloadableMetadata)>(
app_handle: &AppHandle,
meta: DownloadableMetadata,
@ -231,9 +176,8 @@ pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &Downloada
let status = GameStatusManager::fetch_state(&meta.id);
push_game_update(app_handle, &meta, status);
push_game_update(app_handle, &meta.id, status);
}
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
fn handle_invalid_database(
_e: RustbreakError,

View File

@ -0,0 +1,22 @@
use crate::{DATA_ROOT_DIR, DB};
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemData {
client_id: String,
base_url: String,
data_dir: String,
log_level: String,
}
impl SystemData {
pub fn new(client_id: String, base_url: String, data_dir: String, log_level: String) -> Self {
Self {
client_id,
base_url,
data_dir,
log_level,
}
}
}

View File

@ -0,0 +1,4 @@
pub mod commands;
pub mod db;
pub mod debug;
pub mod settings;

View File

@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
// ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4,
}
}
}
// Ideally use pointers instead of a macro to assign the settings
// fn deserialize_into<T>(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error>
// where T: for<'a> Deserialize<'a>
// {
// *t = serde_json::from_value(v)?;
// Ok(())
// }

View File

@ -1,26 +0,0 @@
use crate::{DATA_ROOT_DIR, DB};
use log::LevelFilter;
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemData {
client_id: String,
base_url: String,
data_dir: String,
log_level: String,
}
#[tauri::command]
pub fn fetch_system_data() -> Result<SystemData, String> {
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
let system_data = SystemData {
client_id: db_handle.auth.as_ref().unwrap().client_id.clone(),
base_url: db_handle.base_url.clone(),
data_dir: DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
log_level: std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
};
drop(db_handle);
Ok(system_data)
}

View File

@ -0,0 +1,31 @@
use std::sync::{mpsc::SendError, Arc, Mutex};
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState};
#[tauri::command]
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.pause_downloads()
}
#[tauri::command]
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.resume_downloads()
}
#[tauri::command]
pub fn move_download_in_queue(
state: tauri::State<'_, Mutex<AppState>>,
old_index: usize,
new_index: usize,
) {
state
.lock()
.unwrap()
.download_manager
.rearrange(old_index, new_index)
}
#[tauri::command]
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
state.lock().unwrap().download_manager.cancel(meta)
}

View File

@ -12,8 +12,9 @@ use std::{
use log::info;
use serde::Serialize;
use crate::error::application_download_error::ApplicationDownloadError;
use super::{
application_download_error::ApplicationDownloadError,
download_manager_builder::{CurrentProgressObject, DownloadAgent},
downloadable_metadata::DownloadableMetadata,
queue::Queue,

View File

@ -7,13 +7,15 @@ use std::{
thread::{spawn, JoinHandle},
};
use log::info;
use log::{debug, error, info};
use tauri::{AppHandle, Emitter};
use crate::games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent};
use crate::{
error::application_download_error::ApplicationDownloadError,
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
};
use super::{
application_download_error::ApplicationDownloadError,
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
downloadable::Downloadable,
@ -54,7 +56,7 @@ whichever download queue order is required.
+----------------------------------------------------------------------------+
This download queue does not actually own any of the DownloadAgents. It is
simply a id-based reference system. The actual Agents are stored in the
simply an id-based reference system. The actual Agents are stored in the
download_agent_registry HashMap, as ordering is no issue here. This is why
appending or removing from the download_queue must be done via signals.
@ -239,6 +241,7 @@ impl DownloadManagerBuilder {
match download_agent.download(&app_handle) {
// Ok(true) is for completed and exited properly
Ok(true) => {
debug!("download {:?} has completed", download_agent.metadata());
download_agent.on_complete(&app_handle);
sender
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
@ -249,6 +252,7 @@ impl DownloadManagerBuilder {
download_agent.on_incomplete(&app_handle);
}
Err(e) => {
error!("download {:?} has error {}", download_agent.metadata(), &e);
download_agent.on_error(&app_handle, e.clone());
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}

View File

@ -2,9 +2,10 @@ use std::sync::Arc;
use tauri::AppHandle;
use crate::error::application_download_error::ApplicationDownloadError;
use super::{
application_download_error::ApplicationDownloadError, download_manager::DownloadStatus,
download_thread_control_flag::DownloadThreadControl,
download_manager::DownloadStatus, download_thread_control_flag::DownloadThreadControl,
downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject,
};

View File

@ -1,4 +1,4 @@
pub mod application_download_error;
pub mod commands;
pub mod download_manager;
pub mod download_manager_builder;
pub mod download_thread_control_flag;

View File

@ -138,19 +138,22 @@ impl ProgressObject {
}
}
#[throttle(10, Duration::from_secs(1))]
#[throttle(50, Duration::from_secs(1))]
fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
progress_object.sender
.send(DownloadManagerSignal::UpdateUIStats(
kilobytes_per_second,
time_remaining,
))
.unwrap();
progress_object
.sender
.send(DownloadManagerSignal::UpdateUIStats(
kilobytes_per_second,
time_remaining,
))
.unwrap();
}
#[throttle(10, Duration::from_secs(1))]
#[throttle(50, Duration::from_secs(1))]
fn update_queue(progress: &ProgressObject) {
progress.sender
progress
.sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}

View File

@ -3,7 +3,7 @@ use std::{
io,
};
use crate::remote::RemoteAccessError;
use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
// TODO: Rename / separate from downloads
#[derive(Debug, Clone)]
@ -28,16 +28,3 @@ impl Display for ApplicationDownloadError {
}
}
}
#[derive(Debug, Clone)]
pub enum SetupError {
Context,
}
impl Display for SetupError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SetupError::Context => write!(f, "Failed to generate contexts for download"),
}
}
}

View File

@ -0,0 +1,10 @@
use serde::Deserialize;
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DropServerError {
pub status_code: usize,
pub status_message: String,
pub message: String,
pub url: String,
}

View File

@ -0,0 +1,16 @@
use std::fmt::Display;
pub enum LibraryError {
MetaNotFound(String),
}
impl Display for LibraryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LibraryError::MetaNotFound(id) => write!(
f,
"Could not locate any installed version of game ID {} in the database",
id
),
}
}
}

View File

@ -0,0 +1,7 @@
pub mod application_download_error;
pub mod drop_server_error;
pub mod library_error;
pub mod process_error;
pub mod remote_access_error;
pub mod setup_error;
pub mod user_error;

View File

@ -0,0 +1,28 @@
use std::{fmt::Display, io::Error};
pub enum ProcessError {
SetupRequired,
NotInstalled,
AlreadyRunning,
NotDownloaded,
InvalidID,
InvalidVersion,
IOError(Error),
InvalidPlatform,
}
impl Display for ProcessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
ProcessError::SetupRequired => "Game not set up",
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(),
ProcessError::InvalidPlatform => "This Game cannot be played on the current platform",
};
write!(f, "{}", s)
}
}

View File

@ -1,15 +1,13 @@
use std::{
error::Error,
fmt::{Display, Formatter},
sync::{Arc, Mutex},
sync::Arc,
};
use http::StatusCode;
use log::{info, warn};
use serde::Deserialize;
use url::{ParseError, Url};
use url::ParseError;
use crate::{AppState, AppStatus, DB};
use super::drop_server_error::DropServerError;
#[derive(Debug, Clone)]
pub enum RemoteAccessError {
@ -67,82 +65,4 @@ impl From<ParseError> for RemoteAccessError {
RemoteAccessError::ParsingError(err)
}
}
impl std::error::Error for RemoteAccessError {}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DropServerError {
pub status_code: usize,
pub status_message: String,
pub message: String,
pub url: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DropHealthcheck {
app_name: String,
}
fn use_remote_logic<'a>(
url: String,
state: tauri::State<'_, Mutex<AppState<'a>>>,
) -> Result<(), RemoteAccessError> {
info!("connecting to url {}", url);
let base_url = Url::parse(&url)?;
// Test Drop url
let test_endpoint = base_url.join("/api/v1")?;
let response = reqwest::blocking::get(test_endpoint.to_string())?;
let result: DropHealthcheck = response.json()?;
if result.app_name != "Drop" {
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
return Err(RemoteAccessError::InvalidEndpoint);
}
let mut app_state = state.lock().unwrap();
app_state.status = AppStatus::SignedOut;
drop(app_state);
let mut db_state = DB.borrow_data_mut().unwrap();
db_state.base_url = base_url.to_string();
drop(db_state);
DB.save().unwrap();
Ok(())
}
#[tauri::command]
pub fn use_remote<'a>(
url: String,
state: tauri::State<'_, Mutex<AppState<'a>>>,
) -> Result<(), String> {
let result = use_remote_logic(url, state);
if result.is_err() {
return Err(result.err().unwrap().to_string());
}
Ok(())
}
#[tauri::command]
pub fn gen_drop_url(path: String) -> Result<String, String> {
let base_url = {
let handle = DB.borrow_data().unwrap();
if handle.base_url.is_empty() {
return Ok("".to_string());
};
Url::parse(&handle.base_url).unwrap()
};
let url = base_url.join(&path).unwrap();
Ok(url.to_string())
}

View File

@ -0,0 +1,14 @@
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone)]
pub enum SetupError {
Context,
}
impl Display for SetupError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SetupError::Context => write!(f, "Failed to generate contexts for download"),
}
}
}

View File

@ -0,0 +1,66 @@
use std::{
fmt::Display,
ops::{FromResidual, Try},
};
use serde::Serialize;
pub enum UserValue<T, D>
where
T: Serialize,
D: Display,
{
Ok(T),
Err(D),
}
impl<T: Serialize, D: Display> Serialize for UserValue<T, D> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
UserValue::Ok(data) => data.serialize(serializer),
UserValue::Err(err) => serializer.serialize_str(err.to_string().as_ref()),
}
}
}
impl<T: Serialize, D: Display> From<Result<T, D>> for UserValue<T, D> {
fn from(value: Result<T, D>) -> Self {
match value {
Ok(data) => UserValue::Ok(data),
Err(data) => UserValue::Err(data),
}
}
}
impl<T: Serialize, D: Display> Try for UserValue<T, D> {
type Output = T;
type Residual = D;
fn from_output(output: Self::Output) -> Self {
Self::Ok(output)
}
fn branch(self) -> std::ops::ControlFlow<Self::Residual, Self::Output> {
match self {
UserValue::Ok(data) => std::ops::ControlFlow::Continue(data),
UserValue::Err(e) => std::ops::ControlFlow::Break(e),
}
}
}
impl<T: Serialize, D: Display> FromResidual for UserValue<T, D> {
fn from_residual(residual: <Self as std::ops::Try>::Residual) -> Self {
UserValue::Err(residual)
}
}
impl<T: Serialize, D: Display, U> FromResidual<Result<U, D>> for UserValue<T, D> {
fn from_residual(residual: Result<U, D>) -> Self {
match residual {
Ok(_) => unreachable!(),
Err(e) => UserValue::Err(e),
}
}
}

View File

@ -0,0 +1,57 @@
use std::sync::Mutex;
use tauri::AppHandle;
use crate::{
error::{
library_error::LibraryError, remote_access_error::RemoteAccessError, user_error::UserValue,
},
games::library::{get_current_meta, uninstall_game_logic},
AppState,
};
use super::{
library::{
fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct,
Game, GameVersionOption,
},
state::{GameStatusManager, GameStatusWithTransient},
};
#[tauri::command]
pub fn fetch_library(app: AppHandle) -> UserValue<Vec<Game>, RemoteAccessError> {
fetch_library_logic(app).into()
}
#[tauri::command]
pub fn fetch_game(
game_id: String,
app: tauri::AppHandle,
) -> UserValue<FetchGameStruct, RemoteAccessError> {
fetch_game_logic(game_id, app).into()
}
#[tauri::command]
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
GameStatusManager::fetch_state(&id)
}
#[tauri::command]
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> UserValue<(), LibraryError> {
let meta = match get_current_meta(&game_id) {
Some(data) => data,
None => return UserValue::Err(LibraryError::MetaNotFound(game_id)),
};
println!("{:?}", meta);
uninstall_game_logic(meta, &app_handle);
UserValue::Ok(())
}
#[tauri::command]
pub fn fetch_game_verion_options(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> UserValue<Vec<GameVersionOption>, RemoteAccessError> {
fetch_game_verion_options_logic(game_id, state).into()
}

View File

@ -0,0 +1,31 @@
use std::sync::{mpsc::SendError, Arc, Mutex};
use crate::{
download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable},
error::user_error::UserValue,
AppState,
};
use super::download_agent::GameDownloadAgent;
#[tauri::command]
pub fn download_game(
game_id: String,
game_version: String,
install_dir: usize,
state: tauri::State<'_, Mutex<AppState>>,
) -> UserValue<(), SendError<DownloadManagerSignal>> {
let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
game_id,
game_version,
install_dir,
sender,
)) as Box<dyn Downloadable + Send + Sync>);
state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent)
.into()
}

View File

@ -1,6 +1,7 @@
use crate::auth::generate_authorization_header;
use crate::db::{set_game_status, ApplicationTransientStatus, DatabaseImpls};
use crate::download_manager::application_download_error::ApplicationDownloadError;
use crate::database::db::{
set_game_status, ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus,
};
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
use crate::download_manager::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag,
@ -8,9 +9,10 @@ use crate::download_manager::download_thread_control_flag::{
use crate::download_manager::downloadable::Downloadable;
use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::games::library::{on_game_complete, push_game_update};
use crate::remote::RemoteAccessError;
use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent};
use crate::DB;
use log::{debug, error, info};
use rayon::ThreadPoolBuilder;
@ -100,7 +102,7 @@ impl GameDownloadAgent {
let timer = Instant::now();
push_game_update(
app_handle,
&self.metadata(),
&self.metadata().id,
(
None,
Some(ApplicationTransientStatus::Downloading {
@ -208,7 +210,8 @@ impl GameDownloadAgent {
{
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
completed_contexts_lock.clear();
completed_contexts_lock.extend(self.stored_manifest.get_completed_contexts());
completed_contexts_lock
.extend_from_slice(&self.stored_manifest.get_completed_contexts());
}
for (raw_path, chunk) in manifest {
@ -247,9 +250,12 @@ impl GameDownloadAgent {
// TODO: Change return value on Err
pub fn run(&self) -> Result<bool, ()> {
info!("downloading game: {}", self.id);
let max_download_threads = DB.borrow_data().unwrap().settings.max_download_threads;
info!(
"downloading game: {} with {} threads",
self.id, max_download_threads
);
let pool = ThreadPoolBuilder::new()
.num_threads(max_download_threads)
.build()
@ -401,8 +407,18 @@ impl Downloadable for GameDownloadAgent {
.unwrap();
}
fn on_incomplete(&self, _app_handle: &tauri::AppHandle) {
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
let meta = self.metadata();
*self.status.lock().unwrap() = DownloadStatus::Queued;
app_handle
.emit(
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(GameDownloadStatus::Remote {}), None),
},
)
.unwrap();
}
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}

View File

@ -1,58 +0,0 @@
use std::sync::{Arc, Mutex};
use crate::{
download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata},
AppState,
};
use super::download_agent::GameDownloadAgent;
#[tauri::command]
pub fn download_game(
game_id: String,
game_version: String,
install_dir: usize,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), String> {
let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
game_id,
game_version,
install_dir,
sender,
)) as Box<dyn Downloadable + Send + Sync>);
state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent)
.map_err(|_| "An error occurred while communicating with the download manager.".to_string())
}
#[tauri::command]
pub fn pause_game_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.pause_downloads()
}
#[tauri::command]
pub fn resume_game_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.resume_downloads()
}
#[tauri::command]
pub fn move_game_in_queue(
state: tauri::State<'_, Mutex<AppState>>,
old_index: usize,
new_index: usize,
) {
state
.lock()
.unwrap()
.download_manager
.rearrange(old_index, new_index)
}
#[tauri::command]
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
state.lock().unwrap().download_manager.cancel(meta)
}

View File

@ -1,10 +1,10 @@
use crate::download_manager::application_download_error::ApplicationDownloadError;
use crate::download_manager::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag,
};
use crate::download_manager::progress_object::ProgressHandle;
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::DropDownloadContext;
use crate::remote::RemoteAccessError;
use log::{error, warn};
use md5::{Context, Digest};
use reqwest::blocking::{RequestBuilder, Response};

View File

@ -1,5 +1,5 @@
pub mod commands;
pub mod download_agent;
pub mod download_commands;
mod download_logic;
mod manifest;
mod stored_manifest;

View File

@ -8,14 +8,15 @@ use tauri::Emitter;
use tauri::{AppHandle, Manager};
use urlencoding::encode;
use crate::db::GameVersion;
use crate::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus};
use crate::database::db::GameVersion;
use crate::database::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus};
use crate::download_manager::download_manager::DownloadStatus;
use crate::download_manager::downloadable_metadata::DownloadableMetadata;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::process::process_manager::Platform;
use crate::remote::RemoteAccessError;
use crate::{auth::generate_authorization_header, AppState, DB};
use crate::remote::auth::generate_authorization_header;
use crate::{AppState, DB};
#[derive(serde::Serialize)]
pub struct FetchGameStruct {
@ -78,7 +79,7 @@ pub struct GameVersionOption {
// total_size: usize,
}
fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let library_url = base_url.join("/api/v1/client/user/library")?;
@ -118,12 +119,7 @@ fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
Ok(games)
}
#[tauri::command]
pub fn fetch_library(app: AppHandle) -> Result<Vec<Game>, String> {
fetch_library_logic(app).map_err(|e| e.to_string())
}
fn fetch_game_logic(
pub fn fetch_game_logic(
id: String,
app: tauri::AppHandle,
) -> Result<FetchGameStruct, RemoteAccessError> {
@ -184,25 +180,7 @@ fn fetch_game_logic(
Ok(data)
}
#[tauri::command]
pub fn fetch_game(game_id: String, app: tauri::AppHandle) -> Result<FetchGameStruct, String> {
let result = fetch_game_logic(game_id, app);
if result.is_err() {
return Err(result.err().unwrap().to_string());
}
Ok(result.unwrap())
}
#[tauri::command]
pub fn fetch_game_status(id: String) -> Result<GameStatusWithTransient, String> {
let status = GameStatusManager::fetch_state(&id);
Ok(status)
}
fn fetch_game_verion_options_logic(
pub fn fetch_game_verion_options_logic(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersionOption>, RemoteAccessError> {
@ -238,19 +216,7 @@ fn fetch_game_verion_options_logic(
Ok(data)
}
#[tauri::command]
pub fn uninstall_game(
game_id: String,
app_handle: AppHandle,
) -> Result<(), String> {
let meta = get_current_meta(&game_id)?;
println!("{:?}", meta);
uninstall_game_logic(meta, &app_handle);
Ok(())
}
fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
println!("Triggered uninstall for agent");
let mut db_handle = DB.borrow_data_mut().unwrap();
db_handle
@ -261,7 +227,7 @@ fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
push_game_update(
app_handle,
&meta,
&meta.id,
(None, Some(ApplicationTransientStatus::Uninstalling {})),
);
@ -309,7 +275,7 @@ fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
push_game_update(
&app_handle,
&meta,
&meta.id,
(Some(GameDownloadStatus::Remote {}), None),
);
}
@ -317,25 +283,13 @@ fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
}
}
pub fn get_current_meta(game_id: &String) -> Result<DownloadableMetadata, String> {
match DB
.borrow_data()
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
DB.borrow_data()
.unwrap()
.applications
.installed_game_version
.get(game_id)
{
Some(meta) => Ok(meta.clone()),
None => Err(String::from("Could not find installed version")),
}
}
#[tauri::command]
pub fn fetch_game_verion_options(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersionOption>, String> {
fetch_game_verion_options_logic(game_id, state).map_err(|e| e.to_string())
.cloned()
}
pub fn on_game_complete(
@ -414,16 +368,12 @@ pub fn on_game_complete(
Ok(())
}
pub fn push_game_update(
app_handle: &AppHandle,
meta: &DownloadableMetadata,
status: GameStatusWithTransient,
) {
pub fn push_game_update(app_handle: &AppHandle, game_id: &String, status: GameStatusWithTransient) {
app_handle
.emit(
&format!("update_game/{}", meta.id),
&format!("update_game/{}", game_id),
GameUpdateEvent {
game_id: meta.id.clone(),
game_id: game_id.clone(),
status,
},
)

View File

@ -1,3 +1,4 @@
pub mod commands;
pub mod downloads;
pub mod library;
pub mod state;

View File

@ -1,5 +1,5 @@
use crate::{
db::{ApplicationTransientStatus, GameDownloadStatus},
database::db::{ApplicationTransientStatus, GameDownloadStatus},
DB,
};

View File

@ -1,35 +1,35 @@
mod auth;
mod db;
#![feature(try_trait_v2)]
mod database;
mod games;
mod autostart;
mod cleanup;
mod debug;
mod commands;
mod download_manager;
mod error;
mod process;
mod remote;
pub mod settings;
use crate::autostart::{get_autostart_enabled, toggle_autostart};
use crate::db::DatabaseImpls;
use auth::{
auth_initiate, generate_authorization_header, manual_recieve_handshake, recieve_handshake,
retry_connect, sign_out,
};
use crate::database::db::DatabaseImpls;
use autostart::{get_autostart_enabled, toggle_autostart};
use cleanup::{cleanup_and_exit, quit};
use db::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface,
GameDownloadStatus, DATA_ROOT_DIR,
use commands::fetch_state;
use database::commands::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_system_data,
update_settings,
};
use database::db::{DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR};
use download_manager::commands::{
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
};
use debug::fetch_system_data;
use download_manager::download_manager::DownloadManager;
use download_manager::download_manager_builder::DownloadManagerBuilder;
use games::downloads::download_commands::{
cancel_game, download_game, move_game_in_queue, pause_game_downloads, resume_game_downloads,
};
use games::library::{
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, Game,
use games::commands::{
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game,
};
use games::downloads::commands::download_game;
use games::library::Game;
use http::Response;
use http::{header::*, response::Builder as ResponseBuilder};
use log::{debug, info, warn, LevelFilter};
@ -38,11 +38,13 @@ use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
use log4rs::Config;
use process::process_commands::{kill_game, launch_game};
use process::commands::{kill_game, launch_game};
use process::process_manager::ProcessManager;
use remote::{gen_drop_url, use_remote};
use remote::auth::{self, generate_authorization_header, recieve_handshake};
use remote::commands::{
auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote,
};
use serde::{Deserialize, Serialize};
use settings::amend_settings;
use std::path::Path;
use std::sync::Arc;
use std::{
@ -88,14 +90,6 @@ pub struct AppState<'a> {
process_manager: Arc<Mutex<ProcessManager<'a>>>,
}
#[tauri::command]
fn fetch_state(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<String, String> {
let guard = state.lock().unwrap();
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
drop(guard);
Ok(cloned_state)
}
fn setup(handle: AppHandle) -> AppState<'static> {
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
@ -139,6 +133,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
debug!("Database is set up");
// TODO: Account for possible failure
let (app_status, user) = auth::setup().unwrap();
let db_handle = DB.borrow_data().unwrap();
@ -147,8 +142,8 @@ fn setup(handle: AppHandle) -> AppState<'static> {
drop(db_handle);
for (game_id, status) in statuses.into_iter() {
match status {
db::GameDownloadStatus::Remote {} => {}
db::GameDownloadStatus::SetupRequired {
database::db::GameDownloadStatus::Remote {} => {}
database::db::GameDownloadStatus::SetupRequired {
version_name: _,
install_dir,
} => {
@ -157,7 +152,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
missing_games.push(game_id);
}
}
db::GameDownloadStatus::Installed {
database::db::GameDownloadStatus::Installed {
version_name: _,
install_dir,
} => {
@ -222,7 +217,7 @@ pub fn run() {
quit,
fetch_system_data,
// User utils
amend_settings,
update_settings,
// Auth
auth_initiate,
retry_connect,
@ -241,9 +236,9 @@ pub fn run() {
fetch_game_verion_options,
// Downloads
download_game,
move_game_in_queue,
pause_game_downloads,
resume_game_downloads,
move_download_in_queue,
pause_downloads,
resume_downloads,
cancel_game,
uninstall_game,
// Processes

View File

@ -0,0 +1,44 @@
use std::sync::Mutex;
use crate::{
error::{process_error::ProcessError, user_error::UserValue},
AppState, DB,
};
#[tauri::command]
pub fn launch_game(
id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> UserValue<(), ProcessError> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
//let meta = DownloadableMetadata {
// id,
// version: Some(version),
// download_type: DownloadType::Game,
//};
match process_manager_lock.launch_process(id) {
Ok(_) => {}
Err(e) => return UserValue::Err(e),
};
drop(process_manager_lock);
drop(state_lock);
UserValue::Ok(())
}
#[tauri::command]
pub fn kill_game(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> UserValue<(), ProcessError> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
process_manager_lock
.kill_game(game_id)
.map_err(ProcessError::IOError)
.into()
}

View File

@ -1,3 +1,3 @@
pub mod commands;
pub mod compat;
pub mod process_commands;
pub mod process_manager;

View File

@ -1,55 +0,0 @@
use std::sync::Mutex;
use crate::{
db::GameDownloadStatus,
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
games::library::get_current_meta,
AppState, DB,
};
#[tauri::command]
pub fn launch_game(id: String, state: tauri::State<'_, Mutex<AppState>>) -> Result<(), String> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
let version = match DB
.borrow_data()
.unwrap()
.applications
.game_statuses
.get(&id)
.cloned()
{
Some(GameDownloadStatus::Installed {
version_name,
..
}) => version_name,
Some(GameDownloadStatus::SetupRequired {
..
}) => return Err(String::from("Game setup still required")),
_ => return Err(String::from("Game not installed")),
};
let meta = DownloadableMetadata {
id,
version: Some(version),
download_type: DownloadType::Game,
};
process_manager_lock.launch_process(meta)?;
drop(process_manager_lock);
drop(state_lock);
Ok(())
}
#[tauri::command]
pub fn kill_game(game_id: String, state: tauri::State<'_, Mutex<AppState>>) -> Result<(), String> {
let meta = get_current_meta(&game_id)?;
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
process_manager_lock
.kill_game(meta)
.map_err(|x| x.to_string())
}

View File

@ -1,7 +1,7 @@
use std::{
collections::HashMap,
fs::{File, OpenOptions},
io,
io::{self, Error},
path::{Path, PathBuf},
process::{Child, Command, ExitStatus},
sync::{Arc, Mutex},
@ -15,17 +15,17 @@ use tauri::{AppHandle, Manager};
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
use crate::{
db::{ApplicationTransientStatus, GameDownloadStatus, DATA_ROOT_DIR},
download_manager::downloadable_metadata::DownloadableMetadata,
games::library::push_game_update,
games::state::GameStatusManager,
database::db::{ApplicationTransientStatus, GameDownloadStatus, DATA_ROOT_DIR},
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
error::process_error::ProcessError,
games::{library::push_game_update, state::GameStatusManager},
AppState, DB,
};
pub struct ProcessManager<'a> {
current_platform: Platform,
log_output_dir: PathBuf,
processes: HashMap<DownloadableMetadata, Arc<SharedChild>>,
processes: HashMap<String, Arc<SharedChild>>,
app_handle: AppHandle,
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
}
@ -82,8 +82,8 @@ impl ProcessManager<'_> {
*/
(absolute_exe, Vec::new())
}
pub fn kill_game(&mut self, meta: DownloadableMetadata) -> Result<(), io::Error> {
return match self.processes.get(&meta) {
pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
return match self.processes.get(&game_id) {
Some(child) => {
child.kill()?;
child.wait()?;
@ -96,24 +96,26 @@ impl ProcessManager<'_> {
};
}
fn on_process_finish(
&mut self,
meta: DownloadableMetadata,
result: Result<ExitStatus, std::io::Error>,
) {
if !self.processes.contains_key(&meta) {
fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
if !self.processes.contains_key(&game_id) {
warn!("process on_finish was called, but game_id is no longer valid. finished with result: {:?}", result);
return;
}
debug!("process for {:?} exited with {:?}", meta, result);
debug!("process for {:?} exited with {:?}", &game_id, result);
self.processes.remove(&meta);
self.processes.remove(&game_id);
let mut db_handle = DB.borrow_data_mut().unwrap();
let meta = db_handle
.applications
.installed_game_version
.get(&game_id)
.cloned()
.unwrap();
db_handle.applications.transient_statuses.remove(&meta);
let current_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
let current_state = db_handle.applications.game_statuses.get(&game_id).cloned();
if let Some(saved_state) = current_state {
if let GameDownloadStatus::SetupRequired {
version_name,
@ -123,7 +125,7 @@ impl ProcessManager<'_> {
if let Ok(exit_code) = result {
if exit_code.success() {
db_handle.applications.game_statuses.insert(
meta.id.clone(),
game_id.clone(),
GameDownloadStatus::Installed {
version_name: version_name.to_string(),
install_dir: install_dir.to_string(),
@ -135,9 +137,9 @@ impl ProcessManager<'_> {
}
drop(db_handle);
let status = GameStatusManager::fetch_state(&meta.id);
let status = GameStatusManager::fetch_state(&game_id);
push_game_update(&self.app_handle, &meta, status);
push_game_update(&self.app_handle, &game_id, status);
// TODO better management
}
@ -149,28 +151,48 @@ impl ProcessManager<'_> {
.contains_key(&(current.clone(), platform.clone())))
}
pub fn launch_process(&mut self, meta: DownloadableMetadata) -> Result<(), String> {
if self.processes.contains_key(&meta) {
return Err("Game or setup is already running.".to_owned());
pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
if self.processes.contains_key(&game_id) {
return Err(ProcessError::AlreadyRunning);
}
let version = match DB
.borrow_data()
.unwrap()
.applications
.game_statuses
.get(&game_id)
.cloned()
{
Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
Some(GameDownloadStatus::SetupRequired { .. }) => {
return Err(ProcessError::SetupRequired).into()
}
_ => return Err(ProcessError::NotInstalled).into(),
};
let meta = DownloadableMetadata {
id: game_id.clone(),
version: Some(version.clone()),
download_type: DownloadType::Game,
};
let mut db_lock = DB.borrow_data_mut().unwrap();
debug!(
"Launching process {:?} with games {:?}",
meta, db_lock.applications.game_versions
&game_id, db_lock.applications.game_versions
);
let game_status = db_lock
.applications
.game_statuses
.get(&meta.id)
.ok_or("game not installed")?;
.get(&game_id)
.ok_or(ProcessError::NotInstalled)?;
let status_metadata: Option<(&String, &String)> = match game_status {
GameDownloadStatus::Installed {
version_name,
install_dir,
} => Some((version_name, install_dir)),
} => Some((&version_name, &install_dir)),
GameDownloadStatus::SetupRequired {
version_name,
install_dir,
@ -179,7 +201,7 @@ impl ProcessManager<'_> {
};
if status_metadata.is_none() {
return Err("game has not been downloaded.".to_owned());
return Err(ProcessError::NotDownloaded);
}
let (version_name, install_dir) = status_metadata.unwrap();
@ -187,10 +209,10 @@ impl ProcessManager<'_> {
let game_version = db_lock
.applications
.game_versions
.get(&meta.id)
.ok_or("Invalid game ID".to_owned())?
.get(&game_id)
.ok_or(ProcessError::InvalidID)?
.get(version_name)
.ok_or("Invalid version name".to_owned())?;
.ok_or(ProcessError::InvalidVersion)?;
let raw_command: String = match game_status {
GameDownloadStatus::Installed {
@ -222,11 +244,11 @@ impl ProcessManager<'_> {
.create(true)
.open(self.log_output_dir.join(format!(
"{}-{}-{}.log",
meta.id.clone(),
meta.version.clone().unwrap_or_default(),
&game_id,
&version,
current_time.timestamp()
)))
.map_err(|v| v.to_string())?;
.map_err(ProcessError::IOError)?;
let error_file = OpenOptions::new()
.write(true)
@ -235,11 +257,11 @@ impl ProcessManager<'_> {
.create(true)
.open(self.log_output_dir.join(format!(
"{}-{}-{}-error.log",
meta.id.clone(),
meta.version.clone().unwrap_or_default(),
&game_id,
&version,
current_time.timestamp()
)))
.map_err(|v| v.to_string())?;
.map_err(ProcessError::IOError)?;
let current_platform = self.current_platform.clone();
let target_platform = game_version.platform.clone();
@ -247,20 +269,21 @@ impl ProcessManager<'_> {
let game_launcher = self
.game_launchers
.get(&(current_platform, target_platform))
.ok_or("Invalid version for this platform.")
.map_err(|e| e.to_string())?;
.ok_or(ProcessError::InvalidPlatform)?;
let launch_process = game_launcher.launch_process(
&meta,
command.to_str().unwrap().to_owned(),
args,
target_current_dir,
log_file,
error_file,
)?;
let launch_process = game_launcher
.launch_process(
&meta,
command.to_str().unwrap().to_owned(),
args,
target_current_dir,
log_file,
error_file,
)
.map_err(ProcessError::IOError)?;
let launch_process_handle =
Arc::new(SharedChild::new(launch_process).map_err(|e| e.to_string())?);
Arc::new(SharedChild::new(launch_process).map_err(ProcessError::IOError)?);
db_lock
.applications
@ -269,7 +292,7 @@ impl ProcessManager<'_> {
push_game_update(
&self.app_handle,
&meta,
&meta.id,
(None, Some(ApplicationTransientStatus::Running {})),
);
@ -284,7 +307,7 @@ impl ProcessManager<'_> {
let app_state_handle = app_state.lock().unwrap();
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
process_manager_handle.on_process_finish(wait_thread_game_id, result);
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
// As everything goes out of scope, they should get dropped
// But just to explicit about it
@ -292,7 +315,7 @@ impl ProcessManager<'_> {
drop(app_state_handle);
});
self.processes.insert(meta, wait_thread_handle);
self.processes.insert(meta.id, wait_thread_handle);
info!("finished spawning process");
@ -315,7 +338,7 @@ pub trait ProcessHandler: Send + 'static {
current_dir: &str,
log_file: File,
error_file: File,
) -> Result<Child, String>;
) -> Result<Child, Error>;
}
struct NativeGameLauncher;
@ -328,14 +351,13 @@ impl ProcessHandler for NativeGameLauncher {
current_dir: &str,
log_file: File,
error_file: File,
) -> Result<Child, String> {
) -> Result<Child, Error> {
Command::new(command)
.current_dir(current_dir)
.stdout(log_file)
.stderr(error_file)
.args(args)
.spawn()
.map_err(|v| v.to_string())
}
}
@ -350,12 +372,11 @@ impl ProcessHandler for UMULauncher {
_current_dir: &str,
_log_file: File,
_error_file: File,
) -> Result<Child, String> {
) -> Result<Child, Error> {
UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command)
.game_id(String::from("0"))
.launch_args(args)
.build()
.spawn()
.map_err(|x| x.to_string())
}
}

View File

@ -8,8 +8,8 @@ use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
db::{DatabaseAuth, DatabaseImpls},
remote::{DropServerError, RemoteAccessError},
database::db::{DatabaseAuth, DatabaseImpls},
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppState, AppStatus, User, DB,
};
@ -73,7 +73,6 @@ pub fn fetch_user() -> Result<User, RemoteAccessError> {
.get(endpoint.to_string())
.header("Authorization", header)
.send()?;
if response.status() != 200 {
let err: DropServerError = response.json().unwrap();
warn!("{:?}", err);
@ -85,9 +84,7 @@ pub fn fetch_user() -> Result<User, RemoteAccessError> {
return Err(RemoteAccessError::InvalidResponse(err));
}
let user = response.json()?;
Ok(user)
response.json::<User>().map_err(|e| e.into())
}
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
@ -138,12 +135,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
Ok(())
}
#[tauri::command]
pub fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), String> {
recieve_handshake(app, format!("handshake/{}", token));
Ok(())
}
pub fn recieve_handshake(app: AppHandle, path: String) {
// Tell the app we're processing
app.emit("auth/processing", ()).unwrap();
@ -158,7 +149,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
app.emit("auth/finished", ()).unwrap();
}
fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
let base_url = {
let db_lock = DB.borrow_data().unwrap();
Url::parse(&db_lock.base_url.clone())?
@ -189,71 +180,15 @@ fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
Ok(())
}
#[tauri::command]
pub fn auth_initiate() -> Result<(), String> {
let result = auth_initiate_wrapper();
if result.is_err() {
return Err(result.err().unwrap().to_string());
}
Ok(())
}
#[tauri::command]
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) -> Result<(), ()> {
let (app_status, user) = setup()?;
let mut guard = state.lock().unwrap();
guard.status = app_status;
guard.user = user;
drop(guard);
Ok(())
}
pub fn setup() -> Result<(AppStatus, Option<User>), ()> {
pub fn setup() -> Result<(AppStatus, Option<User>), RemoteAccessError> {
let data = DB.borrow_data().unwrap();
let auth = data.auth.clone();
drop(data);
if auth.is_some() {
let user_result = fetch_user();
if user_result.is_err() {
let error = user_result.err().unwrap();
warn!("auth setup failed with: {}", error);
match error {
RemoteAccessError::FetchError(_) => {
return Ok((AppStatus::ServerUnavailable, None))
}
_ => return Ok((AppStatus::SignedInNeedsReauth, None)),
}
}
return Ok((AppStatus::SignedIn, Some(user_result.unwrap())));
let user_result = fetch_user()?;
return Ok((AppStatus::SignedIn, Some(user_result)));
}
Ok((AppStatus::SignedOut, None))
}
#[tauri::command]
pub fn sign_out(app: AppHandle) -> Result<(), String> {
// Clear auth from database
{
let mut handle = DB.borrow_data_mut().unwrap();
handle.auth = None;
drop(handle);
DB.save().unwrap();
}
// Update app state
{
let app_state = app.state::<Mutex<AppState>>();
let mut app_state_handle = app_state.lock().unwrap();
app_state_handle.status = AppStatus::SignedOut;
app_state_handle.user = None;
}
// Emit event for frontend
app.emit("auth/signedout", ()).unwrap();
Ok(())
}

View File

@ -0,0 +1,78 @@
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
error::{remote_access_error::RemoteAccessError, user_error::UserValue},
AppState, AppStatus, DB,
};
use super::{
auth::{auth_initiate_logic, recieve_handshake, setup},
remote::use_remote_logic,
};
#[tauri::command]
pub fn use_remote(
url: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> UserValue<(), RemoteAccessError> {
UserValue::Ok(use_remote_logic(url, state)?)
}
#[tauri::command]
pub fn gen_drop_url(path: String) -> UserValue<String, RemoteAccessError> {
let base_url = {
let handle = DB.borrow_data().unwrap();
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
};
let url = base_url.join(&path).unwrap();
UserValue::Ok(url.to_string())
}
#[tauri::command]
pub fn sign_out(app: AppHandle) {
// Clear auth from database
{
let mut handle = DB.borrow_data_mut().unwrap();
handle.auth = None;
drop(handle);
DB.save().unwrap();
}
// Update app state
{
let app_state = app.state::<Mutex<AppState>>();
let mut app_state_handle = app_state.lock().unwrap();
app_state_handle.status = AppStatus::SignedOut;
app_state_handle.user = None;
}
// Emit event for frontend
app.emit("auth/signedout", ()).unwrap();
}
#[tauri::command]
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) -> UserValue<(), RemoteAccessError> {
let (app_status, user) = setup()?;
let mut guard = state.lock().unwrap();
guard.status = app_status;
guard.user = user;
drop(guard);
UserValue::Ok(())
}
#[tauri::command]
pub fn auth_initiate() -> UserValue<(), RemoteAccessError> {
auth_initiate_logic().into()
}
#[tauri::command]
pub fn manual_recieve_handshake(app: AppHandle, token: String) {
recieve_handshake(app, format!("handshake/{}", token));
}

View File

@ -0,0 +1,3 @@
pub mod auth;
pub mod commands;
pub mod remote;

View File

@ -0,0 +1,49 @@
use std::{
error::Error,
fmt::{Display, Formatter},
sync::{Arc, Mutex},
};
use http::StatusCode;
use log::{info, warn};
use serde::Deserialize;
use url::{ParseError, Url};
use crate::{error::remote_access_error::RemoteAccessError, AppState, AppStatus, DB};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DropHealthcheck {
app_name: String,
}
pub fn use_remote_logic(
url: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<(), RemoteAccessError> {
info!("connecting to url {}", url);
let base_url = Url::parse(&url)?;
// Test Drop url
let test_endpoint = base_url.join("/api/v1")?;
let response = reqwest::blocking::get(test_endpoint.to_string())?;
let result: DropHealthcheck = response.json()?;
if result.app_name != "Drop" {
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
return Err(RemoteAccessError::InvalidEndpoint);
}
let mut app_state = state.lock().unwrap();
app_state.status = AppStatus::SignedOut;
drop(app_state);
let mut db_state = DB.borrow_data_mut().unwrap();
db_state.base_url = base_url.to_string();
drop(db_state);
DB.save().unwrap();
Ok(())
}

View File

@ -1,35 +0,0 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::DB;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
// ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4
}
}
}
fn deserialize_into<T>(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error>
where T: for<'a> Deserialize<'a>
{
*t = serde_json::from_value(v)?;
Ok(())
}
#[tauri::command]
pub fn amend_settings(new_settings: Value) {
let db_lock = DB.borrow_data_mut().unwrap();
let mut current_settings = db_lock.settings.clone();
let e = deserialize_into(new_settings, &mut current_settings);
println!("Amend status: {:?}", e);
println!("New settings: {:?}", current_settings);
}