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

2
Cargo.lock generated
View File

@@ -3537,6 +3537,7 @@ dependencies = [
"futures",
"lazy_static",
"log",
"paste",
"regex",
"reqwest",
"serde",
@@ -3545,6 +3546,7 @@ dependencies = [
"sha2 0.9.9",
"sled",
"sys-info",
"tauri",
"thiserror",
"tokio",
"tokio-stream",

View File

@@ -31,6 +31,8 @@ thiserror = "1.0"
tracing = "0.1"
tracing-error = "0.2"
tauri = { version = "1.2", optional = true}
paste = { version = "1.0", optional = true}
async-tungstenite = { version = "0.20.0", features = ["tokio-runtime", "tokio-native-tls"] }
futures = "0.3"
@@ -45,3 +47,4 @@ dunce = "1.0.3"
winreg = "0.11.0"
[features]
tauri = ["dep:tauri", "dep:paste"]

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(

2689
theseus_gui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ tauri-build = { version = "1.2", features = [] }
regex = "1.5"
[dependencies]
theseus = { path = "../../theseus" }
theseus = { path = "../../theseus", features = ["tauri"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -7,7 +7,6 @@ use theseus::prelude::*;
#[tauri::command]
pub async fn profile_create_empty() -> Result<PathBuf> {
let res = profile_create::profile_create_empty().await?;
State::sync().await?;
Ok(res)
}

View File

@@ -9,7 +9,8 @@ mod api;
// Should be called in launcher initialization
#[tauri::command]
async fn initialize_state() -> api::Result<()> {
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
theseus::EventState::init(app).await?;
State::get().await?;
Ok(())
}

View File

@@ -0,0 +1,80 @@
/*
Event listeners for interacting with the Rust api
These are all async functions that return a promise that resolves to the payload object (whatever Rust is trying to deliver)
*/
/*
callback is a function that takes a single argument, which is the payload object (whatever Rust is trying to deliver)
You can call these to await any kind of emitted signal from Rust, and then do something with the payload object
An example place to put this is at the start of main.js before the state is initialized- that way
you can listen for any emitted signal from Rust and do something with it as the state is being initialized
Example:
import { loading_listener } from '@/helpers/events'
await loading_listener((event) => {
// event.event is the event name (useful if you want to use a single callback fn for multiple event types)
// event.payload is the payload object
console.log(event)
})
Putting that in a script will print any emitted signal from rust
*/
import { listen } from '@tauri-apps/api/event'
/// Payload for the 'loading' event
/*
LoadingPayload {
event: "StateInit", "PackDownload", etc
- Certain states have additional fields:
- PackDownload: {
pack_name: name of the pack
pack_id, optional, the id of the modpack
pack_version, optional, the version of the modpack
- MinecraftDownload: {
profile_name: name of the profile
profile_uuid: unique identification of the profile
loader_uuid: unique identification of the loading bar
fraction: number, (as a fraction of 1, how much we'vel oaded so far). If null, by convention, loading is finished
message: message to display to the user
}
*/
export async function loading_listener(callback) {
return await listen('loading', (event) => callback(event.payload))
}
/// Payload for the 'process' event
/*
ProcessPayload {
uuid: unique identification of the process in the state (currently identified by PID, but that will change)
pid: process ID
event: event type ("Launched", "Finished")
message: message to display to the user
}
*/
export async function process_listener(callback) {
return await listen('process', (event) => callback(event.payload))
}
/// Payload for the 'profile' event
/*
ProfilePayload {
uuid: unique identification of the process in the state (currently identified by path, but that will change)
name: name of the profile
path: path to profile
event: event type ("Created", "Added", "Edited", "Removed")
}
*/
export async function profile_listener(callback) {
return await listen('profile', (event) => callback(event.payload))
}
/// Payload for the 'warning' event
/*
WarningPayload {
message: message to display to the user
}
*/
export async function warning_listener(callback) {
return await listen('warning', (event) => callback(event.payload))
}

View File

@@ -118,23 +118,16 @@ async fn main() -> theseus::Result<()> {
println!("Minecraft PID: {}", pid);
// Wait 5 seconds
println!("Waiting 20 seconds to gather logs...");
sleep(Duration::from_secs(20)).await;
let stdout = process::get_stdout_by_pid(pid).await?;
println!("Logs after 5sec <<< {stdout} >>> end stdout");
println!("Waiting 5 seconds to gather logs...");
sleep(Duration::from_secs(5)).await;
let _stdout = process::get_stdout_by_pid(pid).await?;
let _stderr = process::get_stderr_by_pid(pid).await?;
// println!("Logs after 5sec <<< {stdout} >>> end stdout");
println!(
"All running process PIDs {:?}",
process::get_all_running_pids().await?
);
println!(
"All running process paths {:?}",
process::get_all_running_profile_paths().await?
);
println!(
"All running process profiles {:?}",
process::get_all_running_profiles().await?
);
// hold the lock to the process until it ends
println!("Waiting for process to end...");