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(); }