Event handling (#75)

* working on amcros

* fleshed out draft

* added feature support

* finished loading

* Fixed issue with multiple data types in macro

* Working, and added more loading uses

* added window scopes

* clippy, fmt

* working other variants

* fmt; clippy

* prettier

* refactored emissions to use increment

* fixed deadlock

* doc changes

* clippy, prettier

* uuid change

* restructured events to util

* loading restructure

* merge fixes

* comments mistake

* better cfg tauri feature structuring

* added extra fields to some loading enum variants

* removed Option<>

* added pack + version labels

* doc change
This commit is contained in:
Wyatt Verchere
2023-04-16 10:12:37 -07:00
committed by GitHub
parent f8173d3b78
commit b120b5cfa8
22 changed files with 3519 additions and 102 deletions

View File

@@ -45,7 +45,7 @@ pub async fn get_optimal_jre_key(profile: &Profile) -> crate::Result<String> {
.minecraft
.versions
.iter()
.find(|it| it.id == profile.metadata.game_version.as_ref())
.find(|it| it.id == profile.metadata.game_version)
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Invalid or unknown Minecraft version: {}",

View File

@@ -1,12 +1,13 @@
use crate::config::MODRINTH_API_URL;
use crate::data::ModLoader;
use crate::event::emit::{init_loading, loading_try_for_each_concurrent};
use crate::event::LoadingBarType;
use crate::state::{ModrinthProject, ModrinthVersion, SideType};
use crate::util::fetch::{
fetch, fetch_json, fetch_mirrors, write, write_cached_icon,
};
use crate::State;
use async_zip::tokio::read::seek::ZipFileReader;
use futures::TryStreamExt;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -133,19 +134,20 @@ pub async fn install_pack_from_version_id(
None
};
install_pack(file, icon, Some(version.project_id)).await
install_pack(file, icon, Some(version.project_id), Some(version.id)).await
}
pub async fn install_pack_from_file(path: PathBuf) -> crate::Result<PathBuf> {
let file = fs::read(path).await?;
install_pack(bytes::Bytes::from(file), None, None).await
install_pack(bytes::Bytes::from(file), None, None, None).await
}
async fn install_pack(
file: bytes::Bytes,
icon: Option<PathBuf>,
project_id: Option<String>,
version_id: Option<String>,
) -> crate::Result<PathBuf> {
let state = &State::get().await?;
@@ -215,22 +217,39 @@ async fn install_pack(
.into());
};
let pack_name = pack.name.clone();
let profile = crate::api::profile_create::profile_create(
pack.name,
game_version.clone(),
mod_loader.unwrap_or(ModLoader::Vanilla),
loader_version,
icon,
project_id,
project_id.clone(),
)
.await?;
let loading_bar = init_loading(
LoadingBarType::PackDownload {
pack_name ,
pack_id: project_id,
pack_version: version_id,
},
100.0,
"Downloading modpack...",
)
.await?;
let num_files = pack.files.len();
use futures::StreamExt;
futures::stream::iter(pack.files.into_iter())
.map(Ok::<PackFile, crate::Error>)
.try_for_each_concurrent(None, |project| {
loading_try_for_each_concurrent(
futures::stream::iter(pack.files.into_iter())
.map(Ok::<PackFile, crate::Error>),
None,
Some(&loading_bar),
100.0,
num_files,
None,
|project| {
let profile = profile.clone();
async move {
//TODO: Future update: prompt user for optional files in a modpack
if let Some(env) = project.env {
@@ -266,11 +285,11 @@ async fn install_pack(
_ => {}
};
}
Ok(())
}
})
.await?;
},
)
.await?;
let extract_overrides = |overrides: String| async {
let reader = Cursor::new(&file);

View File

@@ -1,6 +1,7 @@
//! Theseus profile management interface
use crate::{
auth::{self, refresh},
event::{emit::emit_profile, ProfilePayloadType},
launcher::download,
state::MinecraftChild,
};
@@ -20,6 +21,17 @@ use tokio::{fs, process::Command, sync::RwLock};
pub async fn remove(path: &Path) -> crate::Result<()> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get(path) {
emit_profile(
profile.uuid,
profile.path.clone(),
&profile.metadata.name,
ProfilePayloadType::Removed,
)
.await?;
}
profiles.remove(path).await?;
Ok(())
@@ -46,7 +58,17 @@ where
let mut profiles = state.profiles.write().await;
match profiles.0.get_mut(path) {
Some(ref mut profile) => action(profile).await,
Some(ref mut profile) => {
emit_profile(
profile.uuid,
profile.path.clone(),
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
action(profile).await
}
None => Err(crate::ErrorKind::UnmanagedProfileError(
path.display().to_string(),
)
@@ -219,7 +241,7 @@ pub async fn run_credentials(
.minecraft
.versions
.iter()
.find(|it| it.id == profile.metadata.game_version.as_ref())
.find(|it| it.id == profile.metadata.game_version)
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Invalid or unknown Minecraft version: {}",
@@ -325,6 +347,7 @@ pub async fn run_credentials(
&memory,
&resolution,
credentials,
&profile,
)
.await?;
@@ -335,8 +358,9 @@ pub async fn run_credentials(
"Process failed to stay open.".to_string(),
)
})?;
let mchild_arc =
state_children.insert_process(pid, path.to_path_buf(), mc_process);
let mchild_arc = state_children
.insert_process(pid, path.to_path_buf(), mc_process)
.await?;
Ok(mchild_arc)
}

View File

@@ -1,5 +1,9 @@
//! Theseus profile management interface
use crate::{jre, prelude::ModLoader};
use crate::{
event::{emit::emit_profile, ProfilePayloadType},
jre,
prelude::ModLoader,
};
pub use crate::{
state::{JavaSettings, Profile},
State,
@@ -135,7 +139,8 @@ pub async fn profile_create(
// Fully canonicalize now that its created for storing purposes
let path = canonicalize(&path)?;
let mut profile = Profile::new(name, game_version, path.clone()).await?;
let mut profile =
Profile::new(uuid, name, game_version, path.clone()).await?;
if let Some(ref icon) = icon {
let bytes = tokio::fs::read(icon).await?;
profile
@@ -167,9 +172,16 @@ pub async fn profile_create(
println!("Could not detect optimal JRE: {optimal_version_key}, falling back to system default.");
}
emit_profile(
uuid,
path.clone(),
&profile.metadata.name,
ProfilePayloadType::Created,
)
.await?;
{
let mut profiles = state.profiles.write().await;
profiles.insert(profile)?;
profiles.insert(profile).await?;
}
State::sync().await?;

View File

@@ -85,6 +85,9 @@ pub enum ErrorKind {
#[error("Error parsing date: {0}")]
ChronoParseError(#[from] chrono::ParseError),
#[error("Event error: {0}")]
EventError(#[from] crate::event::EventError),
#[error("Zip error: {0}")]
ZipError(#[from] async_zip::error::ZipError),

327
theseus/src/event/emit.rs Normal file
View File

@@ -0,0 +1,327 @@
use crate::event::{
EventError, LoadingBar, LoadingBarId, LoadingBarType, ProcessPayloadType,
ProfilePayloadType,
};
use futures::prelude::*;
use std::path::PathBuf;
#[cfg(feature = "tauri")]
use crate::event::{
LoadingPayload, ProcessPayload, ProfilePayload, WarningPayload,
};
#[cfg(feature = "tauri")]
use tauri::Manager;
/*
Events are a way we can communciate with the Tauri frontend from the Rust backend.
We include a feature flag for Tauri, so that we can compile this code without Tauri.
To use events, we need to do the following:
1) Make sure we are using the tauri feature flag
2) Initialize the EventState with EventState::init() *before* initializing the theseus State
3) Call emit_x functions to send events to the frontend
For emit_loading() specifically, we need to inialize the loading bar with init_loading() first and pass the received loader in
For example:
pub async fn loading_function() -> crate::Result<()> {
loading_function()).await;
}
pub async fn loading_function() -> crate::Result<()> {
let loading_bar = init_loading(LoadingBarType::StateInit, 100.0, "Loading something long...").await;
for i in 0..100 {
emit_loading(&loading_bar, 1.0, None).await?;
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
*/
// Initialize a loading bar for use in emit_loading
// This will generate a LoadingBarId, which is used to refer to the loading bar uniquely.
// total is the total amount of work to be done- all emissions will be considered a fraction of this value (should be 1 or 100 for simplicity)
// default_message is the message to display on the loading bar if no message is passed to emit_loading
pub async fn init_loading(
bar_type: LoadingBarType,
total: f64,
default_message: &str,
) -> crate::Result<LoadingBarId> {
let event_state = crate::EventState::get().await?;
let key = LoadingBarId::new(bar_type);
event_state.loading_bars.write().await.insert(
key.clone(),
LoadingBar {
loading_bar_id: key.clone(),
message: default_message.to_string(),
total,
current: 0.0,
},
);
// attempt an initial loading_emit event to the frontend
emit_loading(&key, 0.0, None).await?;
Ok(key)
}
// emit_loading emits a loading event to the frontend
// key refers to the loading bar to update
// increment refers to by what relative increment to the loading struct's total to update
// message is the message to display on the loading bar- if None, use the loading bar's default one
// By convention, fraction is the fraction of the progress bar that is filled
#[allow(unused_variables)]
pub async fn emit_loading(
key: &LoadingBarId,
increment_frac: f64,
message: Option<&str>,
) -> crate::Result<()> {
let event_state = crate::EventState::get().await?;
let mut loading_bar = event_state.loading_bars.write().await;
let loading_bar = match loading_bar.get_mut(key) {
Some(f) => f,
None => {
return Err(EventError::NoLoadingBar(key.clone()).into());
}
};
// Tick up loading bar
loading_bar.current += increment_frac;
let display_frac = loading_bar.current / loading_bar.total;
let display_frac = if display_frac > 1.0 {
None // by convention, when its done, we submit None
// any further updates will be ignored (also sending None)
} else {
Some(display_frac)
};
// Emit event to tauri
#[cfg(feature = "tauri")]
event_state
.app
.emit_all(
"loading",
LoadingPayload {
fraction: display_frac,
message: message.unwrap_or(&loading_bar.message).to_string(),
event: key.key.clone(),
loader_uuid: key.uuid,
},
)
.map_err(EventError::from)?;
Ok(())
}
// emit_warning(message)
#[allow(dead_code)]
#[allow(unused_variables)]
pub async fn emit_warning(message: &str) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get().await?;
event_state
.app
.emit_all(
"warning",
WarningPayload {
message: message.to_string(),
},
)
.map_err(EventError::from)?;
}
Ok(())
}
// emit_process(pid, event, message)
#[allow(unused_variables)]
pub async fn emit_process(
uuid: uuid::Uuid,
pid: u32,
event: ProcessPayloadType,
message: &str,
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get().await?;
event_state
.app
.emit_all(
"process",
ProcessPayload {
uuid,
pid,
event,
message: message.to_string(),
},
)
.map_err(EventError::from)?;
}
Ok(())
}
// emit_profile(path, event)
#[allow(unused_variables)]
pub async fn emit_profile(
uuid: uuid::Uuid,
path: PathBuf,
name: &str,
event: ProfilePayloadType,
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get().await?;
event_state
.app
.emit_all(
"profile",
ProfilePayload {
uuid,
path,
name: name.to_string(),
event,
},
)
.map_err(EventError::from)?;
}
Ok(())
}
// loading_join! macro
// loading_join!(key: Option<&LoadingBarId>, total: f64, message: Option<&str>; task1, task2, task3...)
// This will submit a loading event with the given message for each task as they complete
// task1, task2, task3 are async tasks that yuo want to to join on await on
// Key is the key to use for which loading bar to submit these results to- a LoadingBarId. If None, it does nothing
// Total is the total amount of progress that the loading bar should take up by all futures in this (will be split evenly amongst them).
// If message is Some(t) you will overwrite this loading bar's message with a custom one
// For example, if you want the tasks to range as 0.1, 0.2, 0.3 (of the progress bar), you would do:
// loading_join!(loading_bar, 0.1; task1, task2, task3)
// This will await on each of the tasks, and as each completes, it will emit a loading event for 0.033, 0.066, 0.099, etc
// This should function as a drop-in replacement for tokio::try_join_all! in most cases- except the function *itself* calls ? rather than needing it.
#[cfg(feature = "tauri")]
#[macro_export]
macro_rules! loading_join {
($key:expr, $total:expr, $message:expr; $($future:expr $(,)?)+) => {{
let mut num_futures = 0;
$(
{
let _ = &$future; // useless to allow matching to $future
num_futures += 1;
}
)*
let increment = $total / num_futures as f64;
// Create tokio::pinned values
$(
paste::paste! {
tokio::pin! {
let [<unique_name_ $future>] = $future;
}
}
)*
$(
paste::paste! {
let mut [<result_ $future>] = None;
}
)*
// Resolve each future and call respective loading as each resolves in any order
for _ in 0..num_futures {
paste::paste! {
tokio::select! {
$(
v = &mut [<unique_name_ $future>], if ![<result_$future>].is_some() => {
if let Some(key) = $key {
$crate::event::emit::emit_loading(key, increment, $message).await?;
}
[<result_ $future>] = Some(v);
},
)*
else => break,
}
}
}
// Extract values out of option, then out of error, returning if any errors happened
$(
paste::paste! {
let [<result_ $future>] = [<result_ $future>].take().unwrap()?; // unwrap here acceptable as numbers of futures and resolved values is guaranteed to be the same
}
)*
paste::paste!{
($(
[<result_ $future>], // unwrap here acceptable as numbers of futures and resolved values is guaranteed to be the same
)+)
}
}};
}
#[cfg(not(feature = "tauri"))]
#[macro_export]
macro_rules! loading_join {
($start:expr, $end:expr, $message:expr; $($future:expr $(,)?)+) => {{
tokio::try_join!($($future),+)?
}};
}
// A drop in replacement to try_for_each_concurrent that emits loading events as it goes
// Key is the key to use for which loading bar- a LoadingBarId. If None, does nothing
// Total is the total amount of progress that the loading bar should take up by all futures in this (will be split evenly amongst them).
// If message is Some(t) you will overwrite this loading bar's message with a custom one
// num_futs is the number of futures that will be run, which is needed as we allow Iterator to be passed in, which doesn't have a size
#[cfg(feature = "tauri")]
pub async fn loading_try_for_each_concurrent<I, F, Fut, T>(
stream: I,
limit: Option<usize>,
key: Option<&LoadingBarId>,
total: f64,
num_futs: usize, // num is in here as we allow Iterator to be passed in, which doesn't have a size
message: Option<&str>,
f: F,
) -> crate::Result<()>
where
I: futures::TryStreamExt<Error = crate::Error> + TryStream<Ok = T>,
F: FnMut(T) -> Fut + Send,
Fut: Future<Output = crate::Result<()>> + Send,
T: Send,
{
let mut f = f;
stream
.try_for_each_concurrent(limit, |item| {
let f = f(item);
async move {
f.await?;
if let Some(key) = key {
emit_loading(key, total / (num_futs as f64), message)
.await?;
}
Ok(())
}
})
.await
}
#[cfg(not(feature = "tauri"))]
pub async fn loading_try_for_each_concurrent<I, F, Fut, T>(
stream: I,
limit: Option<usize>,
_key: Option<&LoadingBarId>,
_total: f64,
_num_futs: usize, // num is in here as we allow Iterator to be passed in, which doesn't have a size
_message: Option<&str>,
f: F,
) -> crate::Result<()>
where
I: futures::TryStreamExt<Error = crate::Error> + TryStream<Ok = T>,
F: FnMut(T) -> Fut + Send,
Fut: Future<Output = crate::Result<()>> + Send,
T: Send,
{
let mut f = f;
stream
.try_for_each_concurrent(limit, |item| {
let f = f(item);
async move {
f.await?;
Ok(())
}
})
.await
}

157
theseus/src/event/mod.rs Normal file
View File

@@ -0,0 +1,157 @@
//! Theseus state management system
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt, path::PathBuf, sync::Arc};
use tokio::sync::OnceCell;
use tokio::sync::RwLock;
use uuid::Uuid;
pub mod emit;
// Global event state
// Stores the Tauri app handle and other event-related state variables
static EVENT_STATE: OnceCell<Arc<EventState>> = OnceCell::const_new();
pub struct EventState {
/// Tauri app
#[cfg(feature = "tauri")]
pub app: tauri::AppHandle,
pub loading_bars: RwLock<HashMap<LoadingBarId, LoadingBar>>,
}
impl EventState {
#[cfg(feature = "tauri")]
pub async fn init(app: tauri::AppHandle) -> crate::Result<Arc<Self>> {
EVENT_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
app,
loading_bars: RwLock::new(HashMap::new()),
}))
})
.await
.map(Arc::clone)
}
#[cfg(not(feature = "tauri"))]
pub async fn init() -> crate::Result<Arc<Self>> {
EVENT_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
loading_bars: RwLock::new(HashMap::new()),
}))
})
.await
.map(Arc::clone)
}
#[cfg(feature = "tauri")]
pub async fn get() -> crate::Result<Arc<Self>> {
Ok(EVENT_STATE.get().ok_or(EventError::NotInitialized)?.clone())
}
// Initialization requires no app handle in non-tauri mode, so we can just use the same function
#[cfg(not(feature = "tauri"))]
pub async fn get() -> crate::Result<Arc<Self>> {
Self::init().await
}
}
#[derive(Debug, Clone)]
pub struct LoadingBar {
pub loading_bar_id: LoadingBarId,
pub message: String,
pub total: f64,
pub current: f64,
}
// Loading Bar Id lets us uniquely identify loading bars stored in the state
// the uuid lets us identify loading bars across threads
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
pub struct LoadingBarId {
pub key: LoadingBarType,
pub uuid: Uuid,
}
impl LoadingBarId {
pub fn new(key: LoadingBarType) -> Self {
Self {
key,
uuid: Uuid::new_v4(),
}
}
}
impl fmt::Display for LoadingBarId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}-{}", self.key, self.uuid)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
pub enum LoadingBarType {
StateInit,
PackDownload {
pack_name: String,
pack_id: Option<String>,
pack_version: Option<String>,
},
MinecraftDownload {
profile_uuid: Uuid,
profile_name: String,
},
ProfileSync,
}
#[derive(Serialize, Clone)]
pub struct LoadingPayload {
pub event: LoadingBarType,
pub loader_uuid: Uuid,
pub fraction: Option<f64>, // by convention, if optional, it means the loading is done
pub message: String,
}
#[derive(Serialize, Clone)]
pub struct WarningPayload {
pub message: String,
}
#[derive(Serialize, Clone)]
pub struct ProcessPayload {
pub uuid: Uuid, // processes in state are going to be identified by UUIDs, as they might change to different processes
pub pid: u32,
pub event: ProcessPayloadType,
pub message: String,
}
#[derive(Serialize, Clone)]
pub enum ProcessPayloadType {
Launched,
// Finishing, // TODO: process restructing incoming, currently this is never emitted
// Finished, // TODO: process restructing incoming, currently this is never emitted
}
#[derive(Serialize, Clone)]
pub struct ProfilePayload {
pub uuid: Uuid,
pub path: PathBuf,
pub name: String,
pub event: ProfilePayloadType,
}
#[derive(Serialize, Clone)]
pub enum ProfilePayloadType {
Created,
Added, // also triggered when Created
Edited,
Removed,
}
#[derive(Debug, thiserror::Error)]
pub enum EventError {
#[error("Event state was not properly initialized")]
NotInitialized,
#[error("Non-existent loading bar of key: {0}")]
NoLoadingBar(LoadingBarId),
#[cfg(feature = "tauri")]
#[error("Tauri error: {0}")]
TauriError(#[from] tauri::Error),
}

View File

@@ -1,6 +1,11 @@
//! Downloader for Minecraft data
use crate::{
event::{
emit::{emit_loading, init_loading, loading_try_for_each_concurrent},
LoadingBarId, LoadingBarType,
},
process::Profile,
state::State,
util::{fetch::*, platform::OsExt},
};
@@ -19,14 +24,25 @@ use tokio::{fs, sync::OnceCell};
pub async fn download_minecraft(
st: &State,
version: &GameVersionInfo,
profile: &Profile,
) -> crate::Result<()> {
log::info!("Downloading Minecraft version {}", version.id);
let assets_index = download_assets_index(st, version).await?;
let loading_bar = init_loading(
LoadingBarType::MinecraftDownload {
// If we are downloading minecraft for a profile, provide its name and uuid
profile_name: profile.metadata.name.clone(),
profile_uuid: profile.uuid,
},
100.0,
"Downloading Minecraft...",
)
.await?;
tokio::try_join! {
download_client(st, version),
download_assets(st, version.assets == "legacy", &assets_index),
download_libraries(st, version.libraries.as_slice(), &version.id)
download_client(st, version, Some(&loading_bar)),
download_assets(st, version.assets == "legacy", &assets_index, Some(&loading_bar)),
download_libraries(st, version.libraries.as_slice(), &version.id, Some(&loading_bar))
}?;
log::info!("Done downloading Minecraft!");
@@ -74,6 +90,7 @@ pub async fn download_version_info(
pub async fn download_client(
st: &State,
version_info: &GameVersionInfo,
loading_bar: Option<&LoadingBarId>,
) -> crate::Result<()> {
let version = &version_info.id;
log::debug!("Locating client for version {version}");
@@ -101,6 +118,9 @@ pub async fn download_client(
write(&path, &bytes, &st.io_semaphore).await?;
log::info!("Fetched client version {version}");
}
if let Some(loading_bar) = loading_bar {
emit_loading(loading_bar, 20.0, None).await?;
}
log::debug!("Client loaded for version {version}!");
Ok(())
@@ -138,11 +158,20 @@ pub async fn download_assets(
st: &State,
with_legacy: bool,
index: &AssetsIndex,
loading_bar: Option<&LoadingBarId>,
) -> crate::Result<()> {
log::debug!("Loading assets");
stream::iter(index.objects.iter())
.map(Ok::<(&String, &Asset), crate::Error>)
.try_for_each_concurrent(None, |(name, asset)| async move {
let num_futs = index.objects.len();
let assets = stream::iter(index.objects.iter())
.map(Ok::<(&String, &Asset), crate::Error>);
loading_try_for_each_concurrent(assets,
None,
loading_bar,
50.0,
num_futs,
None,
|(name, asset)| async move {
let hash = &asset.hash;
let resource_path = st.directories.object_dir(hash);
let url = format!(
@@ -190,6 +219,7 @@ pub async fn download_libraries(
st: &State,
libraries: &[Library],
version: &str,
loading_bar: Option<&LoadingBarId>,
) -> crate::Result<()> {
log::debug!("Loading libraries");
@@ -197,10 +227,10 @@ pub async fn download_libraries(
fs::create_dir_all(st.directories.libraries_dir()),
fs::create_dir_all(st.directories.version_natives_dir(version))
}?;
let num_files = libraries.len();
loading_try_for_each_concurrent(
stream::iter(libraries.iter())
.map(Ok::<&Library, crate::Error>)
.try_for_each_concurrent(None, |library| async move {
.map(Ok::<&Library, crate::Error>), None, loading_bar,50.0,num_files, None,|library| async move {
if let Some(rules) = &library.rules {
if !rules.iter().all(super::parse_rule) {
return Ok(());

View File

@@ -2,6 +2,7 @@
use crate::{process, state as st};
use daedalus as d;
use dunce::canonicalize;
use st::Profile;
use std::{path::Path, process::Stdio};
use tokio::process::{Child, Command};
@@ -57,6 +58,7 @@ pub async fn launch_minecraft(
memory: &st::MemorySettings,
resolution: &st::WindowSize,
credentials: &auth::Credentials,
profile: &Profile, // optional ref to Profile for event tracking
) -> crate::Result<Child> {
let state = st::State::get().await?;
let instance_path = &canonicalize(instance_path)?;
@@ -88,7 +90,7 @@ pub async fn launch_minecraft(
.version_dir(&version_jar)
.join(format!("{version_jar}.jar"));
download::download_minecraft(&state, &version_info).await?;
download::download_minecraft(&state, &version_info, profile).await?;
st::State::sync().await?;
if let Some(processors) = &version_info.processors {

View File

@@ -13,9 +13,11 @@ mod util;
mod api;
mod config;
mod error;
mod event;
mod launcher;
mod state;
pub use api::*;
pub use error::*;
pub use event::EventState;
pub use state::State;

View File

@@ -4,6 +4,9 @@ use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{ChildStderr, ChildStdout};
use tokio::sync::RwLock;
use crate::event::emit::emit_process;
use crate::event::ProcessPayloadType;
use super::Profile;
// Child processes (instances of Minecraft)
@@ -13,6 +16,7 @@ pub struct Children(HashMap<u32, Arc<RwLock<MinecraftChild>>>);
// Minecraft Child, bundles together the PID, the actual Child, and the easily queryable stdout and stderr streams
#[derive(Debug)]
pub struct MinecraftChild {
pub uuid: uuid::Uuid,
pub pid: u32,
pub profile_path: PathBuf, //todo: make UUID when profiles are recognized by UUID
pub child: tokio::process::Child,
@@ -28,12 +32,14 @@ impl Children {
// Inserts a child process to keep track of, and returns a reference to the container struct MinecraftChild
// The threads for stdout and stderr are spawned here
// Unlike a Hashmap's 'insert', this directly returns the reference to the Child rather than any previously stored Child that may exist
pub fn insert_process(
pub async fn insert_process(
&mut self,
pid: u32,
profile_path: PathBuf,
mut child: tokio::process::Child,
) -> Arc<RwLock<MinecraftChild>> {
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
let uuid = uuid::Uuid::new_v4();
// Create std watcher threads for stdout and stderr
let stdout = SharedOutput::new();
if let Some(child_stdout) = child.stdout.take() {
@@ -54,8 +60,17 @@ impl Children {
});
}
emit_process(
uuid,
pid,
ProcessPayloadType::Launched,
"Launched Minecraft",
)
.await?;
// Create MinecraftChild
let mchild = MinecraftChild {
uuid,
pid,
profile_path,
child,
@@ -64,7 +79,7 @@ impl Children {
};
let mchild = Arc::new(RwLock::new(mchild));
self.0.insert(pid, mchild.clone());
mchild
Ok(mchild)
}
// Returns a ref to the child

View File

@@ -1,6 +1,12 @@
//! Theseus state management system
use crate::config::sled_config;
use crate::event::emit::emit_loading;
use crate::event::emit::init_loading;
use crate::event::LoadingBarType;
use crate::jre;
use crate::loading_join;
use std::sync::Arc;
use tokio::sync::{OnceCell, RwLock, Semaphore};
@@ -68,6 +74,8 @@ impl State {
LAUNCHER_STATE
.get_or_try_init(|| {
async {
let loading_bar = init_loading(LoadingBarType::StateInit, 100.0, "Initializing launcher...").await?;
// Directories
let directories = DirectoryInfo::init().await?;
@@ -77,6 +85,8 @@ impl State {
.path(directories.database_file())
.open()?;
emit_loading(&loading_bar, 10.0, None).await?;
// Settings
let mut settings =
Settings::init(&directories.settings_file()).await?;
@@ -87,11 +97,17 @@ impl State {
let io_semaphore =
RwLock::new(Semaphore::new(io_semaphore_max));
let metadata_fut = Metadata::init(&database);
let profiles_fut =
Profiles::init(&directories, &io_semaphore);
// Launcher data
let (metadata, profiles) = tokio::try_join! {
Metadata::init(&database),
Profiles::init(&directories, &io_semaphore),
}?;
let (metadata, profiles) = loading_join! {
Some(&loading_bar), 20.0, Some("Initializing metadata and profiles...");
metadata_fut, profiles_fut
};
emit_loading(&loading_bar, 10.0, None).await?;
let users = Users::init(&database)?;
let children = Children::new();
@@ -101,13 +117,16 @@ impl State {
// On launcher initialization, attempt a tag fetch after tags init
let mut tags = Tags::init(&database)?;
if let Err(tag_fetch_err) =
tags.fetch_update(&io_semaphore).await
tags.fetch_update(&io_semaphore,Some(&loading_bar)).await
{
tracing::error!(
"Failed to fetch tags on launcher init: {}",
tag_fetch_err
);
};
emit_loading(&loading_bar, 10.0, None).await?;
// On launcher initialization, if global java variables are unset, try to find and set them
// (they are required for the game to launch)
if settings.java_globals.count() == 0 {

View File

@@ -1,6 +1,10 @@
use super::settings::{Hooks, MemorySettings, WindowSize};
use crate::config::MODRINTH_API_URL;
use crate::data::DirectoryInfo;
use crate::event::emit::{
emit_profile, init_loading, loading_try_for_each_concurrent,
};
use crate::event::{LoadingBarType, ProfilePayloadType};
use crate::state::projects::Project;
use crate::state::{ModrinthVersion, ProjectType};
use crate::util::fetch::{fetch, fetch_json, write, write_cached_icon};
@@ -17,6 +21,7 @@ use std::{
};
use tokio::sync::Semaphore;
use tokio::{fs, sync::RwLock};
use uuid::Uuid;
const PROFILE_JSON_PATH: &str = "profile.json";
@@ -28,6 +33,7 @@ pub const CURRENT_FORMAT_VERSION: u32 = 1;
// Represent a Minecraft instance.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Profile {
pub uuid: Uuid, // todo: will be used in restructure to refer to profiles
pub path: PathBuf,
pub metadata: ProfileMetadata,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -90,6 +96,7 @@ pub struct JavaSettings {
impl Profile {
#[tracing::instrument]
pub async fn new(
uuid: Uuid,
name: String,
version: String,
path: PathBuf,
@@ -102,6 +109,7 @@ impl Profile {
}
Ok(Self {
uuid,
path: canonicalize(path)?,
metadata: ProfileMetadata {
name,
@@ -368,7 +376,14 @@ impl Profiles {
}
#[tracing::instrument(skip(self))]
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
emit_profile(
profile.uuid,
profile.path.clone(),
&profile.metadata.name,
ProfilePayloadType::Added,
)
.await?;
self.0.insert(
canonicalize(&profile.path)?
.to_str()
@@ -387,6 +402,7 @@ impl Profiles {
path: &'a Path,
) -> crate::Result<&Self> {
self.insert(Self::read_profile_from_dir(&canonicalize(path)?).await?)
.await
}
#[tracing::instrument(skip(self))]
@@ -404,9 +420,21 @@ impl Profiles {
#[tracing::instrument(skip_all)]
pub async fn sync(&self) -> crate::Result<&Self> {
stream::iter(self.0.iter())
.map(Ok::<_, crate::Error>)
.try_for_each_concurrent(None, |(path, profile)| async move {
let loading_bar = init_loading(
LoadingBarType::ProfileSync,
100.0,
"Syncing profiles...",
)
.await?;
let num_futs = self.0.len();
loading_try_for_each_concurrent(
stream::iter(self.0.iter()).map(Ok::<_, crate::Error>),
None,
Some(&loading_bar),
100.0,
num_futs,
None,
|(path, profile)| async move {
let json = serde_json::to_vec(&profile)?;
let json_path = Path::new(&path.to_string_lossy().to_string())
@@ -414,8 +442,9 @@ impl Profiles {
fs::write(json_path, json).await?;
Ok::<_, crate::Error>(())
})
.await?;
},
)
.await?;
Ok(self)
}

View File

@@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize};
use tokio::sync::{RwLock, Semaphore};
use crate::config::{BINCODE_CONFIG, MODRINTH_API_URL};
use crate::event::LoadingBarId;
use crate::loading_join;
use crate::util::fetch::fetch_json;
const CATEGORIES_DB_TREE: &[u8] = b"categories";
@@ -139,6 +141,7 @@ impl Tags {
pub async fn fetch_update(
&mut self,
semaphore: &RwLock<Semaphore>,
loading_bar: Option<&LoadingBarId>,
) -> crate::Result<()> {
let categories = format!("{MODRINTH_API_URL}tag/category");
let loaders = format!("{MODRINTH_API_URL}tag/loader");
@@ -147,6 +150,50 @@ impl Tags {
let donation_platforms =
format!("{MODRINTH_API_URL}tag/donation_platform");
let report_types = format!("{MODRINTH_API_URL}tag/report_type");
let categories_fut = fetch_json::<Vec<Category>>(
Method::GET,
&categories,
None,
None,
semaphore,
);
let loaders_fut = fetch_json::<Vec<Loader>>(
Method::GET,
&loaders,
None,
None,
semaphore,
);
let game_versions_fut = fetch_json::<Vec<GameVersion>>(
Method::GET,
&game_versions,
None,
None,
semaphore,
);
let licenses_fut = fetch_json::<Vec<License>>(
Method::GET,
&licenses,
None,
None,
semaphore,
);
let donation_platforms_fut = fetch_json::<Vec<DonationPlatform>>(
Method::GET,
&donation_platforms,
None,
None,
semaphore,
);
let report_types_fut = fetch_json::<Vec<String>>(
Method::GET,
&report_types,
None,
None,
semaphore,
);
let (
categories,
loaders,
@@ -154,50 +201,14 @@ impl Tags {
licenses,
donation_platforms,
report_types,
) = tokio::try_join!(
fetch_json::<Vec<Category>>(
Method::GET,
&categories,
None,
None,
semaphore
),
fetch_json::<Vec<Loader>>(
Method::GET,
&loaders,
None,
None,
semaphore
),
fetch_json::<Vec<GameVersion>>(
Method::GET,
&game_versions,
None,
None,
semaphore
),
fetch_json::<Vec<License>>(
Method::GET,
&licenses,
None,
None,
semaphore
),
fetch_json::<Vec<DonationPlatform>>(
Method::GET,
&donation_platforms,
None,
None,
semaphore
),
fetch_json::<Vec<String>>(
Method::GET,
&report_types,
None,
None,
semaphore
),
)?;
) = loading_join!(loading_bar, 0.5, None;
categories_fut,
loaders_fut,
game_versions_fut,
licenses_fut,
donation_platforms_fut,
report_types_fut
);
// Store the tags in the database
self.0.categories.insert(