use super::{Profile, ProfilePathId}; use chrono::{DateTime, Utc}; use serde::Deserialize; use serde::Serialize; use std::{collections::HashMap, sync::Arc}; use sysinfo::PidExt; use tokio::process::Child; use tokio::process::Command; use tokio::sync::RwLock; use crate::event::emit::emit_process; use crate::event::ProcessPayloadType; use crate::util::fetch::read_json; use crate::util::io::IOError; use crate::{profile, ErrorKind}; use sysinfo::{ProcessExt, SystemExt}; use tokio::task::JoinHandle; use uuid::Uuid; const PROCESSES_JSON: &str = "processes.json"; // Child processes (instances of Minecraft) // A wrapper over a Hashmap connecting PID -> MinecraftChild pub struct Children(HashMap>>); #[derive(Debug)] pub enum ChildType { // A child process that is being managed by tokio TokioChild(Child), // A child process that was rescued from a cache (e.g. a process that was launched by theseus before the launcher was restarted) // This may not have all the same functionality as a TokioChild RescuedPID(u32), } #[derive(Serialize, Deserialize, Debug)] pub struct ProcessCache { pub pid: u32, pub uuid: Uuid, pub start_time: u64, pub name: String, pub exe: String, pub profile_relative_path: ProfilePathId, pub post_command: Option, } impl ChildType { pub async fn try_wait(&mut self) -> crate::Result> { match self { ChildType::TokioChild(child) => Ok(child .try_wait() .map_err(IOError::from)? .map(|x| x.code().unwrap_or(0))), ChildType::RescuedPID(pid) => { let mut system = sysinfo::System::new(); if !system.refresh_process(sysinfo::Pid::from_u32(*pid)) { return Ok(Some(0)); } let process = system.process(sysinfo::Pid::from_u32(*pid)); if let Some(process) = process { if process.status() == sysinfo::ProcessStatus::Run { Ok(None) } else { Ok(Some(0)) } } else { Ok(Some(0)) } } } } pub async fn kill(&mut self) -> crate::Result<()> { match self { ChildType::TokioChild(child) => { Ok(child.kill().await.map_err(IOError::from)?) } ChildType::RescuedPID(pid) => { let mut system = sysinfo::System::new(); if system.refresh_process(sysinfo::Pid::from_u32(*pid)) { let process = system.process(sysinfo::Pid::from_u32(*pid)); if let Some(process) = process { process.kill(); } } Ok(()) } } } pub fn id(&self) -> Option { match self { ChildType::TokioChild(child) => child.id(), ChildType::RescuedPID(pid) => Some(*pid), } } // Caches the process so that it can be restored if the launcher is restarted // Stored in the caches/metadata/processes.json file pub async fn cache_process( &self, uuid: uuid::Uuid, profile_path_id: ProfilePathId, post_command: Option, ) -> crate::Result<()> { let pid = match self { ChildType::TokioChild(child) => child.id().unwrap_or(0), ChildType::RescuedPID(pid) => *pid, }; let state = crate::State::get().await?; let mut system = sysinfo::System::new(); system.refresh_processes(); let process = system.process(sysinfo::Pid::from_u32(pid)).ok_or_else(|| { crate::ErrorKind::LauncherError(format!( "Could not find process {}", pid )) })?; let start_time = process.start_time(); let name = process.name().to_string(); let exe = process.exe().to_string_lossy().to_string(); let cached_process = ProcessCache { pid, start_time, name, exe, post_command, uuid, profile_relative_path: profile_path_id, }; let children_path = state .directories .caches_meta_dir() .await .join(PROCESSES_JSON); let mut children_caches = if let Ok(children_json) = read_json::>( &children_path, &state.io_semaphore, ) .await { children_json } else { HashMap::new() }; children_caches.insert(uuid, cached_process); crate::util::fetch::write( &children_path, &serde_json::to_vec(&children_caches)?, &state.io_semaphore, ) .await?; Ok(()) } // Removes the process from the cache (ie: on process exit) pub async fn remove_cache(&self, uuid: uuid::Uuid) -> crate::Result<()> { let state = crate::State::get().await?; let children_path = state .directories .caches_meta_dir() .await .join(PROCESSES_JSON); let mut children_caches = if let Ok(children_json) = read_json::>( &children_path, &state.io_semaphore, ) .await { children_json } else { HashMap::new() }; children_caches.remove(&uuid); crate::util::fetch::write( &children_path, &serde_json::to_vec(&children_caches)?, &state.io_semaphore, ) .await?; Ok(()) } } // Minecraft Child, bundles together the PID, the actual Child, and the easily queryable stdout and stderr streams (if needed) #[derive(Debug)] pub struct MinecraftChild { pub uuid: Uuid, pub profile_relative_path: ProfilePathId, pub manager: Option>>, // None when future has completed and been handled pub current_child: Arc>, pub last_updated_playtime: DateTime, // The last time we updated the playtime for the associated profile } impl Children { pub fn new() -> Self { Children(HashMap::new()) } // Loads cached processes from the caches/metadata/processes.json file, re-inserts them into the hashmap, and removes them from the file // This will only be called once, on startup. Only processes who match a cached process (name, time started, pid, etc) will be re-inserted pub async fn rescue_cache(&mut self) -> crate::Result<()> { let state = crate::State::get().await?; let children_path = state .directories .caches_meta_dir() .await .join(PROCESSES_JSON); let mut children_caches = if let Ok(children_json) = read_json::>( &children_path, &state.io_semaphore, ) .await { // Overwrite the file with an empty hashmap- we will re-insert the cached processes let empty = HashMap::::new(); crate::util::fetch::write( &children_path, &serde_json::to_vec(&empty)?, &state.io_semaphore, ) .await?; // Return the cached processes children_json } else { HashMap::new() }; for (_, cache) in children_caches.drain() { let uuid = cache.uuid; match self.insert_cached_process(cache).await { Ok(child) => { self.0.insert(uuid, child); } Err(e) => tracing::warn!( "Failed to rescue cached process {}: {}", uuid, e ), } } Ok(()) } // 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 MinecraftChild rather than any previously stored MinecraftChild that may exist #[tracing::instrument(skip( self, uuid, mc_command, post_command, censor_strings ))] #[tracing::instrument(level = "trace", skip(self))] #[theseus_macros::debug_pin] pub async fn insert_new_process( &mut self, uuid: Uuid, profile_relative_path: ProfilePathId, mut mc_command: Command, post_command: Option, // Command to run after minecraft. censor_strings: HashMap, ) -> crate::Result>> { // Takes the first element of the commands vector and spawns it let child = mc_command.spawn().map_err(IOError::from)?; let child = ChildType::TokioChild(child); // Slots child into manager let pid = child.id().ok_or_else(|| { crate::ErrorKind::LauncherError( "Process immediately failed, could not get PID".to_string(), ) })?; // Caches process so that it can be restored if the launcher is restarted child .cache_process( uuid, profile_relative_path.clone(), post_command.clone(), ) .await?; let current_child = Arc::new(RwLock::new(child)); let manager = Some(tokio::spawn(Self::sequential_process_manager( uuid, post_command, pid, current_child.clone(), profile_relative_path.clone(), ))); emit_process( uuid, pid, ProcessPayloadType::Launched, "Launched Minecraft", ) .await?; let last_updated_playtime = Utc::now(); // Create MinecraftChild let mchild = MinecraftChild { uuid, profile_relative_path, current_child, manager, last_updated_playtime, }; let mchild = Arc::new(RwLock::new(mchild)); self.0.insert(uuid, mchild.clone()); Ok(mchild) } // Rescues a cached process, inserts a child process to keep track of, and returns a reference to the container struct MinecraftChild // Essentially 'reconnects' to a process that was launched by theseus before the launcher was restarted // However, this may not have all the same functionality as a TokioChild, as we only have the PID and not the actual Child // Only processes who match a cached process (name, time started, pid, etc) will be re-inserted. The function fails with an error if the process is notably different. #[tracing::instrument(skip(self, cached_process,))] #[tracing::instrument(level = "trace", skip(self))] #[theseus_macros::debug_pin] pub async fn insert_cached_process( &mut self, cached_process: ProcessCache, ) -> crate::Result>> { let _state = crate::State::get().await?; // Takes the first element of the commands vector and spawns it // Checks processes, compares cached process to actual process // Fails if notably different (meaning that the PID was reused, and we shouldn't reconnect to it) { let mut system = sysinfo::System::new(); system.refresh_processes(); let process = system .process(sysinfo::Pid::from_u32(cached_process.pid)) .ok_or_else(|| { crate::ErrorKind::LauncherError(format!( "Could not find process {}", cached_process.pid )) })?; if cached_process.start_time != process.start_time() { return Err(ErrorKind::LauncherError(format!("Cached process {} has different start time than actual process {}", cached_process.pid, process.start_time())).into()); } if cached_process.name != process.name() { return Err(ErrorKind::LauncherError(format!("Cached process {} has different name than actual process {}", cached_process.pid, process.name())).into()); } if cached_process.exe != process.exe().to_string_lossy() { return Err(ErrorKind::LauncherError(format!("Cached process {} has different exe than actual process {}", cached_process.pid, process.exe().to_string_lossy())).into()); } } let child = ChildType::RescuedPID(cached_process.pid); // Slots child into manager let pid = child.id().ok_or_else(|| { crate::ErrorKind::LauncherError( "Process immediately failed, could not get PID".to_string(), ) })?; // Re-caches process so that it can be restored if the launcher is restarted child .cache_process( cached_process.uuid, cached_process.profile_relative_path.clone(), cached_process.post_command.clone(), ) .await?; let current_child = Arc::new(RwLock::new(child)); let manager = Some(tokio::spawn(Self::sequential_process_manager( cached_process.uuid, cached_process.post_command, pid, current_child.clone(), cached_process.profile_relative_path.clone(), ))); emit_process( cached_process.uuid, pid, ProcessPayloadType::Launched, "Launched Minecraft", ) .await?; let last_updated_playtime = Utc::now(); // Create MinecraftChild let mchild = MinecraftChild { uuid: cached_process.uuid, profile_relative_path: cached_process.profile_relative_path, current_child, manager, last_updated_playtime, }; let mchild = Arc::new(RwLock::new(mchild)); self.0.insert(cached_process.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 #[tracing::instrument(skip(current_child))] #[theseus_macros::debug_pin] async fn sequential_process_manager( uuid: Uuid, post_command: Option, mut current_pid: u32, current_child: Arc>, associated_profile: ProfilePathId, ) -> crate::Result { let current_child = current_child.clone(); // Wait on current Minecraft Child let mut mc_exit_status; let mut last_updated_playtime = Utc::now(); loop { if let Some(t) = current_child.write().await.try_wait().await? { mc_exit_status = t; break; } // sleep for 10ms tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; // Auto-update playtime every minute let diff = Utc::now() .signed_duration_since(last_updated_playtime) .num_seconds(); if diff >= 60 { if let Err(e) = profile::edit(&associated_profile, |prof| { prof.metadata.recent_time_played += diff as u64; async { Ok(()) } }) .await { tracing::warn!( "Failed to update playtime for profile {}: {}", &associated_profile, e ); } last_updated_playtime = Utc::now(); } } // Now fully complete- update playtime one last time let diff = Utc::now() .signed_duration_since(last_updated_playtime) .num_seconds(); if let Err(e) = profile::edit(&associated_profile, |prof| { prof.metadata.recent_time_played += diff as u64; async { Ok(()) } }) .await { tracing::warn!( "Failed to update playtime for profile {}: {}", &associated_profile, e ); } // Publish play time update // Allow failure, it will be stored locally and sent next time // Sent in another thread as first call may take a couple seconds and hold up process ending let associated_profile_clone = associated_profile.clone(); tokio::spawn(async move { if let Err(e) = profile::try_update_playtime(&associated_profile_clone.clone()) .await { tracing::warn!( "Failed to update playtime for profile {}: {}", &associated_profile_clone, e ); } }); { // Clear game played for Discord RPC // May have other active processes, so we clear to the next running process let state = crate::State::get().await?; let _ = state.discord_rpc.clear_to_default(true).await; } // If in tauri, window should show itself again after process exists if it was hidden #[cfg(feature = "tauri")] { let window = crate::EventState::get_main_window().await?; if let Some(window) = window { window.unminimize()?; } } { let current_child = current_child.write().await; current_child.remove_cache(uuid).await?; } if !mc_exit_status == 0 { emit_process( uuid, current_pid, ProcessPayloadType::Finished, "Exited process", ) .await?; 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 // First, create the command by splitting arguments let post_command = if let Some(hook) = post_command { let mut cmd = hook.split(' '); if let Some(command) = cmd.next() { let mut command = Command::new(command); command .args(&cmd.collect::>()) .current_dir(associated_profile.get_full_path().await?); Some(command) } else { None } } else { None }; if let Some(mut m_command) = post_command { { let mut current_child: tokio::sync::RwLockWriteGuard< '_, ChildType, > = current_child.write().await; let new_child = m_command.spawn().map_err(IOError::from)?; current_pid = new_child.id().ok_or_else(|| { crate::ErrorKind::LauncherError( "Process immediately failed, could not get PID" .to_string(), ) })?; *current_child = ChildType::TokioChild(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().await? { mc_exit_status = t; break; } // sleep for 10ms tokio::time::sleep(tokio::time::Duration::from_millis(10)) .await; } } emit_process( uuid, current_pid, ProcessPayloadType::Finished, "Exited process", ) .await?; Ok(mc_exit_status) } // Returns a ref to the child pub fn get(&self, uuid: &Uuid) -> Option>> { self.0.get(uuid).cloned() } // Gets all PID keys pub fn keys(&self) -> Vec { self.0.keys().cloned().collect() } // Get exit status of a child by PID // Returns None if the child is still running pub async fn exit_status(&self, uuid: &Uuid) -> crate::Result> { if let Some(child) = self.get(uuid) { let child = child.write().await; let status = child.current_child.write().await.try_wait().await?; Ok(status) } else { Ok(None) } } // Gets all PID keys of running children 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 child = child.write().await; if child .current_child .write() .await .try_wait() .await? .is_none() { keys.push(key); } } } Ok(keys) } // Gets all PID keys of running children with a given profile path pub async fn running_keys_with_profile( &self, profile_path: ProfilePathId, ) -> crate::Result> { let running_keys = self.running_keys().await?; let mut keys = Vec::new(); for key in running_keys { if let Some(child) = self.get(&key) { let child = child.clone(); let child = child.read().await; if child.profile_relative_path == profile_path { keys.push(key); } } } Ok(keys) } // Gets all profiles of running children pub async fn running_profile_paths( &self, ) -> crate::Result> { let mut profiles = Vec::new(); for key in self.keys() { if let Some(child) = self.get(&key) { let child = child.clone(); let child = child.write().await; if child .current_child .write() .await .try_wait() .await? .is_none() { profiles.push(child.profile_relative_path.clone()); } } } Ok(profiles) } // Gets all profiles of running children // Returns clones because it would be serialized anyway pub async fn running_profiles(&self) -> crate::Result> { let mut profiles = Vec::new(); for key in self.keys() { if let Some(child) = self.get(&key) { let child = child.clone(); let child = child.write().await; if child .current_child .write() .await .try_wait() .await? .is_none() { if let Some(prof) = crate::api::profile::get( &child.profile_relative_path.clone(), None, ) .await? { profiles.push(prof); } } } } Ok(profiles) } } impl Default for Children { fn default() -> Self { Self::new() } }