feat: Launching
This commit is contained in:
parent
71d584e5a3
commit
06215367c2
28 changed files with 632 additions and 224 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3070,6 +3070,7 @@ dependencies = [
|
|||
"thiserror 1.0.64",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-error",
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ webbrowser = { version = "1.0" }
|
|||
|
||||
# io
|
||||
tokio = { version = "1.40", features = [ "full" ] }
|
||||
tokio-util = { version = "0.7", features = [ "compat" ] }
|
||||
tokio-stream = { version = "0.1.16", features = [ "fs" ] }
|
||||
notify = { version = "6.1", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.4", default-features = false }
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ authors = { workspace = true }
|
|||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = [ "cli" ]
|
||||
tauri = [
|
||||
"dep:tauri",
|
||||
"dep:tauri-specta",
|
||||
|
|
@ -51,6 +51,7 @@ serde_with = { workspace = true }
|
|||
# utils
|
||||
sea-orm = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
futures-lite = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -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, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
80
packages/core/examples/launch.rs
Normal file
80
packages/core/examples/launch.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use chrono::{Days, Utc};
|
||||
use onelauncher_core::api::cluster::dao::update_cluster;
|
||||
use onelauncher_core::api::proxy::ProxyDynamic;
|
||||
use onelauncher_core::api::{self, setting_profiles};
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use onelauncher_core::initialize_core;
|
||||
use onelauncher_core::store::credentials::MinecraftCredentials;
|
||||
use onelauncher_core::store::{CoreOptions, Dirs, State};
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() -> LauncherResult<()> {
|
||||
initialize_core(CoreOptions::default(), ProxyDynamic::default()).await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let dirs = Dirs::get().await?;
|
||||
|
||||
// We get the default credentials (or for the sake of this example, return an empty player profile)
|
||||
println!("Getting credentials...");
|
||||
let mut cred_store = state.credentials.write().await;
|
||||
let creds = cred_store
|
||||
.get_default()
|
||||
.await?
|
||||
.unwrap_or_else(|| MinecraftCredentials {
|
||||
access_token: String::new(),
|
||||
refresh_token: String::new(),
|
||||
username: "Player1".to_string(),
|
||||
id: uuid::Uuid::nil(),
|
||||
expires: Utc::now().checked_add_days(Days::new(7)).unwrap(),
|
||||
});
|
||||
|
||||
println!("Credentials: {creds:?}");
|
||||
drop(cred_store);
|
||||
|
||||
// We create a settings profile so that we can run hooks
|
||||
let profile_name = "profile with hooks example";
|
||||
if setting_profiles::dao::get_profile_by_name(profile_name).await?.is_none() {
|
||||
setting_profiles::create_profile(profile_name, async |mut profile| {
|
||||
profile.hook_pre = Set(Some("echo this is a pre-hook!".to_string()));
|
||||
profile.hook_post = Set(Some("echo this is my post-hook!".to_string()));
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
profile.hook_wrapper = Set(Some("mangohud".to_string()));
|
||||
}
|
||||
|
||||
Ok(profile)
|
||||
}).await?;
|
||||
}
|
||||
|
||||
// Here we create the cluster
|
||||
let cluster_name = "Launchable";
|
||||
let path = dirs.clusters_dir().join(cluster_name);
|
||||
|
||||
let mut cluster = if let Some(cluster) = api::cluster::dao::get_cluster_by_path(&path).await? {
|
||||
cluster
|
||||
} else {
|
||||
api::cluster::create_cluster(cluster_name, "1.20.4", GameLoader::Fabric, None, None).await?
|
||||
};
|
||||
|
||||
// We set the clusters
|
||||
update_cluster(&mut cluster, async |mut cluster| {
|
||||
cluster.setting_profile_name = Set(Some(profile_name.to_string()));
|
||||
|
||||
Ok(cluster)
|
||||
}).await?;
|
||||
|
||||
println!("Using cluster: {} at '{}'", cluster.name, cluster.path);
|
||||
|
||||
// Launch the game
|
||||
let process = api::game::launch::launch_minecraft(&mut cluster, creds, None).await?;
|
||||
println!("Process: {:?}", process.read().await);
|
||||
|
||||
// We wait for the process to finish so that we can run code after
|
||||
process.read().await.child_type.write().await.wait().await?;
|
||||
println!("Successfully exited !");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ use sea_orm::{ActiveValue::Set, IntoActiveModel, prelude::*};
|
|||
|
||||
use crate::{error::{DaoError, LauncherResult}, store::State};
|
||||
|
||||
pub type ClusterId = i64;
|
||||
|
||||
/// Inserts a cluster in the database.
|
||||
pub async fn insert_cluster(
|
||||
name: &str,
|
||||
|
|
@ -34,7 +36,7 @@ pub async fn insert_cluster(
|
|||
|
||||
/// Updates an existing cluster in the database.
|
||||
pub async fn update_cluster_by_id<B>(
|
||||
id: u64,
|
||||
id: ClusterId,
|
||||
block: B,
|
||||
) -> LauncherResult<clusters::Model>
|
||||
where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel> {
|
||||
|
|
@ -66,7 +68,7 @@ where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveMo
|
|||
}
|
||||
|
||||
/// Deletes a cluster by its ID from the **database**.
|
||||
pub async fn delete_cluster_by_id(id: u64) -> LauncherResult<()> {
|
||||
pub async fn delete_cluster_by_id(id: ClusterId) -> LauncherResult<()> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
|
|
@ -82,7 +84,7 @@ pub async fn delete_cluster_by_id(id: u64) -> LauncherResult<()> {
|
|||
}
|
||||
|
||||
/// Gets a cluster by its ID from the database.
|
||||
pub async fn get_cluster_by_id(id: u64) -> LauncherResult<Option<clusters::Model>> {
|
||||
pub async fn get_cluster_by_id(id: ClusterId) -> LauncherResult<Option<clusters::Model>> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use chrono::Utc;
|
||||
use dao::ClusterId;
|
||||
use interpulse::api::minecraft::VersionInfo;
|
||||
use onelauncher_entity::cluster_stage::ClusterStage;
|
||||
use onelauncher_entity::{clusters, java_versions};
|
||||
|
|
@ -31,6 +32,8 @@ pub enum ClusterError {
|
|||
MissingJavaVersion,
|
||||
#[error("cluster is in downloading stage")]
|
||||
ClusterDownloading,
|
||||
#[error("cluster is already running")]
|
||||
ClusterAlreadyRunning,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -102,17 +105,17 @@ 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: &mut clusters::Model) -> LauncherResult<&clusters::Model> {
|
||||
pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option<bool>) -> LauncherResult<&clusters::Model> {
|
||||
const INGRESS_TOTAL: f64 = 100.0;
|
||||
const INGRESS_TASKS: f64 = 4.0;
|
||||
const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS;
|
||||
|
||||
tracing::debug!("preparing cluster {}", cluster.name);
|
||||
|
||||
if cluster.stage == ClusterStage::Ready {
|
||||
tracing::info!("cluster is already ready");
|
||||
return Ok(cluster);
|
||||
}
|
||||
// if cluster.stage == ClusterStage::Ready {
|
||||
// tracing::info!("cluster is already ready");
|
||||
// return Ok(cluster);
|
||||
// }
|
||||
|
||||
let ingress_id = init_ingress(
|
||||
IngressType::PrepareCluster {
|
||||
|
|
@ -144,7 +147,10 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model) -> LauncherResult<&c
|
|||
.versions
|
||||
.iter()
|
||||
.find(|v| v.id == cluster.mc_version)
|
||||
.ok_or_else(|| ClusterError::InvalidVersion(cluster.mc_version.clone()))?;
|
||||
.ok_or_else(|| ClusterError::InvalidVersion(cluster.mc_version.clone()))?
|
||||
.clone();
|
||||
|
||||
drop(metadata);
|
||||
|
||||
let loader_version = metadata::get_loader_version(
|
||||
&cluster.mc_version,
|
||||
|
|
@ -155,10 +161,10 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model) -> LauncherResult<&c
|
|||
|
||||
// TASK 2
|
||||
let mut version_info = metadata::download_version_info(
|
||||
version,
|
||||
&version,
|
||||
loader_version.as_ref(),
|
||||
Some(&SubIngress::new(&ingress_id, INGRESS_STEP)),
|
||||
None,
|
||||
force,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
@ -173,7 +179,7 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model) -> LauncherResult<&c
|
|||
};
|
||||
|
||||
// TASK 3
|
||||
download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), None).await?;
|
||||
download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), force).await?;
|
||||
|
||||
// TASK 4
|
||||
run_forge_processors(cluster, &mut version_info, java_version, &SubIngress::new(&ingress_id, INGRESS_STEP)).await?;
|
||||
|
|
@ -303,7 +309,7 @@ async fn run_forge_processors(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_playtime(id: u64, duration: i64) -> LauncherResult<clusters::Model> {
|
||||
pub async fn update_playtime(id: ClusterId, 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();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use serde::Serialize;
|
|||
|
||||
use crate::{error::{DaoError, LauncherResult}, send_error, store::Dirs, utils::io};
|
||||
|
||||
use super::dao;
|
||||
use super::dao::{self, ClusterId};
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
|
|
@ -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<Vec<u64>> {
|
||||
pub async fn sync_clusters() -> LauncherResult<Vec<ClusterId>> {
|
||||
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<SyncActio
|
|||
/// Syncs the cluster with the database.
|
||||
/// Checks if the cluster in the database exists and if the directory exists.
|
||||
#[tracing::instrument]
|
||||
pub async fn sync_cluster_by_id(id: u64) -> LauncherResult<SyncAction> {
|
||||
pub async fn sync_cluster_by_id(id: ClusterId) -> LauncherResult<SyncAction> {
|
||||
let cluster = dao::get_cluster_by_id(id)
|
||||
.await?
|
||||
.ok_or(DaoError::NotFound)?;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ use futures::AsyncReadExt;
|
|||
use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType};
|
||||
use interpulse::api::modded::SidedDataEntry;
|
||||
use interpulse::utils::get_path_from_artifact;
|
||||
use onelauncher_entity::resolution::Resolution;
|
||||
|
||||
use crate::constants::{self, DUMMY_REPLACE_NEWLINE};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::credentials::MinecraftCredentials;
|
||||
use crate::store::Core;
|
||||
use crate::utils::io;
|
||||
|
||||
|
|
@ -20,9 +22,8 @@ pub fn java_arguments(
|
|||
libraries_path: &Path,
|
||||
classpaths: &str,
|
||||
version: &str,
|
||||
mem_min: usize,
|
||||
mem_max: usize,
|
||||
custom_args: Vec<String>,
|
||||
mem_max: u32,
|
||||
custom_args: String,
|
||||
java_arch: &str,
|
||||
) -> LauncherResult<Vec<String>> {
|
||||
let mut parsed = Vec::new();
|
||||
|
|
@ -56,75 +57,72 @@ pub fn java_arguments(
|
|||
}
|
||||
|
||||
parsed.push(format!("-Xmx{mem_max}M"));
|
||||
parsed.push(format!("-Xms{mem_min}M"));
|
||||
|
||||
for arg in custom_args {
|
||||
if !arg.is_empty() {
|
||||
parsed.push(arg);
|
||||
}
|
||||
if !custom_args.is_empty() {
|
||||
parsed.push(custom_args);
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
// #[allow(clippy::too_many_arguments)]
|
||||
// pub fn minecraft_arguments(
|
||||
// args: Option<&[Argument]>,
|
||||
// legacy_args: Option<&str>,
|
||||
// creds: &MinecraftCredentials,
|
||||
// version: &str,
|
||||
// asset_index: &str,
|
||||
// game_directory: &Path,
|
||||
// assets_directory: &Path,
|
||||
// version_type: VersionType,
|
||||
// resolution: Resolution,
|
||||
// java_arch: &str,
|
||||
// ) -> LauncherResult<Vec<String>> {
|
||||
// if let Some(args) = args {
|
||||
// let mut parsed = Vec::new();
|
||||
// parse_arguments(
|
||||
// args,
|
||||
// &mut parsed,
|
||||
// |arg| {
|
||||
// parse_minecraft_argument(
|
||||
// arg,
|
||||
// &creds.access_token,
|
||||
// &creds.username,
|
||||
// creds.id,
|
||||
// version,
|
||||
// asset_index,
|
||||
// game_directory,
|
||||
// assets_directory,
|
||||
// version_type,
|
||||
// resolution,
|
||||
// )
|
||||
// },
|
||||
// java_arch,
|
||||
// )?;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn minecraft_arguments(
|
||||
args: Option<&[Argument]>,
|
||||
legacy_args: Option<&str>,
|
||||
creds: &MinecraftCredentials,
|
||||
version: &str,
|
||||
asset_index: &str,
|
||||
game_directory: &Path,
|
||||
assets_directory: &Path,
|
||||
version_type: VersionType,
|
||||
resolution: Resolution,
|
||||
java_arch: &str,
|
||||
) -> LauncherResult<Vec<String>> {
|
||||
if let Some(args) = args {
|
||||
let mut parsed = Vec::new();
|
||||
parse_arguments(
|
||||
args,
|
||||
&mut parsed,
|
||||
|arg| {
|
||||
parse_minecraft_argument(
|
||||
arg,
|
||||
&creds.access_token,
|
||||
&creds.username,
|
||||
creds.id,
|
||||
version,
|
||||
asset_index,
|
||||
game_directory,
|
||||
assets_directory,
|
||||
version_type,
|
||||
resolution,
|
||||
)
|
||||
},
|
||||
java_arch,
|
||||
)?;
|
||||
|
||||
// Ok(parsed)
|
||||
// } else if let Some(legacy_args) = legacy_args {
|
||||
// let mut parsed = Vec::new();
|
||||
// for arg in legacy_args.split(' ') {
|
||||
// parsed.push(parse_minecraft_argument(
|
||||
// &arg.replace(' ', DUMMY_REPLACE_NEWLINE),
|
||||
// &creds.access_token,
|
||||
// &creds.username,
|
||||
// creds.id,
|
||||
// version,
|
||||
// asset_index,
|
||||
// game_directory,
|
||||
// assets_directory,
|
||||
// version_type,
|
||||
// resolution,
|
||||
// )?);
|
||||
// }
|
||||
Ok(parsed)
|
||||
} else if let Some(legacy_args) = legacy_args {
|
||||
let mut parsed = Vec::new();
|
||||
for arg in legacy_args.split(' ') {
|
||||
parsed.push(parse_minecraft_argument(
|
||||
&arg.replace(' ', DUMMY_REPLACE_NEWLINE),
|
||||
&creds.access_token,
|
||||
&creds.username,
|
||||
creds.id,
|
||||
version,
|
||||
asset_index,
|
||||
game_directory,
|
||||
assets_directory,
|
||||
version_type,
|
||||
resolution,
|
||||
)?);
|
||||
}
|
||||
|
||||
// Ok(parsed)
|
||||
// } else {
|
||||
// Ok(Vec::new())
|
||||
// }
|
||||
// }
|
||||
Ok(parsed)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn processor_arguments<T: AsRef<str>, S: ::std::hash::BuildHasher>(
|
||||
libraries_path: &Path,
|
||||
|
|
@ -206,7 +204,7 @@ pub fn parse_minecraft_argument(
|
|||
game_directory: &Path,
|
||||
assets_directory: &Path,
|
||||
version_type: VersionType,
|
||||
resolution: (u32, u32),
|
||||
resolution: Resolution,
|
||||
) -> LauncherResult<String> {
|
||||
#[allow(clippy::literal_string_with_formatting_args)]
|
||||
Ok(argument
|
||||
|
|
@ -235,8 +233,8 @@ pub fn parse_minecraft_argument(
|
|||
&io::canonicalize(assets_directory)?.to_string_lossy(),
|
||||
)
|
||||
.replace("${version_type}", version_type.as_str())
|
||||
.replace("${resolution_width}", &resolution.0.to_string())
|
||||
.replace("${resolution_height}", &resolution.1.to_string()))
|
||||
.replace("${resolution_width}", &resolution.width.to_string())
|
||||
.replace("${resolution_height}", &resolution.height.to_string()))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
|
|
|||
|
|
@ -1,22 +1,38 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use onelauncher_entity::{cluster_stage::ClusterStage, prelude::model::*};
|
||||
use tokio::sync::RwLock;
|
||||
use chrono::Utc;
|
||||
use discord_rich_presence::activity::Timestamps;
|
||||
use merge::Merge;
|
||||
use onelauncher_entity::prelude::model::*;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
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};
|
||||
use crate::api::cluster::dao::update_cluster;
|
||||
use crate::api::cluster::{ClusterError, prepare_cluster};
|
||||
use crate::api::game::{arguments, log, metadata};
|
||||
use crate::api::java::{self, JavaError};
|
||||
use crate::api::setting_profiles::dao::get_profile_by_name;
|
||||
use crate::api::setting_profiles::get_global_profile;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::credentials::MinecraftCredentials;
|
||||
use crate::store::processes::Process;
|
||||
use crate::store::{Dirs, State};
|
||||
use crate::utils::io;
|
||||
|
||||
#[tracing::instrument]
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn launch_minecraft(
|
||||
cluster: &mut Cluster,
|
||||
creds: &MinecraftCredentials,
|
||||
creds: MinecraftCredentials,
|
||||
force: Option<bool>,
|
||||
) -> LauncherResult<Arc<RwLock<Process>>> {
|
||||
if cluster.stage.is_downloading() {
|
||||
return Err(ClusterError::ClusterDownloading.into());
|
||||
} else if cluster.stage == ClusterStage::NotReady {
|
||||
prepare_cluster(cluster).await?;
|
||||
}
|
||||
|
||||
prepare_cluster(cluster, force).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? {
|
||||
|
|
@ -25,45 +41,167 @@ pub async fn launch_minecraft(
|
|||
}
|
||||
|
||||
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;
|
||||
if !state.settings.read().await.allow_parallel_running_clusters {
|
||||
let running = state.processes.has_running(cluster.id).await;
|
||||
if running {
|
||||
return Err(ClusterError::ClusterAlreadyRunning.into());
|
||||
}
|
||||
}
|
||||
|
||||
let cwd = &io::canonicalize(&cluster.path)?;
|
||||
|
||||
let metadata = state.metadata.read().await;
|
||||
let versions = &metadata.get_vanilla()?.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 version = versions[version_index].clone();
|
||||
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,
|
||||
let loader_version = metadata::get_loader_version(
|
||||
&cluster.mc_version,
|
||||
cluster.mc_loader,
|
||||
cluster.mc_loader_version.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
drop(metadata);
|
||||
let version_name = loader_version
|
||||
.as_ref()
|
||||
.map_or(version.id.clone(), |it| {
|
||||
format!("{}-{}", version.id, it.id)
|
||||
});
|
||||
|
||||
let java = java::get_recommended_java(&version_info, Some(&settings)).await?;
|
||||
if java.is_none() {
|
||||
let version_info =
|
||||
metadata::download_version_info(&version, loader_version.as_ref(), None, None).await?;
|
||||
|
||||
let Some(java) = java::get_recommended_java(&version_info, Some(&settings)).await? else {
|
||||
return Err(JavaError::MissingJava.into());
|
||||
};
|
||||
|
||||
let java_path = PathBuf::from(&java.absolute_path);
|
||||
java::check_java_runtime(&java_path, false).await?;
|
||||
|
||||
let dirs = Dirs::get().await?;
|
||||
let client_jar_path = dirs
|
||||
.versions_dir()
|
||||
.join(&version_name)
|
||||
.join(format!("{version_name}.jar"));
|
||||
|
||||
let mut command = if let Some(wrapper) = &settings.hook_wrapper {
|
||||
let mut cmd = Command::new(wrapper);
|
||||
cmd.arg(&java.absolute_path);
|
||||
cmd
|
||||
} else {
|
||||
Command::new(&java.absolute_path)
|
||||
};
|
||||
|
||||
let env_vars = settings.launch_env.iter().flat_map(|it| {
|
||||
it.split(' ').map(|it| {
|
||||
let mut split = it.split('=');
|
||||
let key = split.next().unwrap_or_default();
|
||||
let value = split.next().unwrap_or_default();
|
||||
(key.to_string(), value.to_string())
|
||||
})
|
||||
});
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if std::env::var("CARGO").is_ok() {
|
||||
command.env_remove("DYLD_FALLBACK_LIBRARY_PATH");
|
||||
}
|
||||
|
||||
command.env_remove("_JAVA_OPTIONS");
|
||||
command.envs(env_vars);
|
||||
|
||||
let args = version_info.arguments.clone().unwrap_or_default();
|
||||
|
||||
todo!()
|
||||
}
|
||||
command
|
||||
.args(
|
||||
arguments::java_arguments(
|
||||
args.get(&interpulse::api::minecraft::ArgumentType::Jvm)
|
||||
.map(Vec::as_slice),
|
||||
&dirs.natives_dir().join(&version_name),
|
||||
&dirs.libraries_dir(),
|
||||
&arguments::classpaths(
|
||||
&dirs.libraries_dir(),
|
||||
version_info.libraries.as_slice(),
|
||||
&client_jar_path,
|
||||
&java.arch,
|
||||
updated,
|
||||
)?,
|
||||
&version_name,
|
||||
settings.mem_max.unwrap_or(2048),
|
||||
settings.launch_args.clone().unwrap_or_default(),
|
||||
&java.arch,
|
||||
)?
|
||||
.into_iter(),
|
||||
)
|
||||
.arg(version_info.main_class.clone())
|
||||
.args(
|
||||
arguments::minecraft_arguments(
|
||||
args.get(&interpulse::api::minecraft::ArgumentType::Game)
|
||||
.map(Vec::as_slice),
|
||||
version_info.minecraft_arguments.as_deref(),
|
||||
&creds,
|
||||
&version.id,
|
||||
&version_info.asset_index.id,
|
||||
cwd,
|
||||
&dirs.assets_dir(),
|
||||
version.type_,
|
||||
settings.res.unwrap_or_default(),
|
||||
&java.arch,
|
||||
)?
|
||||
.into_iter(),
|
||||
)
|
||||
.current_dir(cwd);
|
||||
|
||||
// TODO: Make all options configurable?
|
||||
let mc_options = &[(
|
||||
String::from("fullscreen"),
|
||||
settings.force_fullscreen.unwrap_or(false).to_string(),
|
||||
)];
|
||||
|
||||
if !mc_options.is_empty() {
|
||||
let options_path = cwd.join("options.txt");
|
||||
let mut options_string = if options_path.exists() {
|
||||
io::read_to_string(&options_path).await?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
for (key, value) in mc_options {
|
||||
let re = regex::Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
|
||||
if re.is_match(&options_string) {
|
||||
let replaced_string = re
|
||||
.replace_all(&options_string, &format!("{key}:{value}"))
|
||||
.to_string();
|
||||
options_string = replaced_string;
|
||||
} else {
|
||||
use std::fmt::Write;
|
||||
let _ = write!(options_string, "\n{key}:{value}");
|
||||
}
|
||||
}
|
||||
|
||||
io::write(&options_path, options_string).await?;
|
||||
}
|
||||
|
||||
let start_time = Utc::now().timestamp();
|
||||
update_cluster(cluster, async |mut cluster| {
|
||||
cluster.last_played = Set(Some(start_time));
|
||||
Ok(cluster)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let censors = log::create_censors(&creds);
|
||||
|
||||
if let Some(discord) = &state.rpc {
|
||||
discord.set_message(
|
||||
&format!("Playing {}", cluster.name),
|
||||
Some(Timestamps::new().start(start_time)),
|
||||
).await;
|
||||
}
|
||||
|
||||
state.processes.spawn(cluster.id, cwd.clone(), creds, censors, &settings, command).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,36 @@
|
|||
use std::sync::LazyLock;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::store::credentials::MinecraftCredentials;
|
||||
|
||||
static WHOAMI: LazyLock<(String, String)> = LazyLock::new(|| {
|
||||
(whoami::username(), whoami::realname())
|
||||
});
|
||||
pub type CensorMap = HashMap<String, String>;
|
||||
|
||||
#[must_use]
|
||||
pub fn censor_line(
|
||||
pub fn create_censors(
|
||||
creds: &MinecraftCredentials,
|
||||
mut output: String
|
||||
) -> String {
|
||||
let username = &WHOAMI.0;
|
||||
let realname = &WHOAMI.1;
|
||||
) -> CensorMap {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
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}");
|
||||
let username = whoami::username();
|
||||
let realname = whoami::realname();
|
||||
|
||||
output
|
||||
map.insert(format!("/{username}/"), "/{ENV_USERNAME}/".to_string());
|
||||
map.insert(format!("\\{username}\\"), "\\{ENV_USERNAME}\\".to_string());
|
||||
map.insert(format!("/{realname}/"), "/{ENV_REALNAME}/".to_string());
|
||||
map.insert(format!("\\{realname}\\"), "\\{ENV_REALNAME}\\".to_string());
|
||||
|
||||
if !creds.access_token.is_empty() {
|
||||
map.insert(creds.access_token.clone(), "{MC_ACCESS_TOKEN}".to_string());
|
||||
}
|
||||
|
||||
map.insert(creds.username.clone(), "{MC_USERNAME}".to_string());
|
||||
map.insert(creds.id.as_simple().to_string(), "{MC_UUID}".to_string());
|
||||
map.insert(creds.id.as_hyphenated().to_string(), "{MC_UUID}".to_string());
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
pub fn censor_line(censors: &CensorMap, line: &mut String) {
|
||||
for (key, value) in censors {
|
||||
*line = line.replace(key, value);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ pub async fn download_minecraft(
|
|||
}
|
||||
|
||||
// MARK: Version Info
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[tracing::instrument(skip(ingress, force))]
|
||||
pub async fn download_version_info(
|
||||
version: &Version,
|
||||
loader: Option<&LoaderVersion>,
|
||||
|
|
@ -120,7 +120,7 @@ pub async fn download_assets_index(
|
|||
tracing::debug!("loading assets index for version {}", version.id);
|
||||
let path = Dirs::get_assets_index_dir()
|
||||
.await?
|
||||
.join(version.asset_index.id.clone());
|
||||
.join(format!("{}.json", version.asset_index.id));
|
||||
|
||||
// let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0));
|
||||
// let ingress_ref = ingress_ref.as_ref();
|
||||
|
|
@ -274,10 +274,10 @@ pub async fn download_libraries(
|
|||
tracing::debug!("loading libraries for version {}", version);
|
||||
|
||||
let lib_dir = Dirs::get_libraries_dir().await?;
|
||||
let natives_dir = Dirs::get_natives_dir().await?;
|
||||
let natives_dest = Dirs::get_natives_dir().await?.join(version);
|
||||
|
||||
io::create_dir_all(&lib_dir).await?;
|
||||
io::create_dir_all(&natives_dir).await?;
|
||||
io::create_dir_all(&natives_dest).await?;
|
||||
|
||||
let num_files = libraries.len();
|
||||
let ingress = ingress.ingress_sub(|total| total / num_files as f64);
|
||||
|
|
@ -297,15 +297,16 @@ pub async fn download_libraries(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let artifact_path = interpulse::utils::get_path_from_artifact(&lib.name)?;
|
||||
let path = lib_dir.join(&artifact_path);
|
||||
|
||||
if path.exists() && !force.unwrap_or(false) {
|
||||
tracing::debug!("library {} is installed, skipping", &lib.name);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tokio::try_join! {
|
||||
async {
|
||||
let artifact_path = interpulse::utils::get_path_from_artifact(&lib.name)?;
|
||||
let path = lib_dir.join(&artifact_path);
|
||||
|
||||
if path.exists() && !force.unwrap_or(false) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(interpulse::api::minecraft::LibraryDownloads {
|
||||
artifact: Some(ref artifact), ..
|
||||
}) = lib.downloads {
|
||||
|
|
@ -343,6 +344,7 @@ pub async fn download_libraries(
|
|||
let parsed = os_key.replace("${arch}", crate::constants::ARCH_WIDTH);
|
||||
if let Some(native) = classifiers.get(&parsed) {
|
||||
tracing::trace!("found native library {}", &lib.name);
|
||||
|
||||
let data = http::fetch_advanced(
|
||||
Method::GET,
|
||||
&native.url,
|
||||
|
|
@ -352,9 +354,12 @@ pub async fn download_libraries(
|
|||
ingress,
|
||||
).await?;
|
||||
|
||||
let dest = natives_dir.join(version);
|
||||
io::unzip_bytes(data.to_vec(), &dest).await?;
|
||||
tracing::trace!("extracted native {} to path {:?}", &lib.name, &dest);
|
||||
io::unzip_bytes_filtered(
|
||||
data.to_vec(),
|
||||
Some(|name: &str| !name.starts_with("META-INF")),
|
||||
&natives_dest
|
||||
).await?;
|
||||
tracing::trace!("extracted native {} to path {:?}", &lib.name, &natives_dest);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -380,6 +385,7 @@ pub async fn download_libraries(
|
|||
/// Gets the loader version for a given Minecraft version and loader
|
||||
/// If `loader_version` is `None`, it will return the latest stable version else
|
||||
/// it will return the specified version if found
|
||||
#[tracing::instrument]
|
||||
pub async fn get_loader_version(
|
||||
mc_version: &str,
|
||||
loader: GameLoader,
|
||||
|
|
@ -405,6 +411,7 @@ pub async fn get_loader_version(
|
|||
.loaders
|
||||
.iter()
|
||||
.find(|it| loader_version.map_or(it.stable, |version| it.id == version))
|
||||
.or_else(|| loaders.loaders.first())
|
||||
.cloned()
|
||||
.ok_or(MetadataError::NoMatchingLoader)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ use crate::{error::LauncherResult, store::State};
|
|||
|
||||
use super::{JavaError, JavaInfo};
|
||||
|
||||
pub type JavaVersionId = i64;
|
||||
|
||||
/// Returns the latest Java version
|
||||
pub async fn get_latest_java() -> LauncherResult<Option<java_versions::Model>> {
|
||||
let db = &State::get().await?.db;
|
||||
|
|
@ -50,7 +52,7 @@ pub async fn get_java_all() -> LauncherResult<Vec<java_versions::Model>> {
|
|||
}
|
||||
|
||||
/// Returns the specific Java version by ID
|
||||
pub async fn get_java_by_id(id: u64) -> LauncherResult<Option<java_versions::Model>> {
|
||||
pub async fn get_java_by_id(id: JavaVersionId) -> LauncherResult<Option<java_versions::Model>> {
|
||||
let db = &State::get().await?.db;
|
||||
|
||||
Ok(java_versions::Entity::find()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ use crate::store::processes::Process;
|
|||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum ProcessPayload {
|
||||
Starting {
|
||||
command: String,
|
||||
},
|
||||
Started {
|
||||
process: Process,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use indicatif::ProgressBar;
|
|||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{constants::CLI_TOTAL_INGRESS, api::proxy::{event::LauncherEvent, LauncherProxy}, LauncherResult};
|
||||
use crate::{api::{processes::ProcessPayload, proxy::{event::LauncherEvent, LauncherProxy}}, constants::CLI_TOTAL_INGRESS, LauncherResult};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ProxyCli {
|
||||
|
|
@ -49,9 +49,22 @@ impl LauncherProxy for ProxyCli {
|
|||
LauncherEvent::Message(message) => {
|
||||
println!("[{:?}] {}", message.level, message.message);
|
||||
},
|
||||
_ => {
|
||||
println!("{:#?}", event);
|
||||
},
|
||||
LauncherEvent::Process(process) => {
|
||||
match process {
|
||||
ProcessPayload::Starting { command } => {
|
||||
println!("Starting process: {command}");
|
||||
},
|
||||
ProcessPayload::Started { process } => {
|
||||
println!("Process started: {process:#?}");
|
||||
},
|
||||
ProcessPayload::Stopped { pid, exit_code } => {
|
||||
println!("Process {pid} exited with code {exit_code}");
|
||||
},
|
||||
ProcessPayload::Output { pid, output } => {
|
||||
println!("Process {pid}: {output}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use onelauncher_entity::setting_profiles;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
|
||||
use crate::store::State;
|
||||
use crate::{error::LauncherResult, store::State};
|
||||
|
||||
pub mod dao {
|
||||
use onelauncher_entity::setting_profiles;
|
||||
|
|
@ -65,6 +66,7 @@ pub mod dao {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the global settings profile
|
||||
pub async fn get_global_profile() -> setting_profiles::Model {
|
||||
let Ok(state) = State::get().await else {
|
||||
return setting_profiles::Model::default_global_profile();
|
||||
|
|
@ -73,3 +75,16 @@ pub async fn get_global_profile() -> setting_profiles::Model {
|
|||
let settings = state.settings.read().await;
|
||||
settings.global_game_settings.clone()
|
||||
}
|
||||
|
||||
/// Creates a new setting profile and inserts it into the database. Returns the inserted entry
|
||||
pub async fn create_profile<F>(name: &str, block: F) -> LauncherResult<setting_profiles::Model>
|
||||
where F: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::ActiveModel> {
|
||||
let model = setting_profiles::ActiveModel {
|
||||
name: Set(name.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let model = block(model).await?;
|
||||
|
||||
dao::insert_profile(model).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ pub enum LauncherError {
|
|||
ReqwestError(#[from] reqwest::Error),
|
||||
#[error(transparent)]
|
||||
InterpulseError(#[from] interpulse::Error),
|
||||
#[error(transparent)]
|
||||
RegexError(#[from] regex::Error),
|
||||
#[error("couldn't acquire semaphore: {0}")]
|
||||
SemaphoreError(#[from] tokio::sync::AcquireError),
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ impl Metadata {
|
|||
}
|
||||
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_vanilla_or_fetch(&mut self) -> LauncherResult<&VanillaManifest> {
|
||||
if !self.initialized() {
|
||||
self.initialize().await?;
|
||||
|
|
@ -67,6 +68,7 @@ impl Metadata {
|
|||
self.get_vanilla()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_modded_or_fetch(&mut self, loader: GameLoader) -> LauncherResult<&ModdedManifest> {
|
||||
if !loader.is_modded() {
|
||||
return Err(MetadataError::NotModdedManifest(loader).into());
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -12,10 +13,11 @@ use tokio::task::JoinHandle;
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::api::cluster;
|
||||
use crate::api::game::log::censor_line;
|
||||
use crate::api::cluster::dao::ClusterId;
|
||||
use crate::api::game::log::{censor_line, CensorMap};
|
||||
use crate::api::processes::ProcessPayload;
|
||||
use crate::api::proxy::event::{send_event, LauncherEvent};
|
||||
use crate::error::{DaoError, LauncherResult};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::send_error;
|
||||
use crate::utils::io::IOError;
|
||||
|
||||
|
|
@ -39,7 +41,7 @@ pub struct ProcessStore {
|
|||
pub struct Process {
|
||||
pub pid: u32,
|
||||
pub started_at: DateTime<Utc>,
|
||||
pub cluster_id: u64,
|
||||
pub cluster_id: ClusterId,
|
||||
pub post_hook: Option<String>,
|
||||
pub account_id: Uuid,
|
||||
#[serde(skip)]
|
||||
|
|
@ -79,7 +81,7 @@ impl ChildType {
|
|||
Self::Owned(child) => child
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
.map(|s| s.code().unwrap_or_default()),
|
||||
.and_then(|s| s.code()),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -109,13 +111,83 @@ impl ProcessStore {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
/// Returns a list of all running processes for a given cluster.
|
||||
pub async fn get_running_by_cluster(
|
||||
&self,
|
||||
cluster_id: ClusterId,
|
||||
) -> Vec<Arc<RwLock<Process>>> {
|
||||
let mut list = Vec::new();
|
||||
let read_guard = self.processes.read().await;
|
||||
|
||||
for process in read_guard.values() {
|
||||
if process.read().await.cluster_id == cluster_id {
|
||||
list.push(process.clone());
|
||||
}
|
||||
}
|
||||
|
||||
list
|
||||
}
|
||||
|
||||
/// Returns a list of all running processes for a given account.
|
||||
pub async fn get_running_by_account(
|
||||
&self,
|
||||
account_id: Uuid,
|
||||
) -> Vec<Arc<RwLock<Process>>> {
|
||||
let mut list = Vec::new();
|
||||
let read_guard = self.processes.read().await;
|
||||
|
||||
for process in read_guard.values() {
|
||||
if process.read().await.account_id == account_id {
|
||||
list.push(process.clone());
|
||||
}
|
||||
}
|
||||
|
||||
list
|
||||
}
|
||||
|
||||
/// Returns a list of all running processes.
|
||||
pub async fn get_running(&self) -> Vec<Arc<RwLock<Process>>> {
|
||||
let mut list = Vec::new();
|
||||
let read_guard = self.processes.read().await;
|
||||
|
||||
for process in read_guard.values() {
|
||||
list.push(process.clone());
|
||||
}
|
||||
|
||||
list
|
||||
}
|
||||
|
||||
/// Returns whether a cluster has a running process.
|
||||
pub async fn has_running(&self, cluster_id: ClusterId) -> bool {
|
||||
let read_guard = self.processes.read().await;
|
||||
|
||||
for process in read_guard.values() {
|
||||
if process.read().await.cluster_id == cluster_id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Spawns a new process (Minecraft Instance)
|
||||
pub async fn spawn(
|
||||
&self,
|
||||
cluster_id: u64,
|
||||
cluster_id: ClusterId,
|
||||
cwd: PathBuf,
|
||||
creds: MinecraftCredentials,
|
||||
censors: CensorMap,
|
||||
settings: &setting_profiles::Model,
|
||||
mut command: Command,
|
||||
) -> LauncherResult<Arc<RwLock<Process>>> {
|
||||
send_event(LauncherEvent::Process(ProcessPayload::Starting { command: format!("{command:?}") })).await;
|
||||
|
||||
if let Some(pre_hook) = &settings.hook_pre {
|
||||
if let Err(err) = run_hook(pre_hook, cwd.clone()).await {
|
||||
send_error!("{err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
command
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
|
|
@ -140,20 +212,19 @@ impl ProcessStore {
|
|||
}
|
||||
}
|
||||
|
||||
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! {
|
||||
while let Ok(Some(mut line)) = tokio::select! {
|
||||
line = stdout.next_line() => line,
|
||||
line = stderr.next_line() => line,
|
||||
} {
|
||||
let censored = censor_line(&creds, line);
|
||||
censor_line(&censors, &mut line);
|
||||
|
||||
send_event(LauncherEvent::Process(ProcessPayload::Output {
|
||||
pid,
|
||||
output: censored,
|
||||
output: line,
|
||||
})).await;
|
||||
}
|
||||
});
|
||||
|
|
@ -164,10 +235,10 @@ impl ProcessStore {
|
|||
started_at,
|
||||
cluster_id,
|
||||
post_hook: settings.hook_post.clone(),
|
||||
account_id,
|
||||
account_id: creds.id,
|
||||
child_type: child_type.clone(),
|
||||
process_loop: Some(tokio::task::spawn(Self::process_loop(
|
||||
cluster_id, settings.hook_post.clone(), child_type,
|
||||
cluster_id, cwd, settings.hook_post.clone(), child_type,
|
||||
))),
|
||||
};
|
||||
|
||||
|
|
@ -185,7 +256,8 @@ impl ProcessStore {
|
|||
}
|
||||
|
||||
async fn process_loop(
|
||||
cluster_id: u64,
|
||||
cluster_id: ClusterId,
|
||||
cwd: PathBuf,
|
||||
post_hook: Option<String>,
|
||||
child: Arc<RwLock<ChildType>>,
|
||||
) -> LauncherResult<i32> {
|
||||
|
|
@ -221,22 +293,8 @@ impl ProcessStore {
|
|||
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);
|
||||
}
|
||||
if let Err(err) = run_hook(&post_hook, cwd).await {
|
||||
send_error!("{err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,13 +304,14 @@ impl ProcessStore {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn run_hook(hook: &str, cwd: &str) -> LauncherResult<Option<i32>> {
|
||||
async fn run_hook(hook: &str, cwd: PathBuf) -> LauncherResult<Option<i32>> {
|
||||
let mut split = hook.split(' ');
|
||||
|
||||
let Some(command) = split.next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
tracing::debug!("running hook");
|
||||
let result = Command::new(command)
|
||||
.args(split)
|
||||
.current_dir(cwd)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ use serde::de::DeserializeOwned;
|
|||
use tempfile::TempDir;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use tokio_util::compat::TokioAsyncWriteCompatExt;
|
||||
|
||||
/// A wrapper around generic and unhelpful [`std::io::Error`] messages.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IOError {
|
||||
|
|
@ -215,6 +217,17 @@ pub async fn copy(
|
|||
})
|
||||
}
|
||||
|
||||
// /// Copies an async reader into an async writer. Returns the bytes copied
|
||||
// pub async fn copy_buf<'a, R, W>(reader: &'a mut R, writer: &'a mut W) -> Result<u64, IOError>
|
||||
// where
|
||||
// R: TokioAsyncReadCompatExt,
|
||||
// W: TokioAsyncWriteCompatExt,
|
||||
// {
|
||||
// tokio::io::copy_buf(reader, writer)
|
||||
// .await
|
||||
// .map_err(IOError::from)
|
||||
// }
|
||||
|
||||
/// Removes a file from the filesystem.
|
||||
pub async fn remove_file(path: impl AsRef<std::path::Path>) -> Result<(), IOError> {
|
||||
let path = path.as_ref();
|
||||
|
|
@ -237,10 +250,7 @@ pub fn tempfile() -> Result<std::fs::File, IOError> {
|
|||
}
|
||||
|
||||
/// Reads a zip archive from a byte array
|
||||
pub async fn read_zip_entries_bytes<F>(
|
||||
data: Vec<u8>,
|
||||
mut f: F,
|
||||
) -> Result<(), IOError>
|
||||
pub async fn read_zip_entries_bytes<F>(data: Vec<u8>, mut f: F) -> Result<(), IOError>
|
||||
where
|
||||
F: AsyncFnMut(
|
||||
usize,
|
||||
|
|
@ -272,11 +282,26 @@ where
|
|||
pub async fn unzip_bytes(
|
||||
data: Vec<u8>,
|
||||
dest_path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
unzip_bytes_filtered(data, None::<fn(&str) -> bool>, dest_path).await
|
||||
}
|
||||
|
||||
/// Unzips a zip archive from a byte array
|
||||
pub async fn unzip_bytes_filtered(
|
||||
data: Vec<u8>,
|
||||
filter_entries: Option<impl Fn(&str) -> bool>,
|
||||
dest_path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
read_zip_entries_bytes(data, async |_, entry, entry_reader| {
|
||||
let path = dest_path
|
||||
.as_ref()
|
||||
.join(sanitize_path(entry.filename().as_str()?));
|
||||
let file_name = entry.filename().as_str()?;
|
||||
|
||||
if let Some(filter) = &filter_entries {
|
||||
if !filter(file_name) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let path = dest_path.as_ref().join(sanitize_path(file_name));
|
||||
|
||||
let is_dir = entry.dir()?;
|
||||
|
||||
|
|
@ -288,20 +313,14 @@ pub async fn unzip_bytes(
|
|||
}
|
||||
|
||||
let file = tokio::fs::File::create(&path).await?;
|
||||
let mut writer = tokio::io::BufWriter::new(file);
|
||||
let writer = tokio::io::BufWriter::new(file);
|
||||
|
||||
let mut buf = vec![0; 8192];
|
||||
loop {
|
||||
let n = futures::AsyncReadExt::read(entry_reader, &mut buf).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
writer.write_all(&buf[..n]).await?;
|
||||
}
|
||||
futures_lite::io::copy(entry_reader, &mut writer.compat_write()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}).await?;
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -331,17 +350,10 @@ pub async fn unzip_file(
|
|||
}
|
||||
|
||||
let file = tokio::fs::File::create(&path).await?;
|
||||
let mut writer = tokio::io::BufWriter::new(file);
|
||||
let mut entry_reader = reader.reader_without_entry(index).await?;
|
||||
let writer = tokio::io::BufWriter::new(file);
|
||||
let entry_reader = reader.reader_without_entry(index).await?;
|
||||
|
||||
let mut buf = vec![0; 8192];
|
||||
loop {
|
||||
let n = futures::AsyncReadExt::read(&mut entry_reader, &mut buf).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
writer.write_all(&buf[..n]).await?;
|
||||
}
|
||||
futures_lite::io::copy(entry_reader, &mut writer.compat_write()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod cluster_stage;
|
||||
pub mod icon;
|
||||
pub mod loader;
|
||||
pub mod resolution;
|
||||
pub mod settings;
|
||||
58
packages/entity/src/domain/resolution.rs
Normal file
58
packages/entity/src/domain/resolution.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use sea_orm::{sea_query::{Nullable, ValueType}, TryGetableFromJson};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Resolution {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl Default for Resolution {
|
||||
fn default() -> Self {
|
||||
Self::new(854, 480)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolution {
|
||||
pub fn new(width: u32, height: u32) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Resolution> for sea_orm::Value {
|
||||
fn from(value: Resolution) -> Self {
|
||||
sea_orm::Value::Json(serde_json::to_value(value).ok().map(Box::new))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryGetableFromJson for Resolution {}
|
||||
|
||||
impl ValueType for Resolution {
|
||||
fn try_from(v: sea_orm::Value) -> Result<Self, sea_orm::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 {
|
||||
"Resolution".to_string()
|
||||
}
|
||||
|
||||
fn array_type() -> sea_orm::sea_query::ArrayType {
|
||||
sea_orm::sea_query::ArrayType::Json
|
||||
}
|
||||
|
||||
fn column_type() -> sea_orm::ColumnType {
|
||||
sea_orm::ColumnType::Json
|
||||
}
|
||||
}
|
||||
|
||||
impl Nullable for Resolution {
|
||||
fn null() -> sea_orm::Value {
|
||||
sea_orm::Value::Json(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||
#[onelauncher_macro::specta]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: u64,
|
||||
pub id: i64,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub name: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||
#[onelauncher_macro::specta]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub cluster_id: u64,
|
||||
pub cluster_id: i64,
|
||||
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
|
||||
pub package_hash: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ use crate::{cluster_stage::ClusterStage, icon::Icon, loader::GameLoader};
|
|||
#[onelauncher_macro::specta]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: u64,
|
||||
pub id: i64,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub path: String,
|
||||
pub stage: ClusterStage,
|
||||
pub created_at: i64,
|
||||
pub group_id: Option<u64>,
|
||||
pub group_id: Option<i64>,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub name: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||
#[onelauncher_macro::specta]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: u64,
|
||||
pub id: i64,
|
||||
pub major_version: u32,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub vendor_name: String,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use merge::Merge;
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::settings::SettingsOsExtra;
|
||||
use crate::{resolution::Resolution, settings::SettingsOsExtra};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, Merge)]
|
||||
#[sea_orm(table_name = "setting_profiles")]
|
||||
|
|
@ -14,9 +14,9 @@ pub struct Model {
|
|||
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
|
||||
#[merge(skip)]
|
||||
pub name: String,
|
||||
pub java_id: Option<u64>,
|
||||
pub res_w: Option<u32>,
|
||||
pub res_h: Option<u32>,
|
||||
pub java_id: Option<i64>,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub res: Option<Resolution>,
|
||||
pub force_fullscreen: Option<bool>,
|
||||
pub mem_max: Option<u32>,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
|
|
@ -84,9 +84,8 @@ impl Model {
|
|||
java_id: None,
|
||||
launch_args: None,
|
||||
launch_env: None,
|
||||
mem_max: Some(3072),
|
||||
res_h: None,
|
||||
res_w: None,
|
||||
mem_max: Some(2048),
|
||||
res: None,
|
||||
os_extra: Some(SettingsOsExtra::default())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ create table packages (
|
|||
create table setting_profiles (
|
||||
name text not null,
|
||||
java_id integer,
|
||||
res_w integer,
|
||||
res_h integer,
|
||||
res json_text,
|
||||
force_fullscreen boolean,
|
||||
mem_max integer,
|
||||
launch_args text,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue