From 70570e1006155f6bdd273f1ee34c0dda43b2d19a Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Thu, 7 Nov 2024 07:05:57 -0400 Subject: [PATCH] use tokio to spawn server --- Cargo.lock | 83 ++++++++++++++- Cargo.toml | 8 ++ src/app.rs | 33 +++--- src/app/platform.rs | 10 ++ src/app/platform/native.rs | 203 +++++++++++++++++++++++++++++++++++-- src/main.rs | 2 +- 6 files changed, 315 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5517f3..2c6d58d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,15 @@ dependencies = [ "winit", ] +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -428,6 +437,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.21.5" @@ -555,12 +579,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -832,6 +857,7 @@ dependencies = [ "egui", "log", "serde", + "tokio", "tracing", "tracing-subscriber", "tracing-wasm", @@ -1195,6 +1221,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + [[package]] name = "gl_generator" version = "0.14.0" @@ -1396,9 +1428,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] @@ -1866,6 +1898,15 @@ dependencies = [ "objc", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -2192,6 +2233,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustix" version = "0.37.27" @@ -2291,6 +2338,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2477,6 +2530,28 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +dependencies = [ + "backtrace", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "toml_datetime" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index 45df725..9efab36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ eframe = { version = "0.29", default-features = false, features = [ "glow", # Use the glow rendering backend. Alternative: "wgpu". "persistence", # Enable restoring app state when restarting the app. ] } + log = "0.4" # You only need serde if you want app persistence: @@ -29,6 +30,13 @@ tracing = "0.1" # native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tokio = { version = "1", features = [ + "time", + "rt", + "macros", + "sync", + "rt-multi-thread", +] } # web: [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/src/app.rs b/src/app.rs index de4f16d..1a2ef5e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -19,7 +19,7 @@ pub struct TemplateApp { // Platform specific fields #[serde(skip)] /// Any platform that impls both [platform::PlatformTrait] and [Default] - platform: PlatformEnum, + platform: Platform, } impl Default for TemplateApp { @@ -70,8 +70,10 @@ impl eframe::App for TemplateApp { /// Called each time the UI needs repainting, which may be many times per second. fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - // Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. - // For inspiration and more examples, go to https://emilk.github.io/egui + // pass the ctx to the platform + if !self.platform.egui_ctx() { + self.platform.set_egui_ctx(ctx.clone()); + } // set the style style::style(ctx); @@ -93,11 +95,25 @@ impl eframe::App for TemplateApp { }); }); + egui::TopBottomPanel::bottom("footer").show(ctx, |ui| { + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + powered_by_egui_and_eframe(ui); + ui.add(egui::github_link_file!( + "https://github.com/PeerPiper/egui-multinode/blob/main/", + "Source code" + )); + egui::warn_if_debug_build(ui); + }); + }); + egui::CentralPanel::default().show(ctx, |ui| { // The central panel the region left after adding TopPanel's and SidePanel's ui.heading("PeerPiper Multinode"); - platform::show(self, ui); + ui.vertical(|ui| { + platform::show(self, ui); + self.platform.show(ctx, ui); + }); ui.add(egui::Slider::new(&mut self.value, 0.0..=10.0).text("value")); if ui.button("Increment").clicked() { @@ -105,15 +121,6 @@ impl eframe::App for TemplateApp { } ui.separator(); - - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - powered_by_egui_and_eframe(ui); - ui.add(egui::github_link_file!( - "https://github.com/PeerPiper/egui-multinode/blob/main/", - "Source code" - )); - egui::warn_if_debug_build(ui); - }); }); } } diff --git a/src/app/platform.rs b/src/app/platform.rs index 7c6cfe2..e80f9b4 100644 --- a/src/app/platform.rs +++ b/src/app/platform.rs @@ -44,6 +44,16 @@ impl PlatformEnum { Self::Web(platform) => platform.close(), } } + + /// Show + pub fn show(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { + match self { + #[cfg(not(target_arch = "wasm32"))] + Self::Native(platform) => platform.show(ctx, ui), + #[cfg(target_arch = "wasm32")] + Self::Web(platform) => platform.show(ui), + } + } } pub(crate) fn show(this: &mut super::TemplateApp, ui: &mut egui::Ui) { diff --git a/src/app/platform/native.rs b/src/app/platform/native.rs index 70eccc6..46b7240 100644 --- a/src/app/platform/native.rs +++ b/src/app/platform/native.rs @@ -3,17 +3,44 @@ //! For example, a native node will only be available here. Whereas the browser needs to connect //! to a remote node, which is handled in the `web` module. -use std::process::{Child, Command}; +use std::io::{BufRead as _, BufReader}; +use std::process::Child; +use std::process::{Command, Stdio}; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tokio::task; + +/// Track whether the Context has been set +pub(crate) struct ContextSet { + /// Whether the Context has been set + pub(crate) set: bool, + + /// The Context + pub(crate) ctx: egui::Context, +} pub(crate) struct Platform { // This is where you would put platform-specific fields server_process: Option, + + inbox: std::sync::mpsc::Receiver, + + log: Arc>>, + + /// Clone of the [egui::Context] so that the platform can trigger repaints + ctx: Arc>, } impl Default for Platform { fn default() -> Self { + let log = Arc::new(Mutex::new(Vec::new())); + let ctx: Arc> = Arc::new(Mutex::new(ContextSet { + set: false, + ctx: egui::Context::default(), + })); + #[cfg(debug_assertions)] - let server_bin_path = { + let program = { let path = std::env::current_dir() .unwrap() .join("../peerpiper/target/debug/peerpiper-server"); @@ -32,14 +59,149 @@ impl Default for Platform { .unwrap() .join("bin/peerpiper-server"); - println!("server_bin_path: {:?}", server_bin_path); + tracing::info!("server_bin_path: {:?}", program); + + // Create a communication channel between parent and child tasks + let (tx, mut rx): (Sender, Receiver) = channel(100); + + // Set up a sync channel to control the child process, mainly to kill() it when eframe exits + let (tx_control, rx_control) = std::sync::mpsc::channel(); + + let (outbox, inbox) = std::sync::mpsc::channel(); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Unable to create Runtime"); + + // Enter the runtime so that `tokio::spawn` is available immediately. + let _enter = rt.enter(); - let server_process = Command::new(server_bin_path) - .spawn() - .expect("Failed to start server"); + let log_clone = log.clone(); + let ctx_clone = ctx.clone(); + + // Execute the runtime in its own thread. + // The future doesn't have to do anything. In this example, it just sleeps forever. + std::thread::spawn(move || { + rt.block_on(async { + // Receive messages from the async task and update the GUI + let message_task = tokio::task::spawn(async move { + while let Some(message) = rx.recv().await { + tracing::info!("[child_msg] {}", message); + // push onto log + log_clone.lock().unwrap().push(message); + // use ctx to repaint, if Set + if ctx_clone.lock().unwrap().set { + ctx_clone.lock().unwrap().ctx.request_repaint(); + } else { + tracing::warn!("No ctx to repaint"); + } + } + }); + + // Spawn the child process in a separate Tokio task + let child_task = task::spawn(async move { + tracing::info!("Spawning child process"); + let mut child = Command::new(program) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to start child process"); + + // Read stdout and stderr from the child process + let stdout = child.stdout.take().expect("Failed to capture stdout"); + let stderr = child.stderr.take().expect("Failed to capture stderr"); + + // Send output from the child process to the parent task + let tx_clone = tx.clone(); + let stdout_task = task::spawn_blocking(move || { + for line in BufReader::new(stdout).lines() { + // clean up the text, as it shows: + //2024-11-06T21:25:08.224398Z  INFO peerpiper_core::libp2p::api: + // get rid of the [ garbage that doesn't display well outside the terminal + // remove anything that starts with ^[[ and ends with m + let text = line.unwrap().to_string(); + let line = text + .split("\x1b") + .map(|s| { + if s.starts_with("[") { + let mut split = s.split("m"); + split.next(); + split.collect::() + } else { + s.to_string() + } + }) + .collect::(); + + tx_clone + .blocking_send(line) + .expect("Failed to send message to parent task"); + } + }); + + let tx_clone = tx.clone(); + let stderr_task = task::spawn_blocking(move || { + for line in BufReader::new(stderr).lines() { + // make sure all the text is preserved, ASCII and UTF-8, everything, + // emojiis and all + let all_text = line.unwrap().chars().collect::>(); + let mut text = String::new(); + for c in all_text { + text.push(c); + } + + let line = text; + tx_clone + .blocking_send(format!("Stderr: {}", line)) + .expect("Failed to send message to parent task"); + } + }); + + tracing::info!("Child process spawned successfully"); + + // Send the child process handle to the parent task + if let Err(e) = tx_control.send(child) { + tracing::error!( + "Failed to send child process handle to parent task: {:?}", + e + ); + } + + // Wait for the stdout and stderr tasks to complete + if let Err(e) = stdout_task.await { + tracing::error!("Stdout task panicked: {:?}", e); + } + + if let Err(e) = stderr_task.await { + tracing::error!("Stderr task panicked: {:?}", e); + } + }); + + // Wait for either the child task to finish or the message task to complete + tokio::select! { + child_result = child_task => { + if let Err(e) = child_result { + tracing::error!("Child task panicked: {:?}", e); + } + } + _ = message_task => { + tracing::info!("Message processing task completed"); + } + } + }); + }); + + // Wait for the child process to start + let server_process = rx_control + .recv() + .expect("Failed to receive child process handle"); Self { server_process: Some(server_process), + inbox, + log, + ctx, } } } @@ -55,6 +217,17 @@ impl Drop for Platform { } impl Platform { + /// Returns whether the ctx is set or not + pub(crate) fn egui_ctx(&self) -> bool { + self.ctx.lock().unwrap().set + } + + /// Stes the ctx + pub(crate) fn set_egui_ctx(&mut self, ctx: egui::Context) { + self.ctx.lock().unwrap().ctx = ctx; + self.ctx.lock().unwrap().set = true; + } + // This is where you would put platform-specific methods pub(crate) fn close(&mut self) { // Kill the server process @@ -81,6 +254,24 @@ impl Platform { } } } + + /// Platform specific UI to show + pub(crate) fn show(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { + // label for Log + ui.separator(); + ui.label("Log:"); + + // SCROLLABLE SECTION for the log + egui::ScrollArea::vertical().show(ui, |ui| { + ui.vertical(|ui| { + for line in self.log.lock().unwrap().iter().rev() { + ui.label(line); + } + }); + }); + + ui.separator(); + } } pub(crate) fn show(this: &mut super::TemplateApp, ui: &mut egui::Ui) { diff --git a/src/main.rs b/src/main.rs index 05178bb..262fe0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ #[cfg(not(target_arch = "wasm32"))] fn main() -> eframe::Result { let _ = tracing_subscriber::fmt() - .with_env_filter("debug") + .with_env_filter("egui_multinode=info,peerpiper_server=debug,eframe=off") .try_init(); let native_options = eframe::NativeOptions {