diff --git a/Cargo.lock b/Cargo.lock index 50ff1ab6..447ce1ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2542,6 +2542,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "merge" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e520ba58faea3487f75df198b1d079644ec226ea3b0507d002c6fa4b8cf93a" +dependencies = [ + "merge_derive", + "num-traits", +] + +[[package]] +name = "merge_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f8ce6efff81cbc83caf4af0905c46e58cb46892f63ad3835e81b47eaf7968" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "mime" version = "0.3.17" @@ -3020,6 +3042,7 @@ dependencies = [ "indicatif", "interpulse", "md-5", + "merge", "murmur2", "onelauncher_entity", "onelauncher_gamemode", @@ -3063,6 +3086,7 @@ version = "0.2.0-alpha.2" dependencies = [ "chrono", "interpulse", + "merge", "onelauncher_macro", "sea-orm", "serde", diff --git a/Cargo.toml b/Cargo.toml index 88c918d5..344305ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,6 +149,7 @@ async-trait = { version = "0.1" } whoami = { version = "1.5" } cc = { version = "1.1" } libc = { version = "0.2" } +merge = { version = "0.2" } # macro syn = { version = "2" } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 0c7852fd..fb736f05 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -67,6 +67,7 @@ url = { workspace = true } async-trait = { workspace = true } paste = { workspace = true } byteorder = { workspace = true } +merge = { workspace = true } # integration discord-rich-presence = { workspace = true } diff --git a/packages/core/examples/cluster.rs b/packages/core/examples/cluster.rs index 98208c3d..586eeea5 100644 --- a/packages/core/examples/cluster.rs +++ b/packages/core/examples/cluster.rs @@ -12,7 +12,7 @@ async fn main() -> LauncherResult<()> { } println!("creating cluster"); - let cluster = api::cluster::create_cluster( + let cluster = &mut api::cluster::create_cluster( "Test Cluster", "1.8.9", GameLoader::Vanilla, @@ -23,7 +23,7 @@ async fn main() -> LauncherResult<()> { println!("cluster created: {cluster:#?}"); println!("preparing cluster for launch"); - api::cluster::prepare_cluster(&cluster).await?; + api::cluster::prepare_cluster(cluster).await?; Ok(()) } \ No newline at end of file diff --git a/packages/core/src/api/cluster/dao.rs b/packages/core/src/api/cluster/dao.rs index f05b5271..0681acb0 100644 --- a/packages/core/src/api/cluster/dao.rs +++ b/packages/core/src/api/cluster/dao.rs @@ -34,7 +34,7 @@ pub async fn insert_cluster( /// Updates an existing cluster in the database. pub async fn update_cluster_by_id( - id: i32, + id: u64, block: B, ) -> LauncherResult where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult { @@ -48,8 +48,25 @@ where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult( + cluster: &mut clusters::Model, + block: B, +) -> LauncherResult<&mut clusters::Model> +where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult { + let state = State::get().await?; + let db = &state.db; + + let model = cluster.clone().into_active_model(); + let model = block(model).await?; + let model = model.update(db).await?; + + *cluster = model; + + Ok(cluster) +} + /// Deletes a cluster by its ID from the **database**. -pub async fn delete_cluster_by_id(id: i32) -> LauncherResult<()> { +pub async fn delete_cluster_by_id(id: u64) -> LauncherResult<()> { let state = State::get().await?; let db = &state.db; @@ -65,7 +82,7 @@ pub async fn delete_cluster_by_id(id: i32) -> LauncherResult<()> { } /// Gets a cluster by its ID from the database. -pub async fn get_cluster_by_id(id: i32) -> LauncherResult> { +pub async fn get_cluster_by_id(id: u64) -> LauncherResult> { let state = State::get().await?; let db = &state.db; diff --git a/packages/core/src/api/cluster/mod.rs b/packages/core/src/api/cluster/mod.rs index 77d69585..f2f2f227 100644 --- a/packages/core/src/api/cluster/mod.rs +++ b/packages/core/src/api/cluster/mod.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use interpulse::api::minecraft::VersionInfo; use onelauncher_entity::cluster_stage::ClusterStage; use onelauncher_entity::{clusters, java_versions}; @@ -26,6 +27,10 @@ use super::ingress::init_ingress; pub enum ClusterError { #[error("version '{0}' was not found")] InvalidVersion(String), + #[error("failed to imply java version")] + MissingJavaVersion, + #[error("cluster is in downloading stage")] + ClusterDownloading, } #[must_use] @@ -97,7 +102,7 @@ pub async fn create_cluster( } /// Make a cluster playable (installs all necessary game files and dependencies required to play) -pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> { +pub async fn prepare_cluster(cluster: &mut clusters::Model) -> LauncherResult<&clusters::Model> { const INGRESS_TOTAL: f64 = 100.0; const INGRESS_TASKS: f64 = 4.0; const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS; @@ -106,7 +111,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> { if cluster.stage == ClusterStage::Ready { tracing::info!("cluster is already ready"); - return Ok(()); + return Ok(cluster); } let ingress_id = init_ingress( @@ -118,7 +123,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> { ) .await?; - dao::update_cluster_by_id(cluster.id, async |mut cluster| { + dao::update_cluster(cluster, async |mut cluster| { cluster.stage = Set(ClusterStage::Downloading); Ok(cluster) }) @@ -128,7 +133,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> { // TASK 1 ingress_id.set_ingress_message("fetching data").await?; let setting_profile = - setting_profiles::get_profile_or_default(cluster.setting_profile_name.as_ref()).await?; + setting_profiles::dao::get_profile_or_default(cluster.setting_profile_name.as_ref()).await?; let state = State::get().await?; let mut metadata = state.metadata.write().await; @@ -160,22 +165,11 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> { let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? { version } else { - // TODO: java runtime not found - should prob download - let java = java::locate_java().await?; + let Some(ver) = &version_info.java_version else { + return Err(ClusterError::MissingJavaVersion.into()); + }; - if java.is_empty() { - tracing::warn!("No Java installations found"); - return Ok(()); - } - - for (path, info) in java { - java::dao::insert_java(path, info).await?; - } - - java::get_recommended_java(&version_info, Some(&setting_profile)).await? - .ok_or_else(|| { - anyhow::anyhow!("No Java installations found, please install a Java runtime") - })? + java::prepare_java(ver.major_version).await? }; // TASK 3 @@ -189,7 +183,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> { if let Err(err) = result { tracing::error!("failed to prepare cluster: {}", err); - dao::update_cluster_by_id(cluster.id, async |mut cluster| { + dao::update_cluster(cluster, async |mut cluster| { cluster.stage = Set(ClusterStage::NotReady); Ok(cluster) }) @@ -197,14 +191,14 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> { return Err(err); } - dao::update_cluster_by_id(cluster.id, async |mut cluster| { + dao::update_cluster(cluster, async |mut cluster| { cluster.stage = Set(ClusterStage::Ready); Ok(cluster) }) .await?; tracing::debug!("cluster is ready"); - Ok(()) + Ok(cluster) } /// Run forge processors @@ -308,3 +302,14 @@ async fn run_forge_processors( Ok(()) } + +pub async fn update_playtime(id: u64, duration: i64) -> LauncherResult { + dao::update_cluster_by_id(id, async |mut cluster| { + let overall_played = cluster.overall_played.take().unwrap_or_default().unwrap_or_default(); + + cluster.overall_played = Set(Some(overall_played + duration)); + cluster.last_played = Set(Some(Utc::now().timestamp())); + + Ok(cluster) + }).await +} \ No newline at end of file diff --git a/packages/core/src/api/cluster/sync.rs b/packages/core/src/api/cluster/sync.rs index 5fb084b1..7587e4b9 100644 --- a/packages/core/src/api/cluster/sync.rs +++ b/packages/core/src/api/cluster/sync.rs @@ -26,7 +26,7 @@ impl SyncAction { /// Syncs all clusters in the database. Returns a list of cluster IDs that are missing. #[tracing::instrument] -pub async fn sync_clusters() -> LauncherResult> { +pub async fn sync_clusters() -> LauncherResult> { let clusters = dao::get_all_clusters().await?; let mut missing_ids = Vec::new(); @@ -92,7 +92,7 @@ pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult LauncherResult { +pub async fn sync_cluster_by_id(id: u64) -> LauncherResult { let cluster = dao::get_cluster_by_id(id) .await? .ok_or(DaoError::NotFound)?; diff --git a/packages/core/src/api/game/launch.rs b/packages/core/src/api/game/launch.rs index e69de29b..771d8738 100644 --- a/packages/core/src/api/game/launch.rs +++ b/packages/core/src/api/game/launch.rs @@ -0,0 +1,69 @@ +use std::sync::Arc; + +use onelauncher_entity::{cluster_stage::ClusterStage, prelude::model::*}; +use tokio::sync::RwLock; +use merge::Merge; + +use crate::{api::{cluster::{prepare_cluster, ClusterError}, game::metadata, java::{self, JavaError}, setting_profiles::{dao::get_profile_by_name, get_global_profile}}, error::LauncherResult, store::{credentials::MinecraftCredentials, processes::Process, State}, utils::io}; + +#[tracing::instrument] +pub async fn launch_minecraft( + cluster: &mut Cluster, + creds: &MinecraftCredentials, +) -> LauncherResult>> { + if cluster.stage.is_downloading() { + return Err(ClusterError::ClusterDownloading.into()); + } else if cluster.stage == ClusterStage::NotReady { + prepare_cluster(cluster).await?; + } + + let mut settings = get_global_profile().await; + if let Some(name) = &cluster.setting_profile_name { + if let Some(profile) = get_profile_by_name(name).await? { + settings.merge(profile); + } + } + + let state = State::get().await?; + let instance_path = &io::canonicalize(&cluster.path)?; + + let mut metadata = state.metadata.write().await; + let versions = &metadata.get_vanilla_or_fetch().await?.versions; + + let version_index = versions + .iter() + .position(|it| it.id == cluster.mc_version) + .ok_or_else(|| anyhow::anyhow!("invalid game version {}", cluster.mc_version))?; + + + let version = &versions[version_index]; + let updated = version_index <= versions.iter().position(|x| x.id == "22w16a").unwrap_or(0); + + let version_jar_name = cluster + .mc_loader_version + .as_ref() + .map_or(version.id.clone(), |it| { + format!("{}-{}", version.id.clone(), it.clone()) + }); + + let loader_version = metadata::get_loader_version(&cluster.mc_version, cluster.mc_loader, cluster.mc_loader_version.as_deref()).await?; + + let version_info = metadata::download_version_info( + version, + loader_version.as_ref(), + None, + None, + ) + .await?; + + drop(metadata); + + let java = java::get_recommended_java(&version_info, Some(&settings)).await?; + if java.is_none() { + return Err(JavaError::MissingJava.into()); + } + + + + todo!() +} \ No newline at end of file diff --git a/packages/core/src/api/game/log.rs b/packages/core/src/api/game/log.rs new file mode 100644 index 00000000..c514a25d --- /dev/null +++ b/packages/core/src/api/game/log.rs @@ -0,0 +1,27 @@ +use std::sync::LazyLock; + +use crate::store::credentials::MinecraftCredentials; + +static WHOAMI: LazyLock<(String, String)> = LazyLock::new(|| { + (whoami::username(), whoami::realname()) +}); + +#[must_use] +pub fn censor_line( + creds: &MinecraftCredentials, + mut output: String +) -> String { + let username = &WHOAMI.0; + let realname = &WHOAMI.1; + + output = output.replace(&format!("/{username}/"), "/{ENV_USERNAME}/"); + output = output.replace(&format!("\\{username}\\"), "\\{ENV_USERNAME}\\"); + output = output.replace(&format!("/{realname}/"), "/{ENV_REALNAME}/"); + output = output.replace(&format!("\\{realname}\\"), "\\{ENV_REALNAME}\\"); + output = output.replace(&creds.access_token, "{MC_ACCESS_TOKEN}"); + output = output.replace(&creds.username, "{MC_USERNAME}"); + output = output.replace(&creds.id.as_simple().to_string(), "{MC_UUID}"); + output = output.replace(&creds.id.as_hyphenated().to_string(), "{MC_UUID}"); + + output +} \ No newline at end of file diff --git a/packages/core/src/api/game/mod.rs b/packages/core/src/api/game/mod.rs index ec9174cd..789afa11 100644 --- a/packages/core/src/api/game/mod.rs +++ b/packages/core/src/api/game/mod.rs @@ -1,4 +1,5 @@ -pub mod metadata; -pub mod launch; pub mod arguments; -pub mod rules; \ No newline at end of file +pub mod launch; +pub mod log; +pub mod metadata; +pub mod rules; diff --git a/packages/core/src/api/ingress.rs b/packages/core/src/api/ingress.rs index ec5c9c04..6f4061cf 100644 --- a/packages/core/src/api/ingress.rs +++ b/packages/core/src/api/ingress.rs @@ -1,7 +1,20 @@ +use serde::Serialize; +use uuid::Uuid; + use crate::LauncherResult; use crate::store::State; use crate::store::ingress::{IngressId, IngressType, SubIngress}; +#[onelauncher_macro::specta(with_event)] +#[derive(Serialize, Debug, Clone)] +pub struct IngressPayload { + pub id: Uuid, + pub message: String, + pub ingress_type: IngressType, + pub percent: Option, + pub total: f64, +} + #[tracing::instrument] pub async fn init_ingress( ingress_type: IngressType, diff --git a/packages/core/src/api/java/dao.rs b/packages/core/src/api/java/dao.rs index e268efc4..7d7db59c 100644 --- a/packages/core/src/api/java/dao.rs +++ b/packages/core/src/api/java/dao.rs @@ -20,13 +20,13 @@ pub async fn get_latest_java() -> LauncherResult> { /// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`] pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult { - let major_version = if info.java_version[0..2].eq("1.") { - info.java_version[2..3].parse::() + let major_version: u32 = if info.java_version[0..2].eq("1.") { + info.java_version[2..3].parse() } else { let split = info.java_version.split_once('.').unwrap_or_default(); - split.0.parse::() + split.0.parse() }.or_else( - |_| info.java_version.parse::() + |_| info.java_version.parse() ).map_err(|e| JavaError::ParseVersion(info.java_version.clone(), e))?; Ok(java_versions::ActiveModel { @@ -50,7 +50,7 @@ pub async fn get_java_all() -> LauncherResult> { } /// Returns the specific Java version by ID -pub async fn get_java_by_id(id: i32) -> LauncherResult> { +pub async fn get_java_by_id(id: u64) -> LauncherResult> { let db = &State::get().await?.db; Ok(java_versions::Entity::find() @@ -60,7 +60,7 @@ pub async fn get_java_by_id(id: i32) -> LauncherResult LauncherResult> { +pub async fn get_all_java_by_major(major: u32) -> LauncherResult> { let db = &State::get().await?.db; Ok(java_versions::Entity::find() @@ -71,7 +71,7 @@ pub async fn get_all_java_by_major(major: i32) -> LauncherResult LauncherResult> { +pub async fn get_latest_java_by_major(major: u32) -> LauncherResult> { let db = &State::get().await?.db; Ok(java_versions::Entity::find() diff --git a/packages/core/src/api/java/mod.rs b/packages/core/src/api/java/mod.rs index 8eebec31..b82f462b 100644 --- a/packages/core/src/api/java/mod.rs +++ b/packages/core/src/api/java/mod.rs @@ -18,6 +18,8 @@ pub enum JavaError { ParseVersion(String, #[source] std::num::ParseIntError), #[error("failed to execute java command")] Execute(#[from] std::io::Error), + #[error("no java installations found")] + MissingJava, } /// Returns the relative path to the Java executable @@ -133,8 +135,43 @@ pub async fn get_recommended_java( return Ok(None); }; - #[allow(clippy::cast_possible_wrap)] - dao::get_latest_java_by_major(supported_ver.major_version as i32).await + dao::get_latest_java_by_major(supported_ver.major_version).await +} + +pub async fn prepare_java( + major: u32, +) -> LauncherResult { + let id = init_ingress( + IngressType::JavaPrepare, + "preparing java", + 100.0, + ) + .await?; + + let java = dao::get_latest_java_by_major(major).await?; + if let Some(java) = &java { + send_ingress(&id, 100.0).await?; + return Ok(java.clone()); + } + + let java = locate_java().await?; + + if !java.is_empty() { + for (path, info) in java { + dao::insert_java(path, info).await?; + } + + if let Some(java) = dao::get_latest_java_by_major(major).await? { + send_ingress(&id, 100.0).await?; + return Ok(java); + } + } + + tracing::warn!("no java installations found on the system, please install java manually or use the launcher to download it"); + + // TODO: java runtime not found - should prob download + + Err(JavaError::MissingJava.into()) } /// Attempts to scan common paths for Java installations. diff --git a/packages/core/src/api/mod.rs b/packages/core/src/api/mod.rs index 63406fc2..8a0362f7 100644 --- a/packages/core/src/api/mod.rs +++ b/packages/core/src/api/mod.rs @@ -4,4 +4,5 @@ pub mod ingress; pub mod game; pub mod proxy; pub mod java; +pub mod processes; pub mod setting_profiles; \ No newline at end of file diff --git a/packages/core/src/api/processes.rs b/packages/core/src/api/processes.rs new file mode 100644 index 00000000..ef3ecb8f --- /dev/null +++ b/packages/core/src/api/processes.rs @@ -0,0 +1,18 @@ +use serde::Serialize; + +use crate::store::processes::Process; + +#[derive(Debug, Serialize)] +pub enum ProcessPayload { + Started { + process: Process, + }, + Stopped { + pid: u32, + exit_code: i32, + }, + Output { + pid: u32, + output: String, + }, +} \ No newline at end of file diff --git a/packages/core/src/api/proxy/event.rs b/packages/core/src/api/proxy/event.rs new file mode 100644 index 00000000..c513d2bc --- /dev/null +++ b/packages/core/src/api/proxy/event.rs @@ -0,0 +1,27 @@ +use serde::Serialize; + +use crate::{error::LauncherResult, store::proxy::ProxyState}; + +#[onelauncher_macro::specta(with_event)] +#[derive(Debug, Serialize)] +pub enum LauncherEvent { + Ingress(crate::api::ingress::IngressPayload), + Message(crate::api::proxy::message::MessagePayload), + Process(crate::api::processes::ProcessPayload), +} + +pub async fn send_event(event: LauncherEvent) { + let Ok(proxy) = ProxyState::get() else { + tracing::error!("failed to get proxy state"); + return; + }; + + if let Err(e) = proxy.send_event(event).await { + tracing::error!("failed to send event: {e}"); + } +} + +pub async fn try_send_event(event: LauncherEvent) -> LauncherResult<()> { + let proxy = ProxyState::get()?; + proxy.send_event(event).await +} diff --git a/packages/core/src/api/proxy/impl/mod.rs b/packages/core/src/api/proxy/impl/mod.rs new file mode 100644 index 00000000..e14d69a2 --- /dev/null +++ b/packages/core/src/api/proxy/impl/mod.rs @@ -0,0 +1,7 @@ +#[cfg(feature = "tauri")] mod proxy_tauri; +#[cfg(feature = "cli")] mod proxy_cli; +mod proxy_empty; + +#[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri; +#[cfg(feature = "cli")] pub use proxy_cli::ProxyCli; +pub use proxy_empty::ProxyEmpty; \ No newline at end of file diff --git a/packages/core/src/api/proxy/impl/proxy_cli.rs b/packages/core/src/api/proxy/impl/proxy_cli.rs new file mode 100644 index 00000000..7b1307af --- /dev/null +++ b/packages/core/src/api/proxy/impl/proxy_cli.rs @@ -0,0 +1,60 @@ +use std::collections::HashMap; + +use indicatif::ProgressBar; +use tokio::sync::RwLock; +use uuid::Uuid; + +use crate::{constants::CLI_TOTAL_INGRESS, api::proxy::{event::LauncherEvent, LauncherProxy}, LauncherResult}; + +#[derive(Default, Debug)] +pub struct ProxyCli { + ingress_feeds: RwLock>, +} + +impl ProxyCli { + #[must_use] + pub fn new() -> Self { + Self::default() + } +} + +#[async_trait::async_trait] +impl LauncherProxy for ProxyCli { + async fn send_event(&self, event: LauncherEvent) -> LauncherResult<()> { + match event { + LauncherEvent::Ingress(ingress) => { + let mut feeds = self.ingress_feeds.write().await; + + let completed = ingress.percent.is_none(); + + if let Some(progress) = feeds.get(&ingress.id) { + if completed { + progress.finish(); + } else { + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] + let new_pos = (ingress.percent.unwrap() * CLI_TOTAL_INGRESS as f64).round() as u64; + progress.set_position(new_pos); + } + } else if !completed { + let progress = ProgressBar::new(CLI_TOTAL_INGRESS); + progress.set_message(ingress.message.clone()); + progress.set_position(0); + progress.set_style(indicatif::ProgressStyle::default_bar().template( + "{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}" + ).unwrap().progress_chars("#>-")); + + feeds.insert(ingress.id, progress); + } + }, + LauncherEvent::Message(message) => { + println!("[{:?}] {}", message.level, message.message); + }, + _ => { + println!("{:#?}", event); + }, + } + + Ok(()) + } + +} diff --git a/packages/core/src/api/proxy/impl/proxy_empty.rs b/packages/core/src/api/proxy/impl/proxy_empty.rs new file mode 100644 index 00000000..18cccb26 --- /dev/null +++ b/packages/core/src/api/proxy/impl/proxy_empty.rs @@ -0,0 +1,48 @@ +use crate::{api::proxy::{event::LauncherEvent, LauncherProxy}, LauncherResult}; + +#[derive(Default, Debug)] +pub struct ProxyEmpty; + +impl ProxyEmpty { + #[must_use] + pub const fn new() -> Self { + Self + } +} + +#[async_trait::async_trait] +impl LauncherProxy for ProxyEmpty { + async fn send_event(&self, event: LauncherEvent) -> LauncherResult<()> { + match event { + LauncherEvent::Ingress(ingress) => { + println!( + "{} [{}] {}", + ingress.percent.map_or("100%".to_string(), |p| format!("{:.2}%", p * 100.0)), + ingress.id, + ingress.message + ); + }, + LauncherEvent::Message(message) => { + println!("[{:?}] {}", message.level, message.message); + }, + LauncherEvent::Process(payload) => { + println!("{payload:#?}"); + }, + } + + Ok(()) + } + + #[cfg(feature = "gui")] + fn hide_main_window(&self) -> crate::LauncherResult<()> { + println!("hidden window"); + Ok(()) + } + + #[cfg(feature = "gui")] + fn show_main_window(&self) -> crate::LauncherResult<()> { + println!("shown window"); + Ok(()) + } + +} diff --git a/packages/core/src/api/proxy/proxy_tauri.rs b/packages/core/src/api/proxy/impl/proxy_tauri.rs similarity index 58% rename from packages/core/src/api/proxy/proxy_tauri.rs rename to packages/core/src/api/proxy/impl/proxy_tauri.rs index 3fd44627..282016be 100644 --- a/packages/core/src/api/proxy/proxy_tauri.rs +++ b/packages/core/src/api/proxy/impl/proxy_tauri.rs @@ -2,9 +2,7 @@ use tauri::{AppHandle, Emitter, Manager}; use tauri_specta::Event; use tracing::{error, warn}; -use crate::{store::ingress::IngressPayload, LauncherResult}; - -use super::{message::{MessageLevel, MessagePayload}, LauncherProxy}; +use crate::{constants::CLI_TOTAL_INGRESS, api::proxy::{event::LauncherEvent, LauncherProxy, message::MessageLevel}, LauncherResult}; #[derive(Debug)] pub struct ProxyTauri { @@ -22,21 +20,20 @@ impl ProxyTauri { #[async_trait::async_trait] impl LauncherProxy for ProxyTauri { - async fn send_ingress(&self, ingress: IngressPayload) -> LauncherResult<()> { - self.handle - .emit(IngressPayload::NAME, ingress) - .map_err(Into::into) - } - - async fn send_message(&self, message: MessagePayload) -> LauncherResult<()> { - match message.level { - MessageLevel::Info => {}, - MessageLevel::Warning => warn!("{}", message.message), - MessageLevel::Error => error!("{}", message.message), - }; + async fn send_event(&self, event: LauncherEvent) -> LauncherResult<()> { + match event { + LauncherEvent::Message(message) => { + match message.level { + MessageLevel::Info => {}, + MessageLevel::Warning => warn!("{}", message.message), + MessageLevel::Error => error!("{}", message.message), + }; + }, + _ => {} + } self.handle - .emit(MessagePayload::NAME, message) + .emit_all(LauncherEvent::NAME, event) .map_err(Into::into) } diff --git a/packages/core/src/api/proxy/message.rs b/packages/core/src/api/proxy/message.rs index 94af74a1..2a63122c 100644 --- a/packages/core/src/api/proxy/message.rs +++ b/packages/core/src/api/proxy/message.rs @@ -1,5 +1,7 @@ use serde::Serialize; +use super::event::send_event; + #[onelauncher_macro::specta(with_event)] #[derive(Serialize, Debug, Clone)] pub struct MessagePayload { @@ -11,48 +13,57 @@ pub struct MessagePayload { #[derive(Serialize, Debug, Clone)] pub enum MessageLevel { Info, - Warning, + Warn, Error, } +pub fn send_message(level: MessageLevel, message: String) { + tokio::spawn(async move { + let payload = MessagePayload { + level, + message, + }; + + send_event(super::event::LauncherEvent::Message(payload)).await; + }); +} + +#[macro_export] +macro_rules! send_message { + ($level:tt, $($message:tt)*) => { + $crate::api::proxy::message::send_message( + $level, + format!($($message)*), + ) + }; +} + +#[macro_export] +macro_rules! send_info { + ($($message:tt)*) => { + $crate::api::proxy::message::send_message( + $crate::api::proxy::message::MessageLevel::Info, + format!($($message)*), + ) + }; +} + #[macro_export] macro_rules! send_warning { - ($($message:tt)*) => {{ - tokio::spawn(async move { - let _ = match $crate::store::proxy::ProxyState::get() { - Ok(proxy) => { - let payload = $crate::api::proxy::message::MessagePayload { - level: $crate::api::proxy::message::MessageLevel::Warning, - message: format!($($message)*), - }; - let _ = proxy.send_message(payload).await; - }, - Err(err) => { - tracing::warn!($($message)*); - tracing::warn!("failed to send warning: {}", err); - }, - }; - }) - }}; + ($($message:tt)*) => { + $crate::api::proxy::message::send_message( + $crate::api::proxy::message::MessageLevel::Warn, + format!($($message)*), + ) + }; } #[macro_export] macro_rules! send_error { - ($($message:tt)*) => {{ - tokio::spawn(async move { - let _ = match $crate::store::proxy::ProxyState::get() { - Ok(proxy) => { - let payload = $crate::api::proxy::message::MessagePayload { - level: $crate::api::proxy::message::MessageLevel::Error, - message: format!($($message)*), - }; - let _ = proxy.send_message(payload).await; - }, - Err(err) => { - tracing::error!($($message)*); - tracing::error!("failed to send error: {}", err); - }, - }; - }) - }}; + ($($message:tt)*) => { + $crate::api::proxy::message::send_message( + $crate::api::proxy::message::MessageLevel::Error, + format!($($message)*), + ) + }; } diff --git a/packages/core/src/api/proxy/mod.rs b/packages/core/src/api/proxy/mod.rs index 9b7f52ef..416e428d 100644 --- a/packages/core/src/api/proxy/mod.rs +++ b/packages/core/src/api/proxy/mod.rs @@ -1,22 +1,12 @@ +mod r#impl; pub mod message; +pub mod event; -#[cfg(feature = "tauri")] mod proxy_tauri; -#[cfg(feature = "cli")] mod proxy_cli; -mod proxy_empty; - -use std::fmt::Debug; - -#[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri; -#[cfg(feature = "cli")] pub use proxy_cli::ProxyCli; -pub use proxy_empty::ProxyEmpty; - -#[cfg(feature = "cli")] pub type ProxyDynamic = ProxyCli; -#[cfg(not(feature = "cli"))] pub type ProxyDynamic = ProxyEmpty; +pub use r#impl::*; #[async_trait::async_trait] -pub trait LauncherProxy: Send + Sync + Debug { - async fn send_ingress(&self, ingress: crate::store::ingress::IngressPayload) -> crate::LauncherResult<()>; - async fn send_message(&self, message: message::MessagePayload) -> crate::LauncherResult<()>; +pub trait LauncherProxy: Send + Sync + std::fmt::Debug { + async fn send_event(&self, event: event::LauncherEvent) -> crate::LauncherResult<()>; #[cfg(feature = "gui")] fn hide_main_window(&self) -> crate::LauncherResult<()>; @@ -24,3 +14,6 @@ pub trait LauncherProxy: Send + Sync + Debug { #[cfg(feature = "gui")] fn show_main_window(&self) -> crate::LauncherResult<()>; } + +#[cfg(feature = "cli")] pub type ProxyDynamic = ProxyCli; +#[cfg(not(feature = "cli"))] pub type ProxyDynamic = ProxyEmpty; \ No newline at end of file diff --git a/packages/core/src/api/proxy/proxy_cli.rs b/packages/core/src/api/proxy/proxy_cli.rs deleted file mode 100644 index 3bb8b311..00000000 --- a/packages/core/src/api/proxy/proxy_cli.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::collections::HashMap; - -use indicatif::ProgressBar; -use tokio::sync::RwLock; -use uuid::Uuid; - -use crate::{constants::CLI_TOTAL_INGRESS, store::ingress::IngressPayload, LauncherResult}; - -use super::LauncherProxy; - -#[derive(Default, Debug)] -pub struct ProxyCli { - ingress_feeds: RwLock>, -} - -impl ProxyCli { - #[must_use] - pub fn new() -> Self { - Self::default() - } -} - -#[async_trait::async_trait] -impl LauncherProxy for ProxyCli { - - async fn send_ingress(&self, ingress: IngressPayload) -> LauncherResult<()> { - let mut feeds = self.ingress_feeds.write().await; - - let completed = ingress.percent.is_none(); - - if let Some(progress) = feeds.get(&ingress.id) { - if completed { - progress.finish(); - } else { - #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] - let new_pos = (ingress.percent.unwrap() * CLI_TOTAL_INGRESS as f64).round() as u64; - progress.set_position(new_pos); - } - } else if !completed { - let progress = ProgressBar::new(CLI_TOTAL_INGRESS); - progress.set_message(ingress.message.clone()); - progress.set_position(0); - progress.set_style(indicatif::ProgressStyle::default_bar().template( - "{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}" - ).unwrap().progress_chars("#>-")); - - feeds.insert(ingress.id, progress); - } - Ok(()) - } - - async fn send_message(&self, message: crate::api::proxy::message::MessagePayload) -> LauncherResult<()> { - println!("[{:?}] {}", message.level, message.message); - Ok(()) - } - -} diff --git a/packages/core/src/api/proxy/proxy_empty.rs b/packages/core/src/api/proxy/proxy_empty.rs deleted file mode 100644 index e74d887f..00000000 --- a/packages/core/src/api/proxy/proxy_empty.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::{store::ingress::IngressPayload, LauncherResult}; - -use super::LauncherProxy; - -#[derive(Default, Debug)] -pub struct ProxyEmpty; - -impl ProxyEmpty { - #[must_use] - pub const fn new() -> Self { - Self - } -} - -#[async_trait::async_trait] -impl LauncherProxy for ProxyEmpty { - - async fn send_ingress(&self, ingress: IngressPayload) -> LauncherResult<()> { - println!( - "{} [{}] {}", - ingress.percent.map_or("100%".to_string(), |p| format!("{:.2}%", p * 100.0)), - ingress.id, - ingress.message - ); - Ok(()) - } - - async fn send_message(&self, message: crate::api::proxy::message::MessagePayload) -> LauncherResult<()> { - println!("[{:?}] {}", message.level, message.message); - Ok(()) - } - - #[cfg(feature = "gui")] - fn hide_main_window(&self) -> crate::LauncherResult<()> { - println!("hidden window"); - Ok(()) - } - - #[cfg(feature = "gui")] - fn show_main_window(&self) -> crate::LauncherResult<()> { - println!("shown window"); - Ok(()) - } - -} diff --git a/packages/core/src/api/setting_profiles.rs b/packages/core/src/api/setting_profiles.rs index 43fcf0fa..038d656c 100644 --- a/packages/core/src/api/setting_profiles.rs +++ b/packages/core/src/api/setting_profiles.rs @@ -1,60 +1,75 @@ use onelauncher_entity::setting_profiles; -use sea_orm::{ActiveModelTrait, EntityTrait}; -use crate::{error::{DaoError, LauncherResult}, store::State}; +use crate::store::State; -pub async fn get_all_profiles() -> LauncherResult> { - let state = State::get().await?; - let db = &state.db; +pub mod dao { + use onelauncher_entity::setting_profiles; + use sea_orm::{ActiveModelTrait, EntityTrait}; - Ok(setting_profiles::Entity::find() - .all(db) - .await?) -} + use crate::{error::{DaoError, LauncherResult}, store::State}; -pub async fn get_profile_or_default(name: Option<&String>) -> LauncherResult { - if let Some(name) = name { - let profile = get_profile_by_name(name).await?; - if let Some(profile) = profile { - return Ok(profile); + pub async fn get_all_profiles() -> LauncherResult> { + let state = State::get().await?; + let db = &state.db; + + Ok(setting_profiles::Entity::find() + .all(db) + .await?) + } + + pub async fn get_profile_or_default(name: Option<&String>) -> LauncherResult { + if let Some(name) = name { + let profile = get_profile_by_name(name).await?; + if let Some(profile) = profile { + return Ok(profile); + } } + + let state = State::get().await?; + let settings = &state.settings.read().await; + Ok(settings.global_game_settings.clone()) } - let state = State::get().await?; - let settings = &state.settings.read().await; - Ok(settings.global_game_settings.clone()) -} + pub async fn get_profile_by_name(name: &str) -> LauncherResult> { + let state = State::get().await?; + let db = &state.db; -pub async fn get_profile_by_name(name: &str) -> LauncherResult> { - let state = State::get().await?; - let db = &state.db; + Ok(setting_profiles::Entity::find_by_id(name) + .one(db) + .await?) + } - Ok(setting_profiles::Entity::find_by_id(name) - .one(db) - .await?) -} - -pub async fn insert_profile(profile: setting_profiles::ActiveModel) -> LauncherResult { - if let Some(name) = profile.name.try_as_ref() { - if name.is_empty() || profile.is_global() { - return Err(DaoError::InvalidValue(name.to_string(), "name".into()).into()); + pub async fn insert_profile(profile: setting_profiles::ActiveModel) -> LauncherResult { + if let Some(name) = profile.name.try_as_ref() { + if name.is_empty() || profile.is_global() { + return Err(DaoError::InvalidValue(name.to_string(), "name".into()).into()); + } } + + let state = State::get().await?; + let db = &state.db; + + Ok(profile.insert(db).await?) } - let state = State::get().await?; - let db = &state.db; + pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> { + let state = State::get().await?; + let db = &state.db; - Ok(profile.insert(db).await?) + let deleted = setting_profiles::Entity::delete_by_id(name).exec(db).await?; + + if deleted.rows_affected == 0 { + return Err(DaoError::NotFound.into()); + } + Ok(()) + } } -pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> { - let state = State::get().await?; - let db = &state.db; +pub async fn get_global_profile() -> setting_profiles::Model { + let Ok(state) = State::get().await else { + return setting_profiles::Model::default_global_profile(); + }; - let deleted = setting_profiles::Entity::delete_by_id(name).exec(db).await?; - - if deleted.rows_affected == 0 { - return Err(DaoError::NotFound.into()); - } - Ok(()) -} \ No newline at end of file + let settings = state.settings.read().await; + settings.global_game_settings.clone() +} diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index 73ec2d36..41a932c2 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -19,6 +19,8 @@ pub enum LauncherError { #[error(transparent)] MinecraftAuthError(#[from] crate::store::credentials::MinecraftAuthError), #[error(transparent)] + ProcessError(#[from] crate::store::processes::ProcessError), + #[error(transparent)] DaoError(#[from] DaoError), #[error(transparent)] diff --git a/packages/core/src/store/ingress.rs b/packages/core/src/store/ingress.rs index 91bc6b21..0db6c539 100644 --- a/packages/core/src/store/ingress.rs +++ b/packages/core/src/store/ingress.rs @@ -4,10 +4,11 @@ use serde::Serialize; use tokio::sync::RwLock; use uuid::Uuid; +use crate::api::proxy::event::try_send_event; use crate::LauncherResult; +use crate::api::ingress::IngressPayload; use super::State; -use super::proxy::ProxyState; #[derive(Default)] pub struct IngressProcessor { @@ -73,21 +74,20 @@ impl IngressProcessor { let uuid = &id.0; let ingress = feeds.get_mut(uuid).ok_or(IngressError::NotFound)?; - let proxy = ProxyState::get()?; - ingress.current += increment; if let Some(message) = message { ingress.message = message; } - proxy - .send_ingress(IngressPayload { - id: uuid.to_owned(), - message: ingress.message.clone(), - ingress_type: ingress.ingress_type.clone(), - percent: Some(ingress.current / ingress.total), - total: ingress.total, - }) + let payload = IngressPayload { + id: uuid.to_owned(), + message: ingress.message.clone(), + ingress_type: ingress.ingress_type.clone(), + percent: Some(ingress.current / ingress.total), + total: ingress.total, + }; + + try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload)) .await?; Ok(()) @@ -113,20 +113,11 @@ pub struct Ingress { pub last_sent: f64, } -#[onelauncher_macro::specta(with_event)] -#[derive(Serialize, Debug, Clone)] -pub struct IngressPayload { - pub id: Uuid, - pub message: String, - pub ingress_type: IngressType, - pub percent: Option, - pub total: f64, -} - #[onelauncher_macro::specta] #[derive(Serialize, Debug, Clone)] pub enum IngressType { Download { file_name: String }, + JavaPrepare, JavaCheck, JavaLocate, MinecraftDownload, @@ -182,7 +173,8 @@ impl SubIngressExt for SubIngress<'_> { fn ingress_sub(&self, total: F) -> Self::Target<'_> where - F: FnOnce(f64) -> f64 { + F: FnOnce(f64) -> f64, + { SubIngress::from_sub(self, total(self.total)) } } @@ -201,7 +193,8 @@ impl SubIngressExt for Option<&SubIngress<'_>> { fn ingress_sub(&self, total: F) -> Self::Target<'_> where - F: FnOnce(f64) -> f64 { + F: FnOnce(f64) -> f64, + { self.map(|sub| SubIngress::from_sub(sub, total(sub.total))) } } @@ -217,16 +210,15 @@ impl Drop for IngressId { let ingress = processor.remove(ingress_uuid).await?; - let proxy = ProxyState::get()?; + let payload = IngressPayload { + id: ingress.id, + message: "Completed".into(), + ingress_type: ingress.ingress_type, + percent: None, + total: ingress.total, + }; - proxy - .send_ingress(IngressPayload { - id: ingress.id, - message: "Completed".into(), - ingress_type: ingress.ingress_type, - percent: None, - total: ingress.total, - }) + try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload)) .await?; tracing::trace!( diff --git a/packages/core/src/store/mod.rs b/packages/core/src/store/mod.rs index 6a4c5974..f49cecaf 100644 --- a/packages/core/src/store/mod.rs +++ b/packages/core/src/store/mod.rs @@ -4,7 +4,8 @@ pub mod ingress; pub mod proxy; pub mod semaphore; pub mod metadata; -pub mod credentials; +pub mod credentials; +pub mod processes; mod core_options; mod dirs; mod settings; diff --git a/packages/core/src/store/processes.rs b/packages/core/src/store/processes.rs new file mode 100644 index 00000000..4c493fef --- /dev/null +++ b/packages/core/src/store/processes.rs @@ -0,0 +1,276 @@ +use std::collections::HashMap; +use std::process::Stdio; +use std::sync::Arc; + +use chrono::{DateTime, Utc}; +use onelauncher_entity::setting_profiles; +use serde::Serialize; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::{Child, Command}; +use tokio::sync::RwLock; +use tokio::task::JoinHandle; +use uuid::Uuid; + +use crate::api::cluster; +use crate::api::game::log::censor_line; +use crate::api::processes::ProcessPayload; +use crate::api::proxy::event::{send_event, LauncherEvent}; +use crate::error::{DaoError, LauncherResult}; +use crate::send_error; +use crate::utils::io::IOError; + +use super::credentials::MinecraftCredentials; +use super::State; + +#[derive(Debug, thiserror::Error)] +pub enum ProcessError { + #[error("launch hook exited with non-zero code {0}")] + HookUnsuccessful(i32), + #[error("pid equaled nothing")] + NoPid, +} + +#[derive(Default)] +pub struct ProcessStore { + processes: RwLock>>>, +} + +#[derive(Debug, Serialize)] +pub struct Process { + pub pid: u32, + pub started_at: DateTime, + pub cluster_id: u64, + pub post_hook: Option, + pub account_id: Uuid, + #[serde(skip)] + pub child_type: Arc>, + #[serde(skip)] + process_loop: Option>>, +} + +#[derive(Debug, Serialize)] +pub enum ChildType { + Owned(#[serde(skip)] Child), +} + +impl Process { + #[must_use] + pub const fn is_running(&self) -> bool { + self.process_loop.is_some() + } +} + +impl ChildType { + #[must_use] + pub const fn new(child: Child) -> Self { + Self::Owned(child) + } + + #[must_use] + pub fn id(&self) -> u32 { + match self { + Self::Owned(child) => child.id().unwrap_or(0), + } + } + + // Attempts to return its exit code, if it has exited. + pub fn try_wait(&mut self) -> LauncherResult> { + Ok(match self { + Self::Owned(child) => child + .try_wait() + .map_err(IOError::from)? + .map(|s| s.code().unwrap_or_default()), + }) + } + + // async blocking to wait for the process to finish and return exit code + pub async fn wait(&mut self) -> LauncherResult { + Ok(match self { + Self::Owned(child) => child + .wait() + .await + .map(|s| s.code().unwrap_or_default()) + .map_err(IOError::from)?, + }) + } + + pub async fn kill(&mut self) -> LauncherResult<()> { + match self { + Self::Owned(child) => child.kill().await.map_err(IOError::from)?, + } + + Ok(()) + } +} + +impl ProcessStore { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + pub async fn spawn( + &self, + cluster_id: u64, + creds: MinecraftCredentials, + settings: &setting_profiles::Model, + mut command: Command, + ) -> LauncherResult>> { + command + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::null()); + + let mut child = command.spawn().map_err(IOError::from)?; + + let started_at = Utc::now(); + let Some(pid) = child.id() else { + return Err(ProcessError::NoPid.into()); + }; + + let stdout = child.stdout.take().expect("couldn't get stdout of child"); + let stderr = child.stderr.take().expect("couldn't get stderr of child"); + + #[cfg(target_os = "linux")] + { + if settings.os_extra.as_ref().is_some_and(|s| s.enable_gamemode.is_some_and(|x| x)) { + if let Err(err) = onelauncher_gamemode::request_start_for_wrapper(pid) { + tracing::warn!("failed to enable gamemode, continuing: {}", err); + } + } + } + + let account_id = creds.id; + tokio::spawn(async move { + let mut stdout = BufReader::new(stdout).lines(); + let mut stderr = BufReader::new(stderr).lines(); + + while let Ok(Some(line)) = tokio::select! { + line = stdout.next_line() => line, + line = stderr.next_line() => line, + } { + let censored = censor_line(&creds, line); + + send_event(LauncherEvent::Process(ProcessPayload::Output { + pid, + output: censored, + })).await; + } + }); + + let child_type = Arc::new(RwLock::new(ChildType::new(child))); + let process = Process { + pid, + started_at, + cluster_id, + post_hook: settings.hook_post.clone(), + account_id, + child_type: child_type.clone(), + process_loop: Some(tokio::task::spawn(Self::process_loop( + cluster_id, settings.hook_post.clone(), child_type, + ))), + }; + + let process = Arc::new(RwLock::new(process)); + let mut write_guard = self.processes.write().await; + write_guard.insert(pid, process.clone()); + tracing::info!("spawned process with pid {}", pid); + + Ok(process) + } + + pub async fn get(&self, pid: u32) -> Option>> { + let read_guard = self.processes.read().await; + read_guard.get(&pid).cloned() + } + + async fn process_loop( + cluster_id: u64, + post_hook: Option, + child: Arc>, + ) -> LauncherResult { + let pid = child.read().await.id(); + let mut last_updated = Utc::now(); + + let exit_code = loop { + let mut child = child.write().await; + if let Some(status) = child.try_wait()? { + tracing::info!("process exited with status: {:?}", status); + break status; + } + + // Every 60 seconds, update the playtime of the process + let duration = Utc::now().signed_duration_since(last_updated).num_seconds(); + if duration > 60 { + last_updated = Utc::now(); + + if let Err(err) = cluster::update_playtime(cluster_id, duration).await { + tracing::error!("failed to update playtime: {}", err); + } + } + + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + }; + + let state = State::get().await?; + let store = &state.processes; + + let mut write_guard = store.processes.write().await; + + write_guard.remove(&pid); + tracing::debug!("removed process with pid {}", pid); + + if let Some(post_hook) = post_hook { + if !post_hook.is_empty() { + let result: LauncherResult<()> = { + let cluster = cluster::dao::get_cluster_by_id(cluster_id) + .await? + .ok_or(DaoError::NotFound)?; + + if let Err(err) = run_hook(&post_hook, &cluster.path).await { + send_error!("{err:?}"); + } + + Ok(()) + }; + + if let Err(err) = result { + send_error!("failed to run post hook: {}", err); + } + } + } + + send_event(LauncherEvent::Process(ProcessPayload::Stopped { pid, exit_code })).await; + + Ok(exit_code) + } +} + +pub async fn run_hook(hook: &str, cwd: &str) -> LauncherResult> { + let mut split = hook.split(' '); + + let Some(command) = split.next() else { + return Ok(None); + }; + + let result = Command::new(command) + .args(split) + .current_dir(cwd) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(IOError::from)? + .wait() + .await + .map_err(IOError::from)?; + + let exit_code = result + .code() + .unwrap_or_else(|| i32::from(!result.success())); + + if result.success() { + Ok(Some(exit_code)) + } else { + Err(ProcessError::HookUnsuccessful(exit_code).into()) + } +} \ No newline at end of file diff --git a/packages/core/src/store/state.rs b/packages/core/src/store/state.rs index a4f4d754..9467f622 100644 --- a/packages/core/src/store/state.rs +++ b/packages/core/src/store/state.rs @@ -6,6 +6,7 @@ use tokio::sync::{OnceCell, RwLock}; use crate::LauncherResult; use super::credentials::CredentialsStore; +use super::processes::ProcessStore; use super::Settings; use super::discord::DiscordRPC; use super::ingress::IngressProcessor; @@ -20,6 +21,7 @@ pub struct State { pub db: DatabaseConnection, pub credentials: RwLock, pub metadata: RwLock, + pub processes: ProcessStore, pub rpc: Option, } @@ -44,6 +46,7 @@ impl State { let db = super::db::create_pool().await?; let credentials = CredentialsStore::initialize().await?; let metadata = Metadata::new(); + let processes = ProcessStore::new(); let rpc = match DiscordRPC::initialize() { Ok(rpc) => Some(rpc), Err(err) => { @@ -58,6 +61,7 @@ impl State { db, credentials: RwLock::new(credentials), metadata: RwLock::new(metadata), + processes, rpc, })) } diff --git a/packages/entity/Cargo.toml b/packages/entity/Cargo.toml index 0a435b67..12162a31 100644 --- a/packages/entity/Cargo.toml +++ b/packages/entity/Cargo.toml @@ -27,4 +27,5 @@ chrono = { workspace = true } uuid = { workspace = true } url = { workspace = true } interpulse = { workspace = true } +merge = { workspace = true } specta = { workspace = true, optional = true } diff --git a/packages/entity/src/domain/cluster_stage.rs b/packages/entity/src/domain/cluster_stage.rs index 920951b6..5d2228cc 100644 --- a/packages/entity/src/domain/cluster_stage.rs +++ b/packages/entity/src/domain/cluster_stage.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use sea_orm::{DeriveActiveEnum, EnumIter}; use serde::{Deserialize, Serialize}; @@ -11,4 +13,21 @@ pub enum ClusterStage { Downloading = 1, Repairing = 2, Ready = 3, -} \ No newline at end of file +} + +impl ClusterStage { + pub fn is_downloading(&self) -> bool { + matches!(self, Self::Downloading | Self::Repairing) + } +} + +impl Display for ClusterStage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + ClusterStage::NotReady => "Not Ready", + ClusterStage::Downloading => "Downloading", + ClusterStage::Repairing => "Repairing", + ClusterStage::Ready => "Ready", + }) + } +} diff --git a/packages/entity/src/domain/mod.rs b/packages/entity/src/domain/mod.rs index 4a77aa5e..c371a48e 100644 --- a/packages/entity/src/domain/mod.rs +++ b/packages/entity/src/domain/mod.rs @@ -1,3 +1,4 @@ pub mod cluster_stage; pub mod icon; -pub mod loader; \ No newline at end of file +pub mod loader; +pub mod settings; \ No newline at end of file diff --git a/packages/entity/src/domain/settings.rs b/packages/entity/src/domain/settings.rs new file mode 100644 index 00000000..72e20684 --- /dev/null +++ b/packages/entity/src/domain/settings.rs @@ -0,0 +1,84 @@ +use sea_orm::{sea_query::{self, Nullable, ValueType}, TryGetableFromJson}; +use serde::{Deserialize, Serialize}; + +cfg_match! { + target_os = "windows" => { + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct SettingsOsExtra { + + } + + impl Default for SettingsOsExtra { + fn default() -> Self { + Self { + + } + } + } + } + target_os = "macos" => { + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct SettingsOsExtra { + + } + + impl Default for SettingsOsExtra { + fn default() -> Self { + Self { + + } + } + } + } + not(any(target_os = "windows", target_os = "macos")) => { + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct SettingsOsExtra { + pub enable_gamemode: Option, + } + + impl Default for SettingsOsExtra { + fn default() -> Self { + Self { + enable_gamemode: Some(true), + } + } + } + } +} + +impl From for sea_query::Value { + fn from(value: SettingsOsExtra) -> Self { + sea_query::Value::Json(serde_json::to_value(value).ok().map(Box::new)) + } +} + +impl TryGetableFromJson for SettingsOsExtra {} + +impl ValueType for SettingsOsExtra { + fn try_from(v: sea_orm::Value) -> Result { + match v { + sea_orm::Value::Json(Some(json)) => Ok( + serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?, + ), + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + "SettingsOsExtra".to_string() + } + + fn array_type() -> sea_query::ArrayType { + sea_query::ArrayType::Json + } + + fn column_type() -> sea_orm::ColumnType { + sea_orm::ColumnType::Json + } +} + +impl Nullable for SettingsOsExtra { + fn null() -> sea_orm::Value { + sea_orm::Value::Json(None) + } +} \ No newline at end of file diff --git a/packages/entity/src/entities/cluster_groups.rs b/packages/entity/src/entities/cluster_groups.rs index 88fdc8fe..5d1b8238 100644 --- a/packages/entity/src/entities/cluster_groups.rs +++ b/packages/entity/src/entities/cluster_groups.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[onelauncher_macro::specta] pub struct Model { #[sea_orm(primary_key)] - pub id: i32, + pub id: u64, #[sea_orm(column_type = "Text")] pub name: String, } diff --git a/packages/entity/src/entities/cluster_packages.rs b/packages/entity/src/entities/cluster_packages.rs index be0652ac..11435101 100644 --- a/packages/entity/src/entities/cluster_packages.rs +++ b/packages/entity/src/entities/cluster_packages.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[onelauncher_macro::specta] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub cluster_id: i32, + pub cluster_id: u64, #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] pub package_hash: String, } diff --git a/packages/entity/src/entities/clusters.rs b/packages/entity/src/entities/clusters.rs index 18ccdd65..ddf84a08 100644 --- a/packages/entity/src/entities/clusters.rs +++ b/packages/entity/src/entities/clusters.rs @@ -8,13 +8,12 @@ use crate::{cluster_stage::ClusterStage, icon::Icon, loader::GameLoader}; #[onelauncher_macro::specta] pub struct Model { #[sea_orm(primary_key)] - pub id: i32, + pub id: u64, #[sea_orm(column_type = "Text")] pub path: String, pub stage: ClusterStage, - pub created_at: i32, - pub updated_at: i32, - pub group_id: Option, + pub created_at: i64, + pub group_id: Option, #[sea_orm(column_type = "Text")] pub name: String, #[sea_orm(column_type = "Text")] @@ -22,15 +21,15 @@ pub struct Model { pub mc_loader: GameLoader, #[sea_orm(column_type = "Text", nullable)] pub mc_loader_version: Option, - pub last_played: Option, - pub overall_played: Option, + pub last_played: Option, + pub overall_played: Option, #[sea_orm(column_type = "Text", nullable)] pub icon_url: Option, #[sea_orm(column_type = "Text", nullable)] pub setting_profile_name: Option, #[sea_orm(column_type = "Text", nullable)] pub linked_pack_id: Option, - pub linked_pack_version: Option, + pub linked_pack_version: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/entity/src/entities/java_versions.rs b/packages/entity/src/entities/java_versions.rs index e9856b52..2a9fcffb 100644 --- a/packages/entity/src/entities/java_versions.rs +++ b/packages/entity/src/entities/java_versions.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; #[onelauncher_macro::specta] pub struct Model { #[sea_orm(primary_key)] - pub id: i32, - pub major_version: i32, + pub id: u64, + pub major_version: u32, #[sea_orm(column_type = "Text")] pub vendor_name: String, #[sea_orm(column_type = "Text")] diff --git a/packages/entity/src/entities/packages.rs b/packages/entity/src/entities/packages.rs index 48941707..84a89d19 100644 --- a/packages/entity/src/entities/packages.rs +++ b/packages/entity/src/entities/packages.rs @@ -17,8 +17,8 @@ pub struct Model { pub display_name: String, #[sea_orm(column_type = "Text")] pub display_version: String, - pub type_id: i32, - pub provider_id: i32, + pub type_id: u32, + pub provider_id: u32, #[sea_orm(column_type = "Text")] pub provider_version: String, #[sea_orm(column_type = "Text")] diff --git a/packages/entity/src/entities/prelude.rs b/packages/entity/src/entities/prelude.rs index 34bac74e..258f4dfe 100644 --- a/packages/entity/src/entities/prelude.rs +++ b/packages/entity/src/entities/prelude.rs @@ -1,8 +1,26 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5 -pub use super::cluster_groups::Entity as ClusterGroups; -pub use super::cluster_packages::Entity as ClusterPackages; -pub use super::clusters::Entity as Clusters; -pub use super::java_versions::Entity as JavaVersions; -pub use super::packages::Entity as Packages; -pub use super::setting_profiles::Entity as SettingProfiles; +pub mod entity { + pub use crate::cluster_groups::Entity as ClusterGroups; + pub use crate::cluster_packages::Entity as ClusterPackages; + pub use crate::clusters::Entity as Clusters; + pub use crate::java_versions::Entity as JavaVersions; + pub use crate::packages::Entity as Packages; + pub use crate::setting_profiles::Entity as SettingProfiles; +} + +pub mod model { + pub use crate::cluster_groups::Model as ClusterGroup; + pub use crate::cluster_packages::Model as ClusterPackage; + pub use crate::clusters::Model as Cluster; + pub use crate::java_versions::Model as JavaVersion; + pub use crate::packages::Model as Package; + pub use crate::setting_profiles::Model as SettingsProfile; + + pub use crate::cluster_groups::ActiveModel as ClusterGroupPartial; + pub use crate::cluster_packages::ActiveModel as ClusterPackagePartial; + pub use crate::clusters::ActiveModel as ClusterPartial; + pub use crate::java_versions::ActiveModel as JavaVersionPartial; + pub use crate::packages::ActiveModel as PackagePartial; + pub use crate::setting_profiles::ActiveModel as SettingsProfilePartial; +} diff --git a/packages/entity/src/entities/setting_profiles.rs b/packages/entity/src/entities/setting_profiles.rs index c68b9e5b..9a38db8a 100644 --- a/packages/entity/src/entities/setting_profiles.rs +++ b/packages/entity/src/entities/setting_profiles.rs @@ -1,19 +1,24 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5 +use merge::Merge; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +use crate::settings::SettingsOsExtra; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, Merge)] #[sea_orm(table_name = "setting_profiles")] #[onelauncher_macro::specta] +#[merge(strategy = merge::option::overwrite_none)] pub struct Model { #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] + #[merge(skip)] pub name: String, - pub java_id: Option, - pub res_w: Option, - pub res_h: Option, + pub java_id: Option, + pub res_w: Option, + pub res_h: Option, pub force_fullscreen: Option, - pub mem_max: Option, + pub mem_max: Option, #[sea_orm(column_type = "Text", nullable)] pub launch_args: Option, #[sea_orm(column_type = "Text", nullable)] @@ -24,6 +29,8 @@ pub struct Model { pub hook_wrapper: Option, #[sea_orm(column_type = "Text", nullable)] pub hook_post: Option, + #[sea_orm(column_type = "Text", nullable)] + pub os_extra: Option } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -80,6 +87,7 @@ impl Model { mem_max: Some(3072), res_h: None, res_w: None, + os_extra: Some(SettingsOsExtra::default()) } } } diff --git a/packages/entity/src/lib.rs b/packages/entity/src/lib.rs index 6c2d2707..46eabd8d 100644 --- a/packages/entity/src/lib.rs +++ b/packages/entity/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(cfg_match)] + mod entities; mod domain; diff --git a/packages/macro/src/lib.rs b/packages/macro/src/lib.rs index e30805fd..cad7159f 100644 --- a/packages/macro/src/lib.rs +++ b/packages/macro/src/lib.rs @@ -3,11 +3,20 @@ mod pin; /// This macro is used to derive the `specta::Type` trait for the annotated item. /// It's mainly meant for entity-generation with sea-orm-cli (it doesn't like commas in attributes) -///
-/// Expands to: +/// +/// #### Expands to: /// ``` /// #[cfg_attr(feature = "specta", derive(specta::Type))] /// ``` +/// +/// ## `with_event` argument +/// Add `#[specta(with_event)]` to add the `tauri_specta::Event` derive. +/// +/// #### Expands to: +/// ``` +/// #[cfg_attr(feature = "specta", derive(specta::Type))] +/// #[cfg_attr(feature = "tauri", derive(tauri_specta::Event))] +/// ``` #[proc_macro_attribute] pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { specta::specta(attr, item) diff --git a/packages/migration/src/init.sql b/packages/migration/src/init.sql index 130e8ef9..98753847 100644 --- a/packages/migration/src/init.sql +++ b/packages/migration/src/init.sql @@ -42,6 +42,7 @@ create table setting_profiles ( hook_pre text, hook_wrapper text, hook_post text, + os_extra json_text, constraint setting_profiles_pk primary key (name), constraint setting_profiles_java_versions_id_fk foreign key (java_id) references java_versions(id) ); @@ -51,7 +52,6 @@ create table clusters ( path text not null, stage integer default (0) not null, created_at integer default (unixepoch()) not null, - updated_at integer default (unixepoch()) not null, group_id integer, name text not null, mc_version text not null, @@ -62,7 +62,7 @@ create table clusters ( icon_url text, setting_profile_name text, linked_pack_id text, - linked_pack_version integer, + linked_pack_version text, constraint clusters_pk primary key (id autoincrement), constraint clusters_cluster_groups_id_fk foreign key (group_id) references cluster_groups(id), constraint clusters_setting_profiles_name_fk foreign key (setting_profile_name) references setting_profiles(name)