feat: process management + os specific settings + proxied events + launch half done

This commit is contained in:
LynithDev 2025-04-17 20:14:25 +02:00
parent a8eb841bd7
commit 71d584e5a3
44 changed files with 1001 additions and 315 deletions

24
Cargo.lock generated
View file

@ -2542,6 +2542,28 @@ dependencies = [
"autocfg", "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]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -3020,6 +3042,7 @@ dependencies = [
"indicatif", "indicatif",
"interpulse", "interpulse",
"md-5", "md-5",
"merge",
"murmur2", "murmur2",
"onelauncher_entity", "onelauncher_entity",
"onelauncher_gamemode", "onelauncher_gamemode",
@ -3063,6 +3086,7 @@ version = "0.2.0-alpha.2"
dependencies = [ dependencies = [
"chrono", "chrono",
"interpulse", "interpulse",
"merge",
"onelauncher_macro", "onelauncher_macro",
"sea-orm", "sea-orm",
"serde", "serde",

View file

@ -149,6 +149,7 @@ async-trait = { version = "0.1" }
whoami = { version = "1.5" } whoami = { version = "1.5" }
cc = { version = "1.1" } cc = { version = "1.1" }
libc = { version = "0.2" } libc = { version = "0.2" }
merge = { version = "0.2" }
# macro # macro
syn = { version = "2" } syn = { version = "2" }

View file

@ -67,6 +67,7 @@ url = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
paste = { workspace = true } paste = { workspace = true }
byteorder = { workspace = true } byteorder = { workspace = true }
merge = { workspace = true }
# integration # integration
discord-rich-presence = { workspace = true } discord-rich-presence = { workspace = true }

View file

@ -12,7 +12,7 @@ async fn main() -> LauncherResult<()> {
} }
println!("creating cluster"); println!("creating cluster");
let cluster = api::cluster::create_cluster( let cluster = &mut api::cluster::create_cluster(
"Test Cluster", "Test Cluster",
"1.8.9", "1.8.9",
GameLoader::Vanilla, GameLoader::Vanilla,
@ -23,7 +23,7 @@ async fn main() -> LauncherResult<()> {
println!("cluster created: {cluster:#?}"); println!("cluster created: {cluster:#?}");
println!("preparing cluster for launch"); println!("preparing cluster for launch");
api::cluster::prepare_cluster(&cluster).await?; api::cluster::prepare_cluster(cluster).await?;
Ok(()) Ok(())
} }

View file

@ -34,7 +34,7 @@ pub async fn insert_cluster(
/// Updates an existing cluster in the database. /// Updates an existing cluster in the database.
pub async fn update_cluster_by_id<B>( pub async fn update_cluster_by_id<B>(
id: i32, id: u64,
block: B, block: B,
) -> LauncherResult<clusters::Model> ) -> LauncherResult<clusters::Model>
where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel> { where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel> {
@ -48,8 +48,25 @@ where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveMo
Ok(model) Ok(model)
} }
pub async fn update_cluster<B>(
cluster: &mut clusters::Model,
block: B,
) -> LauncherResult<&mut clusters::Model>
where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel> {
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**. /// 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 state = State::get().await?;
let db = &state.db; 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. /// Gets a cluster by its ID from the database.
pub async fn get_cluster_by_id(id: i32) -> LauncherResult<Option<clusters::Model>> { pub async fn get_cluster_by_id(id: u64) -> LauncherResult<Option<clusters::Model>> {
let state = State::get().await?; let state = State::get().await?;
let db = &state.db; let db = &state.db;

View file

@ -1,3 +1,4 @@
use chrono::Utc;
use interpulse::api::minecraft::VersionInfo; use interpulse::api::minecraft::VersionInfo;
use onelauncher_entity::cluster_stage::ClusterStage; use onelauncher_entity::cluster_stage::ClusterStage;
use onelauncher_entity::{clusters, java_versions}; use onelauncher_entity::{clusters, java_versions};
@ -26,6 +27,10 @@ use super::ingress::init_ingress;
pub enum ClusterError { pub enum ClusterError {
#[error("version '{0}' was not found")] #[error("version '{0}' was not found")]
InvalidVersion(String), InvalidVersion(String),
#[error("failed to imply java version")]
MissingJavaVersion,
#[error("cluster is in downloading stage")]
ClusterDownloading,
} }
#[must_use] #[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) /// 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_TOTAL: f64 = 100.0;
const INGRESS_TASKS: f64 = 4.0; const INGRESS_TASKS: f64 = 4.0;
const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS; 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 { if cluster.stage == ClusterStage::Ready {
tracing::info!("cluster is already ready"); tracing::info!("cluster is already ready");
return Ok(()); return Ok(cluster);
} }
let ingress_id = init_ingress( let ingress_id = init_ingress(
@ -118,7 +123,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
) )
.await?; .await?;
dao::update_cluster_by_id(cluster.id, async |mut cluster| { dao::update_cluster(cluster, async |mut cluster| {
cluster.stage = Set(ClusterStage::Downloading); cluster.stage = Set(ClusterStage::Downloading);
Ok(cluster) Ok(cluster)
}) })
@ -128,7 +133,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
// TASK 1 // TASK 1
ingress_id.set_ingress_message("fetching data").await?; ingress_id.set_ingress_message("fetching data").await?;
let setting_profile = 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 state = State::get().await?;
let mut metadata = state.metadata.write().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? { let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? {
version version
} else { } else {
// TODO: java runtime not found - should prob download let Some(ver) = &version_info.java_version else {
let java = java::locate_java().await?; return Err(ClusterError::MissingJavaVersion.into());
};
if java.is_empty() { java::prepare_java(ver.major_version).await?
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")
})?
}; };
// TASK 3 // TASK 3
@ -189,7 +183,7 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
if let Err(err) = result { if let Err(err) = result {
tracing::error!("failed to prepare cluster: {}", err); 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); cluster.stage = Set(ClusterStage::NotReady);
Ok(cluster) Ok(cluster)
}) })
@ -197,14 +191,14 @@ pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> {
return Err(err); 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); cluster.stage = Set(ClusterStage::Ready);
Ok(cluster) Ok(cluster)
}) })
.await?; .await?;
tracing::debug!("cluster is ready"); tracing::debug!("cluster is ready");
Ok(()) Ok(cluster)
} }
/// Run forge processors /// Run forge processors
@ -308,3 +302,14 @@ async fn run_forge_processors(
Ok(()) Ok(())
} }
pub async fn update_playtime(id: u64, duration: i64) -> LauncherResult<clusters::Model> {
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
}

View file

@ -26,7 +26,7 @@ impl SyncAction {
/// Syncs all clusters in the database. Returns a list of cluster IDs that are missing. /// Syncs all clusters in the database. Returns a list of cluster IDs that are missing.
#[tracing::instrument] #[tracing::instrument]
pub async fn sync_clusters() -> LauncherResult<Vec<i32>> { pub async fn sync_clusters() -> LauncherResult<Vec<u64>> {
let clusters = dao::get_all_clusters().await?; let clusters = dao::get_all_clusters().await?;
let mut missing_ids = Vec::new(); let mut missing_ids = Vec::new();
@ -92,7 +92,7 @@ pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult<SyncActio
/// Syncs the cluster with the database. /// Syncs the cluster with the database.
/// Checks if the cluster in the database exists and if the directory exists. /// Checks if the cluster in the database exists and if the directory exists.
#[tracing::instrument] #[tracing::instrument]
pub async fn sync_cluster_by_id(id: i32) -> LauncherResult<SyncAction> { pub async fn sync_cluster_by_id(id: u64) -> LauncherResult<SyncAction> {
let cluster = dao::get_cluster_by_id(id) let cluster = dao::get_cluster_by_id(id)
.await? .await?
.ok_or(DaoError::NotFound)?; .ok_or(DaoError::NotFound)?;

View file

@ -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<Arc<RwLock<Process>>> {
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!()
}

View file

@ -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
}

View file

@ -1,4 +1,5 @@
pub mod metadata;
pub mod launch;
pub mod arguments; pub mod arguments;
pub mod rules; pub mod launch;
pub mod log;
pub mod metadata;
pub mod rules;

View file

@ -1,7 +1,20 @@
use serde::Serialize;
use uuid::Uuid;
use crate::LauncherResult; use crate::LauncherResult;
use crate::store::State; use crate::store::State;
use crate::store::ingress::{IngressId, IngressType, SubIngress}; 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<f64>,
pub total: f64,
}
#[tracing::instrument] #[tracing::instrument]
pub async fn init_ingress( pub async fn init_ingress(
ingress_type: IngressType, ingress_type: IngressType,

View file

@ -20,13 +20,13 @@ pub async fn get_latest_java() -> LauncherResult<Option<java_versions::Model>> {
/// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`] /// 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<java_versions::ActiveModel> { pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult<java_versions::ActiveModel> {
let major_version = if info.java_version[0..2].eq("1.") { let major_version: u32 = if info.java_version[0..2].eq("1.") {
info.java_version[2..3].parse::<i32>() info.java_version[2..3].parse()
} else { } else {
let split = info.java_version.split_once('.').unwrap_or_default(); let split = info.java_version.split_once('.').unwrap_or_default();
split.0.parse::<i32>() split.0.parse()
}.or_else( }.or_else(
|_| info.java_version.parse::<i32>() |_| info.java_version.parse()
).map_err(|e| JavaError::ParseVersion(info.java_version.clone(), e))?; ).map_err(|e| JavaError::ParseVersion(info.java_version.clone(), e))?;
Ok(java_versions::ActiveModel { Ok(java_versions::ActiveModel {
@ -50,7 +50,7 @@ pub async fn get_java_all() -> LauncherResult<Vec<java_versions::Model>> {
} }
/// Returns the specific Java version by ID /// Returns the specific Java version by ID
pub async fn get_java_by_id(id: i32) -> LauncherResult<Option<java_versions::Model>> { pub async fn get_java_by_id(id: u64) -> LauncherResult<Option<java_versions::Model>> {
let db = &State::get().await?.db; let db = &State::get().await?.db;
Ok(java_versions::Entity::find() Ok(java_versions::Entity::find()
@ -60,7 +60,7 @@ pub async fn get_java_by_id(id: i32) -> LauncherResult<Option<java_versions::Mod
} }
/// Returns all versions of Java for a given major version /// Returns all versions of Java for a given major version
pub async fn get_all_java_by_major(major: i32) -> LauncherResult<Vec<java_versions::Model>> { pub async fn get_all_java_by_major(major: u32) -> LauncherResult<Vec<java_versions::Model>> {
let db = &State::get().await?.db; let db = &State::get().await?.db;
Ok(java_versions::Entity::find() Ok(java_versions::Entity::find()
@ -71,7 +71,7 @@ pub async fn get_all_java_by_major(major: i32) -> LauncherResult<Vec<java_versio
} }
/// Returns the latest Java version for a given major version /// Returns the latest Java version for a given major version
pub async fn get_latest_java_by_major(major: i32) -> LauncherResult<Option<java_versions::Model>> { pub async fn get_latest_java_by_major(major: u32) -> LauncherResult<Option<java_versions::Model>> {
let db = &State::get().await?.db; let db = &State::get().await?.db;
Ok(java_versions::Entity::find() Ok(java_versions::Entity::find()

View file

@ -18,6 +18,8 @@ pub enum JavaError {
ParseVersion(String, #[source] std::num::ParseIntError), ParseVersion(String, #[source] std::num::ParseIntError),
#[error("failed to execute java command")] #[error("failed to execute java command")]
Execute(#[from] std::io::Error), Execute(#[from] std::io::Error),
#[error("no java installations found")]
MissingJava,
} }
/// Returns the relative path to the Java executable /// Returns the relative path to the Java executable
@ -133,8 +135,43 @@ pub async fn get_recommended_java(
return Ok(None); return Ok(None);
}; };
#[allow(clippy::cast_possible_wrap)] dao::get_latest_java_by_major(supported_ver.major_version).await
dao::get_latest_java_by_major(supported_ver.major_version as i32).await }
pub async fn prepare_java(
major: u32,
) -> LauncherResult<java_versions::Model> {
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. /// Attempts to scan common paths for Java installations.

View file

@ -4,4 +4,5 @@ pub mod ingress;
pub mod game; pub mod game;
pub mod proxy; pub mod proxy;
pub mod java; pub mod java;
pub mod processes;
pub mod setting_profiles; pub mod setting_profiles;

View file

@ -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,
},
}

View file

@ -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
}

View file

@ -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;

View file

@ -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<HashMap<Uuid, ProgressBar>>,
}
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(())
}
}

View file

@ -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(())
}
}

View file

@ -2,9 +2,7 @@ use tauri::{AppHandle, Emitter, Manager};
use tauri_specta::Event; use tauri_specta::Event;
use tracing::{error, warn}; use tracing::{error, warn};
use crate::{store::ingress::IngressPayload, LauncherResult}; use crate::{constants::CLI_TOTAL_INGRESS, api::proxy::{event::LauncherEvent, LauncherProxy, message::MessageLevel}, LauncherResult};
use super::{message::{MessageLevel, MessagePayload}, LauncherProxy};
#[derive(Debug)] #[derive(Debug)]
pub struct ProxyTauri { pub struct ProxyTauri {
@ -22,21 +20,20 @@ impl ProxyTauri {
#[async_trait::async_trait] #[async_trait::async_trait]
impl LauncherProxy for ProxyTauri { impl LauncherProxy for ProxyTauri {
async fn send_ingress(&self, ingress: IngressPayload) -> LauncherResult<()> { async fn send_event(&self, event: LauncherEvent) -> LauncherResult<()> {
self.handle match event {
.emit(IngressPayload::NAME, ingress) LauncherEvent::Message(message) => {
.map_err(Into::into) match message.level {
} MessageLevel::Info => {},
MessageLevel::Warning => warn!("{}", message.message),
async fn send_message(&self, message: MessagePayload) -> LauncherResult<()> { MessageLevel::Error => error!("{}", message.message),
match message.level { };
MessageLevel::Info => {}, },
MessageLevel::Warning => warn!("{}", message.message), _ => {}
MessageLevel::Error => error!("{}", message.message), }
};
self.handle self.handle
.emit(MessagePayload::NAME, message) .emit_all(LauncherEvent::NAME, event)
.map_err(Into::into) .map_err(Into::into)
} }

View file

@ -1,5 +1,7 @@
use serde::Serialize; use serde::Serialize;
use super::event::send_event;
#[onelauncher_macro::specta(with_event)] #[onelauncher_macro::specta(with_event)]
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct MessagePayload { pub struct MessagePayload {
@ -11,48 +13,57 @@ pub struct MessagePayload {
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub enum MessageLevel { pub enum MessageLevel {
Info, Info,
Warning, Warn,
Error, 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_export]
macro_rules! send_warning { macro_rules! send_warning {
($($message:tt)*) => {{ ($($message:tt)*) => {
tokio::spawn(async move { $crate::api::proxy::message::send_message(
let _ = match $crate::store::proxy::ProxyState::get() { $crate::api::proxy::message::MessageLevel::Warn,
Ok(proxy) => { format!($($message)*),
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);
},
};
})
}};
} }
#[macro_export] #[macro_export]
macro_rules! send_error { macro_rules! send_error {
($($message:tt)*) => {{ ($($message:tt)*) => {
tokio::spawn(async move { $crate::api::proxy::message::send_message(
let _ = match $crate::store::proxy::ProxyState::get() { $crate::api::proxy::message::MessageLevel::Error,
Ok(proxy) => { format!($($message)*),
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);
},
};
})
}};
} }

View file

@ -1,22 +1,12 @@
mod r#impl;
pub mod message; pub mod message;
pub mod event;
#[cfg(feature = "tauri")] mod proxy_tauri; pub use r#impl::*;
#[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;
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait LauncherProxy: Send + Sync + Debug { pub trait LauncherProxy: Send + Sync + std::fmt::Debug {
async fn send_ingress(&self, ingress: crate::store::ingress::IngressPayload) -> crate::LauncherResult<()>; async fn send_event(&self, event: event::LauncherEvent) -> crate::LauncherResult<()>;
async fn send_message(&self, message: message::MessagePayload) -> crate::LauncherResult<()>;
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
fn hide_main_window(&self) -> crate::LauncherResult<()>; fn hide_main_window(&self) -> crate::LauncherResult<()>;
@ -24,3 +14,6 @@ pub trait LauncherProxy: Send + Sync + Debug {
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
fn show_main_window(&self) -> crate::LauncherResult<()>; fn show_main_window(&self) -> crate::LauncherResult<()>;
} }
#[cfg(feature = "cli")] pub type ProxyDynamic = ProxyCli;
#[cfg(not(feature = "cli"))] pub type ProxyDynamic = ProxyEmpty;

View file

@ -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<HashMap<Uuid, ProgressBar>>,
}
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(())
}
}

View file

@ -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(())
}
}

View file

@ -1,60 +1,75 @@
use onelauncher_entity::setting_profiles; 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<Vec<setting_profiles::Model>> { pub mod dao {
let state = State::get().await?; use onelauncher_entity::setting_profiles;
let db = &state.db; use sea_orm::{ActiveModelTrait, EntityTrait};
Ok(setting_profiles::Entity::find() use crate::{error::{DaoError, LauncherResult}, store::State};
.all(db)
.await?)
}
pub async fn get_profile_or_default(name: Option<&String>) -> LauncherResult<setting_profiles::Model> { pub async fn get_all_profiles() -> LauncherResult<Vec<setting_profiles::Model>> {
if let Some(name) = name { let state = State::get().await?;
let profile = get_profile_by_name(name).await?; let db = &state.db;
if let Some(profile) = profile {
return Ok(profile); Ok(setting_profiles::Entity::find()
.all(db)
.await?)
}
pub async fn get_profile_or_default(name: Option<&String>) -> LauncherResult<setting_profiles::Model> {
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?; pub async fn get_profile_by_name(name: &str) -> LauncherResult<Option<setting_profiles::Model>> {
let settings = &state.settings.read().await; let state = State::get().await?;
Ok(settings.global_game_settings.clone()) let db = &state.db;
}
pub async fn get_profile_by_name(name: &str) -> LauncherResult<Option<setting_profiles::Model>> { Ok(setting_profiles::Entity::find_by_id(name)
let state = State::get().await?; .one(db)
let db = &state.db; .await?)
}
Ok(setting_profiles::Entity::find_by_id(name) pub async fn insert_profile(profile: setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::Model> {
.one(db) if let Some(name) = profile.name.try_as_ref() {
.await?) 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<setting_profiles::Model> {
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?; pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> {
let db = &state.db; 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<()> { pub async fn get_global_profile() -> setting_profiles::Model {
let state = State::get().await?; let Ok(state) = State::get().await else {
let db = &state.db; return setting_profiles::Model::default_global_profile();
};
let deleted = setting_profiles::Entity::delete_by_id(name).exec(db).await?; let settings = state.settings.read().await;
settings.global_game_settings.clone()
if deleted.rows_affected == 0 { }
return Err(DaoError::NotFound.into());
}
Ok(())
}

View file

@ -19,6 +19,8 @@ pub enum LauncherError {
#[error(transparent)] #[error(transparent)]
MinecraftAuthError(#[from] crate::store::credentials::MinecraftAuthError), MinecraftAuthError(#[from] crate::store::credentials::MinecraftAuthError),
#[error(transparent)] #[error(transparent)]
ProcessError(#[from] crate::store::processes::ProcessError),
#[error(transparent)]
DaoError(#[from] DaoError), DaoError(#[from] DaoError),
#[error(transparent)] #[error(transparent)]

View file

@ -4,10 +4,11 @@ use serde::Serialize;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use uuid::Uuid; use uuid::Uuid;
use crate::api::proxy::event::try_send_event;
use crate::LauncherResult; use crate::LauncherResult;
use crate::api::ingress::IngressPayload;
use super::State; use super::State;
use super::proxy::ProxyState;
#[derive(Default)] #[derive(Default)]
pub struct IngressProcessor { pub struct IngressProcessor {
@ -73,21 +74,20 @@ impl IngressProcessor {
let uuid = &id.0; let uuid = &id.0;
let ingress = feeds.get_mut(uuid).ok_or(IngressError::NotFound)?; let ingress = feeds.get_mut(uuid).ok_or(IngressError::NotFound)?;
let proxy = ProxyState::get()?;
ingress.current += increment; ingress.current += increment;
if let Some(message) = message { if let Some(message) = message {
ingress.message = message; ingress.message = message;
} }
proxy let payload = IngressPayload {
.send_ingress(IngressPayload { id: uuid.to_owned(),
id: uuid.to_owned(), message: ingress.message.clone(),
message: ingress.message.clone(), ingress_type: ingress.ingress_type.clone(),
ingress_type: ingress.ingress_type.clone(), percent: Some(ingress.current / ingress.total),
percent: Some(ingress.current / ingress.total), total: ingress.total,
total: ingress.total, };
})
try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload))
.await?; .await?;
Ok(()) Ok(())
@ -113,20 +113,11 @@ pub struct Ingress {
pub last_sent: f64, 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<f64>,
pub total: f64,
}
#[onelauncher_macro::specta] #[onelauncher_macro::specta]
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub enum IngressType { pub enum IngressType {
Download { file_name: String }, Download { file_name: String },
JavaPrepare,
JavaCheck, JavaCheck,
JavaLocate, JavaLocate,
MinecraftDownload, MinecraftDownload,
@ -182,7 +173,8 @@ impl SubIngressExt for SubIngress<'_> {
fn ingress_sub<F>(&self, total: F) -> Self::Target<'_> fn ingress_sub<F>(&self, total: F) -> Self::Target<'_>
where where
F: FnOnce(f64) -> f64 { F: FnOnce(f64) -> f64,
{
SubIngress::from_sub(self, total(self.total)) SubIngress::from_sub(self, total(self.total))
} }
} }
@ -201,7 +193,8 @@ impl SubIngressExt for Option<&SubIngress<'_>> {
fn ingress_sub<F>(&self, total: F) -> Self::Target<'_> fn ingress_sub<F>(&self, total: F) -> Self::Target<'_>
where where
F: FnOnce(f64) -> f64 { F: FnOnce(f64) -> f64,
{
self.map(|sub| SubIngress::from_sub(sub, total(sub.total))) 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 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 try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload))
.send_ingress(IngressPayload {
id: ingress.id,
message: "Completed".into(),
ingress_type: ingress.ingress_type,
percent: None,
total: ingress.total,
})
.await?; .await?;
tracing::trace!( tracing::trace!(

View file

@ -4,7 +4,8 @@ pub mod ingress;
pub mod proxy; pub mod proxy;
pub mod semaphore; pub mod semaphore;
pub mod metadata; pub mod metadata;
pub mod credentials; pub mod credentials;
pub mod processes;
mod core_options; mod core_options;
mod dirs; mod dirs;
mod settings; mod settings;

View file

@ -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<HashMap<u32, Arc<RwLock<Process>>>>,
}
#[derive(Debug, Serialize)]
pub struct Process {
pub pid: u32,
pub started_at: DateTime<Utc>,
pub cluster_id: u64,
pub post_hook: Option<String>,
pub account_id: Uuid,
#[serde(skip)]
pub child_type: Arc<RwLock<ChildType>>,
#[serde(skip)]
process_loop: Option<JoinHandle<LauncherResult<i32>>>,
}
#[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<Option<i32>> {
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<i32> {
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<Arc<RwLock<Process>>> {
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<Arc<RwLock<Process>>> {
let read_guard = self.processes.read().await;
read_guard.get(&pid).cloned()
}
async fn process_loop(
cluster_id: u64,
post_hook: Option<String>,
child: Arc<RwLock<ChildType>>,
) -> LauncherResult<i32> {
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<Option<i32>> {
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())
}
}

View file

@ -6,6 +6,7 @@ use tokio::sync::{OnceCell, RwLock};
use crate::LauncherResult; use crate::LauncherResult;
use super::credentials::CredentialsStore; use super::credentials::CredentialsStore;
use super::processes::ProcessStore;
use super::Settings; use super::Settings;
use super::discord::DiscordRPC; use super::discord::DiscordRPC;
use super::ingress::IngressProcessor; use super::ingress::IngressProcessor;
@ -20,6 +21,7 @@ pub struct State {
pub db: DatabaseConnection, pub db: DatabaseConnection,
pub credentials: RwLock<CredentialsStore>, pub credentials: RwLock<CredentialsStore>,
pub metadata: RwLock<Metadata>, pub metadata: RwLock<Metadata>,
pub processes: ProcessStore,
pub rpc: Option<DiscordRPC>, pub rpc: Option<DiscordRPC>,
} }
@ -44,6 +46,7 @@ impl State {
let db = super::db::create_pool().await?; let db = super::db::create_pool().await?;
let credentials = CredentialsStore::initialize().await?; let credentials = CredentialsStore::initialize().await?;
let metadata = Metadata::new(); let metadata = Metadata::new();
let processes = ProcessStore::new();
let rpc = match DiscordRPC::initialize() { let rpc = match DiscordRPC::initialize() {
Ok(rpc) => Some(rpc), Ok(rpc) => Some(rpc),
Err(err) => { Err(err) => {
@ -58,6 +61,7 @@ impl State {
db, db,
credentials: RwLock::new(credentials), credentials: RwLock::new(credentials),
metadata: RwLock::new(metadata), metadata: RwLock::new(metadata),
processes,
rpc, rpc,
})) }))
} }

View file

@ -27,4 +27,5 @@ chrono = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
url = { workspace = true } url = { workspace = true }
interpulse = { workspace = true } interpulse = { workspace = true }
merge = { workspace = true }
specta = { workspace = true, optional = true } specta = { workspace = true, optional = true }

View file

@ -1,3 +1,5 @@
use std::fmt::Display;
use sea_orm::{DeriveActiveEnum, EnumIter}; use sea_orm::{DeriveActiveEnum, EnumIter};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -11,4 +13,21 @@ pub enum ClusterStage {
Downloading = 1, Downloading = 1,
Repairing = 2, Repairing = 2,
Ready = 3, Ready = 3,
} }
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",
})
}
}

View file

@ -1,3 +1,4 @@
pub mod cluster_stage; pub mod cluster_stage;
pub mod icon; pub mod icon;
pub mod loader; pub mod loader;
pub mod settings;

View file

@ -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<bool>,
}
impl Default for SettingsOsExtra {
fn default() -> Self {
Self {
enable_gamemode: Some(true),
}
}
}
}
}
impl From<SettingsOsExtra> 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<Self, sea_query::ValueTypeErr> {
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)
}
}

View file

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta] #[onelauncher_macro::specta]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i32, pub id: u64,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
pub name: String, pub name: String,
} }

View file

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta] #[onelauncher_macro::specta]
pub struct Model { pub struct Model {
#[sea_orm(primary_key, auto_increment = false)] #[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")] #[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
pub package_hash: String, pub package_hash: String,
} }

View file

@ -8,13 +8,12 @@ use crate::{cluster_stage::ClusterStage, icon::Icon, loader::GameLoader};
#[onelauncher_macro::specta] #[onelauncher_macro::specta]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i32, pub id: u64,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
pub path: String, pub path: String,
pub stage: ClusterStage, pub stage: ClusterStage,
pub created_at: i32, pub created_at: i64,
pub updated_at: i32, pub group_id: Option<u64>,
pub group_id: Option<i32>,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
pub name: String, pub name: String,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
@ -22,15 +21,15 @@ pub struct Model {
pub mc_loader: GameLoader, pub mc_loader: GameLoader,
#[sea_orm(column_type = "Text", nullable)] #[sea_orm(column_type = "Text", nullable)]
pub mc_loader_version: Option<String>, pub mc_loader_version: Option<String>,
pub last_played: Option<i32>, pub last_played: Option<i64>,
pub overall_played: Option<i32>, pub overall_played: Option<i64>,
#[sea_orm(column_type = "Text", nullable)] #[sea_orm(column_type = "Text", nullable)]
pub icon_url: Option<Icon>, pub icon_url: Option<Icon>,
#[sea_orm(column_type = "Text", nullable)] #[sea_orm(column_type = "Text", nullable)]
pub setting_profile_name: Option<String>, pub setting_profile_name: Option<String>,
#[sea_orm(column_type = "Text", nullable)] #[sea_orm(column_type = "Text", nullable)]
pub linked_pack_id: Option<String>, pub linked_pack_id: Option<String>,
pub linked_pack_version: Option<i32>, pub linked_pack_version: Option<String>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta] #[onelauncher_macro::specta]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i32, pub id: u64,
pub major_version: i32, pub major_version: u32,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
pub vendor_name: String, pub vendor_name: String,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]

View file

@ -17,8 +17,8 @@ pub struct Model {
pub display_name: String, pub display_name: String,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
pub display_version: String, pub display_version: String,
pub type_id: i32, pub type_id: u32,
pub provider_id: i32, pub provider_id: u32,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
pub provider_version: String, pub provider_version: String,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]

View file

@ -1,8 +1,26 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5
pub use super::cluster_groups::Entity as ClusterGroups; pub mod entity {
pub use super::cluster_packages::Entity as ClusterPackages; pub use crate::cluster_groups::Entity as ClusterGroups;
pub use super::clusters::Entity as Clusters; pub use crate::cluster_packages::Entity as ClusterPackages;
pub use super::java_versions::Entity as JavaVersions; pub use crate::clusters::Entity as Clusters;
pub use super::packages::Entity as Packages; pub use crate::java_versions::Entity as JavaVersions;
pub use super::setting_profiles::Entity as SettingProfiles; 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;
}

View file

@ -1,19 +1,24 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5
use merge::Merge;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; 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")] #[sea_orm(table_name = "setting_profiles")]
#[onelauncher_macro::specta] #[onelauncher_macro::specta]
#[merge(strategy = merge::option::overwrite_none)]
pub struct Model { pub struct Model {
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")] #[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
#[merge(skip)]
pub name: String, pub name: String,
pub java_id: Option<i32>, pub java_id: Option<u64>,
pub res_w: Option<i32>, pub res_w: Option<u32>,
pub res_h: Option<i32>, pub res_h: Option<u32>,
pub force_fullscreen: Option<bool>, pub force_fullscreen: Option<bool>,
pub mem_max: Option<i32>, pub mem_max: Option<u32>,
#[sea_orm(column_type = "Text", nullable)] #[sea_orm(column_type = "Text", nullable)]
pub launch_args: Option<String>, pub launch_args: Option<String>,
#[sea_orm(column_type = "Text", nullable)] #[sea_orm(column_type = "Text", nullable)]
@ -24,6 +29,8 @@ pub struct Model {
pub hook_wrapper: Option<String>, pub hook_wrapper: Option<String>,
#[sea_orm(column_type = "Text", nullable)] #[sea_orm(column_type = "Text", nullable)]
pub hook_post: Option<String>, pub hook_post: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
pub os_extra: Option<SettingsOsExtra>
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@ -80,6 +87,7 @@ impl Model {
mem_max: Some(3072), mem_max: Some(3072),
res_h: None, res_h: None,
res_w: None, res_w: None,
os_extra: Some(SettingsOsExtra::default())
} }
} }
} }

View file

@ -1,3 +1,5 @@
#![feature(cfg_match)]
mod entities; mod entities;
mod domain; mod domain;

View file

@ -3,11 +3,20 @@ mod pin;
/// This macro is used to derive the `specta::Type` trait for the annotated item. /// 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) /// It's mainly meant for entity-generation with sea-orm-cli (it doesn't like commas in attributes)
/// <br> ///
/// Expands to: /// #### Expands to:
/// ``` /// ```
/// #[cfg_attr(feature = "specta", derive(specta::Type))] /// #[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] #[proc_macro_attribute]
pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
specta::specta(attr, item) specta::specta(attr, item)

View file

@ -42,6 +42,7 @@ create table setting_profiles (
hook_pre text, hook_pre text,
hook_wrapper text, hook_wrapper text,
hook_post text, hook_post text,
os_extra json_text,
constraint setting_profiles_pk primary key (name), constraint setting_profiles_pk primary key (name),
constraint setting_profiles_java_versions_id_fk foreign key (java_id) references java_versions(id) 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, path text not null,
stage integer default (0) not null, stage integer default (0) not null,
created_at integer default (unixepoch()) not null, created_at integer default (unixepoch()) not null,
updated_at integer default (unixepoch()) not null,
group_id integer, group_id integer,
name text not null, name text not null,
mc_version text not null, mc_version text not null,
@ -62,7 +62,7 @@ create table clusters (
icon_url text, icon_url text,
setting_profile_name text, setting_profile_name text,
linked_pack_id text, linked_pack_id text,
linked_pack_version integer, linked_pack_version text,
constraint clusters_pk primary key (id autoincrement), constraint clusters_pk primary key (id autoincrement),
constraint clusters_cluster_groups_id_fk foreign key (group_id) references cluster_groups(id), 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) constraint clusters_setting_profiles_name_fk foreign key (setting_profile_name) references setting_profiles(name)