diff --git a/pages/library/[id]/index.vue b/pages/library/[id]/index.vue index 5f3c119..9ffd0ce 100644 --- a/pages/library/[id]/index.vue +++ b/pages/library/[id]/index.vue @@ -18,7 +18,11 @@
- +
@@ -168,8 +172,8 @@

- There are no supported versions to install. Please contact your - server admin or try again later. + There are no supported versions to install. Please + contact your server admin or try again later.

@@ -369,4 +373,13 @@ async function install() { installError.value = (error as string).toString(); } } + +async function play() { + try { + await invoke("launch_game", { gameId: game.value.id }); + } catch (e) { + game.value.mName = e as string; + console.error(e); + } +} diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index e5d60b2..603db7b 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -34,9 +34,11 @@ pub enum DatabaseGameStatus { }, SetupRequired { version_name: String, + install_dir: String, }, Installed { version_name: String, + install_dir: String, }, Updating { version_name: String, @@ -100,11 +102,14 @@ impl DatabaseImpls for DatabaseInterface { let data_root_dir = DATA_ROOT_DIR.lock().unwrap(); let db_path = data_root_dir.join("drop.db"); let games_base_dir = data_root_dir.join("games"); + let logs_root_dir = data_root_dir.join("logs"); debug!("Creating data directory at {:?}", data_root_dir); create_dir_all(data_root_dir.clone()).unwrap(); debug!("Creating games directory"); create_dir_all(games_base_dir.clone()).unwrap(); + debug!("Creating logs directory"); + create_dir_all(logs_root_dir.clone()).unwrap(); #[allow(clippy::let_and_return)] let exists = fs::exists(db_path.clone()).unwrap(); diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 5aed80a..3068089 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -4,9 +4,9 @@ use crate::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::downloads::progress_object::ProgressHandle; use crate::remote::RemoteAccessError; use crate::DB; +use core::time; use log::{debug, error, info}; use rayon::ThreadPoolBuilder; -use core::time; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::io; @@ -28,7 +28,7 @@ pub struct GameDownloadAgent { pub id: String, pub version: String, pub control_flag: DownloadThreadControl, - pub target_download_dir: usize, + pub base_dir: String, contexts: Mutex>, pub manifest: Mutex>, pub progress: Arc, @@ -72,12 +72,20 @@ impl GameDownloadAgent { ) -> Self { // Don't run by default let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); + + let db_lock = DB.borrow_data().unwrap(); + let base_dir = db_lock.games.install_dirs[target_download_dir].clone(); + drop(db_lock); + + let base_dir_path = Path::new(&base_dir); + let data_base_dir_path = base_dir_path.join(id.clone()); + Self { id, version, control_flag, manifest: Mutex::new(None), - target_download_dir, + base_dir: data_base_dir_path.to_str().unwrap().to_owned(), contexts: Mutex::new(Vec::new()), progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), sender, @@ -104,7 +112,11 @@ impl GameDownloadAgent { let timer = Instant::now(); self.run().map_err(|_| GameDownloadError::DownloadError)?; - info!("{} took {}ms to download", self.id, timer.elapsed().as_millis()); + info!( + "{} took {}ms to download", + self.id, + timer.elapsed().as_millis() + ); Ok(()) } @@ -187,18 +199,12 @@ impl GameDownloadAgent { } pub fn generate_contexts(&self) -> Result<(), GameDownloadError> { - let db_lock = DB.borrow_data().unwrap(); - let data_base_dir = db_lock.games.install_dirs[self.target_download_dir].clone(); - drop(db_lock); - let manifest = self.manifest.lock().unwrap().clone().unwrap(); let game_id = self.id.clone(); - let data_base_dir_path = Path::new(&data_base_dir); - let mut contexts = Vec::new(); - let base_path = data_base_dir_path.join(game_id.clone()).clone(); - create_dir_all(base_path.clone()).unwrap(); + let base_path = Path::new(&self.base_dir); + create_dir_all(base_path).unwrap(); for (raw_path, chunk) in manifest { let path = base_path.join(Path::new(&raw_path)); @@ -219,6 +225,7 @@ impl GameDownloadAgent { path: path.clone(), checksum: chunk.checksums[index].clone(), length: *length, + permissions: chunk.permissions, }); running_offset += *length as u64; } diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 6735c56..8f82962 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -6,8 +6,11 @@ use crate::DB; use log::warn; use md5::{Context, Digest}; use reqwest::blocking::Response; +use tauri::utils::acl::Permission; +use std::fs::{set_permissions, Permissions}; use std::io::Read; +use std::os::unix::fs::PermissionsExt; use std::{ fs::{File, OpenOptions}, io::{self, BufWriter, Seek, SeekFrom, Write}, @@ -157,7 +160,7 @@ pub fn download_game_chunk( )); } - let mut destination = DropWriter::new(ctx.path); + let mut destination = DropWriter::new(ctx.path.clone()); if ctx.offset != 0 { destination @@ -185,6 +188,13 @@ pub fn download_game_chunk( return Ok(false); }; + // If we complete the file, set the permissions (if on Linux) + #[cfg(unix)] + { + let permissions = Permissions::from_mode(ctx.permissions); + set_permissions(ctx.path, permissions).unwrap(); + } + /* let checksum = pipeline .finish() diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index ca18df5..dfe429d 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -220,9 +220,12 @@ impl DownloadManagerBuilder { info!("Popping consumed data"); let download_agent = self.remove_and_cleanup_game(&game_id); - if let Err(error) = - on_game_complete(game_id, download_agent.version.clone(), &self.app_handle) - { + if let Err(error) = on_game_complete( + game_id, + download_agent.version.clone(), + download_agent.base_dir.clone(), + &self.app_handle, + ) { self.sender .send(DownloadManagerSignal::Error( GameDownloadError::Communication(error), diff --git a/src-tauri/src/downloads/manifest.rs b/src-tauri/src/downloads/manifest.rs index d815861..7c5b3a9 100644 --- a/src-tauri/src/downloads/manifest.rs +++ b/src-tauri/src/downloads/manifest.rs @@ -6,7 +6,7 @@ pub type DropManifest = HashMap; #[derive(Serialize, Deserialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DropChunk { - pub permissions: usize, + pub permissions: u32, pub ids: Vec, pub checksums: Vec, pub lengths: Vec, @@ -23,4 +23,5 @@ pub struct DropDownloadContext { pub path: PathBuf, pub checksum: String, pub length: usize, + pub permissions: u32, } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8e4e2fc..b74eb98 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,6 +26,7 @@ use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Root}; use log4rs::encode::pattern::PatternEncoder; use log4rs::Config; +use process::process_commands::launch_game; use process::process_manager::ProcessManager; use remote::{gen_drop_url, use_remote}; use serde::{Deserialize, Serialize}; @@ -67,7 +68,7 @@ pub struct AppState { #[serde(skip_serializing)] download_manager: Arc, #[serde(skip_serializing)] - process_manager: Arc, + process_manager: Arc>, } #[tauri::command] @@ -104,7 +105,7 @@ fn setup(handle: AppHandle) -> AppState { let games = HashMap::new(); let download_manager = Arc::new(DownloadManagerBuilder::build(handle)); - let process_manager = Arc::new(ProcessManager::new()); + let process_manager = Arc::new(Mutex::new(ProcessManager::new())); debug!("Checking if database is set up"); let is_set_up = DB.database_is_set_up(); @@ -168,6 +169,8 @@ pub fn run() { move_game_in_queue, pause_game_downloads, resume_game_downloads, + // Processes + launch_game, ]) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 15fa7b6..860729a 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -225,15 +225,12 @@ fn fetch_game_verion_options_logic<'a>( let data = response.json::>()?; let state_lock = state.lock().unwrap(); + let process_manager_lock = state_lock.process_manager.lock().unwrap(); let data = data .into_iter() - .filter(|v| { - state_lock - .process_manager - .valid_platform(&v.platform) - .unwrap() - }) + .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap()) .collect::>(); + drop(process_manager_lock); drop(state_lock); Ok(data) @@ -250,6 +247,7 @@ pub fn fetch_game_verion_options<'a>( pub fn on_game_complete( game_id: String, version_name: String, + install_dir: String, app_handle: &AppHandle, ) -> Result<(), RemoteAccessError> { // Fetch game version information from remote @@ -284,9 +282,15 @@ pub fn on_game_complete( DB.save().unwrap(); let status = if data.setup_command.is_empty() { - DatabaseGameStatus::Installed { version_name } + DatabaseGameStatus::Installed { + version_name, + install_dir, + } } else { - DatabaseGameStatus::SetupRequired { version_name } + DatabaseGameStatus::SetupRequired { + version_name, + install_dir, + } }; let mut db_handle = DB.borrow_data_mut().unwrap(); diff --git a/src-tauri/src/process/mod.rs b/src-tauri/src/process/mod.rs index 30cc14c..a4ceb45 100644 --- a/src-tauri/src/process/mod.rs +++ b/src-tauri/src/process/mod.rs @@ -1 +1,2 @@ pub mod process_manager; +pub mod process_commands; \ No newline at end of file diff --git a/src-tauri/src/process/process_commands.rs b/src-tauri/src/process/process_commands.rs new file mode 100644 index 0000000..327728f --- /dev/null +++ b/src-tauri/src/process/process_commands.rs @@ -0,0 +1,16 @@ +use std::sync::Mutex; + +use crate::AppState; + +#[tauri::command] +pub fn launch_game(game_id: String, state: tauri::State<'_, Mutex>) -> Result<(), String> { + let state_lock = state.lock().unwrap(); + let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); + + process_manager_lock.launch_game(game_id)?; + + drop(process_manager_lock); + drop(state_lock); + + Ok(()) +} diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs index e0bda56..8eed7ef 100644 --- a/src-tauri/src/process/process_manager.rs +++ b/src-tauri/src/process/process_manager.rs @@ -1,22 +1,62 @@ -use std::{collections::HashMap, sync::LazyLock}; +use std::{ + collections::HashMap, + fs::{File, OpenOptions}, + path::PathBuf, + process::{Child, Command}, + sync::LazyLock, +}; +use log::info; use serde::{Deserialize, Serialize}; +use crate::{ + db::{DatabaseGameStatus, DATA_ROOT_DIR}, + DB, +}; + pub struct ProcessManager { current_platform: Platform, + log_output_dir: PathBuf, + processes: HashMap, } impl ProcessManager { pub fn new() -> Self { + let root_dir_lock = DATA_ROOT_DIR.lock().unwrap(); + let log_output_dir = root_dir_lock.join("logs"); + drop(root_dir_lock); + ProcessManager { current_platform: if cfg!(windows) { Platform::Windows } else { Platform::Linux }, + + processes: HashMap::new(), + log_output_dir, } } + fn process_command(&self, raw_command: String) -> (String, Vec) { + let command_components = raw_command.split(" ").collect::>(); + let root = match self.current_platform { + Platform::Windows => command_components[0].to_string(), + Platform::Linux => { + let mut root = command_components[0].to_string(); + if !root.starts_with("./") { + root = format!("{}{}", "./", root); + } + root + } + }; + let args = command_components[1..] + .into_iter() + .map(|v| v.to_string()) + .collect(); + (root, args) + } + pub fn valid_platform(&self, platform: &Platform) -> Result { let current = &self.current_platform; let valid_platforms = PROCESS_COMPATABILITY_MATRIX @@ -25,6 +65,63 @@ impl ProcessManager { Ok(valid_platforms.contains(platform)) } + + pub fn launch_game(&mut self, game_id: String) -> Result<(), String> { + if self.processes.contains_key(&game_id) { + return Err("Game or setup is already running.".to_owned()); + } + + let db_lock = DB.borrow_data().unwrap(); + let game_status = db_lock + .games + .games_statuses + .get(&game_id) + .ok_or("Game not installed")?; + + let DatabaseGameStatus::Installed { + version_name, + install_dir, + } = game_status + else { + return Err("Game not installed.".to_owned()); + }; + + let game_version = db_lock + .games + .game_versions + .get(&game_id) + .ok_or("Invalid game ID".to_owned())? + .get(version_name) + .ok_or("Invalid version name".to_owned())?; + + let (command, args) = self.process_command(game_version.launch_command.clone()); + + info!("launching process {} in {}", command, install_dir); + + let current_time = chrono::offset::Local::now(); + let log_file = OpenOptions::new() + .write(true) + .append(true) + .read(true) + .create(true) + .open(self.log_output_dir.join(format!( + "{}-{}.log", + game_id, + current_time.to_rfc3339() + ))) + .map_err(|v| v.to_string())?; + + let launch_process = Command::new(command) + .current_dir(install_dir) + .stdout(log_file) + .args(args) + .spawn() + .map_err(|v| v.to_string())?; + + self.processes.insert(game_id, launch_process); + + Ok(()) + } } #[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone)]