diff --git a/Cargo.lock b/Cargo.lock index 79924ba9..20650ec5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2110,6 +2110,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc_exception" version = "0.1.2" @@ -2733,6 +2744,30 @@ dependencies = [ "winreg 0.10.1", ] +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + [[package]] name = "rustc-demangle" version = "0.1.22" @@ -3344,6 +3379,7 @@ dependencies = [ "rand 0.8.5", "raw-window-handle", "regex", + "rfd", "semver", "serde", "serde_json", @@ -4304,6 +4340,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + [[package]] name = "windows" version = "0.39.0" @@ -4413,6 +4462,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + [[package]] name = "windows_aarch64_msvc" version = "0.39.0" @@ -4425,6 +4480,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + [[package]] name = "windows_i686_gnu" version = "0.39.0" @@ -4437,6 +4498,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + [[package]] name = "windows_i686_msvc" version = "0.39.0" @@ -4449,6 +4516,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + [[package]] name = "windows_x86_64_gnu" version = "0.39.0" @@ -4467,6 +4540,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + [[package]] name = "windows_x86_64_msvc" version = "0.39.0" diff --git a/theseus/src/api/jre.rs b/theseus/src/api/jre.rs index 428104f6..ad0b7b1c 100644 --- a/theseus/src/api/jre.rs +++ b/theseus/src/api/jre.rs @@ -1,4 +1,6 @@ //! Authentication flow interface +use std::path::PathBuf; + use crate::{ launcher::download, prelude::Profile, @@ -139,3 +141,8 @@ pub async fn validate_globals() -> crate::Result { let settings = state.settings.read().await; Ok(settings.java_globals.is_all_valid()) } + +// Validates JRE at a given at a given path +pub async fn check_jre(path: PathBuf) -> crate::Result> { + Ok(jre::check_java_at_filepath(&path)) +} diff --git a/theseus/src/api/mod.rs b/theseus/src/api/mod.rs index de53f69d..dfc29b93 100644 --- a/theseus/src/api/mod.rs +++ b/theseus/src/api/mod.rs @@ -11,7 +11,7 @@ pub mod tags; pub mod data { pub use crate::state::{ DirectoryInfo, Hooks, JavaSettings, MemorySettings, ModLoader, - ProfileMetadata, Settings, WindowSize, + ProfileMetadata, Settings, Theme, WindowSize, }; } diff --git a/theseus/src/api/pack.rs b/theseus/src/api/pack.rs index 121d76cc..36fb6b11 100644 --- a/theseus/src/api/pack.rs +++ b/theseus/src/api/pack.rs @@ -229,8 +229,8 @@ async fn install_pack( .await?; let loading_bar = init_loading( - LoadingBarType::PackDownload { - pack_name , + LoadingBarType::PackDownload { + pack_name, pack_id: project_id, pack_version: version_id, }, diff --git a/theseus/src/api/process.rs b/theseus/src/api/process.rs index 3969f255..d350ddc3 100644 --- a/theseus/src/api/process.rs +++ b/theseus/src/api/process.rs @@ -1,6 +1,8 @@ //! Theseus process management interface use std::path::{Path, PathBuf}; +use uuid::Uuid; + use crate::state::MinecraftChild; pub use crate::{ state::{ @@ -9,31 +11,33 @@ pub use crate::{ State, }; -// Gets whether a child process stored in the state by PID has finished +// Gets whether a child process stored in the state by UUID has finished #[tracing::instrument] -pub async fn has_finished_by_pid(pid: u32) -> crate::Result { - Ok(get_exit_status_by_pid(pid).await?.is_some()) +pub async fn has_finished_by_uuid(uuid: &Uuid) -> crate::Result { + Ok(get_exit_status_by_uuid(uuid).await?.is_some()) } -// Gets the exit status of a child process stored in the state by PID +// Gets the exit status of a child process stored in the state by UUID #[tracing::instrument] -pub async fn get_exit_status_by_pid(pid: u32) -> crate::Result> { +pub async fn get_exit_status_by_uuid( + uuid: &Uuid, +) -> crate::Result> { let state = State::get().await?; let children = state.children.read().await; - Ok(children.exit_status(&pid).await?.and_then(|f| f.code())) + Ok(children.exit_status(uuid).await?.and_then(|f| f.code())) } -// Gets the PID of each stored process in the state +// Gets the UUID of each stored process in the state #[tracing::instrument] -pub async fn get_all_pids() -> crate::Result> { +pub async fn get_all_uuids() -> crate::Result> { let state = State::get().await?; let children = state.children.read().await; Ok(children.keys()) } -// Gets the PID of each *running* stored process in the state +// Gets the UUID of each *running* stored process in the state #[tracing::instrument] -pub async fn get_all_running_pids() -> crate::Result> { +pub async fn get_all_running_uuids() -> crate::Result> { let state = State::get().await?; let children = state.children.read().await; children.running_keys().await @@ -55,62 +59,62 @@ pub async fn get_all_running_profiles() -> crate::Result> { children.running_profiles().await } -// Gets the PID of each stored process in the state by profile path +// Gets the UUID of each stored process in the state by profile path #[tracing::instrument] -pub async fn get_pids_by_profile_path( +pub async fn get_uuids_by_profile_path( profile_path: &Path, -) -> crate::Result> { +) -> crate::Result> { let state = State::get().await?; let children = state.children.read().await; children.running_keys_with_profile(profile_path).await } -// Gets stdout of a child process stored in the state by PID, as a string +// Gets stdout of a child process stored in the state by UUID, as a string #[tracing::instrument] -pub async fn get_stdout_by_pid(pid: u32) -> crate::Result { +pub async fn get_stdout_by_uuid(uuid: &Uuid) -> crate::Result { let state = State::get().await?; // Get stdout from child let children = state.children.read().await; // Extract child or return crate::Error - if let Some(child) = children.get(&pid) { + if let Some(child) = children.get(uuid) { let child = child.read().await; Ok(child.stdout.get_output().await?) } else { Err(crate::ErrorKind::LauncherError(format!( - "No child process with PID {}", - pid + "No child process by UUID {}", + uuid )) .as_error()) } } -// Gets stderr of a child process stored in the state by PID, as a string +// Gets stderr of a child process stored in the state by UUID, as a string #[tracing::instrument] -pub async fn get_stderr_by_pid(pid: u32) -> crate::Result { +pub async fn get_stderr_by_uuid(uuid: &Uuid) -> crate::Result { let state = State::get().await?; // Get stdout from child let children = state.children.read().await; // Extract child or return crate::Error - if let Some(child) = children.get(&pid) { + if let Some(child) = children.get(uuid) { let child = child.read().await; Ok(child.stderr.get_output().await?) } else { Err(crate::ErrorKind::LauncherError(format!( - "No child process with PID {}", - pid + "No child process with UUID {}", + uuid )) .as_error()) } } -// Kill a child process stored in the state by PID, as a string +// Kill a child process stored in the state by UUID, as a string #[tracing::instrument] -pub async fn kill_by_pid(pid: u32) -> crate::Result<()> { +pub async fn kill_by_uuid(uuid: &Uuid) -> crate::Result<()> { let state = State::get().await?; let children = state.children.read().await; - if let Some(mchild) = children.get(&pid) { + if let Some(mchild) = children.get(uuid) { let mut mchild = mchild.write().await; kill(&mut mchild).await } else { @@ -119,13 +123,13 @@ pub async fn kill_by_pid(pid: u32) -> crate::Result<()> { } } -// Wait for a child process stored in the state by PID +// Wait for a child process stored in the state by UUID #[tracing::instrument] -pub async fn wait_for_by_pid(pid: u32) -> crate::Result<()> { +pub async fn wait_for_by_uuid(uuid: &Uuid) -> crate::Result<()> { let state = State::get().await?; let children = state.children.read().await; // No error returned for already killed process - if let Some(mchild) = children.get(&pid) { + if let Some(mchild) = children.get(uuid) { let mut mchild = mchild.write().await; wait_for(&mut mchild).await } else { @@ -137,18 +141,30 @@ pub async fn wait_for_by_pid(pid: u32) -> crate::Result<()> { // Kill a running child process directly, and wait for it to be killed #[tracing::instrument] pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> { - running.child.kill().await?; + running.current_child.write().await.kill().await?; wait_for(running).await } // Await on the completion of a child process directly #[tracing::instrument] pub async fn wait_for(running: &mut MinecraftChild) -> crate::Result<()> { - let result = running.child.wait().await.map_err(|err| { - crate::ErrorKind::LauncherError(format!( - "Error running minecraft: {err}" - )) - })?; + // We do not wait on the Child directly, but wait on the thread manager. + // This way we can still run all cleanup hook functions that happen after. + let result = running + .manager + .take() + .ok_or_else(|| { + crate::ErrorKind::LauncherError(format!( + "Process manager already completed or missing for process {}", + running.uuid + )) + })? + .await? + .map_err(|err| { + crate::ErrorKind::LauncherError(format!( + "Error running minecraft: {err}" + )) + })?; match result.success() { false => Err(crate::ErrorKind::LauncherError(format!( diff --git a/theseus/src/api/profile.rs b/theseus/src/api/profile.rs index 67293fcf..2a62cac6 100644 --- a/theseus/src/api/profile.rs +++ b/theseus/src/api/profile.rs @@ -256,7 +256,7 @@ pub async fn run_credentials( .await?; let pre_launch_hooks = &profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch; - for hook in pre_launch_hooks.iter() { + if let Some(hook) = pre_launch_hooks { // TODO: hook parameters let mut cmd = hook.split(' '); if let Some(command) = cmd.next() { @@ -336,6 +336,23 @@ pub async fn run_credentials( let env_args = &settings.custom_env_args; + // Post post exit hooks + let post_exit_hook = + &profile.hooks.as_ref().unwrap_or(&settings.hooks).post_exit; + + let post_exit_hook = if let Some(hook) = post_exit_hook { + let mut cmd = hook.split(' '); + if let Some(command) = cmd.next() { + let mut command = Command::new(command); + command.args(&cmd.collect::>()).current_dir(path); + Some(command) + } else { + None + } + } else { + None + }; + let mc_process = crate::launcher::launch_minecraft( &profile.metadata.game_version, &profile.metadata.loader_version, @@ -347,20 +364,10 @@ pub async fn run_credentials( &memory, &resolution, credentials, + post_exit_hook, &profile, ) .await?; - // Insert child into state - let mut state_children = state.children.write().await; - let pid = mc_process.id().ok_or_else(|| { - crate::ErrorKind::LauncherError( - "Process failed to stay open.".to_string(), - ) - })?; - let mchild_arc = state_children - .insert_process(pid, path.to_path_buf(), mc_process) - .await?; - - Ok(mchild_arc) + Ok(mc_process) } diff --git a/theseus/src/event/emit.rs b/theseus/src/event/emit.rs index 936924a4..22fb69f1 100644 --- a/theseus/src/event/emit.rs +++ b/theseus/src/event/emit.rs @@ -129,7 +129,7 @@ pub async fn emit_warning(message: &str) -> crate::Result<()> { Ok(()) } -// emit_process(pid, event, message) +// emit_process(uuid, pid, event, message) #[allow(unused_variables)] pub async fn emit_process( uuid: uuid::Uuid, diff --git a/theseus/src/event/mod.rs b/theseus/src/event/mod.rs index 4340d3f2..4d3ed130 100644 --- a/theseus/src/event/mod.rs +++ b/theseus/src/event/mod.rs @@ -93,7 +93,7 @@ pub enum LoadingBarType { pack_name: String, pack_id: Option, pack_version: Option, - }, + }, MinecraftDownload { profile_uuid: Uuid, profile_name: String, @@ -121,11 +121,11 @@ pub struct ProcessPayload { pub event: ProcessPayloadType, pub message: String, } -#[derive(Serialize, Clone)] +#[derive(Serialize, Clone, Debug)] pub enum ProcessPayloadType { Launched, - // Finishing, // TODO: process restructing incoming, currently this is never emitted - // Finished, // TODO: process restructing incoming, currently this is never emitted + Updated, // eg: if the MinecraftChild changes to its post-command process instead of the Minecraft process + Finished, } #[derive(Serialize, Clone)] diff --git a/theseus/src/launcher/download.rs b/theseus/src/launcher/download.rs index 4b6834e2..6b0ea593 100644 --- a/theseus/src/launcher/download.rs +++ b/theseus/src/launcher/download.rs @@ -33,7 +33,7 @@ pub async fn download_minecraft( LoadingBarType::MinecraftDownload { // If we are downloading minecraft for a profile, provide its name and uuid profile_name: profile.metadata.name.clone(), - profile_uuid: profile.uuid, + profile_uuid: profile.uuid, }, 100.0, "Downloading Minecraft...", diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index 757b1c0d..2e35fc08 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -1,10 +1,13 @@ //! Logic for launching Minecraft -use crate::{process, state as st}; +use crate::{ + process, + state::{self as st, MinecraftChild}, +}; use daedalus as d; use dunce::canonicalize; use st::Profile; -use std::{path::Path, process::Stdio}; -use tokio::process::{Child, Command}; +use std::{path::Path, process::Stdio, sync::Arc}; +use tokio::process::Command; mod args; @@ -58,8 +61,9 @@ pub async fn launch_minecraft( memory: &st::MemorySettings, resolution: &st::WindowSize, credentials: &auth::Credentials, + post_exit_hook: Option, profile: &Profile, // optional ref to Profile for event tracking -) -> crate::Result { +) -> crate::Result>> { let state = st::State::get().await?; let instance_path = &canonicalize(instance_path)?; @@ -182,10 +186,10 @@ pub async fn launch_minecraft( // Check if profile has a running profile, and reject running the command if it does // Done late so a quick double call doesn't launch two instances let existing_processes = - process::get_pids_by_profile_path(instance_path).await?; - if let Some(pid) = existing_processes.first() { + process::get_uuids_by_profile_path(instance_path).await?; + if let Some(uuid) = existing_processes.first() { return Err(crate::ErrorKind::LauncherError(format!( - "Profile {} is already running at PID: {pid}", + "Profile {} is already running at UUID: {uuid}", instance_path.display() )) .as_error()); @@ -233,12 +237,15 @@ pub async fn launch_minecraft( .stdout(Stdio::piped()) .stderr(Stdio::piped()); - command.spawn().map_err(|err| { - crate::ErrorKind::LauncherError(format!( - "Error running Minecraft (minecraft-{} @ {}): {err}", - &version.id, - instance_path.display() - )) - .as_error() - }) + // Create Minecraft child by inserting it into the state + // This also spawns the process and prepares the subsequent processes + let mut state_children = state.children.write().await; + state_children + .insert_process( + uuid::Uuid::new_v4(), + instance_path.to_path_buf(), + command, + post_exit_hook, + ) + .await } diff --git a/theseus/src/state/children.rs b/theseus/src/state/children.rs index 2599bd25..42cb46a0 100644 --- a/theseus/src/state/children.rs +++ b/theseus/src/state/children.rs @@ -1,25 +1,29 @@ +use super::Profile; use std::path::{Path, PathBuf}; +use std::process::ExitStatus; use std::{collections::HashMap, sync::Arc}; use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Child; +use tokio::process::Command; use tokio::process::{ChildStderr, ChildStdout}; use tokio::sync::RwLock; use crate::event::emit::emit_process; use crate::event::ProcessPayloadType; - -use super::Profile; +use tokio::task::JoinHandle; +use uuid::Uuid; // Child processes (instances of Minecraft) // A wrapper over a Hashmap connecting PID -> MinecraftChild -pub struct Children(HashMap>>); +pub struct Children(HashMap>>); // Minecraft Child, bundles together the PID, the actual Child, and the easily queryable stdout and stderr streams #[derive(Debug)] pub struct MinecraftChild { - pub uuid: uuid::Uuid, - pub pid: u32, + pub uuid: Uuid, pub profile_path: PathBuf, //todo: make UUID when profiles are recognized by UUID - pub child: tokio::process::Child, + pub manager: Option>>, // None when future has completed and been handled + pub current_child: Arc>, pub stdout: SharedOutput, pub stderr: SharedOutput, } @@ -29,16 +33,18 @@ impl Children { Children(HashMap::new()) } - // Inserts a child process to keep track of, and returns a reference to the container struct MinecraftChild + // Runs the command in process, inserts a child process to keep track of, and returns a reference to the container struct MinecraftChild // The threads for stdout and stderr are spawned here - // Unlike a Hashmap's 'insert', this directly returns the reference to the Child rather than any previously stored Child that may exist + // Unlike a Hashmap's 'insert', this directly returns the reference to the MinecraftChild rather than any previously stored MinecraftChild that may exist pub async fn insert_process( &mut self, - pid: u32, + uuid: Uuid, profile_path: PathBuf, - mut child: tokio::process::Child, + mut mc_command: Command, + post_command: Option, // Command to run after minecraft. ) -> crate::Result>> { - let uuid = uuid::Uuid::new_v4(); + // Takes the first element of the commands vector and spawns it + let mut child = mc_command.spawn()?; // Create std watcher threads for stdout and stderr let stdout = SharedOutput::new(); @@ -55,11 +61,25 @@ impl Children { let stderr_clone = stderr.clone(); tokio::spawn(async move { if let Err(e) = stderr_clone.read_stderr(child_stderr).await { - eprintln!("Stderr thread died with error: {}", e); + eprintln!("Stderr process died with error: {}", e); } }); } + // Slots child into manager + let pid = child.id().ok_or_else(|| { + crate::ErrorKind::LauncherError( + "Process immediately failed, could not get PID".to_string(), + ) + })?; + let current_child = Arc::new(RwLock::new(child)); + let manager = Some(tokio::spawn(Self::sequential_process_manager( + uuid, + post_command, + pid, + current_child.clone(), + ))); + emit_process( uuid, pid, @@ -71,24 +91,88 @@ impl Children { // Create MinecraftChild let mchild = MinecraftChild { uuid, - pid, profile_path, - child, + current_child, stdout, stderr, + manager, }; + let mchild = Arc::new(RwLock::new(mchild)); - self.0.insert(pid, mchild.clone()); + self.0.insert(uuid, mchild.clone()); Ok(mchild) } + // Spawns a new child process and inserts it into the hashmap + // Also, as the process ends, it spawns the follow-up process if it exists + // By convention, ExitStatus is last command's exit status, and we exit on the first non-zero exit status + async fn sequential_process_manager( + uuid: Uuid, + post_command: Option, + mut current_pid: u32, + current_child: Arc>, + ) -> crate::Result { + let current_child = current_child.clone(); + + // Wait on current Minecraft Child + let mut mc_exit_status; + loop { + if let Some(t) = current_child.write().await.try_wait()? { + mc_exit_status = t; + break; + } + } + if !mc_exit_status.success() { + return Ok(mc_exit_status); // Err for a non-zero exit is handled in helper + } + + // If a post-command exist, switch to it and wait on it + if let Some(mut m_command) = post_command { + { + let mut current_child = current_child.write().await; + let new_child = m_command.spawn()?; + current_pid = new_child.id().ok_or_else(|| { + crate::ErrorKind::LauncherError( + "Process immediately failed, could not get PID" + .to_string(), + ) + })?; + *current_child = new_child; + } + emit_process( + uuid, + current_pid, + ProcessPayloadType::Updated, + "Completed Minecraft, switching to post-commands", + ) + .await?; + + loop { + if let Some(t) = current_child.write().await.try_wait()? { + mc_exit_status = t; + break; + } + } + } + + emit_process( + uuid, + current_pid, + ProcessPayloadType::Finished, + "Exited process", + ) + .await?; + + Ok(mc_exit_status) + } + // Returns a ref to the child - pub fn get(&self, pid: &u32) -> Option>> { - self.0.get(pid).cloned() + pub fn get(&self, uuid: &Uuid) -> Option>> { + self.0.get(uuid).cloned() } // Gets all PID keys - pub fn keys(&self) -> Vec { + pub fn keys(&self) -> Vec { self.0.keys().cloned().collect() } @@ -96,25 +180,25 @@ impl Children { // Returns None if the child is still running pub async fn exit_status( &self, - pid: &u32, + uuid: &Uuid, ) -> crate::Result> { - if let Some(child) = self.get(pid) { - let child = child.clone(); - let mut child = child.write().await; - Ok(child.child.try_wait()?) + if let Some(child) = self.get(uuid) { + let child = child.write().await; + let status = child.current_child.write().await.try_wait()?; + Ok(status) } else { Ok(None) } } // Gets all PID keys of running children - pub async fn running_keys(&self) -> crate::Result> { + pub async fn running_keys(&self) -> crate::Result> { let mut keys = Vec::new(); for key in self.keys() { if let Some(child) = self.get(&key) { let child = child.clone(); - let mut child = child.write().await; - if child.child.try_wait()?.is_none() { + let child = child.write().await; + if child.current_child.write().await.try_wait()?.is_none() { keys.push(key); } } @@ -126,7 +210,7 @@ impl Children { pub async fn running_keys_with_profile( &self, profile_path: &Path, - ) -> crate::Result> { + ) -> crate::Result> { let running_keys = self.running_keys().await?; let mut keys = Vec::new(); for key in running_keys { @@ -147,8 +231,8 @@ impl Children { for key in self.keys() { if let Some(child) = self.get(&key) { let child = child.clone(); - let mut child = child.write().await; - if child.child.try_wait()?.is_none() { + let child = child.write().await; + if child.current_child.write().await.try_wait()?.is_none() { profiles.push(child.profile_path.clone()); } } @@ -163,8 +247,8 @@ impl Children { for key in self.keys() { if let Some(child) = self.get(&key) { let child = child.clone(); - let mut child = child.write().await; - if child.child.try_wait()?.is_none() { + 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()) .await? diff --git a/theseus/src/state/settings.rs b/theseus/src/state/settings.rs index 34e2236e..0bf0ba93 100644 --- a/theseus/src/state/settings.rs +++ b/theseus/src/state/settings.rs @@ -1,6 +1,6 @@ //! Theseus settings file use serde::{Deserialize, Serialize}; -use std::{collections::HashSet, path::Path}; +use std::path::Path; use tokio::fs; use super::JavaGlobals; @@ -13,6 +13,7 @@ const CURRENT_FORMAT_VERSION: u32 = 1; #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Settings { + pub theme: Theme, pub memory: MemorySettings, pub game_resolution: WindowSize, pub custom_java_args: Vec, @@ -27,6 +28,7 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { Self { + theme: Theme::Dark, memory: MemorySettings::default(), game_resolution: WindowSize::default(), custom_java_args: Vec::new(), @@ -74,6 +76,15 @@ impl Settings { } } +/// Theseus theme +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Theme { + Dark, + Light, + Oled, +} + /// Minecraft memory settings #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub struct MemorySettings { @@ -105,10 +116,10 @@ impl Default for WindowSize { #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[serde(default)] pub struct Hooks { - #[serde(skip_serializing_if = "HashSet::is_empty")] - pub pre_launch: HashSet, + #[serde(skip_serializing_if = "Option::is_none")] + pub pre_launch: Option, #[serde(skip_serializing_if = "Option::is_none")] pub wrapper: Option, - #[serde(skip_serializing_if = "HashSet::is_empty")] - pub post_exit: HashSet, + #[serde(skip_serializing_if = "Option::is_none")] + pub post_exit: Option, } diff --git a/theseus_gui/src-tauri/Cargo.toml b/theseus_gui/src-tauri/Cargo.toml index fa64f6a3..895beef0 100644 --- a/theseus_gui/src-tauri/Cargo.toml +++ b/theseus_gui/src-tauri/Cargo.toml @@ -19,7 +19,7 @@ theseus = { path = "../../theseus", features = ["tauri"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2", features = ["protocol-asset", "window-close", "window-create"] } +tauri = { version = "1.2", features = ["protocol-asset", "window-close", "window-create", "dialog"] } tokio = { version = "1", features = ["full"] } thiserror = "1.0" tokio-stream = { version = "0.1", features = ["fs"] } diff --git a/theseus_gui/src-tauri/src/api/jre.rs b/theseus_gui/src-tauri/src/api/jre.rs index 708f7850..bca529d2 100644 --- a/theseus_gui/src-tauri/src/api/jre.rs +++ b/theseus_gui/src-tauri/src/api/jre.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::api::Result; use theseus::prelude::JavaVersion; @@ -60,3 +60,10 @@ pub async fn jre_get_optimal_jre_key_by_path(path: &Path) -> Result { pub async fn jre_validate_globals() -> Result { Ok(jre::validate_globals().await?) } + +// Validates JRE at a given path +// Returns None if the path is not a valid JRE +#[tauri::command] +pub async fn jre_get_jre(path: PathBuf) -> Result> { + jre::check_jre(path).await.map_err(|e| e.into()) +} diff --git a/theseus_gui/src-tauri/src/api/mod.rs b/theseus_gui/src-tauri/src/api/mod.rs index 672f62a3..800b494d 100644 --- a/theseus_gui/src-tauri/src/api/mod.rs +++ b/theseus_gui/src-tauri/src/api/mod.rs @@ -33,6 +33,9 @@ pub enum TheseusSerializableError { #[error("No profile found at {0}")] NoProfileFound(String), + + #[error("Improperly formatted environment variables: {0}")] + BadEnvVars(String), } // Generic implementation of From for ErrorTypeA @@ -74,4 +77,5 @@ impl_serialize! { Theseus, IO, NoProfileFound, + BadEnvVars, } diff --git a/theseus_gui/src-tauri/src/api/process.rs b/theseus_gui/src-tauri/src/api/process.rs index 45b61e8f..87cd0813 100644 --- a/theseus_gui/src-tauri/src/api/process.rs +++ b/theseus_gui/src-tauri/src/api/process.rs @@ -2,37 +2,40 @@ use std::path::{Path, PathBuf}; use crate::api::Result; use theseus::prelude::*; +use uuid::Uuid; -// Checks if a process has finished by process PID +// Checks if a process has finished by process UUID #[tauri::command] -pub async fn process_has_finished_by_pid(pid: u32) -> Result { - Ok(process::has_finished_by_pid(pid).await?) +pub async fn process_has_finished_by_uuid(uuid: Uuid) -> Result { + Ok(process::has_finished_by_uuid(&uuid).await?) } -// Gets process exit status by process PID +// Gets process exit status by process UUID #[tauri::command] -pub async fn process_get_exit_status_by_pid(pid: u32) -> Result> { - Ok(process::get_exit_status_by_pid(pid).await?) +pub async fn process_get_exit_status_by_uuid( + uuid: Uuid, +) -> Result> { + Ok(process::get_exit_status_by_uuid(&uuid).await?) } -// Gets all process PIDs +// Gets all process UUIDs #[tauri::command] -pub async fn process_get_all_pids() -> Result> { - Ok(process::get_all_pids().await?) +pub async fn process_get_all_uuids() -> Result> { + Ok(process::get_all_uuids().await?) } -// Gets all running process PIDs +// Gets all running process UUIDs #[tauri::command] -pub async fn process_get_all_running_pids() -> Result> { - Ok(process::get_all_running_pids().await?) +pub async fn process_get_all_running_uuids() -> Result> { + Ok(process::get_all_running_uuids().await?) } -// Gets all process PIDs by profile path +// Gets all process UUIDs by profile path #[tauri::command] -pub async fn process_get_pids_by_profile_path( +pub async fn process_get_uuids_by_profile_path( profile_path: &Path, -) -> Result> { - Ok(process::get_pids_by_profile_path(profile_path).await?) +) -> Result> { + Ok(process::get_uuids_by_profile_path(profile_path).await?) } // Gets the Profile paths of each *running* stored process in the state @@ -47,26 +50,26 @@ pub async fn process_get_all_running_profiles() -> Result> { Ok(process::get_all_running_profiles().await?) } -// Gets process stderr by process PID +// Gets process stderr by process UUID #[tauri::command] -pub async fn process_get_stderr_by_pid(pid: u32) -> Result { - Ok(process::get_stderr_by_pid(pid).await?) +pub async fn process_get_stderr_by_uuid(uuid: Uuid) -> Result { + Ok(process::get_stderr_by_uuid(&uuid).await?) } -// Gets process stdout by process PID +// Gets process stdout by process UUID #[tauri::command] -pub async fn process_get_stdout_by_pid(pid: u32) -> Result { - Ok(process::get_stdout_by_pid(pid).await?) +pub async fn process_get_stdout_by_uuid(uuid: Uuid) -> Result { + Ok(process::get_stdout_by_uuid(&uuid).await?) } -// Kill a process by process PID +// Kill a process by process UUID #[tauri::command] -pub async fn process_kill_by_pid(pid: u32) -> Result<()> { - Ok(process::kill_by_pid(pid).await?) +pub async fn process_kill_by_uuid(uuid: Uuid) -> Result<()> { + Ok(process::kill_by_uuid(&uuid).await?) } -// Wait for a process to finish by process PID +// Wait for a process to finish by process UUID #[tauri::command] -pub async fn process_wait_for_by_pid(pid: u32) -> Result<()> { - Ok(process::wait_for_by_pid(pid).await?) +pub async fn process_wait_for_by_uuid(uuid: Uuid) -> Result<()> { + Ok(process::wait_for_by_uuid(&uuid).await?) } diff --git a/theseus_gui/src-tauri/src/api/profile.rs b/theseus_gui/src-tauri/src/api/profile.rs index 4db06b70..3a59d0e0 100644 --- a/theseus_gui/src-tauri/src/api/profile.rs +++ b/theseus_gui/src-tauri/src/api/profile.rs @@ -1,6 +1,7 @@ use crate::api::Result; use std::path::{Path, PathBuf}; use theseus::prelude::*; +use uuid::Uuid; // Remove a profile // invoke('profile_add_path',path) @@ -73,18 +74,14 @@ pub async fn profile_remove_project( Ok(()) } // Run minecraft using a profile using the default credentials -// Returns a u32 representing the PID, which can be used to poll +// Returns the UUID, which can be used to poll // for the actual Child in the state. // invoke('profile_run', path) #[tauri::command] -pub async fn profile_run(path: &Path) -> Result { - let proc_lock = profile::run(path).await?; - let pid = proc_lock.read().await.child.id().ok_or_else(|| { - theseus::Error::from(theseus::ErrorKind::LauncherError( - "Process failed to stay open.".to_string(), - )) - })?; - Ok(pid) +pub async fn profile_run(path: &Path) -> Result { + let minecraft_child = profile::run(path).await?; + let uuid = minecraft_child.read().await.uuid; + Ok(uuid) } // Run Minecraft using a profile using the default credentials, and wait for the result @@ -97,21 +94,17 @@ pub async fn profile_run_wait(path: &Path) -> Result<()> { } // Run Minecraft using a profile using chosen credentials -// Returns a u32 representing the PID, which can be used to poll +// Returns the UUID, which can be used to poll // for the actual Child in the state. // invoke('profile_run_credentials', {path, credentials})') #[tauri::command] pub async fn profile_run_credentials( path: &Path, credentials: Credentials, -) -> Result { - let proc_lock = profile::run_credentials(path, &credentials).await?; - let pid = proc_lock.read().await.child.id().ok_or_else(|| { - theseus::Error::from(theseus::ErrorKind::LauncherError( - "Process failed to stay open.".to_string(), - )) - })?; - Ok(pid) +) -> Result { + let minecraft_child = profile::run_credentials(path, &credentials).await?; + let uuid = minecraft_child.read().await.uuid; + Ok(uuid) } // Run Minecraft using a profile using the chosen credentials, and wait for the result diff --git a/theseus_gui/src-tauri/src/api/settings.rs b/theseus_gui/src-tauri/src/api/settings.rs index e9f712a4..87e316ad 100644 --- a/theseus_gui/src-tauri/src/api/settings.rs +++ b/theseus_gui/src-tauri/src/api/settings.rs @@ -2,14 +2,17 @@ use crate::api::Result; use serde::{Deserialize, Serialize}; use theseus::prelude::*; +use super::TheseusSerializableError; + // Identical to theseus::settings::Settings except for the custom_java_args field // This allows us to split the custom_java_args string into a Vec here and join it back into a string in the backend #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FrontendSettings { + pub theme: Theme, pub memory: MemorySettings, pub game_resolution: WindowSize, pub custom_java_args: String, - pub custom_env_args: Vec<(String, String)>, + pub custom_env_args: String, pub java_globals: JavaGlobals, pub default_user: Option, pub hooks: Hooks, @@ -23,10 +26,16 @@ pub struct FrontendSettings { pub async fn settings_get() -> Result { let backend_settings = settings::get().await?; let frontend_settings = FrontendSettings { + theme: backend_settings.theme, memory: backend_settings.memory, game_resolution: backend_settings.game_resolution, custom_java_args: backend_settings.custom_java_args.join(" "), - custom_env_args: backend_settings.custom_env_args, + custom_env_args: backend_settings + .custom_env_args + .into_iter() + .map(|(s1, s2)| format!("{s1}={s2}")) + .collect::>() + .join(" "), java_globals: backend_settings.java_globals, default_user: backend_settings.default_user, hooks: backend_settings.hooks, @@ -40,7 +49,25 @@ pub async fn settings_get() -> Result { // invoke('settings_set', settings) #[tauri::command] pub async fn settings_set(settings: FrontendSettings) -> Result<()> { + let custom_env_args: Vec<(String, String)> = settings + .custom_env_args + .split_whitespace() + .map(|s| s.to_string()) + .map(|f| { + let mut split = f.split('='); + if let (Some(name), Some(value)) = (split.next(), split.next()) { + Ok((name.to_string(), value.to_string())) + } else { + Err(TheseusSerializableError::BadEnvVars( + "Invalid environment variable: {}".to_string(), + ) + .into()) + } + }) + .collect::>>()?; + let backend_settings = Settings { + theme: settings.theme, memory: settings.memory, game_resolution: settings.game_resolution, custom_java_args: settings @@ -48,7 +75,7 @@ pub async fn settings_set(settings: FrontendSettings) -> Result<()> { .split_whitespace() .map(|s| s.to_string()) .collect(), - custom_env_args: settings.custom_env_args, + custom_env_args, java_globals: settings.java_globals, default_user: settings.default_user, hooks: settings.hooks, diff --git a/theseus_gui/src-tauri/src/main.rs b/theseus_gui/src-tauri/src/main.rs index 70419043..81b1ec5e 100644 --- a/theseus_gui/src-tauri/src/main.rs +++ b/theseus_gui/src-tauri/src/main.rs @@ -58,17 +58,18 @@ fn main() { api::jre::jre_validate_globals, api::jre::jre_get_optimal_jre_key, api::jre::jre_get_optimal_jre_key_by_path, - api::process::process_get_all_pids, - api::process::process_get_all_running_pids, - api::process::process_get_pids_by_profile_path, + api::jre::jre_get_jre, + api::process::process_get_all_uuids, + api::process::process_get_all_running_uuids, + api::process::process_get_uuids_by_profile_path, api::process::process_get_all_running_profile_paths, api::process::process_get_all_running_profiles, - api::process::process_get_exit_status_by_pid, - api::process::process_has_finished_by_pid, - api::process::process_get_stderr_by_pid, - api::process::process_get_stdout_by_pid, - api::process::process_kill_by_pid, - api::process::process_wait_for_by_pid, + api::process::process_get_exit_status_by_uuid, + api::process::process_has_finished_by_uuid, + api::process::process_get_stderr_by_uuid, + api::process::process_get_stdout_by_uuid, + api::process::process_kill_by_uuid, + api::process::process_wait_for_by_uuid, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/theseus_gui/src/helpers/jre.js b/theseus_gui/src/helpers/jre.js index 4799fc6d..bb5f8c34 100644 --- a/theseus_gui/src/helpers/jre.js +++ b/theseus_gui/src/helpers/jre.js @@ -44,6 +44,12 @@ export async function validate_globals() { return await invoke('jre_validate_globals') } +// Gets java version from a specific path by trying to run 'java -version' on it. +// This also validates it, as it returns null if no valid java version is found at the path +export async function get_jre(path) { + return await invoke('jre_get_jre', { path }) +} + // Gets key for the optimal JRE to use, for a given profile path // The key can be used in the hashmap contained by JavaGlobals in Settings (if it exists) export async function get_optimal_jre_key_by_path(path) { @@ -52,7 +58,7 @@ export async function get_optimal_jre_key_by_path(path) { // Gets key for the optimal JRE to use, for a given profile // The key can be used in the hashmap contained by JavaGlobals in Settings (if it exists) -export async function get_optimal_jre_ke(path) { +export async function get_optimal_jre_key(path) { return await invoke('jre_get_optimal_jre_key', { path }) } diff --git a/theseus_gui/src/helpers/process.js b/theseus_gui/src/helpers/process.js index 30a395c0..905c79d6 100644 --- a/theseus_gui/src/helpers/process.js +++ b/theseus_gui/src/helpers/process.js @@ -5,34 +5,34 @@ */ import { invoke } from '@tauri-apps/api/tauri' -/// Gets if a process has finished by PID +/// Gets if a process has finished by UUID /// Returns bool -export async function has_finished_by_pid(pid) { - return await invoke('process_has_finished_by_pid', { pid }) +export async function has_finished_by_uuid(uuid) { + return await invoke('process_has_finished_by_uuid', { uuid }) } -/// Gets process exit status by PID +/// Gets process exit status by UUID /// Returns u32 -export async function get_exit_status_by_pid(pid) { - return await invoke('process_get_exit_status_by_pid', { pid }) +export async function get_exit_status_by_uuid(uuid) { + return await invoke('process_get_exit_status_by_uuid', { uuid }) } /// Gets all process IDs /// Returns [u32] -export async function get_all_pids() { - return await invoke('process_get_all_pids') +export async function get_all_uuids() { + return await invoke('process_get_all_uuids') } /// Gets all running process IDs /// Returns [u32] -export async function get_all_running_pids() { - return await invoke('process_get_all_running_pids') +export async function get_all_running_uuids() { + return await invoke('process_get_all_running_uuids') } /// Gets all running process IDs with a given profile path /// Returns [u32] -export async function get_pids_by_profile_path(profile_path) { - return await invoke('process_get_pids_by_profile_path', { profile_path }) +export async function get_uuids_by_profile_path(profile_path) { + return await invoke('process_get_uuids_by_profile_path', { profile_path }) } /// Gets all running process IDs with a given profile path @@ -47,19 +47,19 @@ export async function get_all_running_profiles(profile_path) { return await invoke('process_get_all_running_profiles', { profile_path }) } -/// Gets process stderr by PID +/// Gets process stderr by UUID /// Returns String -export async function get_stderr_by_pid(pid) { - return await invoke('process_get_stderr_by_pid', { pid }) +export async function get_stderr_by_uuid(uuid) { + return await invoke('process_get_stderr_by_uuid', { uuid }) } -/// Gets process stdout by PID +/// Gets process stdout by UUID /// Returns String -export async function get_stdout_by_pid(pid) { - return await invoke('process_get_stdout_by_pid', { pid }) +export async function get_stdout_by_uuid(uuid) { + return await invoke('process_get_stdout_by_uuid', { uuid }) } -/// Kills a process by PID -export async function kill_by_pid(pid) { - return await invoke('process_kill_by_pid', { pid }) +/// Kills a process by UUID +export async function kill_by_uuid(uuid) { + return await invoke('process_kill_by_uuid', { uuid }) } diff --git a/theseus_gui/src/helpers/profile.js b/theseus_gui/src/helpers/profile.js index dcd9f312..198f43e0 100644 --- a/theseus_gui/src/helpers/profile.js +++ b/theseus_gui/src/helpers/profile.js @@ -6,12 +6,21 @@ import { invoke } from '@tauri-apps/api/tauri' // Add empty default instance -export async function addDefaultInstance() { +export async function create_empty() { return await invoke('profile_create_empty') } -/// Creates instance -/// Returns a path to the profile created +/// Add instance +/* + name: String, // the name of the profile, and relative path to create + game_version: String, // the game version of the profile + modloader: ModLoader, // the modloader to use + - ModLoader is an enum, with the following variants: Vanilla, Forge, Fabric, Quilt + loader_version: String, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader + icon: Path, // the icon for the profile + - icon is a path to an image file, which will be copied into the profile directory +*/ + export async function create(name, game_version, modloader, loader_version, icon) { return await invoke('profile_create', { name, game_version, modloader, loader_version, icon }) } diff --git a/theseus_playground/src/main.rs b/theseus_playground/src/main.rs index 4eaf86d5..2179d0bf 100644 --- a/theseus_playground/src/main.rs +++ b/theseus_playground/src/main.rs @@ -33,6 +33,8 @@ async fn main() -> theseus::Result<()> { // Initialize state let st = State::get().await?; st.settings.write().await.max_concurrent_downloads = 5; + st.settings.write().await.hooks.post_exit = + Some("echo This is after Minecraft runs- global setting!".to_string()); // Changed the settings, so need to reset the semaphore st.reset_semaphore().await; @@ -88,11 +90,11 @@ async fn main() -> theseus::Result<()> { // (ie: changing the java runtime of an added profile) println!("Editing."); profile::edit(&profile_path, |_profile| { - // Eg: Java- this would let you change the java runtime of the profile instead of using the default - // use theseus::prelude::jre::JAVA__KEY; - // profile.java = Some(JavaSettings { - // jre_key: Some(JAVA_17_KEY.to_string()), - // extra_arguments: None, + // Add some hooks, for instance! + // profile.hooks = Some(Hooks { + // pre_launch: Some("echo This is before Minecraft runs!".to_string()), + // wrapper: None, + // post_exit: None, // }); async { Ok(()) } }) @@ -108,22 +110,22 @@ async fn main() -> theseus::Result<()> { println!("running"); // Run a profile, running minecraft and store the RwLock to the process let proc_lock = profile::run(&canonicalize(&profile_path)?).await?; + let uuid = proc_lock.read().await.uuid; + let pid = proc_lock.read().await.current_child.read().await.id(); - let pid = proc_lock - .read() - .await - .child - .id() - .expect("Could not get PID from process."); - println!("Minecraft PID: {}", pid); + println!("Minecraft UUID: {}", uuid); + println!("Minecraft PID: {:?}", pid); // Wait 5 seconds - println!("Waiting 5 seconds to gather logs..."); - sleep(Duration::from_secs(5)).await; - let _stdout = process::get_stdout_by_pid(pid).await?; - let _stderr = process::get_stderr_by_pid(pid).await?; - // println!("Logs after 5sec <<< {stdout} >>> end stdout"); + println!("Waiting 20 seconds to gather logs..."); + sleep(Duration::from_secs(20)).await; + let stdout = process::get_stdout_by_uuid(&uuid).await?; + println!("Logs after 5sec <<< {stdout} >>> end stdout"); + println!( + "All running process UUID {:?}", + process::get_all_running_uuids().await? + ); println!( "All running process paths {:?}", process::get_all_running_profile_paths().await?