From 3effe8b1c690b7951a277872d5246bd8788f1bcd Mon Sep 17 00:00:00 2001 From: Davide Date: Tue, 5 Nov 2024 17:19:18 +0100 Subject: [PATCH] write system logs to disk and use mc manifest to fetch java profiles --- .../Library/Instance/Tabs/Log/LogsContent.tsx | 4 +- .../carbon_app/src/managers/instance/log.rs | 23 ++- .../carbon_app/src/managers/instance/mod.rs | 24 ++- .../carbon_app/src/managers/instance/run.rs | 143 +++++++++++------- .../src/managers/minecraft/minecraft.rs | 7 +- .../carbon_app/src/managers/minecraft/mod.rs | 70 +++++---- 6 files changed, 159 insertions(+), 112 deletions(-) diff --git a/apps/desktop/packages/mainWindow/src/pages/Library/Instance/Tabs/Log/LogsContent.tsx b/apps/desktop/packages/mainWindow/src/pages/Library/Instance/Tabs/Log/LogsContent.tsx index b3dff91fc..920c16025 100644 --- a/apps/desktop/packages/mainWindow/src/pages/Library/Instance/Tabs/Log/LogsContent.tsx +++ b/apps/desktop/packages/mainWindow/src/pages/Library/Instance/Tabs/Log/LogsContent.tsx @@ -212,7 +212,7 @@ const LogsContent = (props: Props) => { const [columns, setColumns] = createSignal({ timestamp: true, logger: true, - sourceKind: true, + sourceKind: false, threadName: true, level: true }); @@ -254,7 +254,7 @@ const LogsContent = (props: Props) => { class="justify-center hidden" ref={(el) => props.assignScrollBottomRef(el)} > -
+
{ self.create_log(instance_id, Some(file_as_datetime)).await; // read the file and send it to the log - let Ok(file) = tokio::fs::File::open(entry.path()).await else { + let Ok(mut file) = tokio::fs::File::open(entry.path()).await else { tracing::error!({ file_name = ?file_name }, "Failed to open log file"); continue; }; - let mut reader = tokio::io::BufReader::new(file); - - let mut buf = [0; 1024]; let mut stdout_processor = LogProcessor::new(LogEntrySourceKind::StdOut, tx).await; - while let Ok(size) = reader.read(&mut buf).await { - if size == 0 { - break; - } + let mut buf = Vec::new(); + let _ = file.read_to_end(&mut buf).await; - if let Err(e) = stdout_processor.process_data(&buf[..size], None).await - { - tracing::error!({ error = ?e }, "Failed to process stdout data"); - } + if let Err(e) = stdout_processor.process_data(&buf, None).await { + tracing::error!({ error = ?e }, "Failed to process stdout data"); } } } @@ -324,6 +317,10 @@ impl ManagerRef<'_, InstanceManager> { } } +pub fn format_message_as_log4j_event(message: &str) -> String { + format!("\n\t\n\n", Utc::now().timestamp_millis(), message) +} + pub struct LogProcessor { pub parser: LogParser, pub kind: LogEntrySourceKind, diff --git a/crates/carbon_app/src/managers/instance/mod.rs b/crates/carbon_app/src/managers/instance/mod.rs index e573730d8..29986002f 100644 --- a/crates/carbon_app/src/managers/instance/mod.rs +++ b/crates/carbon_app/src/managers/instance/mod.rs @@ -1643,28 +1643,24 @@ impl<'s> ManagerRef<'s, InstanceManager> { None => None, }; - let mut version_info = None; + let mut mc_manifest = None; if let Some(mc_version) = &mc_version { - version_info = Some( - self.app - .minecraft_manager() - .get_minecraft_version(&mc_version) - .await, - ); + let manifest = self.app.minecraft_manager().get_minecraft_manifest().await; + mc_manifest = manifest.ok(); } let required_java_profile = mc_version.clone().and_then(|version| { - let version_info = version_info.unwrap().ok(); - let java = version_info - .map(|version| version.java_version) - .and_then(|version| version.map(|version| version.component)); - - let Some(java) = java else { + let Some(manifest) = mc_manifest else { return None; }; + let java = manifest + .versions + .iter() + .find(|profile| profile.id == version) + .and_then(|version| version.java_profile.clone()); - let Ok(required_java) = MinecraftJavaProfile::try_from(java.as_str()) else { + let Some(required_java) = java else { return None; }; diff --git a/crates/carbon_app/src/managers/instance/run.rs b/crates/carbon_app/src/managers/instance/run.rs index a731d3913..d78985430 100644 --- a/crates/carbon_app/src/managers/instance/run.rs +++ b/crates/carbon_app/src/managers/instance/run.rs @@ -10,7 +10,9 @@ use crate::domain::modplatforms::curseforge::filters::ModFileParameters; use crate::domain::modplatforms::modrinth::search::VersionID; use crate::domain::runtime_path::InstancePath; use crate::domain::vtask::VisualTaskId; -use crate::managers::instance::log::{GameLog, LogEntry, LogEntrySourceKind}; +use crate::managers::instance::log::{ + format_message_as_log4j_event, GameLog, LogEntry, LogEntrySourceKind, +}; use crate::managers::instance::modpack::packinfo; use crate::managers::instance::schema::make_instance_config; use crate::managers::java::java_checker::{JavaChecker, RealJavaChecker}; @@ -50,6 +52,7 @@ use tokio::fs::File; use tokio::io::AsyncWriteExt; use tokio::sync::{watch, Mutex, Semaphore}; use tokio::task::JoinHandle; +use tokio::time::Instant; use tokio::{io::AsyncReadExt, sync::mpsc}; use tracing::{debug, info, trace}; @@ -244,30 +247,66 @@ impl ManagerRef<'_, InstanceManager> { let (log_id, log) = app.instance_manager().create_log(instance_id, None).await; + let now = Utc::now(); + + let log_file_name = format!("{}_{}", now.format("%Y-%m-%d"), now.format("%H-%M-%S")); + + let logs_file_path = instance_path + .get_gdl_logs_path() + .join(format!("{}.log", log_file_name)); + + let logs_file_path_clone = logs_file_path.clone(); + + let file_fut = logs_file_path + .parent() + .map(|p| async { + if let Err(e) = + tokio::fs::create_dir_all(&logs_file_path_clone.parent().unwrap()).await + { + tracing::error!({ error = ?e }, "Failed to create log directory"); + } + }) + .map(|f| async { + f.await; + tokio::fs::File::create(&logs_file_path_clone).await + }); + + let mut file = match file_fut { + Some(f) => f.await.ok(), + None => None, + }; + app.meta_cache_manager() .watch_and_prioritize(Some(instance_id)) .await; let result = app.instance_manager().list_mods(instance_id).await?; + let msg = format!( + "Mods ({} enabled / {} disabled): {}", + result.iter().filter(|mod_| mod_.enabled).count(), + result.iter().filter(|mod_| !mod_.enabled).count(), + result.into_iter().fold(String::new(), |mut acc, mod_| { + acc.push_str("\n\t ["); + if mod_.enabled { + acc.push_str("x]"); + } else { + acc.push_str(" ]"); + } - log.send_modify(|log| { - log.add_entry(LogEntry::system_message(format!( - "Mods: {}", - result.into_iter().fold(String::new(), |mut acc, mod_| { - acc.push_str("\n\t ["); - if mod_.enabled { - acc.push_str("x]"); - } else { - acc.push_str(" ]"); - } + acc.push(' '); + acc.push_str(&mod_.filename); - acc.push(' '); - acc.push_str(&mod_.filename); + acc + }) + ); - acc - }) - ))) + log.send_modify(|log| { + log.add_entry(LogEntry::system_message(msg.clone())); }); + if let Some(file) = file.as_mut() { + file.write_all(format_message_as_log4j_event(&msg).as_bytes()) + .await?; + } let installation_task = tokio::spawn(async move { let instance_manager = app.instance_manager(); @@ -943,10 +982,16 @@ impl ManagerRef<'_, InstanceManager> { || anyhow::anyhow!("instance java version unsupported") )?; + let msg = format!("Suggested Java Profile: {java_profile:?}"); + log.send_modify(|log| { - log.add_entry(LogEntry::system_message(format!("Suggested Java Profile: {java_profile:?}"))) + log.add_entry(LogEntry::system_message(msg.clone())) }); + if let Some(file) = file.as_mut() { + file.write_all(format_message_as_log4j_event(&msg).as_bytes()).await?; + } + let mut required_java_system_profile = SystemJavaProfileName::try_from(java_profile).with_context( || anyhow::anyhow!("System java version unsupported") )?; @@ -1132,10 +1177,16 @@ impl ManagerRef<'_, InstanceManager> { } }; + let msg = format!("Using Java: {java:#?}"); + log.send_modify(|log| { - log.add_entry(LogEntry::system_message(format!("Using Java: {java:#?}"))) + log.add_entry(LogEntry::system_message(msg.clone())) }); + if let Some(file) = file.as_mut() { + file.write_all(format_message_as_log4j_event(&msg).as_bytes()).await?; + } + t_request_modloader_info.start_opaque(); for modloader in version.modloaders.iter() { @@ -1242,7 +1293,7 @@ impl ManagerRef<'_, InstanceManager> { t_request_minecraft_files.start_opaque(); let (lwjgl_group, version_files) = app.minecraft_manager() - .get_all_version_info_files(version_info.clone(), &java.arch, &log) + .get_all_version_info_files(version_info.clone(), &java.arch, &log, file.as_mut()) .await?; downloads.extend(version_files); @@ -1661,16 +1712,6 @@ impl ManagerRef<'_, InstanceManager> { time_at_start = Some(Utc::now()); - let log_file_name = format!( - "{}_{}", - start_time.format("%Y-%m-%d"), - start_time.format("%H-%M-%S") - ); - - let logs_file_path = instance_path - .get_gdl_logs_path() - .join(format!("{}.log", log_file_name)); - tokio::select! { _ = child.wait() => { tracing::info!("Instance waited"); @@ -1679,7 +1720,7 @@ impl ManagerRef<'_, InstanceManager> { tracing::info!("Instance killed"); drop(child.kill().await); }, - _ = read_logs(log.clone(), stdout, stderr, logs_file_path) => { + _ = read_logs(log.clone(), stdout, stderr, file.as_mut()) => { tracing::info!("Instance read logs"); }, _ = update_playtime => { @@ -1702,9 +1743,16 @@ impl ManagerRef<'_, InstanceManager> { } if let Ok(exitcode) = child.wait().await { - log.send_modify(|log| { - log.add_entry(LogEntry::system_message(format!("{exitcode}"))) - }); + let msg = format!("{exitcode}"); + + log.send_modify(|log| log.add_entry(LogEntry::system_message(msg.clone()))); + + if let Some(file) = file.as_mut() { + // TODO: not sure how to handle an error in here + let _ = file + .write_all(format_message_as_log4j_event(&msg).as_bytes()) + .await; + } } let _ = app.rich_presence_manager().stop_activity().await; @@ -1916,7 +1964,7 @@ async fn read_logs( log: watch::Sender, stdout: impl AsyncReadExt + Unpin + Send + 'static, stderr: impl AsyncReadExt + Unpin + Send + 'static, - log_file_path: PathBuf, + file: Option<&mut File>, ) { let (stdout_tx, stdout_rx) = mpsc::channel::>(1000); let (stderr_tx, stderr_rx) = mpsc::channel::>(1000); @@ -1924,9 +1972,9 @@ async fn read_logs( let stdout_task = tokio::spawn(read_pipe(stdout, stdout_tx)); let stderr_task = tokio::spawn(read_pipe(stderr, stderr_tx)); - let process_task = tokio::spawn(process_logs(log, stdout_rx, stderr_rx, log_file_path)); + process_logs(log, stdout_rx, stderr_rx, file).await; - let _ = tokio::join!(stdout_task, stderr_task, process_task); + let _ = tokio::join!(stdout_task, stderr_task); } async fn read_pipe( @@ -1961,25 +2009,8 @@ async fn process_logs( log: watch::Sender, mut stdout_rx: mpsc::Receiver>, mut stderr_rx: mpsc::Receiver>, - log_file_path: PathBuf, + mut file: Option<&mut File>, ) { - let file_fut = log_file_path - .parent() - .map(|p| async { - if let Err(e) = tokio::fs::create_dir_all(log_file_path.parent().unwrap()).await { - tracing::error!({ error = ?e }, "Failed to create log directory"); - } - }) - .map(|f| async { - f.await; - tokio::fs::File::create(&log_file_path).await - }); - - let mut file = match file_fut { - Some(f) => f.await.ok(), - None => None, - }; - let mut stdout_processor = LogProcessor::new(LogEntrySourceKind::StdOut, log.clone()).await; let mut stderr_processor = LogProcessor::new(LogEntrySourceKind::StdErr, log).await; @@ -1987,12 +2018,12 @@ async fn process_logs( loop { tokio::select! { Some(data) = stdout_rx.recv() => { - if let Err(e) = stdout_processor.process_data(&data, file.as_mut()).await { + if let Err(e) = stdout_processor.process_data(&data, file.as_deref_mut()).await { tracing::error!("Failed to process stdout data: {}", e); } } Some(data) = stderr_rx.recv() => { - if let Err(e) = stderr_processor.process_data(&data, file.as_mut()).await { + if let Err(e) = stderr_processor.process_data(&data, file.as_deref_mut()).await { tracing::error!("Failed to process stderr data: {}", e); } } diff --git a/crates/carbon_app/src/managers/minecraft/minecraft.rs b/crates/carbon_app/src/managers/minecraft/minecraft.rs index 87a9798c9..ae69837eb 100644 --- a/crates/carbon_app/src/managers/minecraft/minecraft.rs +++ b/crates/carbon_app/src/managers/minecraft/minecraft.rs @@ -25,7 +25,7 @@ use regex::{Captures, Regex}; use reqwest::Url; use strum_macros::EnumIter; use thiserror::Error; -use tokio::{process::Child, sync::Mutex}; +use tokio::{process::Child, sync::Mutex, time::Instant}; use tracing::{info, trace, warn}; use crate::{ @@ -56,6 +56,7 @@ pub async fn get_manifest( meta_base_url: &Url, ) -> anyhow::Result { let server_url = meta_base_url.join(&format!("minecraft/{}/manifest.json", META_VERSION))?; + let time = Instant::now(); let new_manifest = reqwest_client .get(server_url) .send() @@ -63,6 +64,10 @@ pub async fn get_manifest( .json::() .await?; + let elapsed = time.elapsed(); + + tracing::info!("Fetched manifest {}ms", elapsed.as_millis()); + Ok(new_manifest) } diff --git a/crates/carbon_app/src/managers/minecraft/mod.rs b/crates/carbon_app/src/managers/minecraft/mod.rs index 88a60ea08..9b93d03e8 100644 --- a/crates/carbon_app/src/managers/minecraft/mod.rs +++ b/crates/carbon_app/src/managers/minecraft/mod.rs @@ -13,7 +13,11 @@ use daedalus::{ modded::Manifest, }; use reqwest::Url; -use tokio::sync::{watch, Mutex}; +use tokio::{ + fs::File, + io::AsyncWriteExt, + sync::{watch, Mutex}, +}; use crate::domain::{ java::JavaArch, @@ -26,7 +30,7 @@ use crate::domain::{ use self::minecraft::get_lwjgl_meta; use super::{ - instance::log::{GameLog, LogEntry}, + instance::log::{format_message_as_log4j_event, GameLog, LogEntry}, ManagerRef, }; @@ -92,6 +96,7 @@ impl ManagerRef<'_, MinecraftManager> { version_info: VersionInfo, java_arch: &JavaArch, log: &watch::Sender, + mut file: Option<&mut File>, ) -> anyhow::Result<(LibraryGroup, Vec)> { let runtime_path = &self.app.settings_manager().runtime_path; @@ -111,12 +116,14 @@ impl ManagerRef<'_, MinecraftManager> { ) .await?; - log.send_modify(|log| { - log.add_entry(LogEntry::system_message(format!( - "LWJGL Meta {} - {}", - lwjgl.uid, lwjgl.version - ))) - }); + let msg = format!("LWJGL Meta {} - {}", lwjgl.uid, lwjgl.version); + + log.send_modify(|log| log.add_entry(LogEntry::system_message(msg.clone()))); + + if let Some(file) = file.as_mut() { + file.write_all(format_message_as_log4j_event(&msg).as_bytes()) + .await?; + } let mut libs = version_info .libraries @@ -136,17 +143,23 @@ impl ManagerRef<'_, MinecraftManager> { .to_string_lossy() .to_string(); - log.send_modify(|log| { - log.add_entry(LogEntry::system_message(format!( - "Libraries: \n\t-> {}", - downloadables - .iter() - .map(|v| v.path.to_string_lossy().to_string()) - .map(|v| v.strip_prefix(&runtime_path_str).unwrap_or(&v).to_owned()) - .collect::>() - .join("\n\t-> ") - ))) - }); + let msg = format!( + "Libraries ({}): \n\t-> {}", + downloadables.len(), + downloadables + .iter() + .map(|v| v.path.to_string_lossy().to_string()) + .map(|v| v.strip_prefix(&runtime_path_str).unwrap_or(&v).to_owned()) + .collect::>() + .join("\n\t-> ") + ); + + log.send_modify(|log| log.add_entry(LogEntry::system_message(msg.clone()))); + + if let Some(file) = file.as_mut() { + file.write_all(format_message_as_log4j_event(&msg).as_bytes()) + .await?; + } let client_main_jar = version_download_into_downloadable( version_info @@ -158,12 +171,17 @@ impl ManagerRef<'_, MinecraftManager> { runtime_path, ); - log.send_modify(|log| { - log.add_entry(LogEntry::system_message(format!( - "Client Main Jar: {}", - client_main_jar.path.to_string_lossy().to_string() - ))) - }); + let msg = format!( + "Client Main Jar: {}", + client_main_jar.path.to_string_lossy().to_string() + ); + + log.send_modify(|log| log.add_entry(LogEntry::system_message(msg.clone()))); + + if let Some(file) = file.as_mut() { + file.write_all(format_message_as_log4j_event(&msg).as_bytes()) + .await?; + } let (assets_meta, _) = assets::get_meta( Arc::clone(&self.app.prisma_client), @@ -340,7 +358,7 @@ mod tests { let (_, vanilla_files) = app .minecraft_manager() - .get_all_version_info_files(version_info.clone(), &java_component.arch, &log) + .get_all_version_info_files(version_info.clone(), &java_component.arch, &log, None) .await .unwrap();