From 893610d9a4939927bfdfc3fe1498b2a35b86d63f Mon Sep 17 00:00:00 2001 From: Wyatt Date: Tue, 4 Jul 2023 18:33:08 -0700 Subject: [PATCH 01/10] profiles use names, projects use relative, rename/deletion events --- theseus/src/api/profile.rs | 17 ++-- theseus/src/api/profile_create.rs | 2 +- theseus/src/state/mod.rs | 26 ++++-- theseus/src/state/profiles.rs | 114 ++++++++++++++++++----- theseus/src/state/projects.rs | 46 +++++---- theseus_gui/src/pages/instance/Index.vue | 6 ++ 6 files changed, 157 insertions(+), 54 deletions(-) diff --git a/theseus/src/api/profile.rs b/theseus/src/api/profile.rs index e4de2b372..1bfaca61e 100644 --- a/theseus/src/api/profile.rs +++ b/theseus/src/api/profile.rs @@ -58,7 +58,9 @@ pub async fn get( path: &Path, clear_projects: Option, ) -> crate::Result> { + tracing::debug!("Getting profile: {}", path.display()); let state = State::get().await?; + let profiles = state.profiles.read().await; let mut profile = profiles.0.get(path).cloned(); @@ -316,6 +318,8 @@ pub async fn update_all( } } +/// Updates a project to the latest version +/// Uses and returns the relative path to the project #[tracing::instrument] #[theseus_macros::debug_pin] pub async fn update_project( @@ -382,6 +386,7 @@ pub async fn update_project( } /// Add a project from a version +/// Returns the relative path to the project #[tracing::instrument] pub async fn add_project_from_version( profile_path: &Path, @@ -409,6 +414,7 @@ pub async fn add_project_from_version( } /// Add a project from an FS path +/// Uses and returns the relative path to the project #[tracing::instrument] pub async fn add_project_from_path( profile_path: &Path, @@ -450,6 +456,8 @@ pub async fn add_project_from_path( } /// Toggle whether a project is disabled or not +/// Project path should be relative to the profile +/// returns the new state, relative to the profile #[tracing::instrument] pub async fn toggle_disable_project( profile: &Path, @@ -477,6 +485,7 @@ pub async fn toggle_disable_project( } /// Remove a project from a profile +/// Uses and returns the relative path to the project #[tracing::instrument] pub async fn remove_project( profile: &Path, @@ -832,17 +841,11 @@ pub fn create_mrpack_json( .map(|(k, v)| (k, sanitize_loader_version_string(&v).to_string())) .collect::>(); - let base_path = &profile.path; let files: Result, crate::ErrorKind> = profile .projects .iter() .filter_map(|(mod_path, project)| { - let path = match mod_path.strip_prefix(base_path) { - Ok(path) => path.to_string_lossy().to_string(), - Err(e) => { - return Some(Err(e.into())); - } - }; + let path = mod_path.to_string_lossy().to_string(); // Only Modrinth projects have a modrinth metadata field for the modrinth.json Some(Ok(match project.metadata { diff --git a/theseus/src/api/profile_create.rs b/theseus/src/api/profile_create.rs index 88a802b61..313fec475 100644 --- a/theseus/src/api/profile_create.rs +++ b/theseus/src/api/profile_create.rs @@ -36,7 +36,7 @@ pub async fn profile_create( trace!("Creating new profile. {}", name); let state = State::get().await?; let uuid = Uuid::new_v4(); - let path = state.directories.profiles_dir().join(uuid.to_string()); + let path = state.directories.profiles_dir().join(&name); if path.exists() { if !path.is_dir() { return Err(ProfileCreationError::NotFolder.into()); diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 76b4623c9..aa1bc60b1 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -248,6 +248,7 @@ async fn init_watcher() -> crate::Result> { None, move |res: DebounceEventResult| { futures::executor::block_on(async { + tracing::info!("File watcher event: {:?}", res); tx.send(res).await.unwrap(); }) }, @@ -256,13 +257,16 @@ async fn init_watcher() -> crate::Result> { tokio::task::spawn(async move { while let Some(res) = rx.next().await { match res { - Ok(events) => { + Ok(mut events) => { let mut visited_paths = Vec::new(); - events.iter().for_each(|e| { + // sort events by e.path + events.sort_by(|a, b| a.path.cmp(&b.path)); + events.iter().for_each(|e| { + tracing::debug!("File watcher event: {:?}", serde_json::to_string(&e.path).unwrap()); let mut new_path = PathBuf::new(); + let mut components_iterator = e.path.components(); let mut found = false; - - for component in e.path.components() { + while let Some(component) = components_iterator.next() { new_path.push(component); if found { break; @@ -271,6 +275,8 @@ async fn init_watcher() -> crate::Result> { found = true; } } + // if any remain, it's a subfile + let subfile = components_iterator.next().is_some(); if e.path .components() @@ -282,8 +288,16 @@ async fn init_watcher() -> crate::Result> { { Profile::crash_task(new_path); } else if !visited_paths.contains(&new_path) { - Profile::sync_projects_task(new_path.clone()); - visited_paths.push(new_path); + if subfile { + Profile::sync_projects_task(new_path.clone()); + visited_paths.push(new_path); + } else { + tracing::warn!("No subfile found "); + + Profiles::sync_available_profiles_task( + new_path.clone(), + ); + } } }); } diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index 97001fc8f..080710bcc 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -19,6 +19,7 @@ use notify::{RecommendedWatcher, RecursiveMode}; use notify_debouncer_mini::Debouncer; use reqwest::Method; use serde::{Deserialize, Serialize}; +use tracing::info; use std::io::Cursor; use std::{ collections::HashMap, @@ -183,6 +184,11 @@ impl Profile { }) } + #[inline] + pub fn name(&self) -> String { + self.metadata.name.clone() + } + #[tracing::instrument(skip(self, semaphore, icon))] pub async fn set_icon<'a>( &'a mut self, @@ -224,12 +230,16 @@ impl Profile { pub fn sync_projects_task(path: PathBuf) { tokio::task::spawn(async move { + let span = + tracing::span!(tracing::Level::INFO, "sync_projects_task"); + info!(parent: &span, "Syncing projects for profile {}", path.display()); let res = async { + let _span = span.enter(); let state = State::get().await?; let profile = crate::api::profile::get(&path, None).await?; if let Some(profile) = profile { - let paths = profile.get_profile_project_paths()?; + let paths = profile.get_profile_full_project_paths()?; let projects = crate::state::infer_data_from_files( profile.clone(), @@ -251,6 +261,7 @@ impl Profile { ProfilePayloadType::Synced, ) .await?; + tracing::info!("Done syncing"); } else { tracing::warn!( "Unable to fetch single profile projects: path {path:?} invalid", @@ -269,7 +280,9 @@ impl Profile { }); } - pub fn get_profile_project_paths(&self) -> crate::Result> { + /// Gets paths to projects as their full paths, not just their relative paths + pub fn get_profile_full_project_paths(&self) -> crate::Result> { + tracing::info!("Getting profile project paths, profile path: {}", self.path.display()); let mut files = Vec::new(); let mut read_paths = |path: &str| { let new_path = self.path.join(path); @@ -416,8 +429,9 @@ impl Profile { } }; - let state = State::get().await?; - let path = self.path.join(project_type.get_folder()).join(file_name); + let state: std::sync::Arc = State::get().await?; + let relative_name = PathBuf::new().join(project_type.get_folder()).join(file_name); + let path = self.path.join(relative_name.clone()); write(&path, &bytes, &state.io_semaphore).await?; let hash = get_hash(bytes).await?; @@ -438,32 +452,34 @@ impl Profile { } } - Ok(path) + Ok(relative_name) } + /// Toggle a project's disabled state. + /// 'path' should be relative to the profile's path. #[tracing::instrument(skip(self))] #[theseus_macros::debug_pin] pub async fn toggle_disable_project( &self, - path: &Path, + relative_path: &Path, ) -> crate::Result { let state = State::get().await?; if let Some(mut project) = { - let mut profiles = state.profiles.write().await; + let mut profiles: tokio::sync::RwLockWriteGuard<'_, Profiles> = state.profiles.write().await; if let Some(profile) = profiles.0.get_mut(&self.path) { - profile.projects.remove(path) + profile.projects.remove(relative_path) } else { None } } { - let path = path.to_path_buf(); - let mut new_path = path.clone(); + let relative_path = relative_path.to_path_buf(); + let mut new_path = relative_path.clone(); - if path.extension().map_or(false, |ext| ext == "disabled") { + if relative_path.extension().map_or(false, |ext| ext == "disabled") { project.disabled = false; new_path.set_file_name( - path.file_name() + relative_path.file_name() .unwrap_or_default() .to_string_lossy() .replace(".disabled", ""), @@ -471,12 +487,14 @@ impl Profile { } else { new_path.set_file_name(format!( "{}.disabled", - path.file_name().unwrap_or_default().to_string_lossy() + relative_path.file_name().unwrap_or_default().to_string_lossy() )); project.disabled = true; } - fs::rename(path, &new_path).await?; + let true_path = self.path.join(&relative_path); + let true_new_path = self.path.join(&new_path); + fs::rename(true_path, &true_new_path).await?; let mut profiles = state.profiles.write().await; if let Some(profile) = profiles.0.get_mut(&self.path) { @@ -488,7 +506,7 @@ impl Profile { } else { Err(crate::ErrorKind::InputError(format!( "Project path does not exist: {:?}", - path + relative_path )) .into()) } @@ -496,24 +514,24 @@ impl Profile { pub async fn remove_project( &self, - path: &Path, + relative_path: &Path, dont_remove_arr: Option, ) -> crate::Result<()> { let state = State::get().await?; - if self.projects.contains_key(path) { - fs::remove_file(path).await?; + if self.projects.contains_key(relative_path) { + fs::remove_file(self.path.join(relative_path)).await?; if !dont_remove_arr.unwrap_or(false) { let mut profiles = state.profiles.write().await; if let Some(profile) = profiles.0.get_mut(&self.path) { - profile.projects.remove(path); + profile.projects.remove(relative_path); profile.metadata.date_modified = Utc::now(); } } } else { return Err(crate::ErrorKind::InputError(format!( "Project path does not exist: {:?}", - path + relative_path )) .into()); } @@ -531,6 +549,11 @@ impl Profiles { ) -> crate::Result { let mut profiles = HashMap::new(); fs::create_dir_all(dirs.profiles_dir()).await?; + + file_watcher + .watcher() + .watch(&dirs.profiles_dir(), RecursiveMode::NonRecursive)?; + let mut entries = fs::read_dir(dirs.profiles_dir()).await?; while let Some(entry) = entries.next_entry().await? { let path = entry.path(); @@ -566,7 +589,7 @@ impl Profiles { { let profiles = state.profiles.read().await; for (_profile_path, profile) in profiles.0.iter() { - let paths = profile.get_profile_project_paths()?; + let paths = profile.get_profile_full_project_paths()?; files.push((profile.clone(), paths)); } @@ -661,6 +684,7 @@ impl Profiles { stream::iter(self.0.iter()) .map(Ok::<_, crate::Error>) .try_for_each_concurrent(None, |(path, profile)| async move { + tracing::info!("Syncing profile: {:?}", path); let json = serde_json::to_vec(&profile)?; let json_path = Path::new(&path.to_string_lossy().to_string()) @@ -674,10 +698,56 @@ impl Profiles { Ok(self) } - async fn read_profile_from_dir(path: &Path) -> crate::Result { + async fn read_profile_from_dir(path: &Path) -> crate::Result { let json = fs::read(path.join(PROFILE_JSON_PATH)).await?; let mut profile = serde_json::from_slice::(&json)?; profile.path = PathBuf::from(path); Ok(profile) } + + pub fn sync_available_profiles_task(path: PathBuf) { + tokio::task::spawn(async move { + let span = tracing::span!( + tracing::Level::INFO, + "sync_available_profiles_task" + ); + let res = async { + let _span = span.enter(); + let state = State::get().await?; + + let mut profiles = state.profiles.write().await; + + tracing::info!("Updating: {path}", path = path.display()); + if let Some(profile) = profiles.0.get_mut(&path) { + if !path.exists() { + // if path exists in the state but no longer in the filesystem, remove it from the state list + emit_profile( + profile.uuid, + profile.path.clone(), + &profile.metadata.name, + ProfilePayloadType::Removed, + ) + .await?; + tracing::debug!("Removed!"); + profiles.0.remove(&path); + } + } else if path.exists() { + // if it exists in the filesystem but no longer in the state, add it to the state list + profiles + .insert(Self::read_profile_from_dir(&path).await?) + .await?; + Profile::sync_projects_task(path); + } + Ok::<(), crate::Error>(()) + } + .await; + + match res { + Ok(()) => {} + Err(err) => { + tracing::warn!("Unable to fetch all profiles: {err}") + } + }; + }); + } } diff --git a/theseus/src/state/projects.rs b/theseus/src/state/projects.rs index ba8bdc14e..38c70f434 100644 --- a/theseus/src/state/projects.rs +++ b/theseus/src/state/projects.rs @@ -252,11 +252,15 @@ async fn read_icon_from_file( Ok(None) } + +// Creates Project data from the existing files in the file system, for a given Profile +// Paths must be the full paths to the files in the FS, and not the relative paths +// eg: with get_profile_full_project_paths #[tracing::instrument(skip(profile, io_semaphore, fetch_semaphore))] #[theseus_macros::debug_pin] pub async fn infer_data_from_files( profile: Profile, - paths: Vec, + paths: Vec, cache_dir: PathBuf, io_semaphore: &IoSemaphore, fetch_semaphore: &FetchSemaphore, @@ -339,7 +343,7 @@ pub async fn infer_data_from_files( .flatten() .collect(); - let mut return_projects = HashMap::new(); + let mut return_projects: Vec<(PathBuf, Project)> = Vec::new(); let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new(); for (hash, path) in file_path_hashes { @@ -353,7 +357,7 @@ pub async fn infer_data_from_files( .to_string_lossy() .to_string(); - return_projects.insert( + return_projects.push(( path, Project { disabled: file_name.ends_with(".disabled"), @@ -389,7 +393,7 @@ pub async fn infer_data_from_files( sha512: hash, file_name, }, - ); + )); continue; } } @@ -409,7 +413,7 @@ pub async fn infer_data_from_files( { zip_file_reader } else { - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -417,7 +421,7 @@ pub async fn infer_data_from_files( metadata: ProjectMetadata::Unknown, file_name, }, - ); + )); continue; }; let zip_index_option = zip_file_reader @@ -463,7 +467,7 @@ pub async fn infer_data_from_files( ) .await?; - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -488,7 +492,7 @@ pub async fn infer_data_from_files( project_type: Some("mod".to_string()), }, }, - ); + )); continue; } } @@ -530,7 +534,7 @@ pub async fn infer_data_from_files( ) .await?; - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -549,7 +553,7 @@ pub async fn infer_data_from_files( project_type: Some("mod".to_string()), }, }, - ); + )); continue; } } @@ -596,7 +600,7 @@ pub async fn infer_data_from_files( ) .await?; - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -618,7 +622,7 @@ pub async fn infer_data_from_files( project_type: Some("mod".to_string()), }, }, - ); + )); continue; } } @@ -662,7 +666,7 @@ pub async fn infer_data_from_files( ) .await?; - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -694,7 +698,7 @@ pub async fn infer_data_from_files( project_type: Some("mod".to_string()), }, }, - ); + )); continue; } } @@ -728,7 +732,7 @@ pub async fn infer_data_from_files( io_semaphore, ) .await?; - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -743,13 +747,13 @@ pub async fn infer_data_from_files( project_type: None, }, }, - ); + )); continue; } } } - return_projects.insert( + return_projects.push(( path.clone(), Project { sha512: hash, @@ -757,8 +761,14 @@ pub async fn infer_data_from_files( file_name, metadata: ProjectMetadata::Unknown, }, - ); + )); } + // Project paths should be relative + let return_projects: HashMap = return_projects.into_iter().map(|(h, v)| { + let h = h.strip_prefix(profile.path.clone())?.to_path_buf(); + Ok::<_,crate::Error>((h,v)) + }).collect::>>()?; + Ok(return_projects) } diff --git a/theseus_gui/src/pages/instance/Index.vue b/theseus_gui/src/pages/instance/Index.vue index 8db75a1e3..6ee1e6bfb 100644 --- a/theseus_gui/src/pages/instance/Index.vue +++ b/theseus_gui/src/pages/instance/Index.vue @@ -261,6 +261,12 @@ const handleOptionsClick = async (args) => { const unlistenProfiles = await profile_listener(async (event) => { if (event.path === route.params.id) { + if (event.event === 'removed') { + await router.push({ + path: '/', + }) + return + } instance.value = await get(route.params.id).catch(handleError) } }) From 238ab7fca2075ab8e400a22c7375d747b2be6133 Mon Sep 17 00:00:00 2001 From: Wyatt Date: Wed, 5 Jul 2023 23:28:56 -0700 Subject: [PATCH 02/10] config switch draft --- theseus/src/api/jre.rs | 2 +- theseus/src/api/logs.rs | 43 ++++- theseus/src/api/pack/install_from.rs | 2 +- theseus/src/api/process.rs | 3 +- theseus/src/api/profile.rs | 23 ++- theseus/src/api/profile_create.rs | 7 +- theseus/src/api/settings.rs | 78 ++++++++ theseus/src/launcher/download.rs | 16 +- theseus/src/launcher/mod.rs | 31 ++-- theseus/src/state/children.rs | 16 +- theseus/src/state/dirs.rs | 128 ++++++++------ theseus/src/state/java_globals.rs | 4 + theseus/src/state/metadata.rs | 4 +- theseus/src/state/mod.rs | 167 ++++++++++-------- theseus/src/state/profiles.rs | 48 +++-- theseus/src/state/settings.rs | 61 ++++--- theseus/src/state/tags.rs | 4 +- theseus/src/state/users.rs | 4 +- theseus/src/util/fetch.rs | 2 + theseus/src/util/jre.rs | 2 +- theseus_cli/src/subcommands/user.rs | 2 +- theseus_gui/src-tauri/src/api/logs.rs | 8 +- theseus_gui/src-tauri/src/api/profile.rs | 2 + theseus_gui/src-tauri/src/api/settings.rs | 13 +- theseus_gui/src-tauri/src/api/utils.rs | 13 +- theseus_gui/src/App.vue | 11 +- theseus_gui/src/components/ui/Instance.vue | 6 +- .../components/ui/InstanceCreationModal.vue | 6 +- .../components/ui/InstanceInstallModal.vue | 8 +- theseus_gui/src/helpers/cache.js | 15 ++ theseus_gui/src/helpers/settings.js | 8 + theseus_gui/src/pages/Browse.vue | 6 +- theseus_gui/src/pages/instance/Index.vue | 6 +- theseus_gui/src/pages/instance/Logs.vue | 6 +- theseus_gui/src/pages/instance/Mods.vue | 6 +- theseus_gui/src/pages/instance/Options.vue | 8 +- theseus_gui/src/pages/project/Index.vue | 6 +- 37 files changed, 522 insertions(+), 253 deletions(-) create mode 100644 theseus_gui/src/helpers/cache.js diff --git a/theseus/src/api/jre.rs b/theseus/src/api/jre.rs index 1d1d3d37f..bad0269c2 100644 --- a/theseus/src/api/jre.rs +++ b/theseus/src/api/jre.rs @@ -111,7 +111,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result { ) .await?; - let path = state.directories.java_versions_dir(); + let path = state.directories.java_versions_dir().await; if path.exists() { tokio::fs::remove_dir_all(&path).await?; diff --git a/theseus/src/api/logs.rs b/theseus/src/api/logs.rs index 0936ad0c1..21441fbe0 100644 --- a/theseus/src/api/logs.rs +++ b/theseus/src/api/logs.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use crate::State; use serde::{Deserialize, Serialize}; use tokio::fs::read_to_string; @@ -9,7 +11,7 @@ pub struct Logs { } impl Logs { async fn build( - profile_uuid: uuid::Uuid, + profile_subpath: &Path, datetime_string: String, clear_contents: Option, ) -> crate::Result { @@ -18,7 +20,7 @@ impl Logs { None } else { Some( - get_output_by_datetime(profile_uuid, &datetime_string) + get_output_by_datetime(profile_subpath, &datetime_string) .await?, ) }, @@ -33,7 +35,13 @@ pub async fn get_logs( clear_contents: Option, ) -> crate::Result> { let state = State::get().await?; - let logs_folder = state.directories.profile_logs_dir(profile_uuid); + let profile_path = if let Some(p) = crate::profile::get_by_uuid(profile_uuid, None).await? { + p.path + } else { + return Err(crate::ErrorKind::UnmanagedProfileError(profile_uuid.to_string()).into()); + }; + + let logs_folder = state.directories.profile_logs_dir(&profile_path).await; let mut logs = Vec::new(); if logs_folder.exists() { for entry in std::fs::read_dir(logs_folder)? { @@ -43,7 +51,7 @@ pub async fn get_logs( if let Some(datetime_string) = path.file_name() { logs.push( Logs::build( - profile_uuid, + &profile_path, datetime_string.to_string_lossy().to_string(), clear_contents, ) @@ -64,9 +72,14 @@ pub async fn get_logs_by_datetime( profile_uuid: uuid::Uuid, datetime_string: String, ) -> crate::Result { + let profile_path = if let Some(p) = crate::profile::get_by_uuid(profile_uuid, None).await? { + p.path + } else { + return Err(crate::ErrorKind::UnmanagedProfileError(profile_uuid.to_string()).into()); + }; Ok(Logs { output: Some( - get_output_by_datetime(profile_uuid, &datetime_string).await?, + get_output_by_datetime(&profile_path, &datetime_string).await?, ), datetime_string, }) @@ -74,11 +87,11 @@ pub async fn get_logs_by_datetime( #[tracing::instrument] pub async fn get_output_by_datetime( - profile_uuid: uuid::Uuid, + profile_subpath: &Path, datetime_string: &str, ) -> crate::Result { let state = State::get().await?; - let logs_folder = state.directories.profile_logs_dir(profile_uuid); + let logs_folder = state.directories.profile_logs_dir(profile_subpath).await; Ok( read_to_string(logs_folder.join(datetime_string).join("stdout.log")) .await?, @@ -87,8 +100,14 @@ pub async fn get_output_by_datetime( #[tracing::instrument] pub async fn delete_logs(profile_uuid: uuid::Uuid) -> crate::Result<()> { + let profile_path = if let Some(p) = crate::profile::get_by_uuid(profile_uuid, None).await? { + p.path + } else { + return Err(crate::ErrorKind::UnmanagedProfileError(profile_uuid.to_string()).into()); + }; + let state = State::get().await?; - let logs_folder = state.directories.profile_logs_dir(profile_uuid); + let logs_folder = state.directories.profile_logs_dir(&profile_path).await; for entry in std::fs::read_dir(logs_folder)? { let entry = entry?; let path = entry.path(); @@ -104,8 +123,14 @@ pub async fn delete_logs_by_datetime( profile_uuid: uuid::Uuid, datetime_string: &str, ) -> crate::Result<()> { + let profile_path = if let Some(p) = crate::profile::get_by_uuid(profile_uuid, None).await? { + p.path + } else { + return Err(crate::ErrorKind::UnmanagedProfileError(profile_uuid.to_string()).into()); + }; + let state = State::get().await?; - let logs_folder = state.directories.profile_logs_dir(profile_uuid); + let logs_folder = state.directories.profile_logs_dir(&profile_path).await; std::fs::remove_dir_all(logs_folder.join(datetime_string))?; Ok(()) } diff --git a/theseus/src/api/pack/install_from.rs b/theseus/src/api/pack/install_from.rs index 419bbf1f6..720d9a4ec 100644 --- a/theseus/src/api/pack/install_from.rs +++ b/theseus/src/api/pack/install_from.rs @@ -232,7 +232,7 @@ pub async fn generate_pack_from_version_id( Some( write_cached_icon( filename, - &state.directories.caches_dir(), + &state.directories.caches_dir().await, icon_bytes, &state.io_semaphore, ) diff --git a/theseus/src/api/process.rs b/theseus/src/api/process.rs index ef455a19c..c0d6901f3 100644 --- a/theseus/src/api/process.rs +++ b/theseus/src/api/process.rs @@ -56,7 +56,8 @@ pub async fn get_all_running_profile_paths() -> crate::Result> { pub async fn get_all_running_profiles() -> crate::Result> { let state = State::get().await?; let children = state.children.read().await; - children.running_profiles().await + let ret = children.running_profiles().await?; + Ok(ret) } // Gets the UUID of each stored process in the state by profile path diff --git a/theseus/src/api/profile.rs b/theseus/src/api/profile.rs index 1bfaca61e..d6a81bb62 100644 --- a/theseus/src/api/profile.rs +++ b/theseus/src/api/profile.rs @@ -58,7 +58,6 @@ pub async fn get( path: &Path, clear_projects: Option, ) -> crate::Result> { - tracing::debug!("Getting profile: {}", path.display()); let state = State::get().await?; let profiles = state.profiles.read().await; @@ -73,6 +72,26 @@ pub async fn get( Ok(profile) } +/// Get a profile by uuid +#[tracing::instrument] +pub async fn get_by_uuid( + uuid: uuid::Uuid, + clear_projects: Option, +) -> crate::Result> { + let state = State::get().await?; + + let profiles = state.profiles.read().await; + let mut profile = profiles.0.values().find(|x| x.uuid == uuid).cloned(); + + if clear_projects.unwrap_or(false) { + if let Some(profile) = &mut profile { + profile.projects = HashMap::new(); + } + } + + Ok(profile) +} + /// Edit a profile using a given asynchronous closure pub async fn edit( path: &Path, @@ -121,7 +140,7 @@ pub async fn edit_icon( Some(ref mut profile) => { profile .set_icon( - &state.directories.caches_dir(), + &state.directories.caches_dir().await, &state.io_semaphore, bytes::Bytes::from(bytes), &icon.to_string_lossy(), diff --git a/theseus/src/api/profile_create.rs b/theseus/src/api/profile_create.rs index 313fec475..5ddc1fbfd 100644 --- a/theseus/src/api/profile_create.rs +++ b/theseus/src/api/profile_create.rs @@ -36,7 +36,7 @@ pub async fn profile_create( trace!("Creating new profile. {}", name); let state = State::get().await?; let uuid = Uuid::new_v4(); - let path = state.directories.profiles_dir().join(&name); + let path = state.directories.profiles_dir().await.join(&name); if path.exists() { if !path.is_dir() { return Err(ProfileCreationError::NotFolder.into()); @@ -80,10 +80,11 @@ pub async fn profile_create( Profile::new(uuid, name, game_version, path.clone()).await?; let result = async { if let Some(ref icon) = icon { - let bytes = tokio::fs::read(icon).await?; + let caches_dir = state.directories.caches_dir().await; + let bytes = tokio::fs::read(caches_dir.join(icon)).await?; profile .set_icon( - &state.directories.caches_dir(), + &state.directories.caches_dir().await, &state.io_semaphore, bytes::Bytes::from(bytes), &icon.to_string_lossy(), diff --git a/theseus/src/api/settings.rs b/theseus/src/api/settings.rs index e5486d57c..2d053e4c8 100644 --- a/theseus/src/api/settings.rs +++ b/theseus/src/api/settings.rs @@ -1,5 +1,10 @@ //! Theseus profile management interface +use std::path::PathBuf; + +use tokio::{fs, sync::RwLock}; + +use crate::{prelude::DirectoryInfo, state::{self, Profiles}}; pub use crate::{ state::{ Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize, @@ -19,6 +24,14 @@ pub async fn get() -> crate::Result { #[tracing::instrument] pub async fn set(settings: Settings) -> crate::Result<()> { let state = State::get().await?; + + if settings.loaded_config_dir != state.settings.read().await.loaded_config_dir { + return Err(crate::ErrorKind::OtherError( + "Cannot change config directory as setting".to_string(), + ) + .as_error()); + } + let (reset_io, reset_fetch) = async { let read = state.settings.read().await; ( @@ -42,3 +55,68 @@ pub async fn set(settings: Settings) -> crate::Result<()> { State::sync().await?; Ok(()) } + +/// Sets the new config dir, the location of all Theseus data except for the settings.json itself +/// Takes control of the entire state and blocks until completion +pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> { + + tracing::info!("Setting new config dir: {}", new_config_dir.display()); + if !new_config_dir.is_dir() { + return Err(crate::ErrorKind::FSError( + format!("New config dir is not a folder: {}", new_config_dir.display()), + ) + .as_error()); + } + + // Take control of the state + let mut state_write = State::get_write().await?; + let old_config_dir = state_write.directories.config_dir.read().await.clone(); + + // Set load config dir setting + let settings = { + let mut settings = state_write.settings.write().await; + settings.loaded_config_dir = Some(new_config_dir.clone()); + + // Some java paths are hardcoded to within our config dir, so we need to update them + for key in settings.java_globals.keys() { + if let Some(java) = settings.java_globals.get_mut(&key) { + // If the path is within the old config dir path, update it to the new config dir + if let Ok(relative_path) = PathBuf::from(java.path.clone()).strip_prefix(&old_config_dir) { + java.path = new_config_dir.join(relative_path).to_string_lossy().to_string(); + } + } + } + settings.sync(&state_write.directories.settings_file()).await?; + settings.clone() + }; + + // Set new state information + state_write.directories = DirectoryInfo::init(&settings)?; + + // Move all files over from state_write.directories.config_dir to new_config_dir + + let mut entries = fs::read_dir(old_config_dir).await?; + while let Some(entry) = entries.next_entry().await? { + let entry_path = entry.path(); + if let Some(file_name) = entry_path.file_name() { + // Ignore settings.json + if file_name == "settings.json" { + continue; + } + let new_path = new_config_dir.join(file_name); + fs::rename(entry_path, new_path).await?; + } + } + + // Reset file watcher + let mut file_watcher = state::init_watcher().await?; + + // Reset profiles (for filepaths, file watcher, etc) + state_write.profiles = RwLock::new(Profiles::init(&state_write.directories, &mut file_watcher).await?); + state_write.file_watcher = RwLock::new(file_watcher); + + // TODO: need to be able to safely error out of this function, reverting the changes + + Ok(()) +} + diff --git a/theseus/src/launcher/download.rs b/theseus/src/launcher/download.rs index 67e32cfd5..5a93c416c 100644 --- a/theseus/src/launcher/download.rs +++ b/theseus/src/launcher/download.rs @@ -68,6 +68,7 @@ pub async fn download_version_info( let path = st .directories .version_dir(&version_id) + .await .join(format!("{version_id}.json")); let res = if path.exists() && !force.unwrap_or(false) { @@ -118,6 +119,7 @@ pub async fn download_client( let path = st .directories .version_dir(version) + .await .join(format!("{version}.jar")); if !path.exists() { @@ -149,7 +151,7 @@ pub async fn download_assets_index( let path = st .directories .assets_index_dir() - .join(format!("{}.json", &version.asset_index.id)); + .await.join(format!("{}.json", &version.asset_index.id)); let res = if path.exists() { fs::read(path) @@ -192,7 +194,7 @@ pub async fn download_assets( None, |(name, asset)| async move { let hash = &asset.hash; - let resource_path = st.directories.object_dir(hash); + let resource_path = st.directories.object_dir(hash).await; let url = format!( "https://resources.download.minecraft.net/{sub_hash}/{hash}", sub_hash = &hash[..2] @@ -215,7 +217,7 @@ pub async fn download_assets( let resource = fetch_cell .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) .await?; - let resource_path = st.directories.legacy_assets_dir().join( + let resource_path = st.directories.legacy_assets_dir().await.join( name.replace('/', &String::from(std::path::MAIN_SEPARATOR)) ); write(&resource_path, resource, &st.io_semaphore).await?; @@ -245,8 +247,8 @@ pub async fn download_libraries( tracing::debug!("Loading libraries"); tokio::try_join! { - fs::create_dir_all(st.directories.libraries_dir()), - fs::create_dir_all(st.directories.version_natives_dir(version)) + fs::create_dir_all(st.directories.libraries_dir().await), + fs::create_dir_all(st.directories.version_natives_dir(version).await) }?; let num_files = libraries.len(); loading_try_for_each_concurrent( @@ -262,7 +264,7 @@ pub async fn download_libraries( tokio::try_join! { async { let artifact_path = d::get_path_from_artifact(&library.name)?; - let path = st.directories.libraries_dir().join(&artifact_path); + let path = st.directories.libraries_dir().await.join(&artifact_path); match library.downloads { _ if path.exists() => Ok(()), @@ -314,7 +316,7 @@ pub async fn download_libraries( let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?; let reader = std::io::Cursor::new(&data); if let Ok(mut archive) = zip::ZipArchive::new(reader) { - match archive.extract(&st.directories.version_natives_dir(version)) { + match archive.extract(&st.directories.version_natives_dir(version).await) { Ok(_) => tracing::info!("Fetched native {}", &library.name), Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err) } diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index eacd54c5d..bc22285e8 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -177,8 +177,11 @@ pub async fn install_minecraft( let client_path = state .directories .version_dir(&version_jar) + .await .join(format!("{version_jar}.jar")); + let libraries_dir = state.directories.libraries_dir().await; + if let Some(ref mut data) = version_info.data { processor_rules! { data; @@ -195,7 +198,7 @@ pub async fn install_minecraft( client => instance_path.to_string_lossy(), server => ""; "LIBRARY_DIR": - client => state.directories.libraries_dir().to_string_lossy(), + client => libraries_dir.to_string_lossy(), server => ""; } @@ -218,13 +221,13 @@ pub async fn install_minecraft( let child = Command::new(&java_version.path) .arg("-cp") .arg(args::get_class_paths_jar( - &state.directories.libraries_dir(), + &libraries_dir, &cp, &java_version.architecture, )?) .arg( args::get_processor_main_class(args::get_lib_path( - &state.directories.libraries_dir(), + &libraries_dir, &processor.jar, false, )?) @@ -237,7 +240,7 @@ pub async fn install_minecraft( })?, ) .args(args::get_processor_arguments( - &state.directories.libraries_dir(), + &libraries_dir, &processor.args, data, )?) @@ -282,7 +285,7 @@ pub async fn install_minecraft( Ok(()) } -#[tracing::instrument] +#[tracing::instrument(skip(profile))] #[theseus_macros::debug_pin] #[allow(clippy::too_many_arguments)] pub async fn launch_minecraft( @@ -310,7 +313,9 @@ pub async fn launch_minecraft( let state = State::get().await?; let metadata = state.metadata.read().await; - let instance_path = &canonicalize(&profile.path)?; + + let instance_path = state.directories.profiles_dir().await.join(&profile.path); + let instance_path = &canonicalize(instance_path)?; let version = metadata .minecraft @@ -350,6 +355,7 @@ pub async fn launch_minecraft( let client_path = state .directories .version_dir(&version_jar) + .await .join(format!("{version_jar}.jar")); let args = version_info.arguments.clone().unwrap_or_default(); @@ -379,10 +385,10 @@ pub async fn launch_minecraft( args::get_jvm_arguments( args.get(&d::minecraft::ArgumentType::Jvm) .map(|x| x.as_slice()), - &state.directories.version_natives_dir(&version_jar), - &state.directories.libraries_dir(), + &state.directories.version_natives_dir(&version_jar).await, + &state.directories.libraries_dir().await, &args::get_class_paths( - &state.directories.libraries_dir(), + &state.directories.libraries_dir().await, version_info.libraries.as_slice(), &client_path, &java_version.architecture, @@ -405,7 +411,7 @@ pub async fn launch_minecraft( &version.id, &version_info.asset_index.id, instance_path, - &state.directories.assets_dir(), + &state.directories.assets_dir().await, &version.type_, *resolution, &java_version.architecture, @@ -430,7 +436,8 @@ pub async fn launch_minecraft( let logs_dir = { let st = State::get().await?; st.directories - .profile_logs_dir(profile.uuid) + .profile_logs_dir(&profile.path) + .await .join(&datetime_string) }; fs::create_dir_all(&logs_dir)?; @@ -490,7 +497,7 @@ pub async fn launch_minecraft( state_children .insert_process( Uuid::new_v4(), - instance_path.to_path_buf(), + profile.path.clone(), stdout_log_path, command, post_exit_hook, diff --git a/theseus/src/state/children.rs b/theseus/src/state/children.rs index bbb7163d4..2634980d8 100644 --- a/theseus/src/state/children.rs +++ b/theseus/src/state/children.rs @@ -24,7 +24,7 @@ pub struct Children(HashMap>>); #[derive(Debug)] pub struct MinecraftChild { pub uuid: Uuid, - pub profile_path: PathBuf, //todo: make UUID when profiles are recognized by UUID + pub profile_relative_path: PathBuf, pub manager: Option>>, // None when future has completed and been handled pub current_child: Arc>, pub output: SharedOutput, @@ -39,12 +39,12 @@ impl Children { // The threads for stdout and stderr are spawned here // Unlike a Hashmap's 'insert', this directly returns the reference to the MinecraftChild rather than any previously stored MinecraftChild that may exist - #[tracing::instrument(skip(self))] + #[tracing::instrument(skip(self, mc_command, post_command, censor_strings))] #[theseus_macros::debug_pin] pub async fn insert_process( &mut self, uuid: Uuid, - profile_path: PathBuf, + profile_relative_path: PathBuf, log_path: PathBuf, mut mc_command: Command, post_command: Option, // Command to run after minecraft. @@ -56,7 +56,7 @@ impl Children { // Create std watcher threads for stdout and stderr let shared_output = SharedOutput::build(&log_path, censor_strings).await?; - if let Some(child_stdout) = child.stdout.take() { + if let Some(child_stdout) = child.stdout.take() { let stdout_clone = shared_output.clone(); tokio::spawn(async move { if let Err(e) = stdout_clone.read_stdout(child_stdout).await { @@ -98,7 +98,7 @@ impl Children { // Create MinecraftChild let mchild = MinecraftChild { uuid, - profile_path, + profile_relative_path, current_child, output: shared_output, manager, @@ -243,7 +243,7 @@ impl Children { if let Some(child) = self.get(&key) { let child = child.clone(); let child = child.read().await; - if child.profile_path == profile_path { + if child.profile_relative_path == profile_path { keys.push(key); } } @@ -259,7 +259,7 @@ impl Children { let child = child.clone(); let child = child.write().await; if child.current_child.write().await.try_wait()?.is_none() { - profiles.push(child.profile_path.clone()); + profiles.push(child.profile_relative_path.clone()); } } } @@ -276,7 +276,7 @@ impl Children { let child = child.write().await; if child.current_child.write().await.try_wait()?.is_none() { if let Some(prof) = crate::api::profile::get( - &child.profile_path.clone(), + &child.profile_relative_path.clone(), None, ) .await? diff --git a/theseus/src/state/dirs.rs b/theseus/src/state/dirs.rs index f564e7d79..fd30cf977 100644 --- a/theseus/src/state/dirs.rs +++ b/theseus/src/state/dirs.rs @@ -1,17 +1,38 @@ //! Theseus directory information use std::fs; -use std::path::PathBuf; +use std::path::{PathBuf, Path}; + +use tokio::sync::RwLock; + +use super::Settings; #[derive(Debug)] pub struct DirectoryInfo { - pub config_dir: PathBuf, + pub settings_dir: PathBuf, + pub config_dir: RwLock, pub working_dir: PathBuf, } impl DirectoryInfo { + + // Get the settings directory + // init() is not needed for this function + pub fn get_initial_settings_dir() -> Option { + Self::env_path("THESEUS_CONFIG_DIR") + .or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus"))) + } + + #[inline] + pub fn get_initial_settings_file() -> crate::Result { + let settings_dir = Self::get_initial_settings_dir().ok_or(crate::ErrorKind::FSError( + "Could not find valid config dir".to_string(), + ))?; + Ok(settings_dir.join("settings.json")) + } + /// Get all paths needed for Theseus to operate properly #[tracing::instrument] - pub fn init() -> crate::Result { + pub fn init(settings : &Settings) -> crate::Result { // Working directory let working_dir = std::env::current_dir().map_err(|err| { crate::ErrorKind::FSError(format!( @@ -19,143 +40,148 @@ impl DirectoryInfo { )) })?; - // Config directory - let config_dir = Self::env_path("THESEUS_CONFIG_DIR") - .or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus"))) - .ok_or(crate::ErrorKind::FSError( - "Could not find valid config dir".to_string(), - ))?; + let settings_dir = Self::get_initial_settings_dir() .ok_or(crate::ErrorKind::FSError( + "Could not find valid settings dir".to_string(), + ))?; - fs::create_dir_all(&config_dir).map_err(|err| { + fs::create_dir_all(&settings_dir).map_err(|err| { crate::ErrorKind::FSError(format!( "Error creating Theseus config directory: {err}" )) })?; + // config directory (for instances, etc.) + // by default this is the same as the settings directory + let config_dir = settings.loaded_config_dir.clone().ok_or(crate::ErrorKind::FSError( + "Could not find valid config dir".to_string(), + ))?; + Ok(Self { - config_dir, + settings_dir, + // config_dir: RwLock::new(config_dir), + config_dir: RwLock::new(config_dir), working_dir, }) } /// Get the Minecraft instance metadata directory #[inline] - pub fn metadata_dir(&self) -> PathBuf { - self.config_dir.join("meta") + pub async fn metadata_dir(&self) -> PathBuf { + self.config_dir.read().await.join("meta") } /// Get the Minecraft java versions metadata directory #[inline] - pub fn java_versions_dir(&self) -> PathBuf { - self.metadata_dir().join("java_versions") + pub async fn java_versions_dir(&self) -> PathBuf { + self.metadata_dir().await.join("java_versions") } /// Get the Minecraft versions metadata directory #[inline] - pub fn versions_dir(&self) -> PathBuf { - self.metadata_dir().join("versions") + pub async fn versions_dir(&self) -> PathBuf { + self.metadata_dir().await.join("versions") } /// Get the metadata directory for a given version #[inline] - pub fn version_dir(&self, version: &str) -> PathBuf { - self.versions_dir().join(version) + pub async fn version_dir(&self, version: &str) -> PathBuf { + self.versions_dir().await.join(version) } /// Get the Minecraft libraries metadata directory #[inline] - pub fn libraries_dir(&self) -> PathBuf { - self.metadata_dir().join("libraries") + pub async fn libraries_dir(&self) -> PathBuf { + self.metadata_dir().await.join("libraries") } /// Get the Minecraft assets metadata directory #[inline] - pub fn assets_dir(&self) -> PathBuf { - self.metadata_dir().join("assets") + pub async fn assets_dir(&self) -> PathBuf { + self.metadata_dir().await.join("assets") } /// Get the assets index directory #[inline] - pub fn assets_index_dir(&self) -> PathBuf { - self.assets_dir().join("indexes") + pub async fn assets_index_dir(&self) -> PathBuf { + self.assets_dir().await.join("indexes") } /// Get the assets objects directory #[inline] - pub fn objects_dir(&self) -> PathBuf { - self.assets_dir().join("objects") + pub async fn objects_dir(&self) -> PathBuf { + self.assets_dir().await.join("objects") } /// Get the directory for a specific object #[inline] - pub fn object_dir(&self, hash: &str) -> PathBuf { - self.objects_dir().join(&hash[..2]).join(hash) + pub async fn object_dir(&self, hash: &str) -> PathBuf { + self.objects_dir().await.join(&hash[..2]).join(hash) } /// Get the Minecraft legacy assets metadata directory #[inline] - pub fn legacy_assets_dir(&self) -> PathBuf { - self.metadata_dir().join("resources") + pub async fn legacy_assets_dir(&self) -> PathBuf { + self.metadata_dir().await.join("resources") } /// Get the Minecraft legacy assets metadata directory #[inline] - pub fn natives_dir(&self) -> PathBuf { - self.metadata_dir().join("natives") + pub async fn natives_dir(&self) -> PathBuf { + self.metadata_dir().await.join("natives") } /// Get the natives directory for a version of Minecraft #[inline] - pub fn version_natives_dir(&self, version: &str) -> PathBuf { - self.natives_dir().join(version) + pub async fn version_natives_dir(&self, version: &str) -> PathBuf { + self.natives_dir().await.join(version) } /// Get the directory containing instance icons #[inline] - pub fn icon_dir(&self) -> PathBuf { - self.config_dir.join("icons") + pub async fn icon_dir(&self) -> PathBuf { + self.config_dir.read().await.join("icons") } /// Get the profiles directory for created profiles #[inline] - pub fn profiles_dir(&self) -> PathBuf { - self.config_dir.join("profiles") + pub async fn profiles_dir(&self) -> PathBuf { + self.config_dir.read().await.join("profiles") } /// Gets the logs dir for a given profile #[inline] - pub fn profile_logs_dir(&self, profile: uuid::Uuid) -> PathBuf { - self.profiles_dir() - .join(profile.to_string()) + pub async fn profile_logs_dir(&self, profile_relative_path: &Path) -> PathBuf { + self.profiles_dir().await + .join(profile_relative_path) .join("modrinth_logs") } #[inline] - pub fn launcher_logs_dir(&self) -> PathBuf { - self.config_dir.join("launcher_logs") + pub async fn launcher_logs_dir(&self) -> PathBuf { + self.config_dir.read().await.join("launcher_logs") } /// Get the file containing the global database #[inline] - pub fn database_file(&self) -> PathBuf { - self.config_dir.join("data.bin") + pub async fn database_file(&self) -> PathBuf { + self.config_dir.read().await.join("data.bin") } /// Get the settings file for Theseus #[inline] pub fn settings_file(&self) -> PathBuf { - self.config_dir.join("settings.json") + self.settings_dir.join("settings.json") } /// Get the cache directory for Theseus #[inline] - pub fn caches_dir(&self) -> PathBuf { - self.config_dir.join("caches") + pub async fn caches_dir(&self) -> PathBuf { + self.config_dir.read().await.join("caches") } #[inline] - pub fn caches_meta_dir(&self) -> PathBuf { - self.config_dir.join("caches").join("metadata") + pub async fn caches_meta_dir(&self) -> PathBuf { + self.config_dir.read().await.join("caches").join("metadata") } /// Get path from environment variable diff --git a/theseus/src/state/java_globals.rs b/theseus/src/state/java_globals.rs index 9ef33bc17..10b8071e2 100644 --- a/theseus/src/state/java_globals.rs +++ b/theseus/src/state/java_globals.rs @@ -35,6 +35,10 @@ impl JavaGlobals { self.0.len() } + pub fn keys(&self) -> Vec { + self.0.keys().cloned().collect() + } + // Validates that every path here is a valid Java version and that the version matches the version stored here // If false, when checked, the user should be prompted to reselect the Java version pub async fn is_all_valid(&self) -> bool { diff --git a/theseus/src/state/metadata.rs b/theseus/src/state/metadata.rs index 388300889..b32a32519 100644 --- a/theseus/src/state/metadata.rs +++ b/theseus/src/state/metadata.rs @@ -61,7 +61,7 @@ impl Metadata { io_semaphore: &IoSemaphore, ) -> crate::Result { let mut metadata = None; - let metadata_path = dirs.caches_meta_dir().join("metadata.json"); + let metadata_path = dirs.caches_meta_dir().await.join("metadata.json"); if let Ok(metadata_json) = read_json::(&metadata_path, io_semaphore).await @@ -107,7 +107,7 @@ impl Metadata { let state = State::get().await?; let metadata_path = - state.directories.caches_meta_dir().join("metadata.json"); + state.directories.caches_meta_dir().await.join("metadata.json"); write( &metadata_path, diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index aa1bc60b1..0b192a6bf 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -49,7 +49,8 @@ mod safe_processes; pub use self::safe_processes::*; // Global state -static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); +// RwLock on state only has concurrent reads, except for config dir change +static LAUNCHER_STATE:OnceCell> = OnceCell::const_new(); pub struct State { /// Information on the location of files used in the launcher pub directories: DirectoryInfo, @@ -86,85 +87,97 @@ pub struct State { impl State { /// Get the current launcher state, initializing it if needed + pub async fn get() -> crate::Result>> { + Ok(Arc::new(LAUNCHER_STATE + .get_or_try_init(Self::initialize_state) + .await?.read().await)) + } + + /// Get the current launcher state, initializing it if needed + /// Takes writing control of the state, blocking all other uses of it + /// Only used for state change such as changing the config directory + pub async fn get_write() -> crate::Result> { + Ok(LAUNCHER_STATE + .get_or_try_init(Self::initialize_state) + .await?.write().await) + } + #[tracing::instrument] #[theseus_macros::debug_pin] - pub async fn get() -> crate::Result> { - LAUNCHER_STATE - .get_or_try_init(|| { - async { - let loading_bar = init_loading_unsafe( - LoadingBarType::StateInit, - 100.0, - "Initializing launcher", - ) - .await?; - - let mut file_watcher = init_watcher().await?; - - let directories = DirectoryInfo::init()?; - emit_loading(&loading_bar, 10.0, None).await?; - - // Settings - let settings = - Settings::init(&directories.settings_file()).await?; - let fetch_semaphore = FetchSemaphore(RwLock::new( - Semaphore::new(settings.max_concurrent_downloads), - )); - let io_semaphore = IoSemaphore(RwLock::new( - Semaphore::new(settings.max_concurrent_writes), - )); - emit_loading(&loading_bar, 10.0, None).await?; - - let metadata_fut = - Metadata::init(&directories, &io_semaphore); - let profiles_fut = - Profiles::init(&directories, &mut file_watcher); - let tags_fut = Tags::init( - &directories, - &io_semaphore, - &fetch_semaphore, - ); - let users_fut = Users::init(&directories, &io_semaphore); - // Launcher data - let (metadata, profiles, tags, users) = loading_join! { - Some(&loading_bar), 70.0, Some("Loading metadata"); - metadata_fut, - profiles_fut, - tags_fut, - users_fut, - }?; - - let children = Children::new(); - let auth_flow = AuthTask::new(); - let safety_processes = SafeProcesses::new(); - emit_loading(&loading_bar, 10.0, None).await?; - - Ok(Arc::new(Self { - directories, - fetch_semaphore, - fetch_semaphore_max: RwLock::new( - settings.max_concurrent_downloads as u32, - ), - io_semaphore, - io_semaphore_max: RwLock::new( - settings.max_concurrent_writes as u32, - ), - metadata: RwLock::new(metadata), - settings: RwLock::new(settings), - profiles: RwLock::new(profiles), - users: RwLock::new(users), - children: RwLock::new(children), - auth_flow: RwLock::new(auth_flow), - tags: RwLock::new(tags), - safety_processes: RwLock::new(safety_processes), - file_watcher: RwLock::new(file_watcher), - })) - } - }) - .await - .map(Arc::clone) + async fn initialize_state() -> crate::Result> { + let loading_bar = init_loading_unsafe( + LoadingBarType::StateInit, + 100.0, + "Initializing launcher", + ) + .await?; + + // Settings + let settings = + Settings::init(&DirectoryInfo::get_initial_settings_file()?).await?; + + let directories = DirectoryInfo::init(&settings)?; + + emit_loading(&loading_bar, 10.0, None).await?; + + let mut file_watcher = init_watcher().await?; + + let fetch_semaphore = FetchSemaphore(RwLock::new( + Semaphore::new(settings.max_concurrent_downloads), + )); + let io_semaphore = IoSemaphore(RwLock::new( + Semaphore::new(settings.max_concurrent_writes), + )); + emit_loading(&loading_bar, 10.0, None).await?; + + let metadata_fut = + Metadata::init(&directories, &io_semaphore); + let profiles_fut = + Profiles::init(&directories, &mut file_watcher); + let tags_fut = Tags::init( + &directories, + &io_semaphore, + &fetch_semaphore, + ); + let users_fut = Users::init(&directories, &io_semaphore); + // Launcher data + let (metadata, profiles, tags, users) = loading_join! { + Some(&loading_bar), 70.0, Some("Loading metadata"); + metadata_fut, + profiles_fut, + tags_fut, + users_fut, + }?; + + let children = Children::new(); + let auth_flow = AuthTask::new(); + let safety_processes = SafeProcesses::new(); + emit_loading(&loading_bar, 10.0, None).await?; + + Ok::, crate::Error>(RwLock::new(Self { + directories, + fetch_semaphore, + fetch_semaphore_max: RwLock::new( + settings.max_concurrent_downloads as u32, + ), + io_semaphore, + io_semaphore_max: RwLock::new( + settings.max_concurrent_writes as u32, + ), + metadata: RwLock::new(metadata), + settings: RwLock::new(settings), + profiles: RwLock::new(profiles), + users: RwLock::new(users), + children: RwLock::new(children), + auth_flow: RwLock::new(auth_flow), + tags: RwLock::new(tags), + safety_processes: RwLock::new(safety_processes), + file_watcher: RwLock::new(file_watcher), + })) + } + /// Updates state with data from the web pub fn update() { tokio::task::spawn(Metadata::update()); @@ -240,7 +253,7 @@ impl State { } } -async fn init_watcher() -> crate::Result> { +pub async fn init_watcher() -> crate::Result> { let (mut tx, mut rx) = channel(1); let file_watcher = new_debouncer( diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index 080710bcc..d71cf6d13 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -241,10 +241,11 @@ impl Profile { if let Some(profile) = profile { let paths = profile.get_profile_full_project_paths()?; + let caches_dir = state.directories.caches_dir().await; let projects = crate::state::infer_data_from_files( profile.clone(), paths, - state.directories.caches_dir(), + caches_dir, &state.io_semaphore, &state.fetch_semaphore, ) @@ -261,7 +262,6 @@ impl Profile { ProfilePayloadType::Synced, ) .await?; - tracing::info!("Done syncing"); } else { tracing::warn!( "Unable to fetch single profile projects: path {path:?} invalid", @@ -282,7 +282,6 @@ impl Profile { /// Gets paths to projects as their full paths, not just their relative paths pub fn get_profile_full_project_paths(&self) -> crate::Result> { - tracing::info!("Getting profile project paths, profile path: {}", self.path.display()); let mut files = Vec::new(); let mut read_paths = |path: &str| { let new_path = self.path.join(path); @@ -429,7 +428,7 @@ impl Profile { } }; - let state: std::sync::Arc = State::get().await?; + let state = State::get().await?; let relative_name = PathBuf::new().join(project_type.get_folder()).join(file_name); let path = self.path.join(relative_name.clone()); write(&path, &bytes, &state.io_semaphore).await?; @@ -548,19 +547,21 @@ impl Profiles { file_watcher: &mut Debouncer, ) -> crate::Result { let mut profiles = HashMap::new(); - fs::create_dir_all(dirs.profiles_dir()).await?; + let profiles_dir = dirs.profiles_dir().await; + fs::create_dir_all(&profiles_dir).await?; file_watcher .watcher() - .watch(&dirs.profiles_dir(), RecursiveMode::NonRecursive)?; + .watch(&profiles_dir, RecursiveMode::NonRecursive)?; - let mut entries = fs::read_dir(dirs.profiles_dir()).await?; + let mut entries = fs::read_dir(dirs.profiles_dir().await).await?; while let Some(entry) = entries.next_entry().await? { let path = entry.path(); if path.is_dir() { - let prof = match Self::read_profile_from_dir(&path).await { + let prof = match Self::read_profile_from_dir(&path, &dirs).await { Ok(prof) => Some(prof), Err(err) => { + tracing::warn!( "Error loading profile: {err}. Skipping..." ); @@ -570,7 +571,7 @@ impl Profiles { if let Some(profile) = prof { let path = canonicalize(path)?; Profile::watch_fs(&path, file_watcher).await?; - profiles.insert(path, profile); + profiles.insert(profile.path.clone(), profile); } } } @@ -595,13 +596,14 @@ impl Profiles { } } + let caches_dir = state.directories.caches_dir().await; future::try_join_all(files.into_iter().map( |(profile, files)| async { let profile_path = profile.path.clone(); let inferred = super::projects::infer_data_from_files( profile, files, - state.directories.caches_dir(), + caches_dir.clone(), &state.io_semaphore, &state.fetch_semaphore, ) @@ -681,14 +683,14 @@ impl Profiles { #[tracing::instrument(skip_all)] pub async fn sync(&self) -> crate::Result<&Self> { + let state = State::get().await?; + let profiles_dir = &state.directories.profiles_dir().await; stream::iter(self.0.iter()) .map(Ok::<_, crate::Error>) .try_for_each_concurrent(None, |(path, profile)| async move { - tracing::info!("Syncing profile: {:?}", path); let json = serde_json::to_vec(&profile)?; - let json_path = Path::new(&path.to_string_lossy().to_string()) - .join(PROFILE_JSON_PATH); + let json_path = profiles_dir.join(&path).join(PROFILE_JSON_PATH); fs::write(json_path, json).await?; Ok::<_, crate::Error>(()) @@ -698,10 +700,21 @@ impl Profiles { Ok(self) } - async fn read_profile_from_dir(path: &Path) -> crate::Result { + async fn read_profile_from_dir(path: &Path, dirs : &DirectoryInfo) -> crate::Result { let json = fs::read(path.join(PROFILE_JSON_PATH)).await?; let mut profile = serde_json::from_slice::(&json)?; - profile.path = PathBuf::from(path); + + profile.path = PathBuf::from(path.strip_prefix(dirs.profiles_dir().await)?); + + // Cache icons were changed to be relative in v0.2.3. This strips the cache dir from the icon path if it exists. + let cache_dir = dirs.caches_dir().await; + if let Some(icon) = &mut profile.metadata.icon { + *icon = match icon.strip_prefix(cache_dir) { + Ok(path) => path.to_path_buf(), + Err(_) => icon.clone(), + }; + } + Ok(profile) } @@ -714,10 +727,9 @@ impl Profiles { let res = async { let _span = span.enter(); let state = State::get().await?; - + let dirs = &state.directories; let mut profiles = state.profiles.write().await; - tracing::info!("Updating: {path}", path = path.display()); if let Some(profile) = profiles.0.get_mut(&path) { if !path.exists() { // if path exists in the state but no longer in the filesystem, remove it from the state list @@ -734,7 +746,7 @@ impl Profiles { } else if path.exists() { // if it exists in the filesystem but no longer in the state, add it to the state list profiles - .insert(Self::read_profile_from_dir(&path).await?) + .insert(Self::read_profile_from_dir(&path, &dirs).await?) .await?; Profile::sync_projects_task(path); } diff --git a/theseus/src/state/settings.rs b/theseus/src/state/settings.rs index c1de10697..678ee8262 100644 --- a/theseus/src/state/settings.rs +++ b/theseus/src/state/settings.rs @@ -4,10 +4,10 @@ use crate::{ State, }; use serde::{Deserialize, Serialize}; -use std::path::Path; +use std::path::{Path, PathBuf}; use tokio::fs; -use super::JavaGlobals; +use super::{JavaGlobals, DirectoryInfo}; // TODO: convert to semver? const CURRENT_FORMAT_VERSION: u32 = 1; @@ -15,7 +15,6 @@ const CURRENT_FORMAT_VERSION: u32 = 1; // Types /// Global Theseus settings #[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(default)] pub struct Settings { pub theme: Theme, pub memory: MemorySettings, @@ -41,31 +40,8 @@ pub struct Settings { pub advanced_rendering: bool, #[serde(default)] pub onboarded: bool, -} - -impl Default for Settings { - fn default() -> Self { - Self { - theme: Theme::Dark, - memory: MemorySettings::default(), - game_resolution: WindowSize::default(), - custom_java_args: Vec::new(), - custom_env_args: Vec::new(), - java_globals: JavaGlobals::new(), - default_user: None, - hooks: Hooks::default(), - max_concurrent_downloads: 10, - max_concurrent_writes: 10, - version: CURRENT_FORMAT_VERSION, - collapsed_navigation: false, - hide_on_process: false, - default_page: DefaultPage::Home, - developer_mode: false, - opt_out_analytics: false, - advanced_rendering: true, - onboarded: false, - } - } + #[serde(default = "DirectoryInfo::get_initial_settings_dir")] + pub loaded_config_dir: Option, } impl Settings { @@ -85,7 +61,29 @@ impl Settings { .map_err(crate::Error::from) }) } else { - Ok(Settings::default()) + Ok(Self { + theme: Theme::Dark, + memory: MemorySettings::default(), + game_resolution: WindowSize::default(), + custom_java_args: Vec::new(), + custom_env_args: Vec::new(), + java_globals: JavaGlobals::new(), + default_user: None, + hooks: Hooks::default(), + max_concurrent_downloads: 10, + max_concurrent_writes: 10, + version: CURRENT_FORMAT_VERSION, + collapsed_navigation: false, + hide_on_process: false, + default_page: DefaultPage::Home, + developer_mode: false, + opt_out_analytics: false, + advanced_rendering: true, + onboarded: false, + + // By default, the config directory is the same as the settings directory + loaded_config_dir: DirectoryInfo::get_initial_settings_dir(), + }) } } @@ -123,6 +121,11 @@ impl Settings { }; } + // #[inline] + // pub fn get_config_dir(&self) -> crate::Result { + // self.config_dir.clone() + // } + #[tracing::instrument(skip(self))] pub async fn sync(&self, to: &Path) -> crate::Result<()> { fs::write(to, serde_json::to_vec(self)?) diff --git a/theseus/src/state/tags.rs b/theseus/src/state/tags.rs index dab2762cd..01ce8c452 100644 --- a/theseus/src/state/tags.rs +++ b/theseus/src/state/tags.rs @@ -28,7 +28,7 @@ impl Tags { fetch_semaphore: &FetchSemaphore, ) -> crate::Result { let mut tags = None; - let tags_path = dirs.caches_meta_dir().join("tags.json"); + let tags_path = dirs.caches_meta_dir().await.join("tags.json"); if let Ok(tags_json) = read_json::(&tags_path, io_semaphore).await { @@ -60,7 +60,7 @@ impl Tags { let tags_fetch = Tags::fetch(&state.fetch_semaphore).await?; let tags_path = - state.directories.caches_meta_dir().join("tags.json"); + state.directories.caches_meta_dir().await.join("tags.json"); write( &tags_path, diff --git a/theseus/src/state/users.rs b/theseus/src/state/users.rs index 6d402a6da..ec0878cbe 100644 --- a/theseus/src/state/users.rs +++ b/theseus/src/state/users.rs @@ -17,7 +17,7 @@ impl Users { dirs: &DirectoryInfo, io_semaphore: &IoSemaphore, ) -> crate::Result { - let users_path = dirs.caches_meta_dir().join(USERS_JSON); + let users_path = dirs.caches_meta_dir().await.join(USERS_JSON); let users = read_json(&users_path, io_semaphore).await.ok(); if let Some(users) = users { @@ -29,7 +29,7 @@ impl Users { pub async fn save(&self) -> crate::Result<()> { let state = State::get().await?; - let users_path = state.directories.caches_meta_dir().join(USERS_JSON); + let users_path = state.directories.caches_meta_dir().await.join(USERS_JSON); write( &users_path, &serde_json::to_vec(&self.0)?, diff --git a/theseus/src/util/fetch.rs b/theseus/src/util/fetch.rs index b60fe1f1e..f65a04f22 100644 --- a/theseus/src/util/fetch.rs +++ b/theseus/src/util/fetch.rs @@ -218,6 +218,7 @@ pub async fn write<'a>( Ok(()) } +// Writes a icon to the cache and returns the *relative* path of the icon within the cache directory #[tracing::instrument(skip(bytes, semaphore))] pub async fn write_cached_icon( icon_path: &str, @@ -236,6 +237,7 @@ pub async fn write_cached_icon( write(&path, &bytes, semaphore).await?; let path = dunce::canonicalize(path)?; + let path = path.strip_prefix(cache_dir)?.to_path_buf(); Ok(path) } diff --git a/theseus/src/util/jre.rs b/theseus/src/util/jre.rs index 05e57d86c..882abe30b 100644 --- a/theseus/src/util/jre.rs +++ b/theseus/src/util/jre.rs @@ -201,7 +201,7 @@ async fn get_all_autoinstalled_jre_path() -> Result, JREError> let state = State::get().await.map_err(|_| JREError::StateError)?; let mut jre_paths = HashSet::new(); - let base_path = state.directories.java_versions_dir(); + let base_path = state.directories.java_versions_dir().await; if base_path.is_dir() { if let Ok(dir) = std::fs::read_dir(base_path) { diff --git a/theseus_cli/src/subcommands/user.rs b/theseus_cli/src/subcommands/user.rs index e866b982f..a6e7ec641 100644 --- a/theseus_cli/src/subcommands/user.rs +++ b/theseus_cli/src/subcommands/user.rs @@ -151,7 +151,7 @@ impl UserDefault { ) -> Result<()> { info!("Setting user {} as default", self.user.as_hyphenated()); - let state: std::sync::Arc = State::get().await?; + let state = State::get().await?; let mut settings = state.settings.write().await; if settings.default_user == Some(self.user) { diff --git a/theseus_gui/src-tauri/src/api/logs.rs b/theseus_gui/src-tauri/src/api/logs.rs index c81dbe039..b76b3db41 100644 --- a/theseus_gui/src-tauri/src/api/logs.rs +++ b/theseus_gui/src-tauri/src/api/logs.rs @@ -50,7 +50,13 @@ pub async fn logs_get_output_by_datetime( profile_uuid: Uuid, datetime_string: String, ) -> Result { - Ok(logs::get_output_by_datetime(profile_uuid, &datetime_string).await?) + let profile_path = if let Some(p) = crate::profile::get_by_uuid(profile_uuid, None).await? { + p.path + } else { + return Err(theseus::Error::from(theseus::ErrorKind::UnmanagedProfileError(profile_uuid.to_string())).into()); + }; + + Ok(logs::get_output_by_datetime(&profile_path, &datetime_string).await?) } /// Delete all logs for a profile by profile id diff --git a/theseus_gui/src-tauri/src/api/profile.rs b/theseus_gui/src-tauri/src/api/profile.rs index c2650932b..82fa10c05 100644 --- a/theseus_gui/src-tauri/src/api/profile.rs +++ b/theseus_gui/src-tauri/src/api/profile.rs @@ -228,6 +228,8 @@ pub async fn profile_run_credentials( ) -> Result { let minecraft_child = profile::run_credentials(path, &credentials).await?; let uuid = minecraft_child.read().await.uuid; + + Ok(uuid) } diff --git a/theseus_gui/src-tauri/src/api/settings.rs b/theseus_gui/src-tauri/src/api/settings.rs index 9b90cf80d..7a6341013 100644 --- a/theseus_gui/src-tauri/src/api/settings.rs +++ b/theseus_gui/src-tauri/src/api/settings.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use crate::api::Result; use serde::{Deserialize, Serialize}; use theseus::prelude::*; @@ -22,7 +24,7 @@ pub struct FrontendSettings { pub fn init() -> tauri::plugin::TauriPlugin { tauri::plugin::Builder::new("settings") - .invoke_handler(tauri::generate_handler![settings_get, settings_set,]) + .invoke_handler(tauri::generate_handler![settings_get, settings_set, settings_change_config_dir]) .build() } @@ -41,3 +43,12 @@ pub async fn settings_set(settings: Settings) -> Result<()> { settings::set(settings).await?; Ok(()) } + +// Change config directory +// Seizes the entire State to do it +// invoke('plugin:settings|settings_change_config_dir', new_dir) +#[tauri::command] +pub async fn settings_change_config_dir(new_config_dir: PathBuf) -> Result<()> { + settings::set_config_dir(new_config_dir).await?; + Ok(()) +} diff --git a/theseus_gui/src-tauri/src/api/utils.rs b/theseus_gui/src-tauri/src/api/utils.rs index e3dfcb25f..08be69908 100644 --- a/theseus_gui/src-tauri/src/api/utils.rs +++ b/theseus_gui/src-tauri/src/api/utils.rs @@ -1,7 +1,7 @@ use theseus::{handler, prelude::CommandPayload, State}; use crate::api::Result; -use std::{env, process::Command}; +use std::{env, process::Command, path::PathBuf}; pub fn init() -> tauri::plugin::TauriPlugin { tauri::plugin::Builder::new("utils") @@ -12,6 +12,7 @@ pub fn init() -> tauri::plugin::TauriPlugin { safety_check_safe_loading_bars, get_opening_command, await_sync, + get_cache_path ]) .build() } @@ -122,6 +123,14 @@ pub async fn handle_command(command: String) -> Result<()> { #[tauri::command] pub async fn await_sync() -> Result<()> { State::sync().await?; - tracing::info!("State synced"); + tracing::debug!("State synced"); Ok(()) } + +// Get cached path from relative path +// Returns "%CACHE_DIR%/relative_path" +#[tauri::command] +pub async fn get_cache_path() -> Result { + let state = theseus::State::get().await?; + Ok(state.directories.caches_dir().await) +} diff --git a/theseus_gui/src/App.vue b/theseus_gui/src/App.vue index 298206330..b474f3de5 100644 --- a/theseus_gui/src/App.vue +++ b/theseus_gui/src/App.vue @@ -14,7 +14,7 @@ import { import { useLoading, useTheming } from '@/store/state' import AccountsCard from '@/components/ui/AccountsCard.vue' import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue' -import { get } from '@/helpers/settings' +import { get, change_config_dir } from '@/helpers/settings' import Breadcrumbs from '@/components/ui/Breadcrumbs.vue' import RunningAppBar from '@/components/ui/RunningAppBar.vue' import SplashScreen from '@/components/ui/SplashScreen.vue' @@ -96,6 +96,12 @@ const handleClose = async () => { window.getCurrent().close() } +const do_change = async () => { + console.log("123") + await change_config_dir('/home/thesuzerain/newone') + console.log("456") +} + window.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => { await handleClose() }) @@ -151,6 +157,9 @@ const accounts = ref(null) +