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 {