You've already forked AstralRinth
forked from didirus/AstralRinth
Bugs again (#703)
* initial * more fixes * logs * more fixes * working rescuer * minor log display fix * mac fixes * minor fix * libsselinux1 * linux error * actions test * more bugs. Modpack page! BIG changes * changed minimum 64 -> 8 * removed modpack page moved to modal * removed unnecessary css * mac compile * many revs * Merge colorful logs (#725) * make implementation not dumb * run prettier * null -> true * Add line numbers & make errors more robust. * improvments * changes; virtual scroll --------- Co-authored-by: qtchaos <72168435+qtchaos@users.noreply.github.com> * omorphia colors, comments fix * fixes; _JAVA_OPTIONS * revs * mac specific * more mac * some fixes * quick fix * add java reinstall option --------- Co-authored-by: qtchaos <72168435+qtchaos@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
//! Authentication flow interface
|
||||
use crate::{hydra::init::DeviceLoginSuccess, launcher::auth as inner, State};
|
||||
use crate::{
|
||||
hydra::{self, init::DeviceLoginSuccess},
|
||||
launcher::auth as inner,
|
||||
State,
|
||||
};
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::state::AuthTask;
|
||||
@@ -44,20 +48,34 @@ pub async fn refresh(user: uuid::Uuid) -> crate::Result<Credentials> {
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let fetch_semaphore = &state.fetch_semaphore;
|
||||
if Utc::now() > credentials.expires
|
||||
&& inner::refresh_credentials(&mut credentials, fetch_semaphore)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
users.remove(credentials.id).await?;
|
||||
let offline = *state.offline.read().await;
|
||||
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Please re-authenticate with your Minecraft account!".to_string(),
|
||||
)
|
||||
.as_error());
|
||||
if !offline {
|
||||
let fetch_semaphore: &crate::util::fetch::FetchSemaphore =
|
||||
&state.fetch_semaphore;
|
||||
if Utc::now() > credentials.expires
|
||||
&& inner::refresh_credentials(&mut credentials, fetch_semaphore)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
users.remove(credentials.id).await?;
|
||||
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Please re-authenticate with your Minecraft account!"
|
||||
.to_string(),
|
||||
)
|
||||
.as_error());
|
||||
}
|
||||
|
||||
// Update player info from bearer token
|
||||
let player_info = hydra::stages::player_info::fetch_info(&credentials.access_token).await.map_err(|_err| {
|
||||
crate::ErrorKind::HydraError("No Minecraft account for your profile. Make sure you own the game and have set a username through the official Minecraft launcher."
|
||||
.to_string())
|
||||
})?;
|
||||
|
||||
credentials.username = player_info.name;
|
||||
users.insert(&credentials).await?;
|
||||
}
|
||||
users.insert(&credentials).await?;
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::state::CredentialsStore;
|
||||
use crate::util::fetch::{fetch_advanced, fetch_json};
|
||||
|
||||
use crate::util::io;
|
||||
use crate::util::jre::extract_java_majorminor_version;
|
||||
use crate::{
|
||||
state::JavaGlobals,
|
||||
@@ -92,7 +93,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
let packages = fetch_json::<Vec<Package>>(
|
||||
Method::GET,
|
||||
&format!(
|
||||
"https://api.azul.com/metadata/v1/zulu/packages?arch={}&java_version={}&os={}&archive_type=zip&javafx_bundled=false&java_package_type=jdk&page_size=1",
|
||||
"https://api.azul.com/metadata/v1/zulu/packages?arch={}&java_version={}&os={}&archive_type=zip&javafx_bundled=false&java_package_type=jre&page_size=1",
|
||||
std::env::consts::ARCH, java_version, std::env::consts::OS
|
||||
),
|
||||
None,
|
||||
@@ -124,6 +125,17 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
))
|
||||
})?;
|
||||
|
||||
// removes the old installation of java
|
||||
if let Some(file) = archive.file_names().next() {
|
||||
if let Some(dir) = file.split("/").next() {
|
||||
let path = path.join(dir);
|
||||
|
||||
if path.exists() {
|
||||
io::remove_dir_all(path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit_loading(&loading_bar, 0.0, Some("Extracting java")).await?;
|
||||
archive.extract(&path).map_err(|_| {
|
||||
crate::Error::from(crate::ErrorKind::InputError(
|
||||
@@ -180,6 +192,20 @@ pub async fn check_jre(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
|
||||
Ok(jre::check_java_at_filepath(&path).await)
|
||||
}
|
||||
|
||||
// Test JRE at a given path
|
||||
pub async fn test_jre(
|
||||
path: PathBuf,
|
||||
major_version: u32,
|
||||
minor_version: u32,
|
||||
) -> crate::Result<bool> {
|
||||
let jre = match jre::check_java_at_filepath(&path).await {
|
||||
Some(jre) => jre,
|
||||
None => return Ok(false),
|
||||
};
|
||||
let (major, minor) = extract_java_majorminor_version(&jre.version)?;
|
||||
Ok(major == major_version && minor == minor_version)
|
||||
}
|
||||
|
||||
// Gets maximum memory in KiB.
|
||||
pub async fn get_max_memory() -> crate::Result<u64> {
|
||||
Ok(sys_info::mem_info()
|
||||
|
||||
@@ -1,30 +1,70 @@
|
||||
use std::io::{Read, SeekFrom};
|
||||
|
||||
use crate::{
|
||||
prelude::Credentials,
|
||||
util::io::{self, IOError},
|
||||
{state::ProfilePathId, State},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use futures::TryFutureExt;
|
||||
use serde::Serialize;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncReadExt, AsyncSeekExt},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Logs {
|
||||
pub datetime_string: String,
|
||||
pub output: Option<String>,
|
||||
pub filename: String,
|
||||
pub output: Option<CensoredString>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct LatestLogCursor {
|
||||
pub cursor: u64,
|
||||
pub output: CensoredString,
|
||||
pub new_file: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)] // Not deserialize
|
||||
#[serde(transparent)]
|
||||
pub struct CensoredString(String);
|
||||
impl CensoredString {
|
||||
pub fn censor(mut s: String, credentials_set: &Vec<Credentials>) -> Self {
|
||||
let username = whoami::username();
|
||||
s = s
|
||||
.replace(&format!("/{}/", username), "/{COMPUTER_USERNAME}/")
|
||||
.replace(&format!("\\{}\\", username), "\\{COMPUTER_USERNAME}\\");
|
||||
for credentials in credentials_set {
|
||||
s = s
|
||||
.replace(&credentials.access_token, "{MINECRAFT_ACCESS_TOKEN}")
|
||||
.replace(&credentials.username, "{MINECRAFT_USERNAME}")
|
||||
.replace(
|
||||
&credentials.id.as_simple().to_string(),
|
||||
"{MINECRAFT_UUID}",
|
||||
)
|
||||
.replace(
|
||||
&credentials.id.as_hyphenated().to_string(),
|
||||
"{MINECRAFT_UUID}",
|
||||
);
|
||||
}
|
||||
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Logs {
|
||||
async fn build(
|
||||
profile_subpath: &ProfilePathId,
|
||||
datetime_string: String,
|
||||
filename: String,
|
||||
clear_contents: Option<bool>,
|
||||
) -> crate::Result<Self> {
|
||||
Ok(Self {
|
||||
output: if clear_contents.unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
get_output_by_datetime(profile_subpath, &datetime_string)
|
||||
.await?,
|
||||
)
|
||||
Some(get_output_by_filename(profile_subpath, &filename).await?)
|
||||
},
|
||||
datetime_string,
|
||||
filename,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -51,33 +91,31 @@ pub async fn get_logs(
|
||||
for entry in std::fs::read_dir(&logs_folder)
|
||||
.map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
{
|
||||
let entry =
|
||||
let entry: std::fs::DirEntry =
|
||||
entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Some(datetime_string) = path.file_name() {
|
||||
logs.push(
|
||||
Logs::build(
|
||||
&profile_path,
|
||||
datetime_string.to_string_lossy().to_string(),
|
||||
clear_contents,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
if let Some(file_name) = path.file_name() {
|
||||
let file_name = file_name.to_string_lossy().to_string();
|
||||
|
||||
logs.push(
|
||||
Logs::build(&profile_path, file_name, clear_contents).await,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut logs = logs.into_iter().collect::<crate::Result<Vec<Logs>>>()?;
|
||||
logs.sort_by_key(|x| x.datetime_string.clone());
|
||||
logs.sort_by_key(|x| x.filename.clone());
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs_by_datetime(
|
||||
pub async fn get_logs_by_filename(
|
||||
profile_path: ProfilePathId,
|
||||
datetime_string: String,
|
||||
filename: String,
|
||||
) -> crate::Result<Logs> {
|
||||
let profile_path =
|
||||
if let Some(p) = crate::profile::get(&profile_path, None).await? {
|
||||
@@ -89,23 +127,66 @@ pub async fn get_logs_by_datetime(
|
||||
.into());
|
||||
};
|
||||
Ok(Logs {
|
||||
output: Some(
|
||||
get_output_by_datetime(&profile_path, &datetime_string).await?,
|
||||
),
|
||||
datetime_string,
|
||||
output: Some(get_output_by_filename(&profile_path, &filename).await?),
|
||||
filename,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_output_by_datetime(
|
||||
pub async fn get_output_by_filename(
|
||||
profile_subpath: &ProfilePathId,
|
||||
datetime_string: &str,
|
||||
) -> crate::Result<String> {
|
||||
file_name: &str,
|
||||
) -> crate::Result<CensoredString> {
|
||||
let state = State::get().await?;
|
||||
let logs_folder =
|
||||
state.directories.profile_logs_dir(profile_subpath).await?;
|
||||
let path = logs_folder.join(datetime_string).join("stdout.log");
|
||||
Ok(io::read_to_string(&path).await?)
|
||||
let path = logs_folder.join(file_name);
|
||||
|
||||
let credentials: Vec<Credentials> =
|
||||
state.users.read().await.clone().0.into_values().collect();
|
||||
|
||||
// Load .gz file into String
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "gz" {
|
||||
let file = std::fs::File::open(&path)
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let mut contents = [0; 1024];
|
||||
let mut result = String::new();
|
||||
let mut gz =
|
||||
flate2::read::GzDecoder::new(std::io::BufReader::new(file));
|
||||
|
||||
while gz
|
||||
.read(&mut contents)
|
||||
.map_err(|e| IOError::with_path(e, &path))?
|
||||
> 0
|
||||
{
|
||||
result.push_str(&String::from_utf8_lossy(&contents));
|
||||
contents = [0; 1024];
|
||||
}
|
||||
return Ok(CensoredString::censor(result, &credentials));
|
||||
} else if ext == "log" {
|
||||
let mut result = String::new();
|
||||
let mut contents = [0; 1024];
|
||||
let mut file = std::fs::File::open(&path)
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
// iteratively read the file to a String
|
||||
while file
|
||||
.read(&mut contents)
|
||||
.map_err(|e| IOError::with_path(e, &path))?
|
||||
> 0
|
||||
{
|
||||
result.push_str(&String::from_utf8_lossy(&contents));
|
||||
contents = [0; 1024];
|
||||
}
|
||||
let result = CensoredString::censor(result, &credentials);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
Err(crate::ErrorKind::OtherError(format!(
|
||||
"File extension not supported: {}",
|
||||
path.display()
|
||||
))
|
||||
.into())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -135,9 +216,9 @@ pub async fn delete_logs(profile_path: ProfilePathId) -> crate::Result<()> {
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs_by_datetime(
|
||||
pub async fn delete_logs_by_filename(
|
||||
profile_path: ProfilePathId,
|
||||
datetime_string: &str,
|
||||
filename: &str,
|
||||
) -> crate::Result<()> {
|
||||
let profile_path =
|
||||
if let Some(p) = crate::profile::get(&profile_path, None).await? {
|
||||
@@ -151,7 +232,71 @@ pub async fn delete_logs_by_datetime(
|
||||
|
||||
let state = State::get().await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
|
||||
let path = logs_folder.join(datetime_string);
|
||||
let path = logs_folder.join(filename);
|
||||
io::remove_dir_all(&path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_latest_log_cursor(
|
||||
profile_path: ProfilePathId,
|
||||
mut cursor: u64, // 0 to start at beginning of file
|
||||
) -> crate::Result<LatestLogCursor> {
|
||||
let profile_path =
|
||||
if let Some(p) = crate::profile::get(&profile_path, None).await? {
|
||||
p.profile_id()
|
||||
} else {
|
||||
return Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_path.to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
let state = State::get().await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
|
||||
let path = logs_folder.join("latest.log");
|
||||
if !path.exists() {
|
||||
// Allow silent failure if latest.log doesn't exist (as the instance may have been launched, but not yet created the file)
|
||||
return Ok(LatestLogCursor {
|
||||
cursor: 0,
|
||||
new_file: false,
|
||||
output: CensoredString("".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
let mut file = File::open(&path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let metadata = file
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
|
||||
let mut new_file = false;
|
||||
if cursor > metadata.len() {
|
||||
// Cursor is greater than file length, reset cursor to 0
|
||||
// Likely cause is that the file was rotated while the log was being read
|
||||
cursor = 0;
|
||||
new_file = true;
|
||||
}
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
file.seek(SeekFrom::Start(cursor))
|
||||
.map_err(|e| IOError::with_path(e, &path))
|
||||
.await?; // Seek to cursor
|
||||
let bytes_read = file
|
||||
.read_to_end(&mut buffer)
|
||||
.map_err(|e| IOError::with_path(e, &path))
|
||||
.await?; // Read to end of file
|
||||
let output = String::from_utf8_lossy(&buffer).to_string(); // Convert to String
|
||||
let cursor = cursor + bytes_read as u64; // Update cursor
|
||||
|
||||
let credentials: Vec<Credentials> =
|
||||
state.users.read().await.clone().0.into_values().collect();
|
||||
let output = CensoredString::censor(output, &credentials);
|
||||
Ok(LatestLogCursor {
|
||||
cursor,
|
||||
new_file,
|
||||
output,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -218,6 +218,10 @@ async fn import_atlauncher_unmanaged(
|
||||
prof.metadata.linked_data = Some(LinkedData {
|
||||
project_id: description.project_id.clone(),
|
||||
version_id: description.version_id.clone(),
|
||||
locked: Some(
|
||||
description.project_id.is_some()
|
||||
&& description.version_id.is_some(),
|
||||
),
|
||||
});
|
||||
prof.metadata.icon = description.icon.clone();
|
||||
prof.metadata.game_version = game_version.clone();
|
||||
|
||||
@@ -306,6 +306,7 @@ async fn import_mmc_unmanaged(
|
||||
&description,
|
||||
&backup_name,
|
||||
&dependencies,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ pub async fn recache_icon(
|
||||
}
|
||||
}
|
||||
|
||||
async fn copy_dotminecraft(
|
||||
pub async fn copy_dotminecraft(
|
||||
profile_path_id: ProfilePathId,
|
||||
dotminecraft: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
|
||||
@@ -153,6 +153,7 @@ pub fn get_profile_from_pack(
|
||||
linked_data: Some(LinkedData {
|
||||
project_id: Some(project_id),
|
||||
version_id: Some(version_id),
|
||||
locked: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -179,20 +180,29 @@ pub async fn generate_pack_from_version_id(
|
||||
title: String,
|
||||
icon_url: Option<String>,
|
||||
profile_path: ProfilePathId,
|
||||
|
||||
// Existing loading bar. Unlike when existing_loading_bar is used, this one is pre-initialized with PackFileDownload
|
||||
// For example, you might use this if multiple packs are being downloaded at once and you want to use the same loading bar
|
||||
initialized_loading_bar: Option<LoadingBarId>,
|
||||
) -> crate::Result<CreatePack> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let loading_bar = init_loading(
|
||||
LoadingBarType::PackFileDownload {
|
||||
profile_path: profile_path.get_full_path().await?,
|
||||
pack_name: title,
|
||||
icon: icon_url,
|
||||
pack_version: version_id.clone(),
|
||||
},
|
||||
100.0,
|
||||
"Downloading pack file",
|
||||
)
|
||||
.await?;
|
||||
let loading_bar = if let Some(bar) = initialized_loading_bar {
|
||||
emit_loading(&bar, 0.0, Some("Downloading pack file")).await?;
|
||||
bar
|
||||
} else {
|
||||
init_loading(
|
||||
LoadingBarType::PackFileDownload {
|
||||
profile_path: profile_path.get_full_path().await?,
|
||||
pack_name: title,
|
||||
icon: icon_url,
|
||||
pack_version: version_id.clone(),
|
||||
},
|
||||
100.0,
|
||||
"Downloading pack file",
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?;
|
||||
let creds = state.credentials.read().await;
|
||||
@@ -313,6 +323,7 @@ pub async fn set_profile_information(
|
||||
description: &CreatePackDescription,
|
||||
backup_name: &str,
|
||||
dependencies: &HashMap<PackDependency, String>,
|
||||
ignore_lock: bool, // do not change locked status
|
||||
) -> crate::Result<()> {
|
||||
let mut game_version: Option<&String> = None;
|
||||
let mut mod_loader = None;
|
||||
@@ -370,6 +381,14 @@ pub async fn set_profile_information(
|
||||
prof.metadata.linked_data = Some(LinkedData {
|
||||
project_id: description.project_id.clone(),
|
||||
version_id: description.version_id.clone(),
|
||||
locked: if !ignore_lock {
|
||||
Some(
|
||||
description.project_id.is_some()
|
||||
&& description.version_id.is_some(),
|
||||
)
|
||||
} else {
|
||||
prof.metadata.linked_data.as_ref().and_then(|x| x.locked)
|
||||
},
|
||||
});
|
||||
prof.metadata.icon = description.icon.clone();
|
||||
prof.metadata.game_version = game_version.clone();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::config::MODRINTH_API_URL;
|
||||
use crate::event::emit::{
|
||||
emit_loading, init_or_edit_loading, loading_try_for_each_concurrent,
|
||||
};
|
||||
@@ -5,13 +6,16 @@ use crate::event::LoadingBarType;
|
||||
use crate::pack::install_from::{
|
||||
set_profile_information, EnvType, PackFile, PackFileHash,
|
||||
};
|
||||
use crate::prelude::ProfilePathId;
|
||||
use crate::prelude::{ModrinthVersion, ProfilePathId, ProjectMetadata};
|
||||
use crate::state::{ProfileInstallStage, Profiles, SideType};
|
||||
use crate::util::fetch::{fetch_mirrors, write};
|
||||
use crate::util::fetch::{fetch_json, fetch_mirrors, write};
|
||||
use crate::util::io;
|
||||
use crate::{profile, State};
|
||||
use async_zip::tokio::read::seek::ZipFileReader;
|
||||
use reqwest::Method;
|
||||
use serde_json::json;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::path::{Component, PathBuf};
|
||||
|
||||
@@ -43,6 +47,7 @@ pub async fn install_zipped_mrpack(
|
||||
title,
|
||||
icon_url,
|
||||
profile_path.clone(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -52,7 +57,7 @@ pub async fn install_zipped_mrpack(
|
||||
};
|
||||
|
||||
// Install pack files, and if it fails, fail safely by removing the profile
|
||||
let result = install_zipped_mrpack_files(create_pack).await;
|
||||
let result = install_zipped_mrpack_files(create_pack, false).await;
|
||||
|
||||
// Check existing managed packs for potential updates
|
||||
tokio::task::spawn(Profiles::update_modrinth_versions());
|
||||
@@ -72,6 +77,7 @@ pub async fn install_zipped_mrpack(
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn install_zipped_mrpack_files(
|
||||
create_pack: CreatePack,
|
||||
ignore_lock: bool,
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
let state = &State::get().await?;
|
||||
|
||||
@@ -126,6 +132,7 @@ pub async fn install_zipped_mrpack_files(
|
||||
&description,
|
||||
&pack.name,
|
||||
&pack.dependencies,
|
||||
ignore_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -182,15 +189,20 @@ pub async fn install_zipped_mrpack_files(
|
||||
.await?;
|
||||
drop(creds);
|
||||
|
||||
// Convert windows path to unix path.
|
||||
// .mrpacks no longer generate windows paths, but this is here for backwards compatibility before this was fixed
|
||||
// https://github.com/modrinth/theseus/issues/595
|
||||
let project_path = project.path.replace('\\', "/");
|
||||
|
||||
let path =
|
||||
std::path::Path::new(&project.path).components().next();
|
||||
std::path::Path::new(&project_path).components().next();
|
||||
if let Some(path) = path {
|
||||
match path {
|
||||
Component::CurDir | Component::Normal(_) => {
|
||||
let path = profile_path
|
||||
.get_full_path()
|
||||
.await?
|
||||
.join(&project.path);
|
||||
.join(&project_path);
|
||||
write(&path, &file, &state.io_semaphore)
|
||||
.await?;
|
||||
}
|
||||
@@ -337,31 +349,65 @@ pub async fn remove_all_related_files(
|
||||
})
|
||||
.await?;
|
||||
|
||||
let num_files = pack.files.len();
|
||||
use futures::StreamExt;
|
||||
loading_try_for_each_concurrent(
|
||||
futures::stream::iter(pack.files.into_iter())
|
||||
.map(Ok::<PackFile, crate::Error>),
|
||||
None,
|
||||
None,
|
||||
0.0,
|
||||
num_files,
|
||||
None,
|
||||
|project| {
|
||||
let profile_path = profile_path.clone();
|
||||
async move {
|
||||
// Remove this file if a corresponding one exists in the filesystem
|
||||
let existing_file =
|
||||
profile_path.get_full_path().await?.join(&project.path);
|
||||
if existing_file.exists() {
|
||||
io::remove_file(&existing_file).await?;
|
||||
}
|
||||
// First, remove all modrinth projects by their version hashes
|
||||
// Remove all modrinth projects by their version hashes
|
||||
// We need to do a fetch to get the project ids from Modrinth
|
||||
let state = State::get().await?;
|
||||
let all_hashes = pack
|
||||
.files
|
||||
.iter()
|
||||
.filter_map(|f| Some(f.hashes.get(&PackFileHash::Sha512)?.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let creds = state.credentials.read().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
// First, get project info by hash
|
||||
let files_url = format!("{}version_files", MODRINTH_API_URL);
|
||||
|
||||
let hash_projects = fetch_json::<HashMap<String, ModrinthVersion>>(
|
||||
Method::POST,
|
||||
&files_url,
|
||||
None,
|
||||
Some(json!({
|
||||
"hashes": all_hashes,
|
||||
"algorithm": "sha512",
|
||||
})),
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
let to_remove = hash_projects
|
||||
.into_values()
|
||||
.map(|p| p.project_id)
|
||||
.collect::<Vec<_>>();
|
||||
let profile =
|
||||
profile::get(&profile_path, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_path.to_string(),
|
||||
)
|
||||
})?;
|
||||
for (project_id, project) in &profile.projects {
|
||||
if let ProjectMetadata::Modrinth { project, .. } = &project.metadata
|
||||
{
|
||||
if to_remove.contains(&project.id) {
|
||||
let path = profile
|
||||
.get_profile_full_path()
|
||||
.await?
|
||||
.join(project_id.0.clone());
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over all Modrinth project file paths in the json, and remove them
|
||||
// (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized)
|
||||
for file in pack.files {
|
||||
let path = profile_path.get_full_path().await?.join(file.path);
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over each 'overrides' file and remove it
|
||||
for index in 0..zip_reader.file().entries().len() {
|
||||
|
||||
@@ -2,16 +2,13 @@
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::state::{MinecraftChild, ProfilePathId};
|
||||
pub use crate::{
|
||||
state::{
|
||||
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
|
||||
},
|
||||
State,
|
||||
};
|
||||
use crate::{
|
||||
state::{MinecraftChild, ProfilePathId},
|
||||
util::io::IOError,
|
||||
};
|
||||
|
||||
// Gets whether a child process stored in the state by UUID has finished
|
||||
#[tracing::instrument]
|
||||
@@ -26,7 +23,7 @@ pub async fn get_exit_status_by_uuid(
|
||||
) -> crate::Result<Option<i32>> {
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
Ok(children.exit_status(uuid).await?.and_then(|f| f.code()))
|
||||
children.exit_status(uuid).await
|
||||
}
|
||||
|
||||
// Gets the UUID of each stored process in the state
|
||||
@@ -72,26 +69,6 @@ pub async fn get_uuids_by_profile_path(
|
||||
children.running_keys_with_profile(profile_path).await
|
||||
}
|
||||
|
||||
// Gets output of a child process stored in the state by UUID, as a string
|
||||
#[tracing::instrument]
|
||||
pub async fn get_output_by_uuid(uuid: &Uuid) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
// Get stdout from child
|
||||
let children = state.children.read().await;
|
||||
|
||||
// Extract child or return crate::Error
|
||||
if let Some(child) = children.get(uuid) {
|
||||
let child = child.read().await;
|
||||
Ok(child.output.get_output().await?)
|
||||
} else {
|
||||
Err(crate::ErrorKind::LauncherError(format!(
|
||||
"No child process by UUID {}",
|
||||
uuid
|
||||
))
|
||||
.as_error())
|
||||
}
|
||||
}
|
||||
|
||||
// Kill a child process stored in the state by UUID, as a string
|
||||
#[tracing::instrument]
|
||||
pub async fn kill_by_uuid(uuid: &Uuid) -> crate::Result<()> {
|
||||
@@ -124,13 +101,7 @@ pub async fn wait_for_by_uuid(uuid: &Uuid) -> crate::Result<()> {
|
||||
// Kill a running child process directly
|
||||
#[tracing::instrument(skip(running))]
|
||||
pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> {
|
||||
running
|
||||
.current_child
|
||||
.write()
|
||||
.await
|
||||
.kill()
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
running.current_child.write().await.kill().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! Theseus profile management interface
|
||||
use crate::pack::install_from::CreatePackProfile;
|
||||
use crate::prelude::ProfilePathId;
|
||||
use crate::profile;
|
||||
use crate::state::LinkedData;
|
||||
use crate::util::io::{self, canonicalize};
|
||||
use crate::{
|
||||
event::{emit::emit_profile, ProfilePayloadType},
|
||||
prelude::ModLoader,
|
||||
};
|
||||
use crate::{pack, profile, ErrorKind};
|
||||
pub use crate::{
|
||||
state::{JavaSettings, Profile},
|
||||
State,
|
||||
@@ -102,6 +102,12 @@ pub async fn profile_create(
|
||||
}
|
||||
|
||||
profile.metadata.linked_data = linked_data;
|
||||
if let Some(linked_data) = &mut profile.metadata.linked_data {
|
||||
linked_data.locked = Some(
|
||||
linked_data.project_id.is_some()
|
||||
&& linked_data.version_id.is_some(),
|
||||
);
|
||||
}
|
||||
|
||||
emit_profile(
|
||||
uuid,
|
||||
@@ -154,6 +160,59 @@ pub async fn profile_create_from_creator(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profile_create_from_duplicate(
|
||||
copy_from: ProfilePathId,
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
let profile = profile::get(©_from, None).await?.ok_or_else(|| {
|
||||
ErrorKind::UnmanagedProfileError(copy_from.to_string())
|
||||
})?;
|
||||
|
||||
let profile_path_id = profile_create(
|
||||
profile.metadata.name.clone(),
|
||||
profile.metadata.game_version.clone(),
|
||||
profile.metadata.loader,
|
||||
profile.metadata.loader_version.clone().map(|it| it.id),
|
||||
profile.metadata.icon.clone(),
|
||||
profile.metadata.icon_url.clone(),
|
||||
profile.metadata.linked_data.clone(),
|
||||
Some(true),
|
||||
Some(true),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Copy it over using the import system (essentially importing from the same profile)
|
||||
let state = State::get().await?;
|
||||
let bar = pack::import::copy_dotminecraft(
|
||||
profile_path_id.clone(),
|
||||
copy_from.get_full_path().await?,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
crate::launcher::install_minecraft(&profile, Some(bar)).await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// emit profile edited
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
&profile.profile_id(),
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
Ok(profile_path_id)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub(crate) async fn get_loader_version_from_loader(
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::pack::install_from::{
|
||||
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
|
||||
};
|
||||
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
|
||||
use crate::state::ProjectMetadata;
|
||||
use crate::state::{ProjectMetadata, SideType};
|
||||
|
||||
use crate::util::fetch;
|
||||
use crate::util::io::{self, IOError};
|
||||
@@ -109,6 +109,26 @@ pub async fn get_full_path(path: &ProfilePathId) -> crate::Result<PathBuf> {
|
||||
Ok(full_path)
|
||||
}
|
||||
|
||||
/// Get mod's full path in the filesystem
|
||||
#[tracing::instrument]
|
||||
pub async fn get_mod_full_path(
|
||||
profile_path: &ProfilePathId,
|
||||
project_path: &ProjectPathId,
|
||||
) -> crate::Result<PathBuf> {
|
||||
if get(profile_path, Some(true)).await?.is_some() {
|
||||
let full_path = io::canonicalize(
|
||||
project_path.get_full_path(profile_path.clone()).await?,
|
||||
)?;
|
||||
return Ok(full_path);
|
||||
}
|
||||
|
||||
Err(crate::ErrorKind::OtherError(format!(
|
||||
"Tried to get the full path of a nonexistent or unloaded project at path {}!",
|
||||
project_path.get_full_path(profile_path.clone()).await?.display()
|
||||
))
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Edit a profile using a given asynchronous closure
|
||||
pub async fn edit<Fut>(
|
||||
path: &ProfilePathId,
|
||||
@@ -552,6 +572,8 @@ pub async fn export_mrpack(
|
||||
export_path: PathBuf,
|
||||
included_overrides: Vec<String>, // which folders to include in the overrides
|
||||
version_id: Option<String>,
|
||||
description: Option<String>,
|
||||
_name: Option<String>,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let io_semaphore = state.io_semaphore.0.read().await;
|
||||
@@ -585,7 +607,8 @@ pub async fn export_mrpack(
|
||||
|
||||
// Create mrpack json configuration file
|
||||
let version_id = version_id.unwrap_or("1.0.0".to_string());
|
||||
let packfile = create_mrpack_json(&profile, version_id).await?;
|
||||
let packfile =
|
||||
create_mrpack_json(&profile, version_id, description).await?;
|
||||
let modrinth_path_list = get_modrinth_pack_list(&packfile);
|
||||
|
||||
// Build vec of all files in the folder
|
||||
@@ -693,7 +716,7 @@ pub async fn get_potential_override_folders(
|
||||
))
|
||||
})?;
|
||||
// dummy mrpack to get pack list
|
||||
let mrpack = create_mrpack_json(&profile, "0".to_string()).await?;
|
||||
let mrpack = create_mrpack_json(&profile, "0".to_string(), None).await?;
|
||||
let mrpack_files = get_modrinth_pack_list(&mrpack);
|
||||
|
||||
let mut path_list: Vec<PathBuf> = Vec::new();
|
||||
@@ -820,23 +843,12 @@ pub async fn run_credentials(
|
||||
.unwrap_or(&settings.custom_env_args);
|
||||
|
||||
// Post post exit hooks
|
||||
let post_exit_hook =
|
||||
&profile.hooks.as_ref().unwrap_or(&settings.hooks).post_exit;
|
||||
|
||||
let post_exit_hook = if let Some(hook) = post_exit_hook {
|
||||
let mut cmd = hook.split(' ');
|
||||
if let Some(command) = cmd.next() {
|
||||
let mut command = Command::new(command);
|
||||
command
|
||||
.args(&cmd.collect::<Vec<&str>>())
|
||||
.current_dir(path.get_full_path().await?);
|
||||
Some(command)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let post_exit_hook = profile
|
||||
.hooks
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.hooks)
|
||||
.post_exit
|
||||
.clone();
|
||||
|
||||
// Any options.txt settings that we want set, add here
|
||||
let mut mc_set_options: Vec<(String, String)> = vec![];
|
||||
@@ -941,6 +953,7 @@ fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec<String> {
|
||||
pub async fn create_mrpack_json(
|
||||
profile: &Profile,
|
||||
version_id: String,
|
||||
description: Option<String>,
|
||||
) -> crate::Result<PackFormat> {
|
||||
// Add loader version to dependencies
|
||||
let mut dependencies = HashMap::new();
|
||||
@@ -951,6 +964,9 @@ pub async fn create_mrpack_json(
|
||||
(crate::prelude::ModLoader::Forge, Some(v)) => {
|
||||
dependencies.insert(PackDependency::Forge, v.id)
|
||||
}
|
||||
(crate::prelude::ModLoader::NeoForge, Some(v)) => {
|
||||
dependencies.insert(PackDependency::NeoForge, v.id)
|
||||
}
|
||||
(crate::prelude::ModLoader::Fabric, Some(v)) => {
|
||||
dependencies.insert(PackDependency::FabricLoader, v.id)
|
||||
}
|
||||
@@ -981,18 +997,21 @@ pub async fn create_mrpack_json(
|
||||
.projects
|
||||
.iter()
|
||||
.filter_map(|(mod_path, project)| {
|
||||
let path: String = mod_path.0.clone().to_string_lossy().to_string();
|
||||
let path: String = mod_path.get_inner_path_unix().ok()?;
|
||||
|
||||
// Only Modrinth projects have a modrinth metadata field for the modrinth.json
|
||||
Some(Ok(match project.metadata {
|
||||
crate::prelude::ProjectMetadata::Modrinth {
|
||||
ref project,
|
||||
ref version,
|
||||
..
|
||||
} => {
|
||||
let mut env = HashMap::new();
|
||||
env.insert(EnvType::Client, project.client_side.clone());
|
||||
env.insert(EnvType::Server, project.server_side.clone());
|
||||
// TODO: envtype should be a controllable option (in general or at least .mrpack exporting)
|
||||
// For now, assume required.
|
||||
// env.insert(EnvType::Client, project.client_side.clone());
|
||||
// env.insert(EnvType::Server, project.server_side.clone());
|
||||
env.insert(EnvType::Client, SideType::Required);
|
||||
env.insert(EnvType::Server, SideType::Required);
|
||||
|
||||
let primary_file = if let Some(primary_file) =
|
||||
version.files.first()
|
||||
@@ -1037,7 +1056,7 @@ pub async fn create_mrpack_json(
|
||||
format_version: 1,
|
||||
version_id,
|
||||
name: profile.metadata.name.clone(),
|
||||
summary: None,
|
||||
summary: description,
|
||||
files,
|
||||
dependencies,
|
||||
})
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_profile, loading_try_for_each_concurrent},
|
||||
emit::{emit_profile, init_loading, loading_try_for_each_concurrent},
|
||||
ProfilePayloadType,
|
||||
},
|
||||
pack::{self, install_from::generate_pack_from_version_id},
|
||||
prelude::{ProfilePathId, ProjectPathId},
|
||||
profile::get,
|
||||
state::Project,
|
||||
State,
|
||||
state::{ProfileInstallStage, Project},
|
||||
LoadingBarType, State,
|
||||
};
|
||||
use futures::try_join;
|
||||
|
||||
/// Updates a managed modrinth pack to the cached latest version found in 'modrinth_update_version'
|
||||
/// Updates a managed modrinth pack to the version specified by new_version_id
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn update_managed_modrinth(
|
||||
pub async fn update_managed_modrinth_version(
|
||||
profile_path: &ProfilePathId,
|
||||
new_version_id: &String,
|
||||
) -> crate::Result<()> {
|
||||
let profile = get(profile_path, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
@@ -39,19 +40,14 @@ pub async fn update_managed_modrinth(
|
||||
let version_id =
|
||||
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
|
||||
|
||||
// extract modrinth_update_version, returning Ok(()) if it is none
|
||||
let modrinth_update_version = match profile.modrinth_update_version {
|
||||
Some(ref x) if x != version_id => x,
|
||||
_ => return Ok(()), // No update version, or no update needed, return Ok(())
|
||||
};
|
||||
|
||||
// Replace the pack with the new version
|
||||
replace_managed_modrinth(
|
||||
profile_path,
|
||||
&profile,
|
||||
project_id,
|
||||
version_id,
|
||||
Some(modrinth_update_version),
|
||||
Some(new_version_id),
|
||||
true, // switching versions should ignore the lock
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -128,6 +124,7 @@ pub async fn repair_managed_modrinth(
|
||||
project_id,
|
||||
version_id,
|
||||
None,
|
||||
false, // do not ignore lock, as repairing can reset the lock
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -153,32 +150,61 @@ async fn replace_managed_modrinth(
|
||||
project_id: &String,
|
||||
version_id: &String,
|
||||
new_version_id: Option<&String>,
|
||||
ignore_lock: bool,
|
||||
) -> crate::Result<()> {
|
||||
crate::profile::edit(profile_path, |profile| {
|
||||
profile.install_stage = ProfileInstallStage::Installing;
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Fetch .mrpacks for both old and new versions
|
||||
// TODO: this will need to be updated if we revert the hacky pack method we needed for compiler speed
|
||||
let old_pack_creator = generate_pack_from_version_id(
|
||||
project_id.clone(),
|
||||
version_id.clone(),
|
||||
profile.metadata.name.clone(),
|
||||
None,
|
||||
profile_path.clone(),
|
||||
);
|
||||
|
||||
// download in parallel, then join. If new_version_id is None, we don't need to download the new pack, so we clone the old one
|
||||
let (old_pack_creator, new_pack_creator) =
|
||||
if let Some(new_version_id) = new_version_id {
|
||||
let shared_loading_bar = init_loading(
|
||||
LoadingBarType::PackFileDownload {
|
||||
profile_path: profile_path.get_full_path().await?,
|
||||
pack_name: profile.metadata.name.clone(),
|
||||
icon: None,
|
||||
pack_version: version_id.clone(),
|
||||
},
|
||||
200.0, // These two downloads will share the same loading bar
|
||||
"Downloading pack file",
|
||||
)
|
||||
.await?;
|
||||
|
||||
// download in parallel, then join.
|
||||
try_join!(
|
||||
old_pack_creator,
|
||||
generate_pack_from_version_id(
|
||||
project_id.clone(),
|
||||
version_id.clone(),
|
||||
profile.metadata.name.clone(),
|
||||
None,
|
||||
profile_path.clone(),
|
||||
Some(shared_loading_bar.clone())
|
||||
),
|
||||
generate_pack_from_version_id(
|
||||
project_id.clone(),
|
||||
new_version_id.clone(),
|
||||
profile.metadata.name.clone(),
|
||||
None,
|
||||
profile_path.clone()
|
||||
profile_path.clone(),
|
||||
Some(shared_loading_bar)
|
||||
)
|
||||
)?
|
||||
} else {
|
||||
let mut old_pack_creator = old_pack_creator.await?;
|
||||
// If new_version_id is None, we don't need to download the new pack, so we clone the old one
|
||||
let mut old_pack_creator = generate_pack_from_version_id(
|
||||
project_id.clone(),
|
||||
version_id.clone(),
|
||||
profile.metadata.name.clone(),
|
||||
None,
|
||||
profile_path.clone(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
old_pack_creator.description.existing_loading_bar = None;
|
||||
(old_pack_creator.clone(), old_pack_creator)
|
||||
};
|
||||
@@ -197,7 +223,11 @@ async fn replace_managed_modrinth(
|
||||
// - install all overrides
|
||||
// - edits the profile to update the new data
|
||||
// - (functionals almost identically to rteinstalling the pack 'in-place')
|
||||
pack::install_mrpack::install_zipped_mrpack_files(new_pack_creator).await?;
|
||||
pack::install_mrpack::install_zipped_mrpack_files(
|
||||
new_pack_creator,
|
||||
ignore_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -49,10 +49,19 @@ pub async fn set(settings: Settings) -> crate::Result<()> {
|
||||
}
|
||||
.await;
|
||||
|
||||
let updated_discord_rpc = {
|
||||
let read = state.settings.read().await;
|
||||
settings.disable_discord_rpc != read.disable_discord_rpc
|
||||
};
|
||||
|
||||
{
|
||||
*state.settings.write().await = settings;
|
||||
}
|
||||
|
||||
if updated_discord_rpc {
|
||||
state.discord_rpc.clear_to_default(true).await?;
|
||||
}
|
||||
|
||||
if reset_io {
|
||||
state.reset_io_semaphore().await;
|
||||
}
|
||||
|
||||
@@ -140,11 +140,15 @@ impl Drop for LoadingBarId {
|
||||
#[cfg(not(any(feature = "tauri", feature = "cli")))]
|
||||
bars.remove(&loader_uuid);
|
||||
}
|
||||
let _ = SafeProcesses::complete(
|
||||
crate::state::ProcessType::LoadingBar,
|
||||
loader_uuid,
|
||||
)
|
||||
.await;
|
||||
// complete calls state, and since a LoadingBarId is created in state initialization, we only complete if its already initializaed
|
||||
// to avoid an infinite loop.
|
||||
if crate::State::initialized() {
|
||||
let _ = SafeProcesses::complete(
|
||||
crate::state::ProcessType::LoadingBar,
|
||||
loader_uuid,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ pub async fn launch_minecraft(
|
||||
memory: &st::MemorySettings,
|
||||
resolution: &st::WindowSize,
|
||||
credentials: &auth::Credentials,
|
||||
post_exit_hook: Option<Command>,
|
||||
post_exit_hook: Option<String>,
|
||||
profile: &Profile,
|
||||
) -> crate::Result<Arc<tokio::sync::RwLock<MinecraftChild>>> {
|
||||
if profile.install_stage == ProfileInstallStage::PackInstalling
|
||||
@@ -406,7 +406,6 @@ pub async fn launch_minecraft(
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
|
||||
command
|
||||
.args(
|
||||
args::get_jvm_arguments(
|
||||
@@ -447,14 +446,17 @@ pub async fn launch_minecraft(
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.current_dir(instance_path.clone())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
// CARGO-set DYLD_LIBRARY_PATH breaks Minecraft on macOS during testing on playground
|
||||
#[cfg(target_os = "macos")]
|
||||
if std::env::var("CARGO").is_ok() {
|
||||
command.env_remove("DYLD_FALLBACK_LIBRARY_PATH");
|
||||
}
|
||||
// Java options should be set in instance options (the existence of _JAVA_OPTIONS overwites them)
|
||||
command.env_remove("_JAVA_OPTIONS");
|
||||
|
||||
command.envs(env_args);
|
||||
|
||||
// Overwrites the minecraft options.txt file with the settings from the profile
|
||||
@@ -484,20 +486,6 @@ pub async fn launch_minecraft(
|
||||
io::write(&options_path, options_string).await?;
|
||||
}
|
||||
|
||||
// Get Modrinth logs directories
|
||||
let datetime_string =
|
||||
chrono::Local::now().format("%Y%m%d_%H%M%S").to_string();
|
||||
let logs_dir = {
|
||||
let st = State::get().await?;
|
||||
st.directories
|
||||
.profile_logs_dir(&profile.profile_id())
|
||||
.await?
|
||||
.join(&datetime_string)
|
||||
};
|
||||
io::create_dir_all(&logs_dir).await?;
|
||||
|
||||
let stdout_log_path = logs_dir.join("stdout.log");
|
||||
|
||||
crate::api::profile::edit(&profile.profile_id(), |prof| {
|
||||
prof.metadata.last_played = Some(Utc::now());
|
||||
|
||||
@@ -559,10 +547,9 @@ pub async fn launch_minecraft(
|
||||
// This also spawns the process and prepares the subsequent processes
|
||||
let mut state_children = state.children.write().await;
|
||||
state_children
|
||||
.insert_process(
|
||||
.insert_new_process(
|
||||
Uuid::new_v4(),
|
||||
profile.profile_id(),
|
||||
stdout_log_path,
|
||||
command,
|
||||
post_exit_hook,
|
||||
censor_strings,
|
||||
|
||||
@@ -24,7 +24,9 @@ pub fn start_logger() -> Option<WorkerGuard> {
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info"));
|
||||
.unwrap_or_else(|_| {
|
||||
tracing_subscriber::EnvFilter::new("theseus=info,theseus_gui=info")
|
||||
});
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.with(filter)
|
||||
|
||||
@@ -1,89 +1,278 @@
|
||||
use super::{Profile, ProfilePathId};
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use sysinfo::PidExt;
|
||||
use tokio::process::Child;
|
||||
use tokio::process::Command;
|
||||
use tokio::process::{ChildStderr, ChildStdout};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
|
||||
use crate::event::emit::emit_process;
|
||||
use crate::event::ProcessPayloadType;
|
||||
use crate::profile;
|
||||
use crate::util::fetch::read_json;
|
||||
use crate::util::io::IOError;
|
||||
use crate::{profile, ErrorKind};
|
||||
use sysinfo::{ProcessExt, SystemExt};
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const PROCESSES_JSON: &str = "processes.json";
|
||||
|
||||
// Child processes (instances of Minecraft)
|
||||
// A wrapper over a Hashmap connecting PID -> MinecraftChild
|
||||
pub struct Children(HashMap<Uuid, Arc<RwLock<MinecraftChild>>>);
|
||||
|
||||
// Minecraft Child, bundles together the PID, the actual Child, and the easily queryable stdout and stderr streams
|
||||
#[derive(Debug)]
|
||||
pub enum ChildType {
|
||||
// A child process that is being managed by tokio
|
||||
TokioChild(Child),
|
||||
// A child process that was rescued from a cache (e.g. a process that was launched by theseus before the launcher was restarted)
|
||||
// This may not have all the same functionality as a TokioChild
|
||||
RescuedPID(u32),
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ProcessCache {
|
||||
pub pid: u32,
|
||||
pub uuid: Uuid,
|
||||
pub start_time: u64,
|
||||
pub name: String,
|
||||
pub exe: String,
|
||||
pub profile_relative_path: ProfilePathId,
|
||||
pub post_command: Option<String>,
|
||||
}
|
||||
impl ChildType {
|
||||
pub async fn try_wait(&mut self) -> crate::Result<Option<i32>> {
|
||||
match self {
|
||||
ChildType::TokioChild(child) => Ok(child
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
.map(|x| x.code().unwrap_or(0))),
|
||||
ChildType::RescuedPID(pid) => {
|
||||
let mut system = sysinfo::System::new();
|
||||
if !system.refresh_process(sysinfo::Pid::from_u32(*pid)) {
|
||||
return Ok(Some(0));
|
||||
}
|
||||
let process = system.process(sysinfo::Pid::from_u32(*pid));
|
||||
if let Some(process) = process {
|
||||
if process.status() == sysinfo::ProcessStatus::Run {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(0))
|
||||
}
|
||||
} else {
|
||||
Ok(Some(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn kill(&mut self) -> crate::Result<()> {
|
||||
match self {
|
||||
ChildType::TokioChild(child) => {
|
||||
Ok(child.kill().await.map_err(IOError::from)?)
|
||||
}
|
||||
ChildType::RescuedPID(pid) => {
|
||||
let mut system = sysinfo::System::new();
|
||||
if system.refresh_process(sysinfo::Pid::from_u32(*pid)) {
|
||||
let process = system.process(sysinfo::Pid::from_u32(*pid));
|
||||
if let Some(process) = process {
|
||||
process.kill();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn id(&self) -> Option<u32> {
|
||||
match self {
|
||||
ChildType::TokioChild(child) => child.id(),
|
||||
ChildType::RescuedPID(pid) => Some(*pid),
|
||||
}
|
||||
}
|
||||
|
||||
// Caches the process so that it can be restored if the launcher is restarted
|
||||
// Stored in the caches/metadata/processes.json file
|
||||
pub async fn cache_process(
|
||||
&self,
|
||||
uuid: uuid::Uuid,
|
||||
profile_path_id: ProfilePathId,
|
||||
post_command: Option<String>,
|
||||
) -> crate::Result<()> {
|
||||
let pid = match self {
|
||||
ChildType::TokioChild(child) => child.id().unwrap_or(0),
|
||||
ChildType::RescuedPID(pid) => *pid,
|
||||
};
|
||||
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_processes();
|
||||
let process =
|
||||
system.process(sysinfo::Pid::from_u32(pid)).ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Could not find process {}",
|
||||
pid
|
||||
))
|
||||
})?;
|
||||
let start_time = process.start_time();
|
||||
let name = process.name().to_string();
|
||||
let exe = process.exe().to_string_lossy().to_string();
|
||||
|
||||
let cached_process = ProcessCache {
|
||||
pid,
|
||||
start_time,
|
||||
name,
|
||||
exe,
|
||||
post_command,
|
||||
uuid,
|
||||
profile_relative_path: profile_path_id,
|
||||
};
|
||||
|
||||
let children_path = state
|
||||
.directories
|
||||
.caches_meta_dir()
|
||||
.await
|
||||
.join(PROCESSES_JSON);
|
||||
let mut children_caches = if let Ok(children_json) =
|
||||
read_json::<HashMap<uuid::Uuid, ProcessCache>>(
|
||||
&children_path,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await
|
||||
{
|
||||
children_json
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
children_caches.insert(uuid, cached_process);
|
||||
crate::util::fetch::write(
|
||||
&children_path,
|
||||
&serde_json::to_vec(&children_caches)?,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Removes the process from the cache (ie: on process exit)
|
||||
pub async fn remove_cache(&self, uuid: uuid::Uuid) -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
let children_path = state
|
||||
.directories
|
||||
.caches_meta_dir()
|
||||
.await
|
||||
.join(PROCESSES_JSON);
|
||||
let mut children_caches = if let Ok(children_json) =
|
||||
read_json::<HashMap<uuid::Uuid, ProcessCache>>(
|
||||
&children_path,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await
|
||||
{
|
||||
children_json
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
children_caches.remove(&uuid);
|
||||
crate::util::fetch::write(
|
||||
&children_path,
|
||||
&serde_json::to_vec(&children_caches)?,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Minecraft Child, bundles together the PID, the actual Child, and the easily queryable stdout and stderr streams (if needed)
|
||||
#[derive(Debug)]
|
||||
pub struct MinecraftChild {
|
||||
pub uuid: Uuid,
|
||||
pub profile_relative_path: ProfilePathId,
|
||||
pub manager: Option<JoinHandle<crate::Result<ExitStatus>>>, // None when future has completed and been handled
|
||||
pub current_child: Arc<RwLock<Child>>,
|
||||
pub output: SharedOutput,
|
||||
pub manager: Option<JoinHandle<crate::Result<i32>>>, // None when future has completed and been handled
|
||||
pub current_child: Arc<RwLock<ChildType>>,
|
||||
pub last_updated_playtime: DateTime<Utc>, // The last time we updated the playtime for the associated profile
|
||||
}
|
||||
|
||||
impl Children {
|
||||
pub fn new() -> Children {
|
||||
pub fn new() -> Self {
|
||||
Children(HashMap::new())
|
||||
}
|
||||
|
||||
// Loads cached processes from the caches/metadata/processes.json file, re-inserts them into the hashmap, and removes them from the file
|
||||
// This will only be called once, on startup. Only processes who match a cached process (name, time started, pid, etc) will be re-inserted
|
||||
pub async fn rescue_cache(&mut self) -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
let children_path = state
|
||||
.directories
|
||||
.caches_meta_dir()
|
||||
.await
|
||||
.join(PROCESSES_JSON);
|
||||
|
||||
let mut children_caches = if let Ok(children_json) =
|
||||
read_json::<HashMap<uuid::Uuid, ProcessCache>>(
|
||||
&children_path,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await
|
||||
{
|
||||
// Overwrite the file with an empty hashmap- we will re-insert the cached processes
|
||||
let empty = HashMap::<uuid::Uuid, ProcessCache>::new();
|
||||
crate::util::fetch::write(
|
||||
&children_path,
|
||||
&serde_json::to_vec(&empty)?,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Return the cached processes
|
||||
children_json
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
for (_, cache) in children_caches.drain() {
|
||||
let uuid = cache.uuid;
|
||||
match self.insert_cached_process(cache).await {
|
||||
Ok(child) => {
|
||||
self.0.insert(uuid, child);
|
||||
}
|
||||
Err(e) => tracing::warn!(
|
||||
"Failed to rescue cached process {}: {}",
|
||||
uuid,
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Runs the command in process, 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 MinecraftChild rather than any previously stored MinecraftChild that may exist
|
||||
|
||||
#[tracing::instrument(skip(
|
||||
self,
|
||||
uuid,
|
||||
log_path,
|
||||
mc_command,
|
||||
post_command,
|
||||
censor_strings
|
||||
))]
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn insert_process(
|
||||
pub async fn insert_new_process(
|
||||
&mut self,
|
||||
uuid: Uuid,
|
||||
profile_relative_path: ProfilePathId,
|
||||
log_path: PathBuf,
|
||||
mut mc_command: Command,
|
||||
post_command: Option<Command>, // Command to run after minecraft.
|
||||
post_command: Option<String>, // Command to run after minecraft.
|
||||
censor_strings: HashMap<String, String>,
|
||||
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
||||
// Takes the first element of the commands vector and spawns it
|
||||
let mut child = mc_command.spawn().map_err(IOError::from)?;
|
||||
|
||||
// Create std watcher threads for stdout and stderr
|
||||
let shared_output =
|
||||
SharedOutput::build(&log_path, censor_strings).await?;
|
||||
if let Some(child_stdout) = child.stdout.take() {
|
||||
let stdout_clone = shared_output.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = stdout_clone.read_stdout(child_stdout).await {
|
||||
error!("Stdout process died with error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(child_stderr) = child.stderr.take() {
|
||||
let stderr_clone = shared_output.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = stderr_clone.read_stderr(child_stderr).await {
|
||||
error!("Stderr process died with error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
let child = mc_command.spawn().map_err(IOError::from)?;
|
||||
let child = ChildType::TokioChild(child);
|
||||
|
||||
// Slots child into manager
|
||||
let pid = child.id().ok_or_else(|| {
|
||||
@@ -91,6 +280,15 @@ impl Children {
|
||||
"Process immediately failed, could not get PID".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Caches process so that it can be restored if the launcher is restarted
|
||||
child
|
||||
.cache_process(
|
||||
uuid,
|
||||
profile_relative_path.clone(),
|
||||
post_command.clone(),
|
||||
)
|
||||
.await?;
|
||||
let current_child = Arc::new(RwLock::new(child));
|
||||
let manager = Some(tokio::spawn(Self::sequential_process_manager(
|
||||
uuid,
|
||||
@@ -115,7 +313,6 @@ impl Children {
|
||||
uuid,
|
||||
profile_relative_path,
|
||||
current_child,
|
||||
output: shared_output,
|
||||
manager,
|
||||
last_updated_playtime,
|
||||
};
|
||||
@@ -125,6 +322,96 @@ impl Children {
|
||||
Ok(mchild)
|
||||
}
|
||||
|
||||
// Rescues a cached process, inserts a child process to keep track of, and returns a reference to the container struct MinecraftChild
|
||||
// Essentially 'reconnects' to a process that was launched by theseus before the launcher was restarted
|
||||
// However, this may not have all the same functionality as a TokioChild, as we only have the PID and not the actual Child
|
||||
// Only processes who match a cached process (name, time started, pid, etc) will be re-inserted. The function fails with an error if the process is notably different.
|
||||
#[tracing::instrument(skip(self, cached_process,))]
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn insert_cached_process(
|
||||
&mut self,
|
||||
cached_process: ProcessCache,
|
||||
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
||||
let _state = crate::State::get().await?;
|
||||
|
||||
// Takes the first element of the commands vector and spawns it
|
||||
// Checks processes, compares cached process to actual process
|
||||
// Fails if notably different (meaning that the PID was reused, and we shouldn't reconnect to it)
|
||||
{
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_processes();
|
||||
let process = system
|
||||
.process(sysinfo::Pid::from_u32(cached_process.pid))
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Could not find process {}",
|
||||
cached_process.pid
|
||||
))
|
||||
})?;
|
||||
|
||||
if cached_process.start_time != process.start_time() {
|
||||
return Err(ErrorKind::LauncherError(format!("Cached process {} has different start time than actual process {}", cached_process.pid, process.start_time())).into());
|
||||
}
|
||||
if cached_process.name != process.name() {
|
||||
return Err(ErrorKind::LauncherError(format!("Cached process {} has different name than actual process {}", cached_process.pid, process.name())).into());
|
||||
}
|
||||
if cached_process.exe != process.exe().to_string_lossy() {
|
||||
return Err(ErrorKind::LauncherError(format!("Cached process {} has different exe than actual process {}", cached_process.pid, process.exe().to_string_lossy())).into());
|
||||
}
|
||||
}
|
||||
|
||||
let child = ChildType::RescuedPID(cached_process.pid);
|
||||
|
||||
// Slots child into manager
|
||||
let pid = child.id().ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(
|
||||
"Process immediately failed, could not get PID".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Re-caches process so that it can be restored if the launcher is restarted
|
||||
child
|
||||
.cache_process(
|
||||
cached_process.uuid,
|
||||
cached_process.profile_relative_path.clone(),
|
||||
cached_process.post_command.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let current_child = Arc::new(RwLock::new(child));
|
||||
let manager = Some(tokio::spawn(Self::sequential_process_manager(
|
||||
cached_process.uuid,
|
||||
cached_process.post_command,
|
||||
pid,
|
||||
current_child.clone(),
|
||||
cached_process.profile_relative_path.clone(),
|
||||
)));
|
||||
|
||||
emit_process(
|
||||
cached_process.uuid,
|
||||
pid,
|
||||
ProcessPayloadType::Launched,
|
||||
"Launched Minecraft",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let last_updated_playtime = Utc::now();
|
||||
|
||||
// Create MinecraftChild
|
||||
let mchild = MinecraftChild {
|
||||
uuid: cached_process.uuid,
|
||||
profile_relative_path: cached_process.profile_relative_path,
|
||||
current_child,
|
||||
manager,
|
||||
last_updated_playtime,
|
||||
};
|
||||
|
||||
let mchild = Arc::new(RwLock::new(mchild));
|
||||
self.0.insert(cached_process.uuid, mchild.clone());
|
||||
Ok(mchild)
|
||||
}
|
||||
|
||||
// Spawns a new child process and inserts it into the hashmap
|
||||
// Also, as the process ends, it spawns the follow-up process if it exists
|
||||
// By convention, ExitStatus is last command's exit status, and we exit on the first non-zero exit status
|
||||
@@ -132,28 +419,23 @@ impl Children {
|
||||
#[theseus_macros::debug_pin]
|
||||
async fn sequential_process_manager(
|
||||
uuid: Uuid,
|
||||
post_command: Option<Command>,
|
||||
post_command: Option<String>,
|
||||
mut current_pid: u32,
|
||||
current_child: Arc<RwLock<Child>>,
|
||||
current_child: Arc<RwLock<ChildType>>,
|
||||
associated_profile: ProfilePathId,
|
||||
) -> crate::Result<ExitStatus> {
|
||||
) -> crate::Result<i32> {
|
||||
let current_child = current_child.clone();
|
||||
|
||||
// Wait on current Minecraft Child
|
||||
let mut mc_exit_status;
|
||||
let mut last_updated_playtime = Utc::now();
|
||||
loop {
|
||||
if let Some(t) = current_child
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
{
|
||||
if let Some(t) = current_child.write().await.try_wait().await? {
|
||||
mc_exit_status = t;
|
||||
break;
|
||||
}
|
||||
// sleep for 10ms
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
|
||||
// Auto-update playtime every minute
|
||||
let diff = Utc::now()
|
||||
@@ -168,7 +450,7 @@ impl Children {
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to update playtime for profile {}: {}",
|
||||
associated_profile,
|
||||
&associated_profile,
|
||||
e
|
||||
);
|
||||
}
|
||||
@@ -188,7 +470,7 @@ impl Children {
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to update playtime for profile {}: {}",
|
||||
associated_profile,
|
||||
&associated_profile,
|
||||
e
|
||||
);
|
||||
}
|
||||
@@ -196,13 +478,15 @@ impl Children {
|
||||
// Publish play time update
|
||||
// Allow failure, it will be stored locally and sent next time
|
||||
// Sent in another thread as first call may take a couple seconds and hold up process ending
|
||||
let associated_profile_clone = associated_profile.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
profile::try_update_playtime(&associated_profile).await
|
||||
profile::try_update_playtime(&associated_profile_clone.clone())
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to update playtime for profile {}: {}",
|
||||
associated_profile,
|
||||
&associated_profile_clone,
|
||||
e
|
||||
);
|
||||
}
|
||||
@@ -224,7 +508,12 @@ impl Children {
|
||||
}
|
||||
}
|
||||
|
||||
if !mc_exit_status.success() {
|
||||
{
|
||||
let current_child = current_child.write().await;
|
||||
current_child.remove_cache(uuid).await?;
|
||||
}
|
||||
|
||||
if !mc_exit_status == 0 {
|
||||
emit_process(
|
||||
uuid,
|
||||
current_pid,
|
||||
@@ -237,9 +526,28 @@ impl Children {
|
||||
}
|
||||
|
||||
// If a post-command exist, switch to it and wait on it
|
||||
// First, create the command by splitting arguments
|
||||
let post_command = if let Some(hook) = post_command {
|
||||
let mut cmd = hook.split(' ');
|
||||
if let Some(command) = cmd.next() {
|
||||
let mut command = Command::new(command);
|
||||
command
|
||||
.args(&cmd.collect::<Vec<&str>>())
|
||||
.current_dir(associated_profile.get_full_path().await?);
|
||||
Some(command)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(mut m_command) = post_command {
|
||||
{
|
||||
let mut current_child = current_child.write().await;
|
||||
let mut current_child: tokio::sync::RwLockWriteGuard<
|
||||
'_,
|
||||
ChildType,
|
||||
> = current_child.write().await;
|
||||
let new_child = m_command.spawn().map_err(IOError::from)?;
|
||||
current_pid = new_child.id().ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(
|
||||
@@ -247,7 +555,7 @@ impl Children {
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
*current_child = new_child;
|
||||
*current_child = ChildType::TokioChild(new_child);
|
||||
}
|
||||
emit_process(
|
||||
uuid,
|
||||
@@ -258,12 +566,7 @@ impl Children {
|
||||
.await?;
|
||||
|
||||
loop {
|
||||
if let Some(t) = current_child
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
{
|
||||
if let Some(t) = current_child.write().await.try_wait().await? {
|
||||
mc_exit_status = t;
|
||||
break;
|
||||
}
|
||||
@@ -296,18 +599,10 @@ impl Children {
|
||||
|
||||
// Get exit status of a child by PID
|
||||
// Returns None if the child is still running
|
||||
pub async fn exit_status(
|
||||
&self,
|
||||
uuid: &Uuid,
|
||||
) -> crate::Result<Option<std::process::ExitStatus>> {
|
||||
pub async fn exit_status(&self, uuid: &Uuid) -> crate::Result<Option<i32>> {
|
||||
if let Some(child) = self.get(uuid) {
|
||||
let child = child.write().await;
|
||||
let status = child
|
||||
.current_child
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?;
|
||||
let status = child.current_child.write().await.try_wait().await?;
|
||||
Ok(status)
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -326,7 +621,7 @@ impl Children {
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
keys.push(key);
|
||||
@@ -369,7 +664,7 @@ impl Children {
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
profiles.push(child.profile_relative_path.clone());
|
||||
@@ -392,7 +687,7 @@ impl Children {
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
if let Some(prof) = crate::api::profile::get(
|
||||
@@ -415,107 +710,3 @@ impl Default for Children {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// SharedOutput, a wrapper around a String that can be read from and written to concurrently
|
||||
// Designed to be used with ChildStdout and ChildStderr in a tokio thread to have a simple String storage for the output of a child process
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SharedOutput {
|
||||
output: Arc<RwLock<String>>,
|
||||
log_file: Arc<RwLock<File>>,
|
||||
censor_strings: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl SharedOutput {
|
||||
async fn build(
|
||||
log_file_path: &Path,
|
||||
censor_strings: HashMap<String, String>,
|
||||
) -> crate::Result<Self> {
|
||||
Ok(SharedOutput {
|
||||
output: Arc::new(RwLock::new(String::new())),
|
||||
log_file: Arc::new(RwLock::new(
|
||||
File::create(log_file_path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, log_file_path))?,
|
||||
)),
|
||||
censor_strings,
|
||||
})
|
||||
}
|
||||
|
||||
// Main entry function to a created SharedOutput, returns the log as a String
|
||||
pub async fn get_output(&self) -> crate::Result<String> {
|
||||
let output = self.output.read().await;
|
||||
Ok(output.clone())
|
||||
}
|
||||
|
||||
async fn read_stdout(
|
||||
&self,
|
||||
child_stdout: ChildStdout,
|
||||
) -> crate::Result<()> {
|
||||
let mut buf_reader = BufReader::new(child_stdout);
|
||||
let mut line = String::new();
|
||||
|
||||
while buf_reader
|
||||
.read_line(&mut line)
|
||||
.await
|
||||
.map_err(IOError::from)?
|
||||
> 0
|
||||
{
|
||||
let val_line = self.censor_log(line.clone());
|
||||
|
||||
{
|
||||
let mut output = self.output.write().await;
|
||||
output.push_str(&val_line);
|
||||
}
|
||||
{
|
||||
let mut log_file = self.log_file.write().await;
|
||||
log_file
|
||||
.write_all(val_line.as_bytes())
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
}
|
||||
|
||||
line.clear();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_stderr(
|
||||
&self,
|
||||
child_stderr: ChildStderr,
|
||||
) -> crate::Result<()> {
|
||||
let mut buf_reader = BufReader::new(child_stderr);
|
||||
let mut line = String::new();
|
||||
|
||||
while buf_reader
|
||||
.read_line(&mut line)
|
||||
.await
|
||||
.map_err(IOError::from)?
|
||||
> 0
|
||||
{
|
||||
let val_line = self.censor_log(line.clone());
|
||||
|
||||
{
|
||||
let mut output = self.output.write().await;
|
||||
output.push_str(&val_line);
|
||||
}
|
||||
{
|
||||
let mut log_file = self.log_file.write().await;
|
||||
log_file
|
||||
.write_all(val_line.as_bytes())
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
}
|
||||
|
||||
line.clear();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn censor_log(&self, mut val: String) -> String {
|
||||
for (find, replace) in &self.censor_strings {
|
||||
val = val.replace(find, replace);
|
||||
}
|
||||
|
||||
val
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ impl DirectoryInfo {
|
||||
&self,
|
||||
profile_id: &ProfilePathId,
|
||||
) -> crate::Result<PathBuf> {
|
||||
Ok(profile_id.get_full_path().await?.join("modrinth_logs"))
|
||||
Ok(profile_id.get_full_path().await?.join("logs"))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -16,17 +16,22 @@ pub struct DiscordGuard {
|
||||
impl DiscordGuard {
|
||||
/// Initialize discord IPC client, and attempt to connect to it
|
||||
/// If it fails, it will still return a DiscordGuard, but the client will be unconnected
|
||||
pub async fn init() -> crate::Result<DiscordGuard> {
|
||||
pub async fn init(is_offline: bool) -> crate::Result<DiscordGuard> {
|
||||
let mut dipc =
|
||||
DiscordIpcClient::new("1084015525241311292").map_err(|e| {
|
||||
DiscordIpcClient::new("1123683254248148992").map_err(|e| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Could not create Discord client {}",
|
||||
e,
|
||||
))
|
||||
})?;
|
||||
let res = dipc.connect(); // Do not need to connect to Discord to use app
|
||||
let connected = if res.is_ok() {
|
||||
Arc::new(AtomicBool::new(true))
|
||||
|
||||
let connected = if !is_offline {
|
||||
let res = dipc.connect(); // Do not need to connect to Discord to use app
|
||||
if res.is_ok() {
|
||||
Arc::new(AtomicBool::new(true))
|
||||
} else {
|
||||
Arc::new(AtomicBool::new(false))
|
||||
}
|
||||
} else {
|
||||
Arc::new(AtomicBool::new(false))
|
||||
};
|
||||
@@ -51,11 +56,46 @@ impl DiscordGuard {
|
||||
true
|
||||
}
|
||||
|
||||
// check online
|
||||
pub async fn check_online(&self) -> bool {
|
||||
let state = match State::get().await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let offline = state.offline.read().await;
|
||||
if *offline {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Set the activity to the given message
|
||||
/// First checks if discord is disabled, and if so, clear the activity instead
|
||||
pub async fn set_activity(
|
||||
&self,
|
||||
msg: &str,
|
||||
reconnect_if_fail: bool,
|
||||
) -> crate::Result<()> {
|
||||
if !self.check_online().await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if discord is disabled, and if so, clear the activity instead
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
if settings.disable_discord_rpc {
|
||||
Ok(self.clear_activity(true).await?)
|
||||
} else {
|
||||
Ok(self.force_set_activity(msg, reconnect_if_fail).await?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the activity to the given message, regardless of if discord is disabled or offline
|
||||
/// Should not be used except for in the above method, or if it is already known that discord is enabled (specifically for state initialization) and we are connected to the internet
|
||||
pub async fn force_set_activity(
|
||||
&self,
|
||||
msg: &str,
|
||||
reconnect_if_fail: bool,
|
||||
) -> crate::Result<()> {
|
||||
// Attempt to connect if not connected. Do not continue if it fails, as the client.set_activity can panic if it never was connected
|
||||
if !self.retry_if_not_ready().await {
|
||||
@@ -99,14 +139,13 @@ impl DiscordGuard {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
/// Clear the activity
|
||||
/// Clear the activity entirely ('disabling' the RPC until the next set_activity)
|
||||
pub async fn clear_activity(
|
||||
&self,
|
||||
reconnect_if_fail: bool,
|
||||
) -> crate::Result<()> {
|
||||
// Attempt to connect if not connected. Do not continue if it fails, as the client.clear_activity can panic if it never was connected
|
||||
if !self.retry_if_not_ready().await {
|
||||
if !self.check_online().await || !self.retry_if_not_ready().await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -138,7 +177,7 @@ impl DiscordGuard {
|
||||
res.map_err(could_not_clear_err)?;
|
||||
}
|
||||
Ok(())
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Clear the activity, but if there is a running profile, set the activity to that instead
|
||||
pub async fn clear_to_default(
|
||||
@@ -147,6 +186,15 @@ impl DiscordGuard {
|
||||
) -> crate::Result<()> {
|
||||
let state: Arc<tokio::sync::RwLockReadGuard<'_, State>> =
|
||||
State::get().await?;
|
||||
|
||||
{
|
||||
let settings = state.settings.read().await;
|
||||
if settings.disable_discord_rpc {
|
||||
println!("Discord is disabled, clearing activity");
|
||||
return self.clear_activity(true).await;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(existing_child) = state
|
||||
.children
|
||||
.read()
|
||||
|
||||
@@ -69,6 +69,8 @@ impl Metadata {
|
||||
) -> crate::Result<Self> {
|
||||
let mut metadata = None;
|
||||
let metadata_path = dirs.caches_meta_dir().await.join("metadata.json");
|
||||
let metadata_backup_path =
|
||||
dirs.caches_meta_dir().await.join("metadata.json.bak");
|
||||
|
||||
if let Ok(metadata_json) =
|
||||
read_json::<Metadata>(&metadata_path, io_semaphore).await
|
||||
@@ -85,6 +87,13 @@ impl Metadata {
|
||||
)
|
||||
.await?;
|
||||
|
||||
write(
|
||||
&metadata_backup_path,
|
||||
&serde_json::to_vec(&metadata_fetch).unwrap_or_default(),
|
||||
io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
metadata = Some(metadata_fetch);
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
@@ -96,6 +105,18 @@ impl Metadata {
|
||||
tracing::warn!("Unable to fetch launcher metadata: {err}")
|
||||
}
|
||||
}
|
||||
} else if let Ok(metadata_json) =
|
||||
read_json::<Metadata>(&metadata_backup_path, io_semaphore).await
|
||||
{
|
||||
metadata = Some(metadata_json);
|
||||
std::fs::copy(&metadata_backup_path, &metadata_path).map_err(
|
||||
|err| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Error restoring metadata backup: {err}"
|
||||
))
|
||||
.as_error()
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(meta) = metadata {
|
||||
@@ -118,6 +139,15 @@ impl Metadata {
|
||||
.caches_meta_dir()
|
||||
.await
|
||||
.join("metadata.json");
|
||||
let metadata_backup_path = state
|
||||
.directories
|
||||
.caches_meta_dir()
|
||||
.await
|
||||
.join("metadata.json.bak");
|
||||
|
||||
if metadata_path.exists() {
|
||||
std::fs::copy(&metadata_path, &metadata_backup_path).unwrap();
|
||||
}
|
||||
|
||||
write(
|
||||
&metadata_path,
|
||||
|
||||
@@ -127,6 +127,10 @@ impl State {
|
||||
.await)
|
||||
}
|
||||
|
||||
pub fn initialized() -> bool {
|
||||
LAUNCHER_STATE.initialized()
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
async fn initialize_state() -> crate::Result<RwLock<State>> {
|
||||
@@ -180,16 +184,18 @@ impl State {
|
||||
creds_fut,
|
||||
}?;
|
||||
|
||||
let children = Children::new();
|
||||
let auth_flow = AuthTask::new();
|
||||
let safety_processes = SafeProcesses::new();
|
||||
|
||||
let discord_rpc = DiscordGuard::init().await?;
|
||||
{
|
||||
let discord_rpc = DiscordGuard::init(is_offline).await?;
|
||||
if !settings.disable_discord_rpc && !is_offline {
|
||||
// Add default Idling to discord rich presence
|
||||
let _ = discord_rpc.set_activity("Idling...", true).await;
|
||||
// Force add to avoid recursion
|
||||
let _ = discord_rpc.force_set_activity("Idling...", true).await;
|
||||
}
|
||||
|
||||
let children = Children::new();
|
||||
|
||||
// Starts a loop of checking if we are online, and updating
|
||||
Self::offine_check_loop();
|
||||
|
||||
@@ -238,11 +244,6 @@ impl State {
|
||||
|
||||
/// Updates state with data from the web, if we are online
|
||||
pub fn update() {
|
||||
tokio::task::spawn(Metadata::update());
|
||||
tokio::task::spawn(Tags::update());
|
||||
tokio::task::spawn(Profiles::update_projects());
|
||||
tokio::task::spawn(Profiles::update_modrinth_versions());
|
||||
tokio::task::spawn(CredentialsStore::update_creds());
|
||||
tokio::task::spawn(async {
|
||||
if let Ok(state) = crate::State::get().await {
|
||||
if !*state.offline.read().await {
|
||||
@@ -252,8 +253,9 @@ impl State {
|
||||
let res4 = Profiles::update_projects();
|
||||
let res5 = Settings::update_java();
|
||||
let res6 = CredentialsStore::update_creds();
|
||||
let res7 = Settings::update_default_user();
|
||||
|
||||
let _ = join!(res1, res2, res3, res4, res5, res6);
|
||||
let _ = join!(res1, res2, res3, res4, res5, res6, res7);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,11 +124,22 @@ impl ProjectPathId {
|
||||
&self,
|
||||
profile: ProfilePathId,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let _state = State::get().await?;
|
||||
let profile_dir = profile.get_full_path().await?;
|
||||
Ok(profile_dir.join(&self.0))
|
||||
}
|
||||
|
||||
// Gets inner path in unix convention as a String
|
||||
// ie: 'mods\myproj' -> 'mods/myproj'
|
||||
// Used for exporting to mrpack, which should have a singular convention
|
||||
pub fn get_inner_path_unix(&self) -> crate::Result<String> {
|
||||
Ok(self
|
||||
.0
|
||||
.components()
|
||||
.map(|c| c.as_os_str().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/"))
|
||||
}
|
||||
|
||||
// Create a new ProjectPathId from a relative path
|
||||
pub fn new(path: &Path) -> Self {
|
||||
ProjectPathId(PathBuf::from(path))
|
||||
@@ -193,6 +204,15 @@ pub struct ProfileMetadata {
|
||||
pub struct LinkedData {
|
||||
pub project_id: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
|
||||
#[serde(default = "default_locked")]
|
||||
pub locked: Option<bool>,
|
||||
}
|
||||
|
||||
// Called if linked_data is present but locked is not
|
||||
// Meaning this is a legacy profile, and we should consider it locked
|
||||
pub fn default_locked() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -722,7 +742,15 @@ impl Profiles {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(profile) = prof {
|
||||
// Clear out modrinth_logs of all files in profiles folder (these are legacy)
|
||||
// TODO: should be removed in a future build
|
||||
let modrinth_logs = path.join("modrinth_logs");
|
||||
if modrinth_logs.exists() {
|
||||
let _ = std::fs::remove_dir_all(modrinth_logs);
|
||||
}
|
||||
|
||||
let path = io::canonicalize(path)?;
|
||||
Profile::watch_fs(&path, file_watcher).await?;
|
||||
profiles.insert(profile.profile_id(), profile);
|
||||
|
||||
@@ -31,6 +31,8 @@ pub struct Settings {
|
||||
pub version: u32,
|
||||
pub collapsed_navigation: bool,
|
||||
#[serde(default)]
|
||||
pub disable_discord_rpc: bool,
|
||||
#[serde(default)]
|
||||
pub hide_on_process: bool,
|
||||
#[serde(default)]
|
||||
pub default_page: DefaultPage,
|
||||
@@ -49,8 +51,10 @@ pub struct Settings {
|
||||
impl Settings {
|
||||
#[tracing::instrument]
|
||||
pub async fn init(file: &Path) -> crate::Result<Self> {
|
||||
if file.exists() {
|
||||
fs::read(&file)
|
||||
let mut rescued = false;
|
||||
|
||||
let settings = if file.exists() {
|
||||
let loaded_settings = fs::read(&file)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
@@ -61,9 +65,25 @@ impl Settings {
|
||||
.and_then(|it| {
|
||||
serde_json::from_slice::<Settings>(&it)
|
||||
.map_err(crate::Error::from)
|
||||
})
|
||||
});
|
||||
// settings is corrupted. Back up the file and create a new one
|
||||
if let Err(ref err) = loaded_settings {
|
||||
tracing::error!("Failed to load settings file: {err}. ");
|
||||
let backup_file = file.with_extension("json.bak");
|
||||
tracing::error!("Corrupted settings file will be backed up as {}, and a new settings file will be created.", backup_file.display());
|
||||
let _ = fs::rename(file, backup_file).await;
|
||||
rescued = true;
|
||||
}
|
||||
loaded_settings.ok()
|
||||
} else {
|
||||
Ok(Self {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(settings) = settings {
|
||||
Ok(settings)
|
||||
} else {
|
||||
// Create new settings file
|
||||
let settings = Self {
|
||||
theme: Theme::Dark,
|
||||
memory: MemorySettings::default(),
|
||||
force_fullscreen: false,
|
||||
@@ -77,16 +97,21 @@ impl Settings {
|
||||
max_concurrent_writes: 10,
|
||||
version: CURRENT_FORMAT_VERSION,
|
||||
collapsed_navigation: false,
|
||||
disable_discord_rpc: false,
|
||||
hide_on_process: false,
|
||||
default_page: DefaultPage::Home,
|
||||
developer_mode: false,
|
||||
opt_out_analytics: false,
|
||||
advanced_rendering: true,
|
||||
fully_onboarded: false,
|
||||
fully_onboarded: rescued, // If we rescued the settings file, we should consider the user fully onboarded
|
||||
|
||||
// By default, the config directory is the same as the settings directory
|
||||
loaded_config_dir: DirectoryInfo::get_initial_settings_dir(),
|
||||
})
|
||||
};
|
||||
if rescued {
|
||||
settings.sync(file).await?;
|
||||
}
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +149,32 @@ impl Settings {
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn update_default_user() {
|
||||
let res = async {
|
||||
let state = State::get().await?;
|
||||
let settings_read = state.settings.read().await;
|
||||
|
||||
if settings_read.default_user.is_none() {
|
||||
drop(settings_read);
|
||||
let users = state.users.read().await;
|
||||
let user = users.0.iter().next().map(|(id, _)| *id);
|
||||
state.settings.write().await.default_user = user;
|
||||
}
|
||||
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("Unable to update default user: {err}")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
||||
fs::write(to, serde_json::to_vec(self)?)
|
||||
|
||||
@@ -32,6 +32,8 @@ impl Tags {
|
||||
) -> crate::Result<Self> {
|
||||
let mut tags = None;
|
||||
let tags_path = dirs.caches_meta_dir().await.join("tags.json");
|
||||
let tags_path_backup =
|
||||
dirs.caches_meta_dir().await.join("tags.json.bak");
|
||||
|
||||
if let Ok(tags_json) = read_json::<Self>(&tags_path, io_semaphore).await
|
||||
{
|
||||
@@ -43,11 +45,28 @@ impl Tags {
|
||||
tracing::warn!("Unable to fetch launcher tags: {err}")
|
||||
}
|
||||
}
|
||||
} else if let Ok(tags_json) =
|
||||
read_json::<Self>(&tags_path_backup, io_semaphore).await
|
||||
{
|
||||
tags = Some(tags_json);
|
||||
std::fs::copy(&tags_path_backup, &tags_path).map_err(|err| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Error restoring tags backup: {err}"
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Some(tags_data) = tags {
|
||||
write(&tags_path, &serde_json::to_vec(&tags_data)?, io_semaphore)
|
||||
.await?;
|
||||
write(
|
||||
&tags_path_backup,
|
||||
&serde_json::to_vec(&tags_data)?,
|
||||
io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(tags_data)
|
||||
} else {
|
||||
Err(crate::ErrorKind::NoValueFor(String::from("launcher tags"))
|
||||
@@ -68,6 +87,14 @@ impl Tags {
|
||||
|
||||
let tags_path =
|
||||
state.directories.caches_meta_dir().await.join("tags.json");
|
||||
let tags_path_backup = state
|
||||
.directories
|
||||
.caches_meta_dir()
|
||||
.await
|
||||
.join("tags.json.bak");
|
||||
if tags_path.exists() {
|
||||
std::fs::copy(&tags_path, &tags_path_backup).unwrap();
|
||||
}
|
||||
|
||||
write(
|
||||
&tags_path,
|
||||
|
||||
Reference in New Issue
Block a user