From 2a9ebd4cdfd5a0289e3977a6d55c5db086bbc670 Mon Sep 17 00:00:00 2001 From: hinto-janaiyo Date: Mon, 5 Dec 2022 22:33:35 -0500 Subject: [PATCH] helper: p2pool - connect major [Helper] APIs to GUI thread Lots of stuff in this commit: 1. Implement [Start/Stop/Restart] and make it not possible for the GUI to interact with that UI if [Helper] is doing stuff. This prevents the obviously bad situation where [Helper] is in the middle of spawning P2Pool, but the user is still allowed to start it again, which would spawn another P2Pool. The main GUI matches on the state and disables the appropriate UI so the user can't do this. 2. Sync P2Pool's [Priv] and [Pub] output so that the GUI thread is only rendering it once a second. All of Gupax also refreshes at least once a second now as well. 3. Match the [ProcessState] with some colors in the GUI 4. GUI thread no longer directly starts/stops/restarts a process. It will call a function in [Helper] that acts as a proxy. 5. The tokio [async_spawn_p2pool_watchdog()] function that was a clone of the PTY version (but had async stuff) and all of the related functions like the async STDOUT/STDERR reader is now completely gone. It doesn't make sense to write the same code twice, both [Simple] and [Advanced] will have a PTY, only difference being the [Simple] UI won't have an input box. 6. P2Pool's exit code is now examined, either success or failure 7. Output was moved into it's own [Arc]. This allows for more efficient writing/reading since before I had to lock all of [Helper], which caused some noticable deadlocks in the GUI. 8. New [tab] field in [State], and GUI option to select the tab that Gupax will start on. --- src/constants.rs | 28 ++++- src/disk.rs | 3 + src/gupax.rs | 37 ++++-- src/helper.rs | 293 +++++++++++++++++++++++++---------------------- src/main.rs | 133 +++++++++++---------- src/p2pool.rs | 26 ++++- 6 files changed, 297 insertions(+), 223 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index 9ce8a80..c3f3b90 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -35,11 +35,27 @@ pub const BYTES_ICON: &[u8] = include_bytes!("../images/icons/icon@2x.png"); #[cfg(not(target_os = "macos"))] pub const BYTES_ICON: &[u8] = include_bytes!("../images/icons/icon.png"); pub const BYTES_BANNER: &[u8] = include_bytes!("../images/banner.png"); -pub const P2POOL_BASE_ARGS: &str = ""; -pub const XMRIG_BASE_ARGS: &str = "--http-host=127.0.0.1 --http-port=18088 --algo=rx/0 --coin=Monero"; pub const HORIZONTAL: &str = "--------------------------------------------"; pub const HORI_DOUBLE: &str = "----------------------------------------------------------------------------------------"; +// P2Pool & XMRig default API stuff +#[cfg(target_os = "windows")] +pub const P2POOL_API_PATH: &str = r"local\stats"; // The default relative FS path of P2Pool's local API +#[cfg(target_family = "unix")] +pub const P2POOL_API_PATH: &str = "local/stats"; +pub const XMRIG_API_URI: &str = "1/summary"; // The default relative URI of XMRig's API + +// Process state tooltips (online, offline, etc) +pub const P2POOL_ALIVE: &str = "P2Pool is online"; +pub const P2POOL_DEAD: &str = "P2Pool is offline"; +pub const P2POOL_FAILED: &str = "P2Pool is offline, and failed when exiting"; +pub const P2POOL_MIDDLE: &str = "P2Pool is in the middle of (re)starting/stopping"; + +pub const XMRIG_ALIVE: &str = "XMRig is online"; +pub const XMRIG_DEAD: &str = "XMRig is offline"; +pub const XMRIG_FAILED: &str = "XMRig is offline, and failed when exiting"; +pub const XMRIG_MIDDLE: &str = "XMRig is in the middle of (re)starting/stopping"; + // This is the typical space added when using // [ui.separator()] or [ui.group()] // Used for subtracting the width/height so @@ -50,8 +66,10 @@ pub const SPACE: f32 = 10.0; pub const RED: egui::Color32 = egui::Color32::from_rgb(230, 50, 50); pub const GREEN: egui::Color32 = egui::Color32::from_rgb(100, 230, 100); pub const YELLOW: egui::Color32 = egui::Color32::from_rgb(230, 230, 100); +pub const GRAY: egui::Color32 = egui::Color32::GRAY; pub const LIGHT_GRAY: egui::Color32 = egui::Color32::LIGHT_GRAY; pub const BLACK: egui::Color32 = egui::Color32::BLACK; +pub const DARK_GRAY: egui::Color32 = egui::Color32::from_rgb(18, 18, 18); // [Duration] constants pub const SECOND: std::time::Duration = std::time::Duration::from_secs(1); @@ -90,6 +108,12 @@ pub const GUPAX_LOCK_WIDTH: &str = "Automatically match the height against the w pub const GUPAX_LOCK_HEIGHT: &str = "Automatically match the width against the height in a 16:10 ratio; aka WIDTH = HEIGHT * 1.6"; pub const GUPAX_NO_LOCK: &str = "Allow individual selection of width and height"; pub const GUPAX_SET: &str = "Set the width/height of the Gupax window to the current values"; +pub const GUPAX_TAB_ABOUT: &str = "Set the tab Gupax starts on to: About"; +pub const GUPAX_TAB_STATUS: &str = "Set the tab Gupax starts on to: Status"; +pub const GUPAX_TAB_GUPAX: &str = "Set the tab Gupax starts on to: Gupax"; +pub const GUPAX_TAB_P2POOL: &str = "Set the tab Gupax starts on to: P2Pool"; +pub const GUPAX_TAB_XMRIG: &str = "Set the tab Gupax starts on to: XMRig"; + pub const GUPAX_SIMPLE: &str = r#"Use simple Gupax settings: - Update button diff --git a/src/disk.rs b/src/disk.rs index 7e22421..24d6862 100644 --- a/src/disk.rs +++ b/src/disk.rs @@ -44,6 +44,7 @@ use figment::providers::{Format,Toml}; use crate::{ constants::*, gupax::Ratio, + Tab, }; use log::*; @@ -154,6 +155,7 @@ impl State { selected_width: APP_DEFAULT_WIDTH as u16, selected_height: APP_DEFAULT_HEIGHT as u16, ratio: Ratio::Width, + tab: Tab::About, }, p2pool: P2pool { simple: true, @@ -586,6 +588,7 @@ pub struct Gupax { pub absolute_xmrig_path: PathBuf, pub selected_width: u16, pub selected_height: u16, + pub tab: Tab, pub ratio: Ratio, } diff --git a/src/gupax.rs b/src/gupax.rs index b1a6914..2049fac 100644 --- a/src/gupax.rs +++ b/src/gupax.rs @@ -29,6 +29,7 @@ use crate::{ update::*, ErrorState, Restart, + Tab, }; use std::{ thread, @@ -86,7 +87,7 @@ impl Gupax { // because I need to use [ui.set_enabled]s, but I can't // find a way to use a [ui.xxx()] with [ui.add_sized()]. // I have to pick one. This one seperates them though. - let height = height/8.0; + let height = if self.simple { height/5.0 } else { height/10.0 }; let width = width - SPACE; let updating = *update.lock().unwrap().updating.lock().unwrap(); ui.vertical(|ui| { @@ -113,12 +114,7 @@ impl Gupax { ui.horizontal(|ui| { ui.group(|ui| { let width = (width - SPACE*7.5)/4.0; - let height = height/8.0; -// let mut style = (*ctx.style()).clone(); -// style.spacing.icon_width_inner = width / 8.0; -// style.spacing.icon_width = width / 6.0; -// style.spacing.icon_spacing = 20.0; -// ctx.set_style(style); + let height = height/10.0; ui.add_sized([width, height], Checkbox::new(&mut self.auto_update, "Auto-update")).on_hover_text(GUPAX_AUTO_UPDATE); ui.separator(); ui.add_sized([width, height], Checkbox::new(&mut self.update_via_tor, "Update via Tor")).on_hover_text(GUPAX_UPDATE_VIA_TOR); @@ -217,19 +213,36 @@ impl Gupax { }); }); ui.style_mut().override_text_style = Some(egui::TextStyle::Button); + // Width/Height locks ui.group(|ui| { - let width = (width/4.0)-(SPACE*1.5); - let height = ui.available_height()/2.0; + let height = ui.available_height()/4.0; ui.horizontal(|ui| { - if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Ratio::Width, "Lock to width")).on_hover_text(GUPAX_LOCK_WIDTH).clicked() { self.ratio = Ratio::Width; } + use Ratio::*; + let width = (width/4.0)-(SPACE*1.5); + if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Width, "Lock to width")).on_hover_text(GUPAX_LOCK_WIDTH).clicked() { self.ratio = Width; } ui.separator(); - if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Ratio::Height, "Lock to height")).on_hover_text(GUPAX_LOCK_HEIGHT).clicked() { self.ratio = Ratio::Height; } + if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Height, "Lock to height")).on_hover_text(GUPAX_LOCK_HEIGHT).clicked() { self.ratio = Height; } ui.separator(); - if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Ratio::None, "No lock")).on_hover_text(GUPAX_NO_LOCK).clicked() { self.ratio = Ratio::None; } + if ui.add_sized([width, height], SelectableLabel::new(self.ratio == None, "No lock")).on_hover_text(GUPAX_NO_LOCK).clicked() { self.ratio = None; } if ui.add_sized([width, height], Button::new("Set")).on_hover_text(GUPAX_SET).clicked() { frame.set_window_size(Vec2::new(self.selected_width as f32, self.selected_height as f32)); } })}); + // Saved [Tab] + ui.group(|ui| { + let height = ui.available_height()/2.0; + let width = (width/5.0)-(SPACE*1.8); + ui.horizontal(|ui| { + if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::About, "About")).on_hover_text(GUPAX_TAB_ABOUT).clicked() { self.tab = Tab::About; } + ui.separator(); + if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::Status, "Status")).on_hover_text(GUPAX_TAB_STATUS).clicked() { self.tab = Tab::Status; } + ui.separator(); + if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::Gupax, "Gupax")).on_hover_text(GUPAX_TAB_GUPAX).clicked() { self.tab = Tab::Gupax; } + ui.separator(); + if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::P2pool, "P2Pool")).on_hover_text(GUPAX_TAB_P2POOL).clicked() { self.tab = Tab::P2pool; } + ui.separator(); + if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::Xmrig, "XMRig")).on_hover_text(GUPAX_TAB_XMRIG).clicked() { self.tab = Tab::Xmrig; } + })}); } fn spawn_file_window_thread(file_window: &Arc>, file_type: FileType) { diff --git a/src/helper.rs b/src/helper.rs index 4a6052b..04cd40a 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -88,21 +88,14 @@ pub struct Process { // The below are the handles to the actual child process. // [Simple] has no STDIN, but [Advanced] does. A PTY (pseudo-terminal) is - // required for P2Pool/XMRig to open their STDIN pipe, so whether [child] - // or [child_pty] actually has a [Some] depends on the users setting. - // [Simple] - child: Option>>, - stdout: Option, // Handle to STDOUT pipe - stderr: Option, // Handle to STDERR pipe - - // [Advanced] (PTY) - child_pty: Option>>>, // STDOUT/STDERR is combined automatically thanks to this PTY, nice + // required for P2Pool/XMRig to open their STDIN pipe. + child: Option>>>, // STDOUT/STDERR is combined automatically thanks to this PTY, nice stdin: Option>, // A handle to the process's MasterPTY/STDIN // This is the process's private output [String], used by both [Simple] and [Advanced]. // The "watchdog" threads mutate this, the "helper" thread synchronizes the [Pub*Api] structs // so that the data in here is cloned there roughly once a second. GUI thread never touches this. - output: String, + output: Arc>, } //---------------------------------------------------------------------------------------------------- [Process] Impl @@ -115,15 +108,12 @@ impl Process { signal: ProcessSignal::None, start: now, uptime: HumanTime::into_human(now.elapsed()), - stdout: Option::None, - stderr: Option::None, stdin: Option::None, child: Option::None, - child_pty: Option::None, // P2Pool log level 1 produces a bit less than 100,000 lines a day. // Assuming each line averages 80 UTF-8 scalars (80 bytes), then this // initial buffer should last around a week (56MB) before resetting. - output: String::with_capacity(56_000_000), + output: Arc::new(Mutex::new(String::with_capacity(56_000_000))), input: vec![String::new()], } } @@ -132,6 +122,15 @@ impl Process { pub fn parse_args(args: &str) -> Vec { args.split_whitespace().map(|s| s.to_owned()).collect() } + + // Convenience functions + pub fn is_alive(&self) -> bool { + self.state == ProcessState::Alive || self.state == ProcessState::Middle + } + + pub fn is_waiting(&self) -> bool { + self.state == ProcessState::Middle || self.state == ProcessState::Waiting + } } //---------------------------------------------------------------------------------------------------- [Process*] Enum @@ -140,7 +139,8 @@ pub enum ProcessState { Alive, // Process is online, GREEN! Dead, // Process is dead, BLACK! Failed, // Process is dead AND exited with a bad code, RED! - Middle, // Process is in the middle of something (starting, stopping, etc), YELLOW! + Middle, // Process is in the middle of something ([re]starting/stopping), YELLOW! + Waiting, // Process was successfully killed by a restart, and is ready to be started again, YELLOW! } #[derive(Copy,Clone,Eq,PartialEq,Debug)] @@ -166,63 +166,80 @@ use tokio::io::{BufReader,AsyncBufReadExt}; impl Helper { //---------------------------------------------------------------------------------------------------- General Functions - pub fn new(instant: std::time::Instant, pub_api_p2pool: Arc>, pub_api_xmrig: Arc>) -> Self { + pub fn new(instant: std::time::Instant, p2pool: Arc>, xmrig: Arc>, pub_api_p2pool: Arc>, pub_api_xmrig: Arc>) -> Self { Self { instant, human_time: HumanTime::into_human(instant.elapsed()), - p2pool: Arc::new(Mutex::new(Process::new(ProcessName::P2pool, String::new(), PathBuf::new()))), - xmrig: Arc::new(Mutex::new(Process::new(ProcessName::Xmrig, String::new(), PathBuf::new()))), priv_api_p2pool: Arc::new(Mutex::new(PrivP2poolApi::new())), priv_api_xmrig: Arc::new(Mutex::new(PrivXmrigApi::new())), // These are created when initializing [App], since it needs a handle to it as well + p2pool, + xmrig, pub_api_p2pool, pub_api_xmrig, } } - // The tokio runtime that blocks while async reading both STDOUT/STDERR - // Cheaper than spawning 2 OS threads just to read 2 pipes (...right? :D) - #[tokio::main] - async fn async_read_stdout_stderr(process: Arc>) { - let process_stdout = Arc::clone(&process); - let process_stderr = Arc::clone(&process); - let stdout = process.lock().unwrap().child.as_ref().unwrap().lock().unwrap().stdout.take().unwrap(); - let stderr = process.lock().unwrap().child.as_ref().unwrap().lock().unwrap().stderr.take().unwrap(); - - // Create STDOUT pipe job - let stdout_job = tokio::spawn(async move { - let mut reader = BufReader::new(stdout).lines(); - while let Ok(Some(line)) = reader.next_line().await { - println!("{}", line); // For debugging. - writeln!(process_stdout.lock().unwrap().output, "{}", line); - } - }); - // Create STDERR pipe job - let stderr_job = tokio::spawn(async move { - let mut reader = BufReader::new(stderr).lines(); - while let Ok(Some(line)) = reader.next_line().await { - println!("{}", line); // For debugging. - writeln!(process_stderr.lock().unwrap().output, "{}", line); - } - }); - // Block and read both until they are closed (automatic when process dies) - // The ordering of STDOUT/STDERR should be automatic thanks to the locks. - tokio::join![stdout_job, stderr_job]; - } - // Reads a PTY which combines STDOUT/STDERR for me, yay - fn read_pty(process: Arc>, reader: Box) { + fn read_pty(output: Arc>, reader: Box) { use std::io::BufRead; let mut stdout = std::io::BufReader::new(reader).lines(); while let Some(Ok(line)) = stdout.next() { - println!("{}", line); // For debugging. - writeln!(process.lock().unwrap().output, "{}", line); +// println!("{}", line); // For debugging. + writeln!(output.lock().unwrap(), "{}", line); } } //---------------------------------------------------------------------------------------------------- P2Pool specific - // Intermediate function that parses the arguments, and spawns the P2Pool watchdog thread. - pub fn spawn_p2pool(helper: &Arc>, state: &crate::disk::P2pool, path: std::path::PathBuf) { + // Read P2Pool's API file. + fn read_p2pool_api(path: &std::path::PathBuf) -> Result { + match std::fs::read_to_string(path) { + Ok(s) => Ok(s), + Err(e) => { warn!("P2Pool API | [{}] read error: {}", path.display(), e); Err(e) }, + } + } + + // Deserialize the above [String] into a [PrivP2poolApi] + fn str_to_priv_p2pool_api(string: &str) -> Result { + match serde_json::from_str::(string) { + Ok(a) => Ok(a), + Err(e) => { warn!("P2Pool API | Could not deserialize API data: {}", e); Err(e) }, + } + } + + // Just sets some signals for the watchdog thread to pick up on. + pub fn stop_p2pool(helper: &Arc>) { + info!("P2Pool | Attempting stop..."); + helper.lock().unwrap().p2pool.lock().unwrap().signal = ProcessSignal::Stop; + helper.lock().unwrap().p2pool.lock().unwrap().state = ProcessState::Middle; + } + + // The "restart frontend" to a "frontend" function. + // Basically calls to kill the current p2pool, waits a little, then starts the below function in a a new thread, then exit. + pub fn restart_p2pool(helper: &Arc>, state: &crate::disk::P2pool, path: std::path::PathBuf) { + info!("P2Pool | Attempting restart..."); + helper.lock().unwrap().p2pool.lock().unwrap().signal = ProcessSignal::Restart; + helper.lock().unwrap().p2pool.lock().unwrap().state = ProcessState::Middle; + + let helper = Arc::clone(&helper); + let state = state.clone(); + let path = path.clone(); + // This thread lives to wait, start p2pool then die. + thread::spawn(move || { + while helper.lock().unwrap().p2pool.lock().unwrap().is_alive() { + warn!("P2Pool Restart | Process still alive, waiting..."); + thread::sleep(SECOND); + } + // Ok, process is not alive, start the new one! + Self::start_p2pool(&helper, &state, path); + }); + info!("P2Pool | Restart ... OK"); + } + + // The "frontend" function that parses the arguments, and spawns either the [Simple] or [Advanced] P2Pool watchdog thread. + pub fn start_p2pool(helper: &Arc>, state: &crate::disk::P2pool, path: std::path::PathBuf) { + helper.lock().unwrap().p2pool.lock().unwrap().state = ProcessState::Middle; + let mut args = Vec::with_capacity(500); let path = path.clone(); let mut api_path = path.clone(); @@ -268,81 +285,29 @@ impl Helper { crate::disk::print_dash(&format!("P2Pool | Launch arguments ... {:#?}", args)); // Spawn watchdog thread - let simple = !state.simple; // Will this process need a PTY (STDIN)? let process = Arc::clone(&helper.lock().unwrap().p2pool); let pub_api = Arc::clone(&helper.lock().unwrap().pub_api_p2pool); let priv_api = Arc::clone(&helper.lock().unwrap().priv_api_p2pool); thread::spawn(move || { - if simple { - Self::spawn_simple_p2pool_watchdog(process, pub_api, priv_api, args, path); - } else { - Self::spawn_pty_p2pool_watchdog(process, pub_api, priv_api, args, path); - } + Self::spawn_p2pool_watchdog(process, pub_api, priv_api, args, path); }); } - // The [Simple] P2Pool watchdog tokio runtime, using async features with no PTY (STDIN). + // The P2Pool watchdog. Spawns 1 OS thread for reading a PTY (STDOUT+STDERR), and combines the [Child] with a PTY so STDIN actually works. #[tokio::main] - async fn spawn_simple_p2pool_watchdog(process: Arc>, pub_api: Arc>, priv_api: Arc>, args: Vec, path: std::path::PathBuf) { - // 1a. Create command - let child = Arc::new(Mutex::new(tokio::process::Command::new(path) - .args(args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::piped()) - .spawn().unwrap())); - - // 2. Set process state - let mut lock = process.lock().unwrap(); - lock.state = ProcessState::Alive; - lock.signal = ProcessSignal::None; - lock.start = Instant::now(); - lock.child = Some(Arc::clone(&child)); - drop(lock); - - // 3. Spawn STDOUT/STDERR thread - let process_clone = Arc::clone(&process); - thread::spawn(move || { - Self::async_read_stdout_stderr(process_clone); - }); - - // 4. Loop forever as watchdog until process dies - loop { - // a. Watch user SIGNAL - if process.lock().unwrap().signal == ProcessSignal::Stop { - process.lock().unwrap().child.as_mut().unwrap().lock().unwrap().kill().await; - process.lock().unwrap().signal = ProcessSignal::None; - } -// let signal = match process.lock().unwrap().signal { -// ProcessSignal::Stop => { crate::disk::print_dash("KILLING P2POOL"); process.lock().unwrap().child.as_mut().unwrap().lock().unwrap().kill().await.unwrap() }, -// ProcessSignal::Restart => process.lock().unwrap().child.as_mut().unwrap().lock().unwrap().kill().await, -// _ => Ok(()), -// }; - // b. Create STDIN task - if !process.lock().unwrap().input.is_empty() { /* process it */ } - // c. Create API task - let async_file_read = { /* tokio async file read job */ }; - // d. Execute async tasks -// tokio::join![signal]; - // f. Sleep (900ms) - std::thread::sleep(MILLI_900); - } - } - - // The [Advanced] P2Pool watchdog. Spawns 1 OS thread for reading a PTY (STDOUT+STDERR), and combines the [Child] with a PTY so STDIN actually works. - #[tokio::main] - async fn spawn_pty_p2pool_watchdog(process: Arc>, pub_api: Arc>, priv_api: Arc>, args: Vec, path: std::path::PathBuf) { + async fn spawn_p2pool_watchdog(process: Arc>, pub_api: Arc>, priv_api: Arc>, args: Vec, mut path: std::path::PathBuf) { // 1a. Create PTY let pty = portable_pty::native_pty_system(); - let mut pair = pty.openpty(portable_pty::PtySize { + let pair = pty.openpty(portable_pty::PtySize { rows: 24, cols: 80, pixel_width: 0, pixel_height: 0, }).unwrap(); // 1b. Create command - let mut cmd = portable_pty::CommandBuilder::new(path); + let mut cmd = portable_pty::CommandBuilder::new(path.as_path()); cmd.args(args); + cmd.cwd(path.as_path().parent().unwrap()); // 1c. Create child let child_pty = Arc::new(Mutex::new(pair.slave.spawn_command(cmd).unwrap())); @@ -351,17 +316,22 @@ impl Helper { lock.state = ProcessState::Alive; lock.signal = ProcessSignal::None; lock.start = Instant::now(); - lock.child_pty = Some(Arc::clone(&child_pty)); + lock.child = Some(Arc::clone(&child_pty)); let reader = pair.master.try_clone_reader().unwrap(); // Get STDOUT/STDERR before moving the PTY lock.stdin = Some(pair.master); drop(lock); // 3. Spawn PTY read thread - let process_clone = Arc::clone(&process); + let output_clone = Arc::clone(&process.lock().unwrap().output); thread::spawn(move || { - Self::read_pty(process_clone, reader); + Self::read_pty(output_clone, reader); }); + path.pop(); + path.push(P2POOL_API_PATH); + let regex = P2poolRegex::new(); + let output = Arc::clone(&process.lock().unwrap().output); + // 4. Loop as watchdog loop { // Set timer @@ -371,14 +341,42 @@ impl Helper { if process.lock().unwrap().signal == ProcessSignal::Stop { child_pty.lock().unwrap().kill(); // This actually sends a SIGHUP to p2pool (closes the PTY, hangs up on p2pool) // Wait to get the exit status + let mut lock = process.lock().unwrap(); + let exit_status = match child_pty.lock().unwrap().wait() { + Ok(e) => if e.success() { lock.state = ProcessState::Dead; "Successful" } else { lock.state = ProcessState::Failed; "Failed" }, + _ => { lock.state = ProcessState::Failed; "Unknown Error" }, + }; + let uptime = lock.uptime.clone(); + info!("P2Pool | Stopped ... Uptime was: [{}], Exit status: [{}]", uptime, exit_status); + // This is written directly into the public API, because sometimes the 900ms event loop can't catch it. + writeln!(pub_api.lock().unwrap().output, "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n", HORI_DOUBLE, uptime, exit_status, HORI_DOUBLE); + lock.signal = ProcessSignal::None; + break + } else if process.lock().unwrap().signal == ProcessSignal::Restart { + child_pty.lock().unwrap().kill(); // This actually sends a SIGHUP to p2pool (closes the PTY, hangs up on p2pool) + // Wait to get the exit status + let mut lock = process.lock().unwrap(); let exit_status = match child_pty.lock().unwrap().wait() { Ok(e) => if e.success() { "Successful" } else { "Failed" }, _ => "Unknown Error", }; - let mut lock = process.lock().unwrap(); let uptime = lock.uptime.clone(); info!("P2Pool | Stopped ... Uptime was: [{}], Exit status: [{}]", uptime, exit_status); - writeln!(lock.output, "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n", HORI_DOUBLE, uptime, exit_status, HORI_DOUBLE); + // This is written directly into the public API, because sometimes the 900ms event loop can't catch it. + writeln!(pub_api.lock().unwrap().output, "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n", HORI_DOUBLE, uptime, exit_status, HORI_DOUBLE); + lock.state = ProcessState::Waiting; + break + // Check if the process is secretly died without us knowing :) + } else if let Ok(Some(code)) = child_pty.lock().unwrap().try_wait() { + let mut lock = process.lock().unwrap(); + let exit_status = match code.success() { + true => { lock.state = ProcessState::Dead; "Successful" }, + false => { lock.state = ProcessState::Failed; "Failed" }, + }; + let uptime = lock.uptime.clone(); + info!("P2Pool | Stopped ... Uptime was: [{}], Exit status: [{}]", uptime, exit_status); + // This is written directly into the public API, because sometimes the 900ms event loop can't catch it. + writeln!(pub_api.lock().unwrap().output, "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n", HORI_DOUBLE, uptime, exit_status, HORI_DOUBLE); lock.signal = ProcessSignal::None; break } @@ -393,6 +391,15 @@ impl Helper { } drop(lock); + // Read API file into string + if let Ok(string) = Self::read_p2pool_api(&path) { + // Deserialize + if let Ok(s) = Self::str_to_priv_p2pool_api(&string) { + // Update the structs. + PubP2poolApi::update_from_priv(&pub_api, &priv_api, &output, process.lock().unwrap().start.elapsed().as_secs_f64(), ®ex); + } + } + // Sleep (only if 900ms hasn't passed) let elapsed = now.elapsed().as_millis(); // Since logic goes off if less than 1000, casting should be safe @@ -400,12 +407,12 @@ impl Helper { } // 5. If loop broke, we must be done here. - info!("P2Pool | Watchdog thread exiting... Goodbye!"); + info!("P2Pool | Advanced watchdog thread exiting... Goodbye!"); } //---------------------------------------------------------------------------------------------------- XMRig specific // Intermediate function that parses the arguments, and spawns the XMRig watchdog thread. - pub fn spawn_xmrig(state: &crate::disk::Xmrig, api_path: &std::path::Path) { + pub fn spawn_xmrig(helper: &Arc>, state: &crate::disk::Xmrig, path: std::path::PathBuf) { let mut args = Vec::with_capacity(500); if state.simple { let rig_name = if state.simple_rig.is_empty() { GUPAX_VERSION.to_string() } else { state.simple_rig.clone() }; // Rig name @@ -661,26 +668,31 @@ impl HumanNumber { // The following STDLIB implementation takes [0.003~] seconds to find all matches given a [String] with 30k lines: // let mut n = 0; // for line in P2POOL_OUTPUT.lines() { -// if line.contains("[0-9].[0-9]+ XMR") { n += 1; } +// if line.contains("You received a payout of [0-9].[0-9]+ XMR") { n += 1; } // } // // This regex function takes [0.0003~] seconds (10x faster): -// let regex = Regex::new("[0-9].[0-9]+ XMR").unwrap(); +// let regex = Regex::new("You received a payout of [0-9].[0-9]+ XMR").unwrap(); // let n = regex.find_iter(P2POOL_OUTPUT).count(); // // Both are nominally fast enough where it doesn't matter too much but meh, why not use regex. struct P2poolRegex { - xmr: regex::Regex, + payout: regex::Regex, + float: regex::Regex, } impl P2poolRegex { fn new() -> Self { - Self { xmr: regex::Regex::new("[0-9].[0-9]+ XMR").unwrap(), } + Self { + payout: regex::Regex::new("You received a payout of [0-9].[0-9]+ XMR").unwrap(), + float: regex::Regex::new("[0-9].[0-9]+").unwrap(), + } } } //---------------------------------------------------------------------------------------------------- Public P2Pool API // GUI thread interfaces with this. +#[derive(Debug, Clone)] pub struct PubP2poolApi { // One off pub mini: bool, @@ -728,20 +740,23 @@ impl PubP2poolApi { } } - // Mutate [PubP2poolApi] with data from a [PrivP2poolApi]. - fn update_from_priv(self, output: String, regex: P2poolRegex, private: PrivP2poolApi, uptime: f64) -> Self { + // Mutate [PubP2poolApi] with data from a [PrivP2poolApi] and the process output. + fn update_from_priv(public: &Arc>, private: &Arc>, output: &Arc>, start: f64, regex: &P2poolRegex) { + let public_clone = public.lock().unwrap().clone(); + let output = output.lock().unwrap().clone(); // 1. Parse STDOUT let (payouts, xmr) = Self::calc_payouts_and_xmr(&output, ®ex); let stdout_parse = Self { - output: output.clone(), + output, payouts, xmr, - ..self // <- So useful + ..public_clone // <- So useful }; // 2. Time calculations - let hour_day_month = Self::update_hour_day_month(stdout_parse, uptime); + let hour_day_month = Self::update_hour_day_month(stdout_parse, start); // 3. Final priv -> pub conversion - Self { + let private = private.lock().unwrap(); + *public.lock().unwrap() = Self { hashrate_15m: HumanNumber::from_u128(private.hashrate_15m), hashrate_1h: HumanNumber::from_u128(private.hashrate_1h), hashrate_24h: HumanNumber::from_u128(private.hashrate_24h), @@ -756,16 +771,13 @@ impl PubP2poolApi { // Essentially greps the output for [x.xxxxxxxxxxxx XMR] where x = a number. // It sums each match and counts along the way, handling an error by not adding and printing to console. fn calc_payouts_and_xmr(output: &str, regex: &P2poolRegex) -> (u128 /* payout count */, f64 /* total xmr */) { - let mut iter = regex.xmr.find_iter(output); + let iter = regex.payout.find_iter(output); let mut result: f64 = 0.0; let mut count: u128 = 0; for i in iter { - if let Some(text) = i.as_str().split_whitespace().next() { - match text.parse::() { - Ok(num) => result += num, - Err(e) => error!("P2Pool | Total XMR sum calculation error: [{}]", e), - } - count += 1; + match regex.float.find(i.as_str()).unwrap().as_str().parse::() { + Ok(num) => { result += num; count += 1; }, + Err(e) => error!("P2Pool | Total XMR sum calculation error: [{}]", e), } } (count, result) @@ -799,7 +811,7 @@ impl PubP2poolApi { // This is the data the "watchdog" threads mutate. // It matches directly to P2Pool's [local/stats] JSON API file (excluding a few stats). // P2Pool seems to initialize all stats at 0 (or 0.0), so no [Option] wrapper seems needed. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] struct PrivP2poolApi { hashrate_15m: u128, hashrate_1h: u128, @@ -825,6 +837,7 @@ impl PrivP2poolApi { } //---------------------------------------------------------------------------------------------------- Public XMRig API +#[derive(Debug, Clone)] pub struct PubXmrigApi { output: String, worker_id: String, @@ -870,7 +883,7 @@ impl PubXmrigApi { // e.g: [wget -qO- localhost:18085/1/summary]. // XMRig doesn't initialize stats at 0 (or 0.0) and instead opts for [null] // which means some elements need to be wrapped in an [Option] or else serde will [panic!]. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct PrivXmrigApi { worker_id: String, resources: Resources, @@ -889,7 +902,7 @@ impl PrivXmrigApi { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] struct Resources { load_average: [Option; 3], } @@ -901,7 +914,7 @@ impl Resources { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct Connection { pool: String, diff: u128, @@ -919,7 +932,7 @@ impl Connection { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] struct Hashrate { total: [Option; 3], } diff --git a/src/main.rs b/src/main.rs index 715fcfa..a0924cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,14 +32,13 @@ use egui::{ }; use egui_extras::RetainedImage; use eframe::{egui,NativeOptions}; - // Logging use log::*; use env_logger::{Builder,WriteStyle}; - // Regex use regex::Regex; - +// Serde +use serde::{Serialize,Deserialize}; // std use std::{ env, @@ -49,7 +48,6 @@ use std::{ time::Instant, path::PathBuf, }; - // Modules mod ferris; mod constants; @@ -106,16 +104,11 @@ pub struct App { // Helper/API State: // This holds everything related to the data processed by the "helper thread". // This includes the "helper" threads public P2Pool/XMRig's API. - helper: Arc>, - p2pool_api: Arc>, - xmrig_api: Arc>, - -// Fix-me. -// These shouldn't exist -// Just for debugging. - p2pool: bool, - xmrig: bool, - + helper: Arc>, // [Helper] state, mostly for Gupax uptime + p2pool: Arc>, // [P2Pool] process state + xmrig: Arc>, // [XMRig] process state + p2pool_api: Arc>, // Public ready-to-print P2Pool API made by the "helper" thread + xmrig_api: Arc>, // Public ready-to-print XMRig API made by the "helper" thread // State from [--flags] no_startup: bool, // Static stuff @@ -146,6 +139,8 @@ impl App { fn new(now: Instant) -> Self { info!("Initializing App Struct..."); + let p2pool = Arc::new(Mutex::new(Process::new(ProcessName::P2pool, String::new(), PathBuf::new()))); + let xmrig = Arc::new(Mutex::new(Process::new(ProcessName::Xmrig, String::new(), PathBuf::new()))); let p2pool_api = Arc::new(Mutex::new(PubP2poolApi::new())); let xmrig_api = Arc::new(Mutex::new(PubXmrigApi::new())); let mut app = Self { @@ -165,17 +160,13 @@ impl App { restart: Arc::new(Mutex::new(Restart::No)), diff: false, error_state: ErrorState::new(), - helper: Arc::new(Mutex::new(Helper::new(now, p2pool_api.clone(), xmrig_api.clone()))), + helper: Arc::new(Mutex::new(Helper::new(now, p2pool.clone(), xmrig.clone(), p2pool_api.clone(), xmrig_api.clone()))), + p2pool, + xmrig, p2pool_api, xmrig_api, -// TODO -// these p2pool/xmrig bools are here for debugging purposes -// they represent the online/offline status. -// fix this later when [Helper] is integrated. resizing: false, alpha: 0, - p2pool: false, - xmrig: false, no_startup: false, now, exe: String::new(), @@ -319,6 +310,8 @@ impl App { // Set state version as compiled in version og.version.lock().unwrap().gupax = GUPAX_VERSION.to_string(); app.state.version.lock().unwrap().gupax = GUPAX_VERSION.to_string(); + // Set saved [Tab] + app.tab = app.state.gupax.tab; drop(og); // Unlock [og] info!("App ... OK"); app @@ -327,8 +320,8 @@ impl App { //---------------------------------------------------------------------------------------------------- [Tab] Enum + Impl // The tabs inside [App]. -#[derive(Clone, Copy, Debug, PartialEq)] -enum Tab { +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum Tab { About, Status, Gupax, @@ -719,6 +712,9 @@ impl eframe::App for App { frame.set_fullscreen(!info.window_info.fullscreen); } + // Refresh AT LEAST once a second + ctx.request_repaint_after(SECOND); + // This sets the top level Ui dimensions. // Used as a reference for other uis. CentralPanel::default().show(ctx, |ui| { @@ -797,8 +793,8 @@ impl eframe::App for App { StayQuit => { let mut text = "".to_string(); if *self.update.lock().unwrap().updating.lock().unwrap() { text = format!("{}\nUpdate is in progress...!", text); } - if self.p2pool { text = format!("{}\nP2Pool is online...!", text); } - if self.xmrig { text = format!("{}\nXMRig is online...!", text); } + if self.p2pool.lock().unwrap().is_alive() { text = format!("{}\nP2Pool is online...!", text); } + if self.xmrig.lock().unwrap().is_alive() { text = format!("{}\nXMRig is online...!", text); } ui.add_sized([width, height], Label::new("--- Are you sure you want to quit? ---")); ui.add_sized([width, height], Label::new(text)) }, @@ -939,17 +935,20 @@ impl eframe::App for App { ui.add_sized([width, height], Label::new(self.os)); ui.separator(); // [P2Pool/XMRig] Status - if self.p2pool { - ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(GREEN))).on_hover_text("P2Pool is online"); - } else { - ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(RED))).on_hover_text("P2Pool is offline"); - } + use ProcessState::*; + match self.p2pool.lock().unwrap().state { + Alive => ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(GREEN))).on_hover_text(P2POOL_ALIVE), + Dead => ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(GRAY))).on_hover_text(P2POOL_DEAD), + Failed => ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(RED))).on_hover_text(P2POOL_FAILED), + Middle|Waiting => ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(YELLOW))).on_hover_text(P2POOL_MIDDLE), + }; ui.separator(); - if self.xmrig { - ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(GREEN))).on_hover_text("XMRig is online"); - } else { - ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(RED))).on_hover_text("XMRig is offline"); - } + match self.xmrig.lock().unwrap().state { + Alive => ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(GREEN))).on_hover_text(XMRIG_ALIVE), + Dead => ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(GRAY))).on_hover_text(XMRIG_DEAD), + Failed => ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(RED))).on_hover_text(XMRIG_FAILED), + Middle|Waiting => ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(YELLOW))).on_hover_text(XMRIG_MIDDLE), + }; }); // [Save/Reset] @@ -1019,29 +1018,30 @@ impl eframe::App for App { }); ui.group(|ui| { let width = (ui.available_width()/3.0)-5.0; -// if self.p2pool { -// if ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart P2Pool").clicked() { self.p2pool = false; } -// if ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop P2Pool").clicked() { self.p2pool = false; } -// ui.add_enabled_ui(false, |ui| { -// if ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start P2Pool").clicked() { -// Helper::spawn_p2pool(&self.helper, &self.state.p2pool, self.state.gupax.absolute_p2pool_path.clone()); -// } -// }); -// } else { -// ui.add_enabled_ui(false, |ui| { -// ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart P2Pool"); -// ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop P2Pool"); -// }); -// if ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start P2Pool").clicked() { -// Helper::spawn_p2pool(&self.helper, &self.state.p2pool, self.state.gupax.absolute_p2pool_path.clone()); -// } -// } - ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart P2Pool"); - if ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop P2Pool").clicked() { - self.helper.lock().unwrap().p2pool.lock().unwrap().signal = ProcessSignal::Stop; - } - if ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start P2Pool").clicked() { - Helper::spawn_p2pool(&self.helper, &self.state.p2pool, self.state.gupax.absolute_p2pool_path.clone()); + if self.p2pool.lock().unwrap().is_waiting() { + ui.add_enabled_ui(false, |ui| { + ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart P2Pool"); + ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop P2Pool"); + ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start P2Pool"); + }); + } else if self.p2pool.lock().unwrap().is_alive() { + if ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart P2Pool").clicked() { + Helper::restart_p2pool(&self.helper, &self.state.p2pool, self.state.gupax.absolute_p2pool_path.clone()); + } + if ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop P2Pool").clicked() { + Helper::stop_p2pool(&self.helper); + } + ui.add_enabled_ui(false, |ui| { + ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start P2Pool"); + }); + } else { + ui.add_enabled_ui(false, |ui| { + ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart P2Pool"); + ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop P2Pool"); + }); + if ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start P2Pool").clicked() { + Helper::start_p2pool(&self.helper, &self.state.p2pool, self.state.gupax.absolute_p2pool_path.clone()); + } } }); }, @@ -1058,9 +1058,13 @@ impl eframe::App for App { }); ui.group(|ui| { let width = (ui.available_width()/3.0)-5.0; - if self.xmrig { - if ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart XMRig").clicked() { self.xmrig = false; } - if ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop XMRig").clicked() { self.xmrig = false; } + if self.xmrig.lock().unwrap().is_alive() { + if ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart XMRig").clicked() { + self.xmrig.lock().unwrap().state = ProcessState::Middle; + } + if ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop XMRig").clicked() { + self.xmrig.lock().unwrap().state = ProcessState::Dead; + } ui.add_enabled_ui(false, |ui| { ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start XMRig"); }); @@ -1069,7 +1073,9 @@ impl eframe::App for App { ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart XMRig"); ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop XMRig"); }); - if ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start XMRig").clicked() { self.xmrig = true; } + if ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start XMRig").clicked() { + Helper::spawn_xmrig(&self.helper, &self.state.xmrig, self.state.gupax.absolute_xmrig_path.clone()); + } } }); }, @@ -1104,6 +1110,7 @@ impl eframe::App for App { ui.hyperlink_to("Made by hinto-janaiyo".to_string(), "https://gupax.io"); ui.label("egui is licensed under MIT & Apache-2.0"); ui.label("Gupax, P2Pool, and XMRig are licensed under GPLv3"); + if cfg!(debug_assertions) { ui.label(format!("{}", self.now.elapsed().as_secs_f64())); } }); } Tab::Status => { @@ -1113,7 +1120,7 @@ impl eframe::App for App { Gupax::show(&mut self.state.gupax, &self.og, &self.state_path, &self.update, &self.file_window, &mut self.error_state, &self.restart, self.width, self.height, frame, ctx, ui); } Tab::P2pool => { - P2pool::show(&mut self.state.p2pool, &mut self.node_vec, &self.og, self.p2pool, &self.ping, &self.regex, &self.helper, &self.p2pool_api, self.width, self.height, ctx, ui); + P2pool::show(&mut self.state.p2pool, &mut self.node_vec, &self.og, &self.ping, &self.regex, &self.helper, &self.p2pool_api, self.width, self.height, ctx, ui); } Tab::Xmrig => { Xmrig::show(&mut self.state.xmrig, &mut self.pool_vec, &self.regex, self.width, self.height, ctx, ui); diff --git a/src/p2pool.rs b/src/p2pool.rs index b92ddd5..6787b4d 100644 --- a/src/p2pool.rs +++ b/src/p2pool.rs @@ -32,24 +32,38 @@ use regex::Regex; use log::*; impl P2pool { - pub fn show(&mut self, node_vec: &mut Vec<(String, Node)>, og: &Arc>, _online: bool, ping: &Arc>, regex: &Regexes, helper: &Arc>, api: &Arc>, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) { + pub fn show(&mut self, node_vec: &mut Vec<(String, Node)>, og: &Arc>, ping: &Arc>, regex: &Regexes, helper: &Arc>, api: &Arc>, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) { let text_edit = height / 22.0; - //---------------------------------------------------------------------------------------------------- Console + //---------------------------------------------------------------------------------------------------- [Simple] Console + if self.simple { ui.group(|ui| { let height = height / 2.5; let width = width - SPACE; ui.style_mut().override_text_style = Some(Monospace); - egui::Frame::none().fill(Color32::from_rgb(18, 18, 18)).show(ui, |ui| { + egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| { ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into())); egui::ScrollArea::vertical().stick_to_bottom(true).max_width(width).max_height(height).auto_shrink([false; 2]).show_viewport(ui, |ui, _| { let lock = api.lock().unwrap(); ui.add_sized([width, height], TextEdit::multiline(&mut lock.output.as_str())); -// if lock.p2pool.lock().unwrap().state == ProcessState::Alive { ctx.request_repaint(); } }); }); -// ui.separator(); -// ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(&mut "".to_string()), r#"Type a command (e.g "help" or "status") and press Enter"#)); }); + //---------------------------------------------------------------------------------------------------- [Advanced] Console + } else { + ui.group(|ui| { + let height = height / 3.0; + let width = width - SPACE; + ui.style_mut().override_text_style = Some(Monospace); + egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| { + ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into())); + egui::ScrollArea::vertical().stick_to_bottom(true).max_width(width).max_height(height).auto_shrink([false; 2]).show_viewport(ui, |ui, _| { + ui.add_sized([width, height], TextEdit::multiline(&mut api.lock().unwrap().output.as_str())); + }); + }); + ui.separator(); + ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(&mut "".to_string()), r#"Type a command (e.g "help" or "status") and press Enter"#)); + }); + } //---------------------------------------------------------------------------------------------------- Args if !self.simple {