From 90d1a1700009c97634bf6421b70013f9937168f4 Mon Sep 17 00:00:00 2001 From: Umatriz Date: Mon, 22 Jul 2024 12:53:05 +0300 Subject: [PATCH 1/5] feat: add custom logger - Collects events - Collects span's data --- Cargo.lock | 12 + crates/client/Cargo.toml | 3 +- crates/client/src/context.rs | 9 +- crates/client/src/main.rs | 34 ++- crates/client/src/subscriber.rs | 214 ++++++++++++++++++ .../src/downloads/downloaders/file.rs | 4 +- 6 files changed, 264 insertions(+), 12 deletions(-) create mode 100644 crates/client/src/subscriber.rs diff --git a/Cargo.lock b/Cargo.lock index fc40adb..5d34b13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,6 +812,7 @@ dependencies = [ "pollster", "serde", "serde_json", + "time", "tokio", "toml", "tracing", @@ -2828,6 +2829,15 @@ dependencies = [ "syn 2.0.69", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "objc" version = "0.2.7" @@ -4138,7 +4148,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index bedd9a8..66bcd0a 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -38,6 +38,7 @@ pollster = "0.3.0" tracing-appender = "0.2.2" once_cell = "1.19.0" parking_lot = { version = "0.12.3", features = ["serde"] } +time = { version = "0.3.36", features = ["local-offset"] } [lints.rust] -rust_2018_idioms = "deny" \ No newline at end of file +rust_2018_idioms = "deny" diff --git a/crates/client/src/context.rs b/crates/client/src/context.rs index c5d065c..9ac9d83 100644 --- a/crates/client/src/context.rs +++ b/crates/client/src/context.rs @@ -1,6 +1,7 @@ use crate::{ errors_pool::ErrorPoolExt, states::States, + subscriber::EguiLayer, views::{self, profiles::ProfilesPage, settings::SettingsPage, ModManager, ProfileInfo, View}, Tab, TabKind, }; @@ -15,7 +16,7 @@ use nomi_core::{ }; pub struct MyContext { - pub collector: EventCollector, + pub egui_layer: EguiLayer, pub launcher_manifest: &'static LauncherManifest, pub file_dialog: FileDialog, @@ -27,7 +28,7 @@ pub struct MyContext { } impl MyContext { - pub fn new(collector: EventCollector) -> Self { + pub fn new(egui_layer: EguiLayer) -> Self { const EMPTY_MANIFEST: &LauncherManifest = &LauncherManifest { latest: Latest { release: String::new(), @@ -39,7 +40,7 @@ impl MyContext { let launcher_manifest_ref = pollster::block_on(get_launcher_manifest()).report_error().unwrap_or(EMPTY_MANIFEST); Self { - collector, + egui_layer, launcher_manifest: launcher_manifest_ref, file_dialog: FileDialog::new(), is_profile_window_open: false, @@ -97,7 +98,7 @@ impl TabViewer for MyContext { .ui(ui), TabKind::Logs => { ScrollArea::horizontal().show(ui, |ui| { - ui.add(egui_tracing::Logs::new(self.collector.clone())); + self.egui_layer.ui(ui); }); } TabKind::DownloadProgress => { diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index dca94b8..2b8bdcb 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -9,6 +9,7 @@ use egui_notify::Toasts; use egui_tracing::EventCollector; use open_directory::open_directory_native; use std::path::Path; +use subscriber::EguiLayer; use ui_ext::TOASTS_ID; use views::{add_tab_menu::AddTab, View}; @@ -33,6 +34,7 @@ pub mod open_directory; pub mod collections; pub mod progress; +pub mod subscriber; pub mod tab; pub use tab::*; @@ -42,7 +44,7 @@ pub mod states; pub use consts::*; fn main() { - let collector = egui_tracing::EventCollector::default().with_level(Level::INFO); + // let collector = egui_tracing::EventCollector::default().with_level(Level::INFO); let appender = tracing_appender::rolling::hourly(DOT_NOMI_LOGS_DIR, "nomi.log"); let (non_blocking, _guard) = tracing_appender::non_blocking(appender); @@ -53,9 +55,11 @@ fn main() { let stdout_sub = Layer::new().with_writer(std::io::stdout.with_max_level(Level::DEBUG)).pretty(); // stdout_sub.set_ansi(false); + let egui_layer = EguiLayer::new().with_level(Level::DEBUG); + let subscriber = tracing_subscriber::registry() .with(EnvFilter::builder().parse("client=debug,nomi_core=debug").unwrap()) - .with(collector.clone()) + .with(egui_layer.clone()) .with(stdout_sub) .with(file_sub); @@ -63,6 +67,8 @@ fn main() { egui_task_manager::setup!(); + add_with_span(2, 2); + let native_options = eframe::NativeOptions { viewport: ViewportBuilder::default().with_inner_size(Vec2::new(1280.0, 720.0)), ..Default::default() @@ -73,20 +79,38 @@ fn main() { native_options, Box::new(|cc| { egui_extras::install_image_loaders(&cc.egui_ctx); - Ok(Box::new(MyTabs::new(collector))) + Ok(Box::new(MyTabs::new(egui_layer))) }), ); info!("Exiting") } +fn add_with_span(a: usize, b: usize) -> usize { + let span = tracing::span!(Level::INFO, "test_span"); + let _guard = span.enter(); + + add(a, b) +} + +#[tracing::instrument(fields(result))] +fn add(a: usize, b: usize) -> usize { + let result = a + b; + + tracing::Span::current().record("result", result); + + info!("Finished the calculations"); + + result +} + struct MyTabs { context: MyContext, dock_state: DockState, } impl MyTabs { - pub fn new(collector: EventCollector) -> Self { + pub fn new(egui_layer: EguiLayer) -> Self { let tabs = [TabKind::Profiles, TabKind::Logs, TabKind::Settings] .map(|kind| Tab { id: kind.id(), kind }) .into(); @@ -104,7 +128,7 @@ impl MyTabs { ); Self { - context: MyContext::new(collector), + context: MyContext::new(egui_layer), dock_state, } } diff --git a/crates/client/src/subscriber.rs b/crates/client/src/subscriber.rs new file mode 100644 index 0000000..963d3ad --- /dev/null +++ b/crates/client/src/subscriber.rs @@ -0,0 +1,214 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use eframe::egui::{self, text::LayoutJob, Color32, Pos2, Sense, TextFormat, Vec2}; +use parking_lot::Mutex; +use time::OffsetDateTime; +use tracing::{ + field::{Field, Visit}, + span, Event, Level, Subscriber, +}; +use tracing_subscriber::{ + layer::Context, + registry::{LookupSpan, Scope, SpanRef}, + Layer, +}; + +#[derive(Clone)] +pub struct EguiLayer { + events: Arc>>, + level: Level, +} + +impl Default for EguiLayer { + fn default() -> Self { + Self::new() + } +} + +impl EguiLayer { + pub fn new() -> Self { + Self { + events: Arc::new(Mutex::new(Vec::new())), + level: Level::INFO, + } + } + + pub fn with_level(mut self, level: Level) -> Self { + self.level = level; + self + } + + pub fn add_event_with_scope(&self, event: EventData, scope: ScopeData) { + if event.level <= self.level { + self.events.lock().push((event, scope)); + } + } + + pub fn ui(&self, ui: &mut egui::Ui) { + egui::Grid::new("egui_logs_ui").show(ui, |ui| { + let lock = self.events.lock(); + for (event, scope) in lock.iter() { + ui.label(format!("{} {} {}", event.time.date(), event.time.time(), event.time.offset())); + ui.label(format!("{}", event.level)); + ui.label(&event.target); + ui.horizontal(|ui| { + if let Some((_, content)) = event.fields.0.iter().find(|(name, _)| name == "message") { + ui.label(content); + } + for (name, content) in &event.fields.0 { + if name == "message" { + continue; + } + + ui.label(format!("{name}: {content}")); + } + }); + ui.end_row(); + ui.vertical(|ui| { + for span in &scope.spans { + let mut job = LayoutJob::default(); + job.append( + "in", + 5.0, + TextFormat { + italics: true, + ..Default::default() + }, + ); + + job.append(span.name, 5.0, TextFormat::default()); + + job.append( + "with", + 5.0, + TextFormat { + italics: true, + ..Default::default() + }, + ); + + for (name, content) in &span.fields.0 { + job.append(&format!("{name}: {content}"), 5.0, TextFormat::default()); + } + + let galley = ui.fonts(|fonts| fonts.layout_job(job)); + let (response, painter) = ui.allocate_painter(Vec2::new(300.0, 18.0), Sense::hover()); + painter.galley(response.rect.left_top(), galley, Color32::WHITE); + } + }); + ui.end_row(); + } + }); + } +} + +impl Layer for EguiLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) { + let mut fields = Fields::default(); + attrs.record(&mut FieldsVisitor(&mut fields)); + + if let Some(span) = ctx.span(id) { + span.extensions_mut().insert(SpanFieldsExtension { fields }); + } + } + + fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) { + let Some(span) = ctx.span(span) else { + return; + }; + + let mut fields = Fields::default(); + values.record(&mut FieldsVisitor(&mut fields)); + + match span.extensions_mut().get_mut::() { + Some(span_fields) => { + span_fields.fields.0.extend(fields.0); + } + None => span.extensions_mut().insert(SpanFieldsExtension { fields }), + }; + } + + fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) { + self.add_event_with_scope(EventData::new(event), ScopeData::new(ctx.event_scope(event))); + } +} + +#[derive(Debug, Clone)] +pub struct EventData { + pub target: String, + pub level: tracing::Level, + pub fields: Fields, + pub time: OffsetDateTime, +} + +impl EventData { + pub fn new(event: &Event<'_>) -> Self { + let metadata = event.metadata(); + + let mut fields = Fields::default(); + event.record(&mut FieldsVisitor(&mut fields)); + + let time = OffsetDateTime::now_local().unwrap_or(OffsetDateTime::now_utc()); + + EventData { + target: metadata.target().to_owned(), + level: metadata.level().to_owned(), + fields, + time, + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct Fields(Vec<(String, String)>); + +struct FieldsVisitor<'a>(&'a mut Fields); + +impl<'a> Visit for FieldsVisitor<'a> { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + self.0 .0.push((field.name().to_string(), format!("{:?}", value))); + } +} + +pub struct SpanFieldsExtension { + fields: Fields, +} + +pub struct ScopeData { + spans: Vec, +} + +impl ScopeData { + pub fn new(scope: Option>) -> Self + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + let spans = scope.map(|scope| scope.from_root().map(SpanData::new).collect()).unwrap_or_default(); + + Self { spans } + } +} + +pub struct SpanData { + pub name: &'static str, + pub fields: Fields, +} + +impl SpanData { + pub fn new(span: SpanRef<'_, S>) -> Self + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + let fields = span + .extensions() + .get::() + .map(|ext| &ext.fields) + .cloned() + .unwrap_or_default(); + + Self { name: span.name(), fields } + } +} diff --git a/crates/nomi-core/src/downloads/downloaders/file.rs b/crates/nomi-core/src/downloads/downloaders/file.rs index 36042e3..5f0bfdc 100644 --- a/crates/nomi-core/src/downloads/downloaders/file.rs +++ b/crates/nomi-core/src/downloads/downloaders/file.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use tracing::error; +use tracing::warn; use crate::{ calculate_sha1, @@ -63,7 +63,7 @@ impl Downloadable for FileDownloader { if hash != calculated_hash { let s = format!("Hashes does not match. {hash} != {calculated_hash}"); - error!("{s}"); + warn!("{s}"); return DownloadResult(Err(DownloadError::Error { url: self.url.clone(), path: self.path.clone(), From 41285277d8de94713d6c1433e84ceee5637b2c11 Mon Sep 17 00:00:00 2001 From: Umatriz Date: Mon, 22 Jul 2024 16:33:40 +0300 Subject: [PATCH 2/5] feat: finished the custom layer --- crates/client/src/main.rs | 10 ++- crates/client/src/subscriber.rs | 113 ++++++++++++++++++++++++-------- 2 files changed, 92 insertions(+), 31 deletions(-) diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 2b8bdcb..75ab2da 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -55,10 +55,10 @@ fn main() { let stdout_sub = Layer::new().with_writer(std::io::stdout.with_max_level(Level::DEBUG)).pretty(); // stdout_sub.set_ansi(false); - let egui_layer = EguiLayer::new().with_level(Level::DEBUG); + let egui_layer = EguiLayer::new().with_level(Level::TRACE); let subscriber = tracing_subscriber::registry() - .with(EnvFilter::builder().parse("client=debug,nomi_core=debug").unwrap()) + .with(EnvFilter::builder().parse("client=trace,nomi_core=debug").unwrap()) .with(egui_layer.clone()) .with(stdout_sub) .with(file_sub); @@ -67,6 +67,12 @@ fn main() { egui_task_manager::setup!(); + tracing::trace!("Test trace!"); + tracing::debug!("Test debug!"); + tracing::warn!("Test warn!"); + tracing::info!("Test info!"); + tracing::error!("Test error!"); + add_with_span(2, 2); let native_options = eframe::NativeOptions { diff --git a/crates/client/src/subscriber.rs b/crates/client/src/subscriber.rs index 963d3ad..6581ee1 100644 --- a/crates/client/src/subscriber.rs +++ b/crates/client/src/subscriber.rs @@ -1,8 +1,11 @@ use std::{collections::BTreeMap, sync::Arc}; -use eframe::egui::{self, text::LayoutJob, Color32, Pos2, Sense, TextFormat, Vec2}; +use eframe::{ + egui::{self, text::LayoutJob, Color32, Pos2, Rect, Sense, Stroke, TextFormat, Vec2}, + epaint::PathStroke, +}; use parking_lot::Mutex; -use time::OffsetDateTime; +use time::{format_description, OffsetDateTime}; use tracing::{ field::{Field, Visit}, span, Event, Level, Subscriber, @@ -44,40 +47,87 @@ impl EguiLayer { } } + pub fn level_color(level: Level) -> Color32 { + match level { + Level::TRACE => Color32::GRAY, + Level::DEBUG => Color32::LIGHT_BLUE, + Level::WARN => Color32::from_rgb(196, 160, 0), + Level::INFO => Color32::LIGHT_GREEN, + Level::ERROR => Color32::LIGHT_RED, + } + } + pub fn ui(&self, ui: &mut egui::Ui) { egui::Grid::new("egui_logs_ui").show(ui, |ui| { let lock = self.events.lock(); for (event, scope) in lock.iter() { - ui.label(format!("{} {} {}", event.time.date(), event.time.time(), event.time.offset())); - ui.label(format!("{}", event.level)); - ui.label(&event.target); + let color = Self::level_color(event.level); + ui.label(format!( + "{} {} {}", + event.time.date(), + event + .time + .time() + .format(&format_description::parse("[hour]:[minute]:[second]").unwrap()) + .unwrap(), + event.time.offset() + )); + ui.colored_label(color, format!("{}", event.level)); ui.horizontal(|ui| { + ui.colored_label(color, format!("{}:", &event.target)); if let Some((_, content)) = event.fields.0.iter().find(|(name, _)| name == "message") { - ui.label(content); + ui.colored_label(color, content); } for (name, content) in &event.fields.0 { if name == "message" { continue; } - ui.label(format!("{name}: {content}")); + ui.colored_label(color, format!("{name}: {content}")); } }); ui.end_row(); - ui.vertical(|ui| { - for span in &scope.spans { - let mut job = LayoutJob::default(); - job.append( - "in", - 5.0, - TextFormat { - italics: true, - ..Default::default() - }, - ); - - job.append(span.name, 5.0, TextFormat::default()); + let draw_layout_job = |ui: &mut egui::Ui, job| { + let galley = ui.fonts(|fonts| fonts.layout_job(job)); + let mut response = ui.allocate_response(Vec2::new(0.0, ui.available_height()), Sense::hover()); + response.rect = response.rect.translate(Vec2::new(25.0, 0.0)); + + ui.painter().galley(response.rect.left_top(), galley, Color32::WHITE); + }; + + if let Some((file, line)) = event.file.as_ref().and_then(|f| event.line.map(|l| (f, l))) { + let mut job = LayoutJob::default(); + + job.append( + "at", + 5.0, + TextFormat { + italics: true, + ..Default::default() + }, + ); + + job.append(&format!("{}:{}", file, line), 5.0, TextFormat::default()); + + draw_layout_job(ui, job); + ui.end_row(); + } + + for span in &scope.spans { + let mut job = LayoutJob::default(); + job.append( + "in", + 5.0, + TextFormat { + italics: true, + ..Default::default() + }, + ); + + job.append(span.name, 5.0, TextFormat::default()); + + if !span.fields.0.is_empty() { job.append( "with", 5.0, @@ -86,17 +136,18 @@ impl EguiLayer { ..Default::default() }, ); + } - for (name, content) in &span.fields.0 { - job.append(&format!("{name}: {content}"), 5.0, TextFormat::default()); - } - - let galley = ui.fonts(|fonts| fonts.layout_job(job)); - let (response, painter) = ui.allocate_painter(Vec2::new(300.0, 18.0), Sense::hover()); - painter.galley(response.rect.left_top(), galley, Color32::WHITE); + for (name, content) in &span.fields.0 { + job.append(&format!("{name}: {content}"), 5.0, TextFormat::default()); } - }); - ui.end_row(); + + draw_layout_job(ui, job); + ui.end_row(); + } + + ui.allocate_space(Vec2::new(0.0, 5.0)); + ui.end_row() } }); } @@ -140,6 +191,8 @@ where pub struct EventData { pub target: String, pub level: tracing::Level, + pub file: Option, + pub line: Option, pub fields: Fields, pub time: OffsetDateTime, } @@ -156,6 +209,8 @@ impl EventData { EventData { target: metadata.target().to_owned(), level: metadata.level().to_owned(), + file: metadata.file().map(ToString::to_string), + line: metadata.line(), fields, time, } From 443f4e3c21089be40f8afbe7c0490b8c89222e48 Mon Sep 17 00:00:00 2001 From: Umatriz Date: Mon, 22 Jul 2024 16:36:24 +0300 Subject: [PATCH 3/5] fix: set `Level` to be `DEBUG` --- crates/client/src/context.rs | 1 - crates/client/src/main.rs | 33 ++------------------------------- crates/client/src/subscriber.rs | 7 ++----- 3 files changed, 4 insertions(+), 37 deletions(-) diff --git a/crates/client/src/context.rs b/crates/client/src/context.rs index 9ac9d83..af2a117 100644 --- a/crates/client/src/context.rs +++ b/crates/client/src/context.rs @@ -9,7 +9,6 @@ use eframe::egui::{self, ScrollArea}; use egui_dock::TabViewer; use egui_file_dialog::FileDialog; use egui_task_manager::TaskManager; -use egui_tracing::EventCollector; use nomi_core::{ repository::launcher_manifest::{Latest, LauncherManifest}, state::get_launcher_manifest, diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 75ab2da..bf7608e 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -6,7 +6,6 @@ use eframe::{ }; use egui_dock::{DockArea, DockState, NodeIndex, Style}; use egui_notify::Toasts; -use egui_tracing::EventCollector; use open_directory::open_directory_native; use std::path::Path; use subscriber::EguiLayer; @@ -44,8 +43,6 @@ pub mod states; pub use consts::*; fn main() { - // let collector = egui_tracing::EventCollector::default().with_level(Level::INFO); - let appender = tracing_appender::rolling::hourly(DOT_NOMI_LOGS_DIR, "nomi.log"); let (non_blocking, _guard) = tracing_appender::non_blocking(appender); @@ -55,10 +52,10 @@ fn main() { let stdout_sub = Layer::new().with_writer(std::io::stdout.with_max_level(Level::DEBUG)).pretty(); // stdout_sub.set_ansi(false); - let egui_layer = EguiLayer::new().with_level(Level::TRACE); + let egui_layer = EguiLayer::new().with_level(Level::DEBUG); let subscriber = tracing_subscriber::registry() - .with(EnvFilter::builder().parse("client=trace,nomi_core=debug").unwrap()) + .with(EnvFilter::builder().parse("client=debug,nomi_core=debug").unwrap()) .with(egui_layer.clone()) .with(stdout_sub) .with(file_sub); @@ -67,14 +64,6 @@ fn main() { egui_task_manager::setup!(); - tracing::trace!("Test trace!"); - tracing::debug!("Test debug!"); - tracing::warn!("Test warn!"); - tracing::info!("Test info!"); - tracing::error!("Test error!"); - - add_with_span(2, 2); - let native_options = eframe::NativeOptions { viewport: ViewportBuilder::default().with_inner_size(Vec2::new(1280.0, 720.0)), ..Default::default() @@ -92,24 +81,6 @@ fn main() { info!("Exiting") } -fn add_with_span(a: usize, b: usize) -> usize { - let span = tracing::span!(Level::INFO, "test_span"); - let _guard = span.enter(); - - add(a, b) -} - -#[tracing::instrument(fields(result))] -fn add(a: usize, b: usize) -> usize { - let result = a + b; - - tracing::Span::current().record("result", result); - - info!("Finished the calculations"); - - result -} - struct MyTabs { context: MyContext, dock_state: DockState, diff --git a/crates/client/src/subscriber.rs b/crates/client/src/subscriber.rs index 6581ee1..0172a85 100644 --- a/crates/client/src/subscriber.rs +++ b/crates/client/src/subscriber.rs @@ -1,9 +1,6 @@ -use std::{collections::BTreeMap, sync::Arc}; +use std::sync::Arc; -use eframe::{ - egui::{self, text::LayoutJob, Color32, Pos2, Rect, Sense, Stroke, TextFormat, Vec2}, - epaint::PathStroke, -}; +use eframe::egui::{self, text::LayoutJob, Color32, Sense, TextFormat, Vec2}; use parking_lot::Mutex; use time::{format_description, OffsetDateTime}; use tracing::{ From 40a1db8541bae798a0fb59348cf6fb24aa5e920b Mon Sep 17 00:00:00 2001 From: Umatriz Date: Tue, 23 Jul 2024 09:24:21 +0300 Subject: [PATCH 4/5] feat: add ability to see game logs in the UI --- Cargo.lock | 17 ++++- Cargo.toml | 4 +- crates/client/src/context.rs | 13 ++-- crates/client/src/states.rs | 6 +- crates/client/src/views.rs | 2 + crates/client/src/views/logs.rs | 87 ++++++++++++++++++++++ crates/client/src/views/profiles.rs | 7 +- crates/nomi-core/Cargo.toml | 2 + crates/nomi-core/src/configs/profile.rs | 9 ++- crates/nomi-core/src/instance/launch.rs | 45 +++++++++-- crates/nomi-core/src/instance/logs.rs | 37 +++++++++ crates/nomi-core/src/instance/mod.rs | 1 + crates/nomi-core/tests/fabric_test.rs | 3 +- crates/nomi-core/tests/full_fabric_test.rs | 6 +- 14 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 crates/client/src/views/logs.rs create mode 100644 crates/nomi-core/src/instance/logs.rs diff --git a/Cargo.lock b/Cargo.lock index 5d34b13..600e4e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2741,6 +2741,8 @@ dependencies = [ "tar", "thiserror", "tokio", + "tokio-stream", + "tokio-util", "toml", "tracing", "tracing-subscriber", @@ -4216,9 +4218,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", @@ -4264,6 +4266,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.11" diff --git a/Cargo.toml b/Cargo.toml index 3e16131..ddccdd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,9 @@ panic = "abort" [workspace.dependencies] async-trait = "0.1.73" -tokio = { version = "1.28.2", features = ["rt", "macros", "process"] } +tokio = { version = "1.38.1", features = ["rt", "macros", "process"] } +tokio-stream = "0.1.15" +tokio-util = "0.7.11" itertools = "0.13.0" typed-builder = "0.18.2" diff --git a/crates/client/src/context.rs b/crates/client/src/context.rs index af2a117..9b5ceba 100644 --- a/crates/client/src/context.rs +++ b/crates/client/src/context.rs @@ -2,10 +2,10 @@ use crate::{ errors_pool::ErrorPoolExt, states::States, subscriber::EguiLayer, - views::{self, profiles::ProfilesPage, settings::SettingsPage, ModManager, ProfileInfo, View}, + views::{self, profiles::ProfilesPage, settings::SettingsPage, Logs, ModManager, ProfileInfo, View}, Tab, TabKind, }; -use eframe::egui::{self, ScrollArea}; +use eframe::egui::{self}; use egui_dock::TabViewer; use egui_file_dialog::FileDialog; use egui_task_manager::TaskManager; @@ -82,6 +82,7 @@ impl TabViewer for MyContext { profiles_state: &mut self.states.profiles, menu_state: &mut self.states.add_profile_menu_state, tabs_state: &mut self.states.tabs, + logs_state: &self.states.logs_state, launcher_manifest: self.launcher_manifest, is_profile_window_open: &mut self.is_profile_window_open, @@ -95,11 +96,11 @@ impl TabViewer for MyContext { file_dialog: &mut self.file_dialog, } .ui(ui), - TabKind::Logs => { - ScrollArea::horizontal().show(ui, |ui| { - self.egui_layer.ui(ui); - }); + TabKind::Logs => Logs { + egui_layer: &self.egui_layer, + logs_state: &mut self.states.logs_state, } + .ui(ui), TabKind::DownloadProgress => { views::DownloadingProgress { manager: &self.manager, diff --git a/crates/client/src/states.rs b/crates/client/src/states.rs index 7a7f14c..24ec799 100644 --- a/crates/client/src/states.rs +++ b/crates/client/src/states.rs @@ -19,7 +19,7 @@ use crate::{ add_tab_menu::TabsState, profiles::ProfilesState, settings::{ClientSettingsState, SettingsState}, - AddProfileMenuState, ModManagerState, ProfileInfoState, ProfilesConfig, + AddProfileMenuState, LogsState, ModManagerState, ProfileInfoState, ProfilesConfig, }, }; @@ -27,6 +27,7 @@ pub struct States { pub tabs: TabsState, pub errors_pool: ErrorsPoolState, + pub logs_state: LogsState, pub java: JavaState, pub profiles: ProfilesState, pub settings: SettingsState, @@ -42,8 +43,9 @@ impl Default for States { Self { tabs: TabsState::new(), - java: JavaState::new(), errors_pool: ErrorsPoolState::default(), + logs_state: LogsState::new(), + java: JavaState::new(), profiles: ProfilesState { currently_downloading_profiles: HashSet::new(), profiles: read_toml_config_sync::(DOT_NOMI_PROFILES_CONFIG).unwrap_or_default(), diff --git a/crates/client/src/views.rs b/crates/client/src/views.rs index 5bab78b..a86f4cb 100644 --- a/crates/client/src/views.rs +++ b/crates/client/src/views.rs @@ -3,6 +3,7 @@ use eframe::egui::Ui; pub mod add_profile_menu; pub mod add_tab_menu; pub mod downloading_progress; +pub mod logs; pub mod mods_manager; pub mod profile_info; pub mod profiles; @@ -11,6 +12,7 @@ pub mod settings; pub use add_profile_menu::*; pub use add_tab_menu::*; pub use downloading_progress::*; +pub use logs::*; pub use mods_manager::*; pub use profile_info::*; pub use profiles::*; diff --git a/crates/client/src/views/logs.rs b/crates/client/src/views/logs.rs new file mode 100644 index 0000000..1822714 --- /dev/null +++ b/crates/client/src/views/logs.rs @@ -0,0 +1,87 @@ +use std::sync::Arc; + +use eframe::egui; +use nomi_core::instance::logs::GameLogsWriter; +use parking_lot::Mutex; + +use crate::subscriber::EguiLayer; + +use super::View; + +pub struct Logs<'a> { + pub egui_layer: &'a EguiLayer, + pub logs_state: &'a mut LogsState, +} + +#[derive(Default)] +pub struct LogsState { + pub selected_tab: LogsPage, + pub game_logs: Arc, +} + +#[derive(Default, PartialEq)] +pub enum LogsPage { + #[default] + Game, + Launcher, +} + +impl LogsState { + pub fn new() -> Self { + Self { ..Default::default() } + } +} + +impl View for Logs<'_> { + fn ui(mut self, ui: &mut eframe::egui::Ui) { + egui::TopBottomPanel::top("logs_page_panel").show_inside(ui, |ui| { + ui.horizontal(|ui| { + ui.selectable_value(&mut self.logs_state.selected_tab, LogsPage::Game, "Game"); + ui.selectable_value(&mut self.logs_state.selected_tab, LogsPage::Launcher, "Launcher"); + }); + }); + + match self.logs_state.selected_tab { + LogsPage::Game => self.game_ui(ui), + LogsPage::Launcher => self.launcher_ui(ui), + } + } +} + +impl Logs<'_> { + pub fn game_ui(&mut self, ui: &mut egui::Ui) { + egui::ScrollArea::both().stick_to_bottom(true).show(ui, |ui| { + ui.vertical(|ui| { + let lock = self.logs_state.game_logs.logs.lock(); + for message in lock.iter() { + ui.label(message); + } + }); + }); + } + + pub fn launcher_ui(&mut self, ui: &mut egui::Ui) { + egui::ScrollArea::both().stick_to_bottom(true).show(ui, |ui| self.egui_layer.ui(ui)); + } +} + +#[derive(Default)] +pub struct GameLogs { + logs: Arc>>, +} + +impl GameLogs { + pub fn new() -> Self { + Self::default() + } + + pub fn clear(&self) { + self.logs.lock().clear(); + } +} + +impl GameLogsWriter for GameLogs { + fn write(&self, data: nomi_core::instance::logs::GameLogsEvent) { + self.logs.lock().push(data.into_message()); + } +} diff --git a/crates/client/src/views/profiles.rs b/crates/client/src/views/profiles.rs index 90de126..e8988b2 100644 --- a/crates/client/src/views/profiles.rs +++ b/crates/client/src/views/profiles.rs @@ -29,7 +29,7 @@ use super::{ add_profile_menu::{AddProfileMenu, AddProfileMenuState}, load_mods, settings::SettingsState, - ModsConfig, ProfileInfoState, TabsState, View, + LogsState, ModsConfig, ProfileInfoState, TabsState, View, }; pub struct ProfilesPage<'a> { @@ -40,6 +40,7 @@ pub struct ProfilesPage<'a> { pub is_profile_window_open: &'a mut bool, + pub logs_state: &'a LogsState, pub tabs_state: &'a mut TabsState, pub profiles_state: &'a mut ProfilesState, pub menu_state: &'a mut AddProfileMenuState, @@ -185,6 +186,8 @@ impl View for ProfilesPage<'_> { let should_load_mods = profile.profile.loader().is_fabric(); let profile_id = profile.profile.id; + let game_logs = self.logs_state.game_logs.clone(); + game_logs.clear(); let run_game = Task::new( "Running the game", Caller::standard(async move { @@ -192,7 +195,7 @@ impl View for ProfilesPage<'_> { load_mods(profile_id).await.report_error(); } - instance.launch(user_data, &java_runner).await.report_error() + instance.launch(user_data, &java_runner, &*game_logs).await.report_error() }), ); diff --git a/crates/nomi-core/Cargo.toml b/crates/nomi-core/Cargo.toml index 13dd49a..6bad17d 100644 --- a/crates/nomi-core/Cargo.toml +++ b/crates/nomi-core/Cargo.toml @@ -8,6 +8,8 @@ repository = "https://github.com/Umatriz/nomi" [dependencies] tokio.workspace = true +tokio-stream.workspace = true +tokio-util.workspace = true anyhow.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/nomi-core/src/configs/profile.rs b/crates/nomi-core/src/configs/profile.rs index a6017b0..546b600 100644 --- a/crates/nomi-core/src/configs/profile.rs +++ b/crates/nomi-core/src/configs/profile.rs @@ -5,7 +5,10 @@ use serde::{Deserialize, Serialize}; use typed_builder::TypedBuilder; use crate::{ - instance::launch::{arguments::UserData, LaunchInstance}, + instance::{ + launch::{arguments::UserData, LaunchInstance}, + logs::GameLogsWriter, + }, repository::{java_runner::JavaRunner, manifest::VersionType}, }; @@ -71,9 +74,9 @@ pub struct VersionProfile { } impl VersionProfile { - pub async fn launch(&self, user_data: UserData, java_runner: &JavaRunner) -> anyhow::Result<()> { + pub async fn launch(&self, user_data: UserData, java_runner: &JavaRunner, logs_writer: &dyn GameLogsWriter) -> anyhow::Result<()> { match &self.state { - ProfileState::Downloaded(instance) => instance.launch(user_data, java_runner).await, + ProfileState::Downloaded(instance) => instance.launch(user_data, java_runner, logs_writer).await, ProfileState::NotDownloaded { .. } => Err(anyhow!("This profile is not downloaded!")), } } diff --git a/crates/nomi-core/src/instance/launch.rs b/crates/nomi-core/src/instance/launch.rs index 52a6c4e..81c4f14 100644 --- a/crates/nomi-core/src/instance/launch.rs +++ b/crates/nomi-core/src/instance/launch.rs @@ -2,12 +2,15 @@ use std::{ fs::{File, OpenOptions}, io, path::{Path, PathBuf}, + process::Stdio, }; use arguments::UserData; use serde::{Deserialize, Serialize}; use tokio::process::Command; -use tracing::{debug, info, trace, warn}; +use tokio_stream::StreamExt; +use tokio_util::codec::{FramedRead, LinesCodec}; +use tracing::{debug, error, info, trace, warn}; use crate::{ downloads::Assets, @@ -20,7 +23,11 @@ use crate::{ use self::arguments::ArgumentsBuilder; -use super::{profile::LoaderProfile, Undefined}; +use super::{ + logs::{GameLogsEvent, GameLogsWriter}, + profile::LoaderProfile, + Undefined, +}; pub mod arguments; pub mod rules; @@ -141,8 +148,8 @@ impl LaunchInstance { Ok(()) } - #[tracing::instrument(skip(self), err)] - pub async fn launch(&self, user_data: UserData, java_runner: &JavaRunner) -> anyhow::Result<()> { + #[tracing::instrument(skip(self, logs_writer), err)] + pub async fn launch(&self, user_data: UserData, java_runner: &JavaRunner, logs_writer: &dyn GameLogsWriter) -> anyhow::Result<()> { let manifest = read_json_config::(&self.settings.manifest_file).await?; let arguments_builder = ArgumentsBuilder::new(self, &manifest).with_classpath().with_userdata(user_data); @@ -163,16 +170,40 @@ impl LaunchInstance { let mut child = Command::new(java_runner.get()) .args(custom_jvm_arguments) .args(loader_jvm_arguments) - .args(dbg!(manifest_jvm_arguments)) + .args(manifest_jvm_arguments) .arg(main_class) - .args(dbg!(manifest_game_arguments)) + .args(manifest_game_arguments) .args(loader_game_arguments) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) // Works incorrectly so let's ignore it for now. // It will work when the instances are implemented. // .current_dir(std::fs::canonicalize(MINECRAFT_DIR)?) .spawn()?; - child.wait().await?.code().inspect(|code| info!("Minecraft exit code: {}", code)); + let stdout = child.stdout.take().expect("child did not have a handle to stdout"); + let stderr = child.stderr.take().expect("child did not have a handle to stdout"); + + // let mut stdout_reader = BufReader::new(stdout).lines(); + // let mut stderr_reader = BufReader::new(stderr).lines(); + + let stdout = FramedRead::new(stdout, LinesCodec::new()); + let stderr = FramedRead::new(stderr, LinesCodec::new()); + + tokio::spawn(async move { + if let Ok(out) = child.wait().await.inspect_err(|e| error!(error = ?e, "Unable to get the exit code")) { + out.code().inspect(|code| info!("Minecraft exit code: {}", code)); + }; + }); + + let mut read = stdout.merge(stderr); + + while let Some(line) = read.next().await { + match line { + Ok(line) => logs_writer.write(GameLogsEvent::new(line)), + Err(e) => error!(error = ?e, "Error occurred while decoding game's output"), + } + } Ok(()) } diff --git a/crates/nomi-core/src/instance/logs.rs b/crates/nomi-core/src/instance/logs.rs new file mode 100644 index 0000000..7a74e5d --- /dev/null +++ b/crates/nomi-core/src/instance/logs.rs @@ -0,0 +1,37 @@ +pub trait GameLogsWriter: Send + Sync { + fn write(&self, data: GameLogsEvent); +} + +pub struct GameLogsEvent { + message: String, +} + +impl GameLogsEvent { + pub fn new(message: String) -> Self { + Self { message } + } + + pub fn into_message(self) -> String { + self.message + } + + pub fn message(&self) -> &str { + &self.message + } +} + +/// `GameLogsWriter` that does nothing with provided events. +pub struct IgnoreLogs; + +impl GameLogsWriter for IgnoreLogs { + fn write(&self, _data: GameLogsEvent) {} +} + +/// `GameLogsWriter` that prints logs into stdout. +pub struct PrintLogs; + +impl GameLogsWriter for PrintLogs { + fn write(&self, data: GameLogsEvent) { + println!("{}", data.into_message()); + } +} diff --git a/crates/nomi-core/src/instance/mod.rs b/crates/nomi-core/src/instance/mod.rs index 5637d6a..970e424 100644 --- a/crates/nomi-core/src/instance/mod.rs +++ b/crates/nomi-core/src/instance/mod.rs @@ -2,6 +2,7 @@ use typed_builder::TypedBuilder; pub mod builder_ext; pub mod launch; +pub mod logs; pub mod profile; pub mod version_marker; diff --git a/crates/nomi-core/tests/fabric_test.rs b/crates/nomi-core/tests/fabric_test.rs index 8e76edf..a9cffef 100644 --- a/crates/nomi-core/tests/fabric_test.rs +++ b/crates/nomi-core/tests/fabric_test.rs @@ -2,6 +2,7 @@ use nomi_core::{ game_paths::GamePaths, instance::{ launch::{arguments::UserData, LaunchSettings}, + logs::PrintLogs, Instance, }, loaders::fabric::Fabric, @@ -48,5 +49,5 @@ async fn vanilla_test() { }; let l = builder.launch_instance(settings, None); - l.launch(UserData::default(), &JavaRunner::default()).await.unwrap(); + l.launch(UserData::default(), &JavaRunner::default(), &PrintLogs).await.unwrap(); } diff --git a/crates/nomi-core/tests/full_fabric_test.rs b/crates/nomi-core/tests/full_fabric_test.rs index f85cc11..9672e4a 100644 --- a/crates/nomi-core/tests/full_fabric_test.rs +++ b/crates/nomi-core/tests/full_fabric_test.rs @@ -4,6 +4,7 @@ use nomi_core::{ game_paths::GamePaths, instance::{ launch::{arguments::UserData, LaunchSettings}, + logs::PrintLogs, Instance, }, loaders::fabric::Fabric, @@ -61,5 +62,8 @@ async fn full_fabric_test() { .state(ProfileState::downloaded(launch)) .build(); - dbg!(profile).launch(UserData::default(), &JavaRunner::default()).await.unwrap(); + dbg!(profile) + .launch(UserData::default(), &JavaRunner::default(), &PrintLogs) + .await + .unwrap(); } From c0b33c60a2a3c61a35e3293cc2bc884e4419d445 Mon Sep 17 00:00:00 2001 From: Umatriz Date: Tue, 23 Jul 2024 09:28:16 +0300 Subject: [PATCH 5/5] remove `dbg!` in `rule.rs` and remove console window --- crates/client/src/main.rs | 3 +++ crates/nomi-core/src/instance/launch/rules.rs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index bf7608e..79fb0e2 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -1,3 +1,6 @@ +// Remove console window +#![windows_subsystem = "windows"] + use collections::{AssetsCollection, GameDownloadingCollection, GameRunnerCollection, JavaCollection}; use context::MyContext; use eframe::{ diff --git a/crates/nomi-core/src/instance/launch/rules.rs b/crates/nomi-core/src/instance/launch/rules.rs index 20bbee7..72bec52 100644 --- a/crates/nomi-core/src/instance/launch/rules.rs +++ b/crates/nomi-core/src/instance/launch/rules.rs @@ -18,7 +18,7 @@ pub fn is_rule_passes(rule: &Rule) -> bool { !(custom_res || demo || quick_realms) } - Some(RuleKind::JvmRule(os)) => os.name.as_ref().map_or(true, |target_os| dbg!(env::consts::OS == target_os)), + Some(RuleKind::JvmRule(os)) => os.name.as_ref().map_or(true, |target_os| env::consts::OS == target_os), None => true, }, @@ -41,7 +41,7 @@ pub fn is_all_rules_passed(rules: &[Rule]) -> bool { pub fn is_library_passes(lib: &Library) -> bool { match lib.rules.as_ref() { - Some(rules) => dbg!(is_all_rules_passed(rules)), + Some(rules) => is_all_rules_passed(rules), None => true, } }