diff --git a/src/cpu.json b/assets/cpu.json similarity index 100% rename from src/cpu.json rename to assets/cpu.json diff --git a/images/banner.png b/assets/images/banner.png similarity index 100% rename from images/banner.png rename to assets/images/banner.png diff --git a/images/benchmarks.png b/assets/images/benchmarks.png similarity index 100% rename from images/benchmarks.png rename to assets/images/benchmarks.png diff --git a/images/ferris/cute.png b/assets/images/ferris/cute.png similarity index 100% rename from images/ferris/cute.png rename to assets/images/ferris/cute.png diff --git a/images/ferris/error.png b/assets/images/ferris/error.png similarity index 100% rename from images/ferris/error.png rename to assets/images/ferris/error.png diff --git a/images/ferris/gesture.png b/assets/images/ferris/gesture.png similarity index 100% rename from images/ferris/gesture.png rename to assets/images/ferris/gesture.png diff --git a/images/ferris/happy.png b/assets/images/ferris/happy.png similarity index 100% rename from images/ferris/happy.png rename to assets/images/ferris/happy.png diff --git a/images/ferris/oops.png b/assets/images/ferris/oops.png similarity index 100% rename from images/ferris/oops.png rename to assets/images/ferris/oops.png diff --git a/images/ferris/panic.png b/assets/images/ferris/panic.png similarity index 100% rename from images/ferris/panic.png rename to assets/images/ferris/panic.png diff --git a/images/ferris/sudo.png b/assets/images/ferris/sudo.png similarity index 100% rename from images/ferris/sudo.png rename to assets/images/ferris/sudo.png diff --git a/images/icons/icon.ico b/assets/images/icons/icon.ico similarity index 100% rename from images/icons/icon.ico rename to assets/images/icons/icon.ico diff --git a/images/icons/icon.png b/assets/images/icons/icon.png similarity index 100% rename from images/icons/icon.png rename to assets/images/icons/icon.png diff --git a/images/icons/icon@2x.png b/assets/images/icons/icon@2x.png similarity index 100% rename from images/icons/icon@2x.png rename to assets/images/icons/icon@2x.png diff --git a/images/local.png b/assets/images/local.png similarity index 100% rename from images/local.png rename to assets/images/local.png diff --git a/images/local_node.png b/assets/images/local_node.png similarity index 100% rename from images/local_node.png rename to assets/images/local_node.png diff --git a/images/payouts.png b/assets/images/payouts.png similarity index 100% rename from images/payouts.png rename to assets/images/payouts.png diff --git a/images/processes.png b/assets/images/processes.png similarity index 100% rename from images/processes.png rename to assets/images/processes.png diff --git a/images/remote.png b/assets/images/remote.png similarity index 100% rename from images/remote.png rename to assets/images/remote.png diff --git a/images/tabs/about.png b/assets/images/tabs/about.png similarity index 100% rename from images/tabs/about.png rename to assets/images/tabs/about.png diff --git a/images/tabs/gupax.png b/assets/images/tabs/gupax.png similarity index 100% rename from images/tabs/gupax.png rename to assets/images/tabs/gupax.png diff --git a/images/tabs/p2pool.png b/assets/images/tabs/p2pool.png similarity index 100% rename from images/tabs/p2pool.png rename to assets/images/tabs/p2pool.png diff --git a/images/tabs/status.png b/assets/images/tabs/status.png similarity index 100% rename from images/tabs/status.png rename to assets/images/tabs/status.png diff --git a/images/tabs/xmrig.png b/assets/images/tabs/xmrig.png similarity index 100% rename from images/tabs/xmrig.png rename to assets/images/tabs/xmrig.png diff --git a/images/thread_model.png b/assets/images/thread_model.png similarity index 100% rename from images/thread_model.png rename to assets/images/thread_model.png diff --git a/images/xvb.png b/assets/images/xvb.png similarity index 100% rename from images/xvb.png rename to assets/images/xvb.png diff --git a/src/app/eframe_impl.rs b/src/app/eframe_impl.rs new file mode 100644 index 0000000..f75fad2 --- /dev/null +++ b/src/app/eframe_impl.rs @@ -0,0 +1,89 @@ +use super::App; +use crate::macros::lock; +use crate::SECOND; +use egui::CentralPanel; +use log::debug; + +impl eframe::App for App { + fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + // *-------* + // | DEBUG | + // *-------* + debug!("App | ----------- Start of [update()] -----------"); + // If closing + self.quit(ctx); + // Handle Keys + let (key, wants_input) = self.keys_handle(ctx); + + // Refresh AT LEAST once a second + debug!("App | Refreshing frame once per second"); + ctx.request_repaint_after(SECOND); + + // Get P2Pool/XMRig process state. + // These values are checked multiple times so + // might as well check only once here to save + // on a bunch of [.lock().unwrap()]s. + debug!("App | Locking and collecting P2Pool state..."); + let p2pool = lock!(self.p2pool); + let p2pool_is_alive = p2pool.is_alive(); + let p2pool_is_waiting = p2pool.is_waiting(); + let p2pool_state = p2pool.state; + drop(p2pool); + debug!("App | Locking and collecting XMRig state..."); + let xmrig = lock!(self.xmrig); + let xmrig_is_alive = xmrig.is_alive(); + let xmrig_is_waiting = xmrig.is_waiting(); + let xmrig_state = xmrig.state; + drop(xmrig); + + // This sets the top level Ui dimensions. + // Used as a reference for other uis. + debug!("App | Setting width/height"); + CentralPanel::default().show(ctx, |ui| { + let available_width = ui.available_width(); + if self.width != available_width { + self.width = available_width; + if self.now.elapsed().as_secs() > 5 { + self.must_resize = true; + } + }; + self.height = ui.available_height(); + }); + self.resize(ctx); + + // If there's an error, display [ErrorState] on the whole screen until user responds + debug!("App | Checking if there is an error in [ErrorState]"); + if self.error_state.error { + self.quit_error_panel(ctx, p2pool_is_alive, xmrig_is_alive, &key); + return; + } + // Compare [og == state] & [node_vec/pool_vec] and enable diff if found. + // The struct fields are compared directly because [Version] + // contains Arc's that cannot be compared easily. + // They don't need to be compared anyway. + debug!("App | Checking diff between [og] & [state]"); + let og = lock!(self.og); + self.diff = og.status != self.state.status + || og.gupax != self.state.gupax + || og.p2pool != self.state.p2pool + || og.xmrig != self.state.xmrig + || og.xvb != self.state.xvb + || self.og_node_vec != self.node_vec + || self.og_pool_vec != self.pool_vec; + drop(og); + + self.top_panel(ctx); + self.bottom_panel( + ctx, + p2pool_state, + xmrig_state, + &key, + wants_input, + p2pool_is_waiting, + xmrig_is_waiting, + p2pool_is_alive, + xmrig_is_alive, + ); + self.middle_panel(ctx, frame, key, p2pool_is_alive, xmrig_is_alive); + } +} diff --git a/src/app/keys.rs b/src/app/keys.rs new file mode 100644 index 0000000..bf8ee91 --- /dev/null +++ b/src/app/keys.rs @@ -0,0 +1,172 @@ +use egui::{Key, Modifiers}; +use log::info; + +use crate::{disk::status::Submenu, utils::macros::flip}; + +use super::{App, Tab}; + +//---------------------------------------------------------------------------------------------------- [Pressed] enum +// These represent the keys pressed during the frame. +// I could use egui's [Key] but there is no option for +// a [None] and wrapping [key_pressed] like [Option] +// meant that I had to destructure like this: +// if let Some(egui::Key)) = key_pressed { /* do thing */ } +// +// That's ugly, so these are used instead so a simple compare can be used. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum KeyPressed { + F11, + Up, + Down, + Esc, + Z, + X, + C, + V, + S, + R, + D, + None, +} + +impl KeyPressed { + #[inline] + pub(super) fn is_f11(&self) -> bool { + *self == Self::F11 + } + #[inline] + pub(super) fn is_z(&self) -> bool { + *self == Self::Z + } + #[inline] + pub(super) fn is_x(&self) -> bool { + *self == Self::X + } + #[inline] + pub(super) fn is_up(&self) -> bool { + *self == Self::Up + } + #[inline] + pub(super) fn is_down(&self) -> bool { + *self == Self::Down + } + #[inline] + pub(super) fn is_esc(&self) -> bool { + *self == Self::Esc + } + #[inline] + pub(super) fn is_s(&self) -> bool { + *self == Self::S + } + #[inline] + pub(super) fn is_r(&self) -> bool { + *self == Self::R + } + #[inline] + pub(super) fn is_d(&self) -> bool { + *self == Self::D + } + #[inline] + pub(super) fn is_c(&self) -> bool { + *self == Self::C + } + #[inline] + pub(super) fn is_v(&self) -> bool { + *self == Self::V + } + // #[inline] + // pub(super) fn is_none(&self) -> bool { + // *self == Self::None + // } +} + +impl App { + pub fn keys_handle(&mut self, ctx: &egui::Context) -> (KeyPressed, bool) { + // If [F11] was pressed, reverse [fullscreen] bool + let key: KeyPressed = ctx.input_mut(|input| { + if input.consume_key(Modifiers::NONE, Key::F11) { + KeyPressed::F11 + } else if input.consume_key(Modifiers::NONE, Key::Z) { + KeyPressed::Z + } else if input.consume_key(Modifiers::NONE, Key::X) { + KeyPressed::X + } else if input.consume_key(Modifiers::NONE, Key::C) { + KeyPressed::C + } else if input.consume_key(Modifiers::NONE, Key::V) { + KeyPressed::V + } else if input.consume_key(Modifiers::NONE, Key::ArrowUp) { + KeyPressed::Up + } else if input.consume_key(Modifiers::NONE, Key::ArrowDown) { + KeyPressed::Down + } else if input.consume_key(Modifiers::NONE, Key::Escape) { + KeyPressed::Esc + } else if input.consume_key(Modifiers::NONE, Key::S) { + KeyPressed::S + } else if input.consume_key(Modifiers::NONE, Key::R) { + KeyPressed::R + } else if input.consume_key(Modifiers::NONE, Key::D) { + KeyPressed::D + } else { + KeyPressed::None + } + }); + // Check if egui wants keyboard input. + // This prevents keyboard shortcuts from clobbering TextEdits. + // (Typing S in text would always [Save] instead) + let wants_input = ctx.wants_keyboard_input(); + + if key.is_f11() { + if ctx.input(|i| i.viewport().maximized == Some(true)) { + info!("fullscreen bool"); + ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true)); + } + // Change Tabs LEFT + } else if key.is_z() && !wants_input { + match self.tab { + Tab::About => self.tab = Tab::Xvb, + Tab::Status => self.tab = Tab::About, + Tab::Gupax => self.tab = Tab::Status, + Tab::P2pool => self.tab = Tab::Gupax, + Tab::Xmrig => self.tab = Tab::P2pool, + Tab::Xvb => self.tab = Tab::Xmrig, + }; + // Change Tabs RIGHT + } else if key.is_x() && !wants_input { + match self.tab { + Tab::About => self.tab = Tab::Status, + Tab::Status => self.tab = Tab::Gupax, + Tab::Gupax => self.tab = Tab::P2pool, + Tab::P2pool => self.tab = Tab::Xmrig, + Tab::Xmrig => self.tab = Tab::Xvb, + Tab::Xvb => self.tab = Tab::About, + }; + // Change Submenu LEFT + } else if key.is_c() && !wants_input { + match self.tab { + Tab::Status => match self.state.status.submenu { + Submenu::Processes => self.state.status.submenu = Submenu::Benchmarks, + Submenu::P2pool => self.state.status.submenu = Submenu::Processes, + Submenu::Benchmarks => self.state.status.submenu = Submenu::P2pool, + }, + Tab::Gupax => flip!(self.state.gupax.simple), + Tab::P2pool => flip!(self.state.p2pool.simple), + Tab::Xmrig => flip!(self.state.xmrig.simple), + _ => (), + }; + // Change Submenu RIGHT + } else if key.is_v() && !wants_input { + match self.tab { + Tab::Status => match self.state.status.submenu { + Submenu::Processes => self.state.status.submenu = Submenu::P2pool, + Submenu::P2pool => self.state.status.submenu = Submenu::Benchmarks, + Submenu::Benchmarks => self.state.status.submenu = Submenu::Processes, + }, + Tab::Gupax => flip!(self.state.gupax.simple), + Tab::P2pool => flip!(self.state.p2pool.simple), + Tab::Xmrig => flip!(self.state.xmrig.simple), + _ => (), + }; + } + (key, wants_input) + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..3756069 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,675 @@ +use crate::components::gupax::FileWindow; +use crate::components::node::Ping; +use crate::components::node::RemoteNode; +use crate::components::node::REMOTE_NODES; +use crate::components::update::Update; +use crate::disk::consts::NODE_TOML; +use crate::disk::consts::POOL_TOML; +use crate::disk::consts::STATE_TOML; +use crate::disk::get_gupax_data_path; +use crate::disk::gupax_p2pool_api::GupaxP2poolApi; +use crate::disk::node::Node; +use crate::disk::pool::Pool; +use crate::disk::state::State; +use crate::errors::ErrorButtons; +use crate::errors::ErrorFerris; +use crate::errors::ErrorState; +use crate::helper::p2pool::ImgP2pool; +use crate::helper::p2pool::PubP2poolApi; +use crate::helper::xmrig::ImgXmrig; +use crate::helper::xmrig::PubXmrigApi; +use crate::helper::Helper; +use crate::helper::Process; +use crate::helper::ProcessName; +use crate::helper::Sys; +use crate::inits::init_text_styles; +use crate::miscs::cmp_f64; +use crate::miscs::get_exe; +use crate::miscs::get_exe_dir; +use crate::miscs::parse_args; +use crate::utils::constants::VISUALS; +use crate::utils::macros::arc_mut; +use crate::utils::macros::lock; +use crate::utils::sudo::SudoState; +use crate::APP_DEFAULT_HEIGHT; +use crate::APP_DEFAULT_WIDTH; +use crate::GUPAX_VERSION; +use crate::OS; +use eframe::CreationContext; +use egui::Vec2; +use log::debug; +use log::error; +use log::info; +use log::warn; +use serde::Deserialize; +use serde::Serialize; +use std::path::PathBuf; +use std::process::exit; +use std::sync::Arc; +use std::sync::Mutex; +use std::time::Instant; + +pub mod eframe_impl; +pub mod keys; +pub mod panels; +pub mod quit; +pub mod resize; +//---------------------------------------------------------------------------------------------------- Struct + Impl +// The state of the outer main [App]. +// See the [State] struct in [state.rs] for the +// actual inner state of the tab settings. +pub struct App { + // Misc state + pub tab: Tab, // What tab are we on? + pub width: f32, // Top-level width + pub height: f32, // Top-level height + // Alpha (transparency) + // This value is used to incrementally increase/decrease + // the transparency when resizing. Basically, it fades + // in/out of black to hide jitter when resizing with [init_text_styles()] + pub alpha: u8, + // This is a one time trigger so [init_text_styles()] isn't + // called 60x a second when resizing the window. Instead, + // it only gets called if this bool is true and the user + // is hovering over egui (ctx.is_pointer_over_area()). + pub must_resize: bool, // Sets the flag so we know to [init_text_styles()] + pub resizing: bool, // Are we in the process of resizing? (For black fade in/out) + // State + pub og: Arc>, // og = Old state to compare against + pub state: State, // state = Working state (current settings) + pub update: Arc>, // State for update data [update.rs] + pub file_window: Arc>, // State for the path selector in [Gupax] + pub ping: Arc>, // Ping data found in [node.rs] + pub og_node_vec: Vec<(String, Node)>, // Manual Node database + pub node_vec: Vec<(String, Node)>, // Manual Node database + pub og_pool_vec: Vec<(String, Pool)>, // Manual Pool database + pub pool_vec: Vec<(String, Pool)>, // Manual Pool database + pub diff: bool, // This bool indicates state changes + // Restart state: + // If Gupax updated itself, this represents that the + // user should (but isn't required to) restart Gupax. + pub restart: Arc>, + // Error State: + // These values are essentially global variables that + // indicate if an error message needs to be displayed + // (it takes up the whole screen with [error_msg] and buttons for ok/quit/etc) + pub error_state: ErrorState, + // 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. + pub helper: Arc>, // [Helper] state, mostly for Gupax uptime + pub pub_sys: Arc>, // [Sys] state, read by [Status], mutated by [Helper] + pub p2pool: Arc>, // [P2Pool] process state + pub xmrig: Arc>, // [XMRig] process state + pub p2pool_api: Arc>, // Public ready-to-print P2Pool API made by the "helper" thread + pub xmrig_api: Arc>, // Public ready-to-print XMRig API made by the "helper" thread + pub p2pool_img: Arc>, // A one-time snapshot of what data P2Pool started with + pub xmrig_img: Arc>, // A one-time snapshot of what data XMRig started with + // STDIN Buffer + pub p2pool_stdin: String, // The buffer between the p2pool console and the [Helper] + pub xmrig_stdin: String, // The buffer between the xmrig console and the [Helper] + // Sudo State + pub sudo: Arc>, // This is just a dummy struct on [Windows]. + // State from [--flags] + pub no_startup: bool, + // Gupax-P2Pool API + // Gupax's P2Pool API (e.g: ~/.local/share/gupax/p2pool/) + // This is a file-based API that contains data for permanent stats. + // The below struct holds everything needed for it, the paths, the + // actual stats, and all the functions needed to mutate them. + pub gupax_p2pool_api: Arc>, + // Static stuff + pub benchmarks: Vec, // XMRig CPU benchmarks + pub pid: sysinfo::Pid, // Gupax's PID + pub max_threads: usize, // Max amount of detected system threads + pub now: Instant, // Internal timer + pub exe: String, // Path for [Gupax] binary + pub dir: String, // Directory [Gupax] binary is in + pub resolution: Vec2, // Frame resolution + pub os: &'static str, // OS + pub admin: bool, // Are we admin? (for Windows) + pub os_data_path: PathBuf, // OS data path (e.g: ~/.local/share/gupax/) + pub gupax_p2pool_api_path: PathBuf, // Gupax-P2Pool API path (e.g: ~/.local/share/gupax/p2pool/) + pub state_path: PathBuf, // State file path + pub node_path: PathBuf, // Node file path + pub pool_path: PathBuf, // Pool file path + pub version: &'static str, // Gupax version + pub name_version: String, // [Gupax vX.X.X] +} + +impl App { + #[cold] + #[inline(never)] + pub fn cc(cc: &CreationContext<'_>, resolution: Vec2, app: Self) -> Self { + init_text_styles( + &cc.egui_ctx, + resolution[0], + crate::miscs::clamp_scale(app.state.gupax.selected_scale), + ); + cc.egui_ctx.set_visuals(VISUALS.clone()); + Self { resolution, ..app } + } + + #[cold] + #[inline(never)] + pub fn save_before_quit(&mut self) { + if let Err(e) = State::save(&mut self.state, &self.state_path) { + error!("State file: {}", e); + } + if let Err(e) = Node::save(&self.node_vec, &self.node_path) { + error!("Node list: {}", e); + } + if let Err(e) = Pool::save(&self.pool_vec, &self.pool_path) { + error!("Pool list: {}", e); + } + } + + #[cold] + #[inline(never)] + pub fn new(now: Instant) -> Self { + info!("Initializing App Struct..."); + info!("App Init | P2Pool & XMRig processes..."); + let p2pool = arc_mut!(Process::new( + ProcessName::P2pool, + String::new(), + PathBuf::new() + )); + let xmrig = arc_mut!(Process::new( + ProcessName::Xmrig, + String::new(), + PathBuf::new() + )); + let p2pool_api = arc_mut!(PubP2poolApi::new()); + let xmrig_api = arc_mut!(PubXmrigApi::new()); + let p2pool_img = arc_mut!(ImgP2pool::new()); + let xmrig_img = arc_mut!(ImgXmrig::new()); + + info!("App Init | Sysinfo..."); + // We give this to the [Helper] thread. + let mut sysinfo = sysinfo::System::new_with_specifics( + sysinfo::RefreshKind::new() + .with_cpu(sysinfo::CpuRefreshKind::everything()) + .with_processes(sysinfo::ProcessRefreshKind::new().with_cpu()) + .with_memory(sysinfo::MemoryRefreshKind::everything()), + ); + sysinfo.refresh_all(); + let pid = match sysinfo::get_current_pid() { + Ok(pid) => pid, + Err(e) => { + error!("App Init | Failed to get sysinfo PID: {}", e); + exit(1) + } + }; + let pub_sys = arc_mut!(Sys::new()); + + // CPU Benchmark data initialization. + info!("App Init | Initializing CPU benchmarks..."); + let benchmarks: Vec = { + let cpu = sysinfo.cpus()[0].brand(); + let mut json: Vec = + serde_json::from_slice(include_bytes!("../../assets/cpu.json")).unwrap(); + json.sort_by(|a, b| cmp_f64(strsim::jaro(&b.cpu, cpu), strsim::jaro(&a.cpu, cpu))); + json + }; + info!("App Init | Assuming user's CPU is: {}", benchmarks[0].cpu); + + info!("App Init | The rest of the [App]..."); + let mut app = Self { + tab: Tab::default(), + ping: arc_mut!(Ping::new()), + width: APP_DEFAULT_WIDTH, + height: APP_DEFAULT_HEIGHT, + must_resize: false, + og: arc_mut!(State::new()), + state: State::new(), + update: arc_mut!(Update::new( + String::new(), + PathBuf::new(), + PathBuf::new(), + true + )), + file_window: FileWindow::new(), + og_node_vec: Node::new_vec(), + node_vec: Node::new_vec(), + og_pool_vec: Pool::new_vec(), + pool_vec: Pool::new_vec(), + restart: arc_mut!(Restart::No), + diff: false, + error_state: ErrorState::new(), + helper: arc_mut!(Helper::new( + now, + pub_sys.clone(), + p2pool.clone(), + xmrig.clone(), + p2pool_api.clone(), + xmrig_api.clone(), + p2pool_img.clone(), + xmrig_img.clone(), + arc_mut!(GupaxP2poolApi::new()) + )), + p2pool, + xmrig, + p2pool_api, + xmrig_api, + p2pool_img, + xmrig_img, + p2pool_stdin: String::with_capacity(10), + xmrig_stdin: String::with_capacity(10), + sudo: arc_mut!(SudoState::new()), + resizing: false, + alpha: 0, + no_startup: false, + gupax_p2pool_api: arc_mut!(GupaxP2poolApi::new()), + pub_sys, + benchmarks, + pid, + max_threads: benri::threads!(), + now, + admin: false, + exe: String::new(), + dir: String::new(), + resolution: Vec2::new(APP_DEFAULT_HEIGHT, APP_DEFAULT_WIDTH), + os: OS, + os_data_path: PathBuf::new(), + gupax_p2pool_api_path: PathBuf::new(), + state_path: PathBuf::new(), + node_path: PathBuf::new(), + pool_path: PathBuf::new(), + version: GUPAX_VERSION, + name_version: format!("Gupax {}", GUPAX_VERSION), + }; + //---------------------------------------------------------------------------------------------------- App init data that *could* panic + info!("App Init | Getting EXE path..."); + let mut panic = String::new(); + // Get exe path + app.exe = match get_exe() { + Ok(exe) => exe, + Err(e) => { + panic = format!("get_exe(): {}", e); + app.error_state + .set(panic.clone(), ErrorFerris::Panic, ErrorButtons::Quit); + String::new() + } + }; + // Get exe directory path + app.dir = match get_exe_dir() { + Ok(dir) => dir, + Err(e) => { + panic = format!("get_exe_dir(): {}", e); + app.error_state + .set(panic.clone(), ErrorFerris::Panic, ErrorButtons::Quit); + String::new() + } + }; + // Get OS data path + app.os_data_path = match get_gupax_data_path() { + Ok(dir) => dir, + Err(e) => { + panic = format!("get_os_data_path(): {}", e); + app.error_state + .set(panic.clone(), ErrorFerris::Panic, ErrorButtons::Quit); + PathBuf::new() + } + }; + + info!("App Init | Setting TOML path..."); + // Set [*.toml] path + app.state_path = app.os_data_path.clone(); + app.state_path.push(STATE_TOML); + app.node_path = app.os_data_path.clone(); + app.node_path.push(NODE_TOML); + app.pool_path = app.os_data_path.clone(); + app.pool_path.push(POOL_TOML); + // Set GupaxP2poolApi path + app.gupax_p2pool_api_path = crate::disk::get_gupax_p2pool_path(&app.os_data_path); + lock!(app.gupax_p2pool_api).fill_paths(&app.gupax_p2pool_api_path); + + // Apply arg state + // It's not safe to [--reset] if any of the previous variables + // are unset (null path), so make sure we just abort if the [panic] String contains something. + info!("App Init | Applying argument state..."); + let mut app = parse_args(app, panic); + + use crate::disk::errors::TomlError::*; + // Read disk state + info!("App Init | Reading disk state..."); + app.state = match State::get(&app.state_path) { + Ok(toml) => toml, + Err(err) => { + error!("State ... {}", err); + let set = match err { + Io(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)), + Path(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)), + Serialize(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)), + Deserialize(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)), + Format(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)), + Merge(e) => Some((e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState)), + _ => None, + }; + if let Some((e, ferris, button)) = set { + app.error_state.set(format!("State file: {}\n\nTry deleting: {}\n\n(Warning: this will delete your Gupax settings)\n\n", e, app.state_path.display()), ferris, button); + } + + State::new() + } + }; + // Clamp window resolution scaling values. + app.state.gupax.selected_scale = crate::miscs::clamp_scale(app.state.gupax.selected_scale); + + app.og = arc_mut!(app.state.clone()); + // Read node list + info!("App Init | Reading node list..."); + app.node_vec = match Node::get(&app.node_path) { + Ok(toml) => toml, + Err(err) => { + error!("Node ... {}", err); + let (e, ferris, button) = match err { + Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState), + Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + }; + app.error_state.set(format!("Node list: {}\n\nTry deleting: {}\n\n(Warning: this will delete your custom node list)\n\n", e, app.node_path.display()), ferris, button); + Node::new_vec() + } + }; + app.og_node_vec = app.node_vec.clone(); + debug!("Node Vec:"); + debug!("{:#?}", app.node_vec); + // Read pool list + info!("App Init | Reading pool list..."); + app.pool_vec = match Pool::get(&app.pool_path) { + Ok(toml) => toml, + Err(err) => { + error!("Pool ... {}", err); + let (e, ferris, button) = match err { + Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState), + Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + }; + app.error_state.set(format!("Pool list: {}\n\nTry deleting: {}\n\n(Warning: this will delete your custom pool list)\n\n", e, app.pool_path.display()), ferris, button); + Pool::new_vec() + } + }; + app.og_pool_vec = app.pool_vec.clone(); + debug!("Pool Vec:"); + debug!("{:#?}", app.pool_vec); + + //---------------------------------------------------------------------------------------------------- + // Read [GupaxP2poolApi] disk files + let mut gupax_p2pool_api = lock!(app.gupax_p2pool_api); + match GupaxP2poolApi::create_all_files(&app.gupax_p2pool_api_path) { + Ok(_) => info!("App Init | Creating Gupax-P2Pool API files ... OK"), + Err(err) => { + error!("GupaxP2poolApi ... {}", err); + let (e, ferris, button) = match err { + Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState), + Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + }; + app.error_state.set(format!("Gupax P2Pool Stats: {}\n\nTry deleting: {}\n\n(Warning: this will delete your P2Pool payout history...!)\n\n", e, app.gupax_p2pool_api_path.display()), ferris, button); + } + } + info!("App Init | Reading Gupax-P2Pool API files..."); + match gupax_p2pool_api.read_all_files_and_update() { + Ok(_) => { + info!( + "GupaxP2poolApi ... Payouts: {} | XMR (atomic-units): {}", + gupax_p2pool_api.payout, gupax_p2pool_api.xmr, + ); + } + Err(err) => { + error!("GupaxP2poolApi ... {}", err); + let (e, ferris, button) = match err { + Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState), + Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), + }; + app.error_state.set(format!("Gupax P2Pool Stats: {}\n\nTry deleting: {}\n\n(Warning: this will delete your P2Pool payout history...!)\n\n", e, app.gupax_p2pool_api_path.display()), ferris, button); + } + }; + drop(gupax_p2pool_api); + lock!(app.helper).gupax_p2pool_api = Arc::clone(&app.gupax_p2pool_api); + + //---------------------------------------------------------------------------------------------------- + let mut og = lock!(app.og); // Lock [og] + // Handle max threads + info!("App Init | Handling max thread overflow..."); + og.xmrig.max_threads = app.max_threads; + let current = og.xmrig.current_threads; + let max = og.xmrig.max_threads; + if current > max { + og.xmrig.current_threads = max; + } + // Handle [node_vec] overflow + info!("App Init | Handling [node_vec] overflow"); + if og.p2pool.selected_index > app.og_node_vec.len() { + warn!( + "App | Overflowing manual node index [{} > {}]", + og.p2pool.selected_index, + app.og_node_vec.len() + ); + let (name, node) = match app.og_node_vec.first() { + Some(zero) => zero.clone(), + None => Node::new_tuple(), + }; + og.p2pool.selected_index = 0; + og.p2pool.selected_name = name.clone(); + og.p2pool.selected_ip = node.ip.clone(); + og.p2pool.selected_rpc = node.rpc.clone(); + og.p2pool.selected_zmq = node.zmq.clone(); + app.state.p2pool.selected_index = 0; + app.state.p2pool.selected_name = name; + app.state.p2pool.selected_ip = node.ip; + app.state.p2pool.selected_rpc = node.rpc; + app.state.p2pool.selected_zmq = node.zmq; + } + // Handle [pool_vec] overflow + info!("App Init | Handling [pool_vec] overflow..."); + if og.xmrig.selected_index > app.og_pool_vec.len() { + warn!( + "App | Overflowing manual pool index [{} > {}], resetting to 1", + og.xmrig.selected_index, + app.og_pool_vec.len() + ); + let (name, pool) = match app.og_pool_vec.first() { + Some(zero) => zero.clone(), + None => Pool::new_tuple(), + }; + og.xmrig.selected_index = 0; + og.xmrig.selected_name = name.clone(); + og.xmrig.selected_ip = pool.ip.clone(); + og.xmrig.selected_port = pool.port.clone(); + app.state.xmrig.selected_index = 0; + app.state.xmrig.selected_name = name; + app.state.xmrig.selected_ip = pool.ip; + app.state.xmrig.selected_port = pool.port; + } + + // Apply TOML values to [Update] + info!("App Init | Applying TOML values to [Update]..."); + let p2pool_path = og.gupax.absolute_p2pool_path.clone(); + let xmrig_path = og.gupax.absolute_xmrig_path.clone(); + let tor = og.gupax.update_via_tor; + app.update = arc_mut!(Update::new(app.exe.clone(), p2pool_path, xmrig_path, tor)); + + // Set state version as compiled in version + info!("App Init | Setting state Gupax version..."); + lock!(og.version).gupax = GUPAX_VERSION.to_string(); + lock!(app.state.version).gupax = GUPAX_VERSION.to_string(); + + // Set saved [Tab] + info!("App Init | Setting saved [Tab]..."); + app.tab = app.state.gupax.tab; + + // Check if [P2pool.node] exists + info!("App Init | Checking if saved remote node still exists..."); + app.state.p2pool.node = RemoteNode::check_exists(&app.state.p2pool.node); + + drop(og); // Unlock [og] + + // Spawn the "Helper" thread. + info!("Helper | Spawning helper thread..."); + Helper::spawn_helper(&app.helper, sysinfo, app.pid, app.max_threads); + info!("Helper ... OK"); + + // Check for privilege. Should be Admin on [Windows] and NOT root on Unix. + info!("App Init | Checking for privilege level..."); + #[cfg(target_os = "windows")] + if is_elevated::is_elevated() { + app.admin = true; + } else { + error!("Windows | Admin user not detected!"); + app.error_state.set(format!("Gupax was not launched as Administrator!\nBe warned, XMRig might have less hashrate!"), ErrorFerris::Sudo, ErrorButtons::WindowsAdmin); + } + #[cfg(target_family = "unix")] + if sudo_check::check() != sudo_check::RunningAs::User { + let id = sudo_check::check(); + error!("Unix | Regular user not detected: [{:?}]", id); + app.error_state.set(format!("Gupax was launched as: [{:?}]\nPlease launch Gupax with regular user permissions.", id), ErrorFerris::Panic, ErrorButtons::Quit); + } + + // macOS re-locates "dangerous" applications into some read-only "/private" directory. + // It _seems_ to be fixed by moving [Gupax.app] into "/Applications". + // So, detect if we are in in "/private" and warn the user. + #[cfg(target_os = "macos")] + if app.exe.starts_with("/private") { + app.error_state.set(format!("macOS thinks Gupax is a virus!\n(macOS has relocated Gupax for security reasons)\n\nThe directory: [{}]\nSince this is a private read-only directory, it causes issues with updates and correctly locating P2Pool/XMRig. Please move Gupax into the [Applications] directory, this lets macOS relax a little.\n", app.exe), ErrorFerris::Panic, ErrorButtons::Quit); + } + + info!("App ... OK"); + app + } + + #[cold] + #[inline(never)] + pub fn gather_backup_hosts(&self) -> Option> { + if !self.state.p2pool.backup_host { + return None; + } + + // INVARIANT: + // We must ensure all nodes are capable of + // sending/receiving valid JSON-RPC requests. + // + // This is done during the `Ping` phase, meaning + // all the nodes listed in our `self.ping` should + // have ping data. We can use this data to filter + // out "dead" nodes. + // + // The user must have at least pinged once so that + // we actually have this data to work off of, else, + // this "backup host" feature will return here + // with 0 extra nodes as we can't be sure that any + // of them are actually online. + // + // Realistically, most of them are, but we can't be sure, + // and checking here without explicitly asking the user + // to connect to nodes is a no-go (also, non-async environment). + if !lock!(self.ping).pinged { + warn!("Backup hosts ... simple node backup: no ping data available, returning None"); + return None; + } + + if self.state.p2pool.simple { + let mut vec = Vec::with_capacity(REMOTE_NODES.len()); + + // Locking during this entire loop should be fine, + // only a few nodes to iter through. + for pinged_node in lock!(self.ping).nodes.iter() { + // Continue if this node is not green/yellow. + if pinged_node.ms > crate::components::node::RED_NODE_PING { + continue; + } + + let (ip, rpc, zmq) = RemoteNode::get_ip_rpc_zmq(pinged_node.ip); + + let node = Node { + ip: ip.into(), + rpc: rpc.into(), + zmq: zmq.into(), + }; + + vec.push(node); + } + + if vec.is_empty() { + warn!("Backup hosts ... simple node backup: no viable nodes found"); + None + } else { + info!("Backup hosts ... simple node backup list: {vec:#?}"); + Some(vec) + } + } else { + Some(self.node_vec.iter().map(|(_, node)| node.clone()).collect()) + } + } +} +//---------------------------------------------------------------------------------------------------- [Tab] Enum + Impl +// The tabs inside [App]. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum Tab { + About, + Status, + Gupax, + P2pool, + Xmrig, + Xvb, +} + +impl Default for Tab { + fn default() -> Self { + Self::About + } +} +//---------------------------------------------------------------------------------------------------- [Restart] Enum +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Restart { + No, // We don't need to restart + Yes, // We updated, user should probably (but isn't required to) restart +} +//---------------------------------------------------------------------------------------------------- CPU Benchmarks. +#[derive(Debug, Serialize, Deserialize)] +pub struct Benchmark { + pub cpu: String, + pub rank: u16, + pub percent: f32, + pub benchmarks: u16, + pub average: f32, + pub high: f32, + pub low: f32, +} +#[cfg(test)] +mod test { + use crate::miscs::cmp_f64; + + #[test] + fn detect_benchmark_cpu() { + use crate::app::Benchmark; + let cpu = "AMD Ryzen 9 5950X 16-Core Processor"; + + let benchmarks: Vec = { + let mut json: Vec = + serde_json::from_slice(include_bytes!("../../assets/cpu.json")).unwrap(); + json.sort_by(|a, b| cmp_f64(strsim::jaro(&b.cpu, cpu), strsim::jaro(&a.cpu, cpu))); + json + }; + + assert!(benchmarks[0].cpu == "AMD Ryzen 9 5950X 16-Core Processor"); + } +} diff --git a/src/app/panels/bottom.rs b/src/app/panels/bottom.rs new file mode 100644 index 0000000..80cfdc8 --- /dev/null +++ b/src/app/panels/bottom.rs @@ -0,0 +1,509 @@ +use std::sync::Arc; + +use crate::app::{keys::KeyPressed, Restart}; +use crate::disk::node::Node; +use crate::disk::pool::Pool; +use crate::disk::state::{Gupax, State}; +use crate::disk::status::Submenu; +use crate::helper::{Helper, ProcessSignal, ProcessState}; +use crate::utils::constants::*; +use crate::utils::errors::{ErrorButtons, ErrorFerris}; +use crate::utils::macros::lock; +use crate::utils::regex::Regexes; +use egui::TextStyle::Name; +use egui::*; +use log::debug; + +use crate::{app::Tab, utils::constants::SPACE}; +impl crate::app::App { + pub fn bottom_panel( + &mut self, + ctx: &egui::Context, + p2pool_state: ProcessState, + xmrig_state: ProcessState, + key: &KeyPressed, + wants_input: bool, + p2pool_is_waiting: bool, + xmrig_is_waiting: bool, + p2pool_is_alive: bool, + xmrig_is_alive: bool, + ) { + // Bottom: app info + state/process buttons + debug!("App | Rendering BOTTOM bar"); + TopBottomPanel::bottom("bottom").show(ctx, |ui| { + let height = self.height / 22.0; + ui.style_mut().override_text_style = Some(Name("Bottom".into())); + ui.horizontal(|ui| { + ui.group(|ui| { + let width = ((self.width / 2.0) / 4.0) - (SPACE * 2.0); + // [Gupax Version] + // Is yellow if the user updated and should (but isn't required to) restart. + match *lock!(self.restart) { + Restart::Yes => ui + .add_sized( + [width, height], + Label::new(RichText::new(&self.name_version).color(YELLOW)), + ) + .on_hover_text(GUPAX_SHOULD_RESTART), + _ => ui.add_sized([width, height], Label::new(&self.name_version)), + }; + ui.separator(); + // [OS] + // Check if admin for windows. + // Unix SHOULDN'T be running as root, and the check is done when + // [App] is initialized, so no reason to check here. + #[cfg(target_os = "windows")] + if self.admin { + ui.add_sized([width, height], Label::new(self.os)); + } else { + ui.add_sized( + [width, height], + Label::new(RichText::new(self.os).color(RED)), + ) + .on_hover_text(WINDOWS_NOT_ADMIN); + } + #[cfg(target_family = "unix")] + ui.add_sized([width, height], Label::new(self.os)); + ui.separator(); + // [P2Pool/XMRig] Status + use crate::helper::ProcessState::*; + match p2pool_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), + Syncing => ui + .add_sized( + [width, height], + Label::new(RichText::new("P2Pool ⏺").color(ORANGE)), + ) + .on_hover_text(P2POOL_SYNCING), + Middle | Waiting | NotMining => ui + .add_sized( + [width, height], + Label::new(RichText::new("P2Pool ⏺").color(YELLOW)), + ) + .on_hover_text(P2POOL_MIDDLE), + }; + ui.separator(); + match xmrig_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), + NotMining => ui + .add_sized( + [width, height], + Label::new(RichText::new("XMRig ⏺").color(ORANGE)), + ) + .on_hover_text(XMRIG_NOT_MINING), + Middle | Waiting | Syncing => ui + .add_sized( + [width, height], + Label::new(RichText::new("XMRig ⏺").color(YELLOW)), + ) + .on_hover_text(XMRIG_MIDDLE), + }; + }); + + // [Save/Reset] + ui.with_layout(Layout::right_to_left(Align::RIGHT), |ui| { + let width = (ui.available_width() / 3.0) - (SPACE * 3.0); + ui.group(|ui| { + ui.set_enabled(self.diff); + let width = width / 2.0; + if key.is_r() && !wants_input && self.diff + || ui + .add_sized([width, height], Button::new("Reset")) + .on_hover_text("Reset changes") + .clicked() + { + let og = lock!(self.og).clone(); + self.state.status = og.status; + self.state.gupax = og.gupax; + self.state.p2pool = og.p2pool; + self.state.xmrig = og.xmrig; + self.node_vec = self.og_node_vec.clone(); + self.pool_vec = self.og_pool_vec.clone(); + } + if key.is_s() && !wants_input && self.diff + || ui + .add_sized([width, height], Button::new("Save")) + .on_hover_text("Save changes") + .clicked() + { + match State::save(&mut self.state, &self.state_path) { + Ok(_) => { + let mut og = lock!(self.og); + og.status = self.state.status.clone(); + og.gupax = self.state.gupax.clone(); + og.p2pool = self.state.p2pool.clone(); + og.xmrig = self.state.xmrig.clone(); + } + Err(e) => { + self.error_state.set( + format!("State file: {}", e), + ErrorFerris::Error, + ErrorButtons::Okay, + ); + } + }; + match Node::save(&self.node_vec, &self.node_path) { + Ok(_) => self.og_node_vec = self.node_vec.clone(), + Err(e) => self.error_state.set( + format!("Node list: {}", e), + ErrorFerris::Error, + ErrorButtons::Okay, + ), + }; + match Pool::save(&self.pool_vec, &self.pool_path) { + Ok(_) => self.og_pool_vec = self.pool_vec.clone(), + Err(e) => self.error_state.set( + format!("Pool list: {}", e), + ErrorFerris::Error, + ErrorButtons::Okay, + ), + }; + } + }); + + // [Simple/Advanced] + [Start/Stop/Restart] + match self.tab { + Tab::Status => { + ui.group(|ui| { + let width = (ui.available_width() / 3.0) - 14.0; + if ui + .add_sized( + [width, height], + SelectableLabel::new( + self.state.status.submenu == Submenu::Benchmarks, + "Benchmarks", + ), + ) + .on_hover_text(STATUS_SUBMENU_HASHRATE) + .clicked() + { + self.state.status.submenu = Submenu::Benchmarks; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new( + self.state.status.submenu == Submenu::P2pool, + "P2Pool", + ), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL) + .clicked() + { + self.state.status.submenu = Submenu::P2pool; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new( + self.state.status.submenu == Submenu::Processes, + "Processes", + ), + ) + .on_hover_text(STATUS_SUBMENU_PROCESSES) + .clicked() + { + self.state.status.submenu = Submenu::Processes; + } + }); + } + Tab::Gupax => { + ui.group(|ui| { + let width = (ui.available_width() / 2.0) - 10.5; + if ui + .add_sized( + [width, height], + SelectableLabel::new(!self.state.gupax.simple, "Advanced"), + ) + .on_hover_text(GUPAX_ADVANCED) + .clicked() + { + self.state.gupax.simple = false; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.state.gupax.simple, "Simple"), + ) + .on_hover_text(GUPAX_SIMPLE) + .clicked() + { + self.state.gupax.simple = true; + } + }); + } + Tab::P2pool => { + ui.group(|ui| { + let width = width / 1.5; + if ui + .add_sized( + [width, height], + SelectableLabel::new(!self.state.p2pool.simple, "Advanced"), + ) + .on_hover_text(P2POOL_ADVANCED) + .clicked() + { + self.state.p2pool.simple = false; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.state.p2pool.simple, "Simple"), + ) + .on_hover_text(P2POOL_SIMPLE) + .clicked() + { + self.state.p2pool.simple = true; + } + }); + ui.group(|ui| { + let width = (ui.available_width() / 3.0) - 5.0; + if p2pool_is_waiting { + ui.add_enabled_ui(false, |ui| { + ui.add_sized([width, height], Button::new("⟲")) + .on_disabled_hover_text(P2POOL_MIDDLE); + ui.add_sized([width, height], Button::new("⏹")) + .on_disabled_hover_text(P2POOL_MIDDLE); + ui.add_sized([width, height], Button::new("▶")) + .on_disabled_hover_text(P2POOL_MIDDLE); + }); + } else if p2pool_is_alive { + if key.is_up() && !wants_input + || ui + .add_sized([width, height], Button::new("⟲")) + .on_hover_text("Restart P2Pool") + .clicked() + { + let _ = lock!(self.og).update_absolute_path(); + let _ = self.state.update_absolute_path(); + Helper::restart_p2pool( + &self.helper, + &self.state.p2pool, + &self.state.gupax.absolute_p2pool_path, + self.gather_backup_hosts(), + ); + } + if key.is_down() && !wants_input + || 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_disabled_hover_text("Start P2Pool"); + }); + } else { + ui.add_enabled_ui(false, |ui| { + ui.add_sized([width, height], Button::new("⟲")) + .on_disabled_hover_text("Restart P2Pool"); + ui.add_sized([width, height], Button::new("⏹")) + .on_disabled_hover_text("Stop P2Pool"); + }); + // Check if address is okay before allowing to start. + let mut text = String::new(); + let mut ui_enabled = true; + if !Regexes::addr_ok(&self.state.p2pool.address) { + ui_enabled = false; + text = format!("Error: {}", P2POOL_ADDRESS); + } else if !Gupax::path_is_file(&self.state.gupax.p2pool_path) { + ui_enabled = false; + text = format!("Error: {}", P2POOL_PATH_NOT_FILE); + } else if !crate::components::update::check_p2pool_path( + &self.state.gupax.p2pool_path, + ) { + ui_enabled = false; + text = format!("Error: {}", P2POOL_PATH_NOT_VALID); + } + ui.set_enabled(ui_enabled); + let color = if ui_enabled { GREEN } else { RED }; + if (ui_enabled && key.is_up() && !wants_input) + || ui + .add_sized( + [width, height], + Button::new(RichText::new("▶").color(color)), + ) + .on_hover_text("Start P2Pool") + .on_disabled_hover_text(text) + .clicked() + { + let _ = lock!(self.og).update_absolute_path(); + let _ = self.state.update_absolute_path(); + Helper::start_p2pool( + &self.helper, + &self.state.p2pool, + &self.state.gupax.absolute_p2pool_path, + self.gather_backup_hosts(), + ); + } + } + }); + } + Tab::Xmrig => { + ui.group(|ui| { + let width = width / 1.5; + if ui + .add_sized( + [width, height], + SelectableLabel::new(!self.state.xmrig.simple, "Advanced"), + ) + .on_hover_text(XMRIG_ADVANCED) + .clicked() + { + self.state.xmrig.simple = false; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.state.xmrig.simple, "Simple"), + ) + .on_hover_text(XMRIG_SIMPLE) + .clicked() + { + self.state.xmrig.simple = true; + } + }); + ui.group(|ui| { + let width = (ui.available_width() / 3.0) - 5.0; + if xmrig_is_waiting { + ui.add_enabled_ui(false, |ui| { + ui.add_sized([width, height], Button::new("⟲")) + .on_disabled_hover_text(XMRIG_MIDDLE); + ui.add_sized([width, height], Button::new("⏹")) + .on_disabled_hover_text(XMRIG_MIDDLE); + ui.add_sized([width, height], Button::new("▶")) + .on_disabled_hover_text(XMRIG_MIDDLE); + }); + } else if xmrig_is_alive { + if key.is_up() && !wants_input + || ui + .add_sized([width, height], Button::new("⟲")) + .on_hover_text("Restart XMRig") + .clicked() + { + let _ = lock!(self.og).update_absolute_path(); + let _ = self.state.update_absolute_path(); + if cfg!(windows) { + Helper::restart_xmrig( + &self.helper, + &self.state.xmrig, + &self.state.gupax.absolute_xmrig_path, + Arc::clone(&self.sudo), + ); + } else { + lock!(self.sudo).signal = ProcessSignal::Restart; + self.error_state.ask_sudo(&self.sudo); + } + } + if key.is_down() && !wants_input + || ui + .add_sized([width, height], Button::new("⏹")) + .on_hover_text("Stop XMRig") + .clicked() + { + if cfg!(target_os = "macos") { + lock!(self.sudo).signal = ProcessSignal::Stop; + self.error_state.ask_sudo(&self.sudo); + } else { + Helper::stop_xmrig(&self.helper); + } + } + ui.add_enabled_ui(false, |ui| { + ui.add_sized([width, height], Button::new("▶")) + .on_disabled_hover_text("Start XMRig"); + }); + } else { + ui.add_enabled_ui(false, |ui| { + ui.add_sized([width, height], Button::new("⟲")) + .on_disabled_hover_text("Restart XMRig"); + ui.add_sized([width, height], Button::new("⏹")) + .on_disabled_hover_text("Stop XMRig"); + }); + let mut text = String::new(); + let mut ui_enabled = true; + if !Gupax::path_is_file(&self.state.gupax.xmrig_path) { + ui_enabled = false; + text = format!("Error: {}", XMRIG_PATH_NOT_FILE); + } else if !crate::components::update::check_xmrig_path( + &self.state.gupax.xmrig_path, + ) { + ui_enabled = false; + text = format!("Error: {}", XMRIG_PATH_NOT_VALID); + } + ui.set_enabled(ui_enabled); + let color = if ui_enabled { GREEN } else { RED }; + if (ui_enabled && key.is_up() && !wants_input) + || ui + .add_sized( + [width, height], + Button::new(RichText::new("▶").color(color)), + ) + .on_hover_text("Start XMRig") + .on_disabled_hover_text(text) + .clicked() + { + let _ = lock!(self.og).update_absolute_path(); + let _ = self.state.update_absolute_path(); + if cfg!(windows) { + Helper::start_xmrig( + &self.helper, + &self.state.xmrig, + &self.state.gupax.absolute_xmrig_path, + Arc::clone(&self.sudo), + ); + } else if cfg!(unix) { + lock!(self.sudo).signal = ProcessSignal::Start; + self.error_state.ask_sudo(&self.sudo); + } + } + } + }); + } + _ => (), + } + }); + }); + }); + } +} diff --git a/src/gupax.rs b/src/app/panels/middle/gupax.rs similarity index 80% rename from src/gupax.rs rename to src/app/panels/middle/gupax.rs index 31ac898..f82f73a 100644 --- a/src/gupax.rs +++ b/src/app/panels/middle/gupax.rs @@ -1,75 +1,17 @@ -// Gupax - GUI Uniting P2Pool And XMRig -// -// Copyright (c) 2022-2023 hinto-janai -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use crate::State; -use crate::{constants::*, macros::*, update::*, ErrorState, Restart, Tab}; -use egui::{ - Button, Checkbox, Label, ProgressBar, RichText, SelectableLabel, Slider, Spinner, TextEdit, - Vec2, -}; -use log::*; -use serde::{Deserialize, Serialize}; -use std::{ - path::Path, - sync::{Arc, Mutex}, - thread, -}; - -//---------------------------------------------------------------------------------------------------- FileWindow -// Struct for writing/reading the path state. -// The opened file picker is started in a new -// thread so main() needs to be in sync. -pub struct FileWindow { - thread: bool, // Is there already a FileWindow thread? - picked_p2pool: bool, // Did the user pick a path for p2pool? - picked_xmrig: bool, // Did the user pick a path for xmrig? - p2pool_path: String, // The picked p2pool path - xmrig_path: String, // The picked p2pool path -} - -impl FileWindow { - pub fn new() -> Arc> { - arc_mut!(Self { - thread: false, - picked_p2pool: false, - picked_xmrig: false, - p2pool_path: String::new(), - xmrig_path: String::new(), - }) - } -} - -#[derive(Debug, Clone)] -pub enum FileType { - P2pool, - Xmrig, -} - -//---------------------------------------------------------------------------------------------------- Ratio Lock -// Enum for the lock ratio in the advanced tab. -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] -pub enum Ratio { - Width, - Height, - None, -} - -//---------------------------------------------------------------------------------------------------- Gupax -impl crate::disk::Gupax { +use crate::app::panels::middle::*; +use crate::app::ErrorState; +use crate::app::Restart; +use crate::components::gupax::FileWindow; +use crate::components::gupax::*; +use crate::components::update::Update; +use crate::disk::state::*; +use crate::macros::lock2; +use egui::Button; +use log::debug; +use std::path::Path; +use std::sync::Arc; +use std::sync::Mutex; +impl Gupax { #[inline(always)] // called once #[allow(clippy::too_many_arguments)] pub fn show( @@ -218,7 +160,7 @@ impl crate::disk::Gupax { Label::new(RichText::new("P2Pool Binary Path ❌").color(RED)), ) .on_hover_text(P2POOL_PATH_NOT_FILE); - } else if !crate::update::check_p2pool_path(&self.p2pool_path) { + } else if !crate::components::update::check_p2pool_path(&self.p2pool_path) { ui.add_sized( [text_edit, height], Label::new(RichText::new("P2Pool Binary Path ❌").color(RED)), @@ -255,7 +197,7 @@ impl crate::disk::Gupax { Label::new(RichText::new(" XMRig Binary Path ❌").color(RED)), ) .on_hover_text(XMRIG_PATH_NOT_FILE); - } else if !crate::update::check_xmrig_path(&self.xmrig_path) { + } else if !crate::components::update::check_xmrig_path(&self.xmrig_path) { ui.add_sized( [text_edit, height], Label::new(RichText::new(" XMRig Binary Path ❌").color(RED)), @@ -491,47 +433,4 @@ impl crate::disk::Gupax { }) }); } - - // Checks if a path is a valid path to a file. - pub fn path_is_file(path: &str) -> bool { - let path = path.to_string(); - match crate::disk::into_absolute_path(path) { - Ok(path) => path.is_file(), - _ => false, - } - } - - #[cold] - #[inline(never)] - fn spawn_file_window_thread(file_window: &Arc>, file_type: FileType) { - use FileType::*; - let name = match file_type { - P2pool => "P2Pool", - Xmrig => "XMRig", - }; - let file_window = file_window.clone(); - lock!(file_window).thread = true; - thread::spawn(move || { - match rfd::FileDialog::new() - .set_title(format!("Select {} Binary for Gupax", name)) - .pick_file() - { - Some(path) => { - info!("Gupax | Path selected for {} ... {}", name, path.display()); - match file_type { - P2pool => { - lock!(file_window).p2pool_path = path.display().to_string(); - lock!(file_window).picked_p2pool = true; - } - Xmrig => { - lock!(file_window).xmrig_path = path.display().to_string(); - lock!(file_window).picked_xmrig = true; - } - }; - } - None => info!("Gupax | No path selected for {}", name), - }; - lock!(file_window).thread = false; - }); - } } diff --git a/src/app/panels/middle/mod.rs b/src/app/panels/middle/mod.rs new file mode 100644 index 0000000..6a40ea1 --- /dev/null +++ b/src/app/panels/middle/mod.rs @@ -0,0 +1,167 @@ +use crate::app::keys::KeyPressed; +use crate::app::Tab; +use crate::utils::constants::*; +use crate::utils::errors::{ErrorButtons, ErrorFerris}; +use crate::utils::macros::lock; +use egui::*; +use log::debug; + +mod gupax; +mod p2pool; +mod status; +mod xmrig; +mod xvb; +impl crate::app::App { + pub fn middle_panel( + &mut self, + ctx: &egui::Context, + frame: &mut eframe::Frame, + key: KeyPressed, + p2pool_is_alive: bool, + xmrig_is_alive: bool, + ) { + // Middle panel, contents of the [Tab] + debug!("App | Rendering CENTRAL_PANEL (tab contents)"); + CentralPanel::default().show(ctx, |ui| { + // This sets the Ui dimensions after Top/Bottom are filled + self.width = ui.available_width(); + self.height = ui.available_height(); + ui.style_mut().override_text_style = Some(TextStyle::Body); + match self.tab { + Tab::About => { + debug!("App | Entering [About] Tab"); + // If [D], show some debug info with [ErrorState] + if key.is_d() { + debug!("App | Entering [Debug Info]"); + #[cfg(feature = "distro")] + let distro = true; + #[cfg(not(feature = "distro"))] + let distro = false; + let p2pool_gui_len = lock!(self.p2pool_api).output.len(); + let xmrig_gui_len = lock!(self.xmrig_api).output.len(); + let gupax_p2pool_api = lock!(self.gupax_p2pool_api); + let debug_info = format!( +"Gupax version: {}\n +Bundled P2Pool version: {}\n +Bundled XMRig version: {}\n +Gupax uptime: {} seconds\n +Selected resolution: {}x{}\n +Internal resolution: {}x{}\n +Operating system: {}\n +Max detected threads: {}\n +Gupax PID: {}\n +State diff: {}\n +Node list length: {}\n +Pool list length: {}\n +Admin privilege: {}\n +Release build: {}\n +Debug build: {}\n +Distro build: {}\n +Build commit: {}\n +OS Data PATH: {}\n +Gupax PATH: {}\n +P2Pool PATH: {}\n +XMRig PATH: {}\n +P2Pool console byte length: {}\n +XMRig console byte length: {}\n +------------------------------------------ P2POOL IMAGE ------------------------------------------ +{:#?}\n +------------------------------------------ XMRIG IMAGE ------------------------------------------ +{:#?}\n +------------------------------------------ GUPAX-P2POOL API ------------------------------------------ +payout: {:#?} +payout_u64: {:#?} +xmr: {:#?} +path_log: {:#?} +path_payout: {:#?} +path_xmr: {:#?}\n +------------------------------------------ WORKING STATE ------------------------------------------ +{:#?}\n +------------------------------------------ ORIGINAL STATE ------------------------------------------ +{:#?}", + GUPAX_VERSION, + P2POOL_VERSION, + XMRIG_VERSION, + self.now.elapsed().as_secs_f32(), + self.state.gupax.selected_width, + self.state.gupax.selected_height, + self.width, + self.height, + OS_NAME, + self.max_threads, + self.pid, + self.diff, + self.node_vec.len(), + self.pool_vec.len(), + self.admin, + !cfg!(debug_assertions), + cfg!(debug_assertions), + distro, + COMMIT, + self.os_data_path.display(), + self.exe, + self.state.gupax.absolute_p2pool_path.display(), + self.state.gupax.absolute_xmrig_path.display(), + p2pool_gui_len, + xmrig_gui_len, + lock!(self.p2pool_img), + lock!(self.xmrig_img), + gupax_p2pool_api.payout, + gupax_p2pool_api.payout_u64, + gupax_p2pool_api.xmr, + gupax_p2pool_api.path_log, + gupax_p2pool_api.path_payout, + gupax_p2pool_api.path_xmr, + self.state, + lock!(self.og), + ); + self.error_state.set(debug_info, ErrorFerris::Cute, ErrorButtons::Debug); + } + let width = self.width; + let height = self.height/30.0; + let max_height = self.height; + ui.add_space(10.0); + ui.vertical_centered(|ui| { + ui.set_max_height(max_height); + // Display [Gupax] banner + let link_width = width/14.0; + ui.add_sized(Vec2::new(width, height*3.0), Image::from_bytes("bytes://banner.png", BYTES_BANNER)); + ui.add_sized([width, height], Label::new("is a GUI for mining")); + ui.add_sized([link_width, height], Hyperlink::from_label_and_url("[Monero]", "https://www.github.com/monero-project/monero")); + ui.add_sized([width, height], Label::new("on")); + ui.add_sized([link_width, height], Hyperlink::from_label_and_url("[P2Pool]", "https://www.github.com/SChernykh/p2pool")); + ui.add_sized([width, height], Label::new("using")); + ui.add_sized([link_width, height], Hyperlink::from_label_and_url("[XMRig]", "https://www.github.com/xmrig/xmrig")); + + ui.add_space(SPACE*2.0); + ui.add_sized([width, height], Label::new(KEYBOARD_SHORTCUTS)); + ui.add_space(SPACE*2.0); + + if cfg!(debug_assertions) { ui.label(format!("Gupax is running in debug mode - {}", self.now.elapsed().as_secs_f64())); } + ui.label(format!("Gupax has been running for {}", lock!(self.pub_sys).gupax_uptime)); + }); + } + Tab::Status => { + debug!("App | Entering [Status] Tab"); + crate::disk::state::Status::show(&mut self.state.status, &self.pub_sys, &self.p2pool_api, &self.xmrig_api, &self.p2pool_img, &self.xmrig_img, p2pool_is_alive, xmrig_is_alive, self.max_threads, &self.gupax_p2pool_api, &self.benchmarks, self.width, self.height, ctx, ui); + } + Tab::Gupax => { + debug!("App | Entering [Gupax] Tab"); + crate::disk::state::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 => { + debug!("App | Entering [P2Pool] Tab"); + crate::disk::state::P2pool::show(&mut self.state.p2pool, &mut self.node_vec, &self.og, &self.ping, &self.p2pool, &self.p2pool_api, &mut self.p2pool_stdin, self.width, self.height, ctx, ui); + } + Tab::Xmrig => { + debug!("App | Entering [XMRig] Tab"); + crate::disk::state::Xmrig::show(&mut self.state.xmrig, &mut self.pool_vec, &self.xmrig, &self.xmrig_api, &mut self.xmrig_stdin, self.width, self.height, ctx, ui); + } + Tab::Xvb => { + debug!("App | Entering [XvB] Tab"); + crate::disk::state::Xvb::show(self.width, self.height, ctx, ui); + } + } + }); + } +} diff --git a/src/app/panels/middle/p2pool/advanced.rs b/src/app/panels/middle/p2pool/advanced.rs new file mode 100644 index 0000000..6ff179f --- /dev/null +++ b/src/app/panels/middle/p2pool/advanced.rs @@ -0,0 +1,305 @@ +use crate::disk::node::Node; +use crate::{disk::state::P2pool, utils::regex::REGEXES}; +use egui::Button; +use egui::Checkbox; +use egui::Slider; + +use crate::constants::*; +use egui::{Color32, ComboBox, Label, RichText, SelectableLabel, TextStyle::*, Ui}; +use log::*; + +impl P2pool { + pub(super) fn advanced( + &mut self, + ui: &mut Ui, + width: f32, + height: f32, + text_edit: f32, + node_vec: &mut Vec<(String, Node)>, + ) { + debug!("P2Pool Tab | Rendering [Node List] elements"); + let mut incorrect_input = false; // This will disable [Add/Delete] on bad input + // [Monero node IP/RPC/ZMQ] + ui.horizontal(|ui| { + ui.group(|ui| { + let width = width/10.0; + ui.vertical(|ui| { + ui.spacing_mut().text_edit_width = width*3.32; + ui.horizontal(|ui| { + let text; + let color; + let len = format!("{:02}", self.name.len()); + if self.name.is_empty() { + text = format!("Name [ {}/30 ]➖", len); + color = Color32::LIGHT_GRAY; + incorrect_input = true; + } else if REGEXES.name.is_match(&self.name) { + text = format!("Name [ {}/30 ]✔", len); + color = Color32::from_rgb(100, 230, 100); + } else { + text = format!("Name [ {}/30 ]❌", len); + color = Color32::from_rgb(230, 50, 50); + incorrect_input = true; + } + ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color))); + ui.text_edit_singleline(&mut self.name).on_hover_text(P2POOL_NAME); + self.name.truncate(30); + }); + ui.horizontal(|ui| { + let text; + let color; + let len = format!("{:03}", self.ip.len()); + if self.ip.is_empty() { + text = format!(" IP [{}/255]➖", len); + color = Color32::LIGHT_GRAY; + incorrect_input = true; + } else if self.ip == "localhost" || REGEXES.ipv4.is_match(&self.ip) || REGEXES.domain.is_match(&self.ip) { + text = format!(" IP [{}/255]✔", len); + color = Color32::from_rgb(100, 230, 100); + } else { + text = format!(" IP [{}/255]❌", len); + color = Color32::from_rgb(230, 50, 50); + incorrect_input = true; + } + ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color))); + ui.text_edit_singleline(&mut self.ip).on_hover_text(P2POOL_NODE_IP); + self.ip.truncate(255); + }); + ui.horizontal(|ui| { + let text; + let color; + let len = self.rpc.len(); + if self.rpc.is_empty() { + text = format!(" RPC [ {}/5 ]➖", len); + color = Color32::LIGHT_GRAY; + incorrect_input = true; + } else if REGEXES.port.is_match(&self.rpc) { + text = format!(" RPC [ {}/5 ]✔", len); + color = Color32::from_rgb(100, 230, 100); + } else { + text = format!(" RPC [ {}/5 ]❌", len); + color = Color32::from_rgb(230, 50, 50); + incorrect_input = true; + } + ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color))); + ui.text_edit_singleline(&mut self.rpc).on_hover_text(P2POOL_RPC_PORT); + self.rpc.truncate(5); + }); + ui.horizontal(|ui| { + let text; + let color; + let len = self.zmq.len(); + if self.zmq.is_empty() { + text = format!(" ZMQ [ {}/5 ]➖", len); + color = Color32::LIGHT_GRAY; + incorrect_input = true; + } else if REGEXES.port.is_match(&self.zmq) { + text = format!(" ZMQ [ {}/5 ]✔", len); + color = Color32::from_rgb(100, 230, 100); + } else { + text = format!(" ZMQ [ {}/5 ]❌", len); + color = Color32::from_rgb(230, 50, 50); + incorrect_input = true; + } + ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color))); + ui.text_edit_singleline(&mut self.zmq).on_hover_text(P2POOL_ZMQ_PORT); + self.zmq.truncate(5); + }); + }); + + ui.vertical(|ui| { + let width = ui.available_width(); + ui.add_space(1.0); + // [Manual node selection] + ui.spacing_mut().slider_width = width - 8.0; + ui.spacing_mut().icon_width = width / 25.0; + // [Ping List] + debug!("P2Pool Tab | Rendering [Node List]"); + let text = RichText::new(format!("{}. {}", self.selected_index+1, self.selected_name)); + ComboBox::from_id_source("manual_nodes").selected_text(text).width(width).show_ui(ui, |ui| { + for (n, (name, node)) in node_vec.iter().enumerate() { + let text = RichText::new(format!("{}. {}\n IP: {}\n RPC: {}\n ZMQ: {}", n+1, name, node.ip, node.rpc, node.zmq)); + if ui.add(SelectableLabel::new(self.selected_name == *name, text)).clicked() { + self.selected_index = n; + let node = node.clone(); + self.selected_name = name.clone(); + self.selected_ip = node.ip.clone(); + self.selected_rpc = node.rpc.clone(); + self.selected_zmq = node.zmq.clone(); + self.name = name.clone(); + self.ip = node.ip; + self.rpc = node.rpc; + self.zmq = node.zmq; + } + } + }); + // [Add/Save] + let node_vec_len = node_vec.len(); + let mut exists = false; + let mut save_diff = true; + let mut existing_index = 0; + for (name, node) in node_vec.iter() { + if *name == self.name { + exists = true; + if self.ip == node.ip && self.rpc == node.rpc && self.zmq == node.zmq { + save_diff = false; + } + break + } + existing_index += 1; + } + ui.horizontal(|ui| { + let text = if exists { LIST_SAVE } else { LIST_ADD }; + let text = format!("{}\n Currently selected node: {}. {}\n Current amount of nodes: {}/1000", text, self.selected_index+1, self.selected_name, node_vec_len); + // If the node already exists, show [Save] and mutate the already existing node + if exists { + ui.set_enabled(!incorrect_input && save_diff); + if ui.add_sized([width, text_edit], Button::new("Save")).on_hover_text(text).clicked() { + let node = Node { + ip: self.ip.clone(), + rpc: self.rpc.clone(), + zmq: self.zmq.clone(), + }; + node_vec[existing_index].1 = node; + self.selected_index = existing_index; + self.selected_ip = self.ip.clone(); + self.selected_rpc = self.rpc.clone(); + self.selected_zmq = self.zmq.clone(); + info!("Node | S | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", existing_index+1, self.name, self.ip, self.rpc, self.zmq); + } + // Else, add to the list + } else { + ui.set_enabled(!incorrect_input && node_vec_len < 1000); + if ui.add_sized([width, text_edit], Button::new("Add")).on_hover_text(text).clicked() { + let node = Node { + ip: self.ip.clone(), + rpc: self.rpc.clone(), + zmq: self.zmq.clone(), + }; + node_vec.push((self.name.clone(), node)); + self.selected_index = node_vec_len; + self.selected_name = self.name.clone(); + self.selected_ip = self.ip.clone(); + self.selected_rpc = self.rpc.clone(); + self.selected_zmq = self.zmq.clone(); + info!("Node | A | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", node_vec_len, self.name, self.ip, self.rpc, self.zmq); + } + } + }); + // [Delete] + ui.horizontal(|ui| { + ui.set_enabled(node_vec_len > 1); + let text = format!("{}\n Currently selected node: {}. {}\n Current amount of nodes: {}/1000", LIST_DELETE, self.selected_index+1, self.selected_name, node_vec_len); + if ui.add_sized([width, text_edit], Button::new("Delete")).on_hover_text(text).clicked() { + let new_name; + let new_node; + match self.selected_index { + 0 => { + new_name = node_vec[1].0.clone(); + new_node = node_vec[1].1.clone(); + node_vec.remove(0); + } + _ => { + node_vec.remove(self.selected_index); + self.selected_index -= 1; + new_name = node_vec[self.selected_index].0.clone(); + new_node = node_vec[self.selected_index].1.clone(); + } + }; + self.selected_name = new_name.clone(); + self.selected_ip = new_node.ip.clone(); + self.selected_rpc = new_node.rpc.clone(); + self.selected_zmq = new_node.zmq.clone(); + self.name = new_name; + self.ip = new_node.ip; + self.rpc = new_node.rpc; + self.zmq = new_node.zmq; + info!("Node | D | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", self.selected_index, self.selected_name, self.selected_ip, self.selected_rpc, self.selected_zmq); + } + }); + ui.horizontal(|ui| { + ui.set_enabled(!self.name.is_empty() || !self.ip.is_empty() || !self.rpc.is_empty() || !self.zmq.is_empty()); + if ui.add_sized([width, text_edit], Button::new("Clear")).on_hover_text(LIST_CLEAR).clicked() { + self.name.clear(); + self.ip.clear(); + self.rpc.clear(); + self.zmq.clear(); + } + }); + }); + }); + }); + ui.add_space(5.0); + + debug!("P2Pool Tab | Rendering [Main/Mini/Peers/Log] elements"); + // [Main/Mini] + ui.horizontal(|ui| { + let height = height / 4.0; + ui.group(|ui| { + ui.horizontal(|ui| { + let width = (width / 4.0) - SPACE; + let height = height + 6.0; + if ui + .add_sized( + [width, height], + SelectableLabel::new(!self.mini, "P2Pool Main"), + ) + .on_hover_text(P2POOL_MAIN) + .clicked() + { + self.mini = false; + } + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.mini, "P2Pool Mini"), + ) + .on_hover_text(P2POOL_MINI) + .clicked() + { + self.mini = true; + } + }) + }); + // [Out/In Peers] + [Log Level] + ui.group(|ui| { + ui.vertical(|ui| { + let text = (ui.available_width() / 10.0) - SPACE; + let width = (text * 8.0) - SPACE; + let height = height / 3.0; + ui.style_mut().spacing.slider_width = width / 1.1; + ui.style_mut().spacing.interact_size.y = height; + ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into())); + ui.horizontal(|ui| { + ui.add_sized([text, height], Label::new("Out peers [10-450]:")); + ui.add_sized([width, height], Slider::new(&mut self.out_peers, 10..=450)) + .on_hover_text(P2POOL_OUT); + ui.add_space(ui.available_width() - 4.0); + }); + ui.horizontal(|ui| { + ui.add_sized([text, height], Label::new(" In peers [10-450]:")); + ui.add_sized([width, height], Slider::new(&mut self.in_peers, 10..=450)) + .on_hover_text(P2POOL_IN); + }); + ui.horizontal(|ui| { + ui.add_sized([text, height], Label::new(" Log level [0-6]:")); + ui.add_sized([width, height], Slider::new(&mut self.log_level, 0..=6)) + .on_hover_text(P2POOL_LOG); + }); + }) + }); + }); + + debug!("P2Pool Tab | Rendering Backup host button"); + ui.group(|ui| { + let width = width - SPACE; + let height = ui.available_height() / 3.0; + // [Backup host] + ui.add_sized( + [width, height], + Checkbox::new(&mut self.backup_host, "Backup host"), + ) + .on_hover_text(P2POOL_BACKUP_HOST_ADVANCED); + }); + } +} diff --git a/src/app/panels/middle/p2pool/mod.rs b/src/app/panels/middle/p2pool/mod.rs new file mode 100644 index 0000000..bef4b1f --- /dev/null +++ b/src/app/panels/middle/p2pool/mod.rs @@ -0,0 +1,166 @@ +use crate::disk::node::Node; +use crate::disk::state::{P2pool, State}; +use crate::helper::p2pool::PubP2poolApi; +// Gupax - GUI Uniting P2Pool And XMRig +// +// Copyright (c) 2022-2023 hinto-janai +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +use crate::{components::node::*, constants::*, helper::*, macros::*, utils::regex::Regexes}; +use egui::{Color32, Label, RichText, TextEdit, TextStyle::*}; +use log::*; + +use std::sync::{Arc, Mutex}; + +mod advanced; +mod simple; + +impl P2pool { + #[inline(always)] // called once + #[allow(clippy::too_many_arguments)] + pub fn show( + &mut self, + node_vec: &mut Vec<(String, Node)>, + _og: &Arc>, + ping: &Arc>, + process: &Arc>, + api: &Arc>, + buffer: &mut String, + width: f32, + height: f32, + _ctx: &egui::Context, + ui: &mut egui::Ui, + ) { + let text_edit = height / 25.0; + //---------------------------------------------------------------------------------------------------- [Simple] Console + debug!("P2Pool Tab | Rendering [Console]"); + ui.group(|ui| { + if self.simple { + let height = height / 2.8; + let width = width - SPACE; + 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 lock!(api).output.as_str()), + ); + }); + }); + //---------------------------------------------------------------------------------------------------- [Advanced] Console + } else { + let height = height / 2.8; + let width = width - SPACE; + 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 lock!(api).output.as_str()), + ); + }); + }); + ui.separator(); + let response = ui + .add_sized( + [width, text_edit], + TextEdit::hint_text( + TextEdit::singleline(buffer), + r#"Type a command (e.g "help" or "status") and press Enter"#, + ), + ) + .on_hover_text(P2POOL_INPUT); + // If the user pressed enter, dump buffer contents into the process STDIN + if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { + response.request_focus(); // Get focus back + let buffer = std::mem::take(buffer); // Take buffer + let mut process = lock!(process); // Lock + if process.is_alive() { + process.input.push(buffer); + } // Push only if alive + } + } + }); + + //---------------------------------------------------------------------------------------------------- Args + if !self.simple { + debug!("P2Pool Tab | Rendering [Arguments]"); + ui.group(|ui| { + ui.horizontal(|ui| { + let width = (width / 10.0) - SPACE; + ui.add_sized([width, text_edit], Label::new("Command arguments:")); + ui.add_sized( + [ui.available_width(), text_edit], + TextEdit::hint_text( + TextEdit::singleline(&mut self.arguments), + r#"--wallet <...> --host <...>"#, + ), + ) + .on_hover_text(P2POOL_ARGUMENTS); + self.arguments.truncate(1024); + }) + }); + ui.set_enabled(self.arguments.is_empty()); + } + + //---------------------------------------------------------------------------------------------------- Address + debug!("P2Pool Tab | Rendering [Address]"); + ui.group(|ui| { + let width = width - SPACE; + ui.spacing_mut().text_edit_width = (width) - (SPACE * 3.0); + let text; + let color; + let len = format!("{:02}", self.address.len()); + if self.address.is_empty() { + text = format!("Monero Address [{}/95] ➖", len); + color = Color32::LIGHT_GRAY; + } else if Regexes::addr_ok(&self.address) { + text = format!("Monero Address [{}/95] ✔", len); + color = Color32::from_rgb(100, 230, 100); + } else { + text = format!("Monero Address [{}/95] ❌", len); + color = Color32::from_rgb(230, 50, 50); + } + ui.add_sized( + [width, text_edit], + Label::new(RichText::new(text).color(color)), + ); + ui.add_sized( + [width, text_edit], + TextEdit::hint_text(TextEdit::singleline(&mut self.address), "4..."), + ) + .on_hover_text(P2POOL_ADDRESS); + self.address.truncate(95); + }); + + let height = ui.available_height(); + if self.simple { + //---------------------------------------------------------------------------------------------------- Simple + self.simple(ui, width, height, ping); + //---------------------------------------------------------------------------------------------------- Advanced + } else { + self.advanced(ui, width, height, text_edit, node_vec); + } + } +} diff --git a/src/app/panels/middle/p2pool/simple.rs b/src/app/panels/middle/p2pool/simple.rs new file mode 100644 index 0000000..82d53a4 --- /dev/null +++ b/src/app/panels/middle/p2pool/simple.rs @@ -0,0 +1,191 @@ +use std::sync::Arc; +use std::sync::Mutex; + +use crate::app::panels::middle::Hyperlink; +use crate::app::panels::middle::ProgressBar; +use crate::app::panels::middle::Spinner; +use crate::components::node::format_ip_location; +use crate::components::node::format_ms; +use crate::components::node::Ping; +use crate::components::node::RemoteNode; +use crate::disk::state::P2pool; +use crate::utils::macros::lock; +use egui::Button; +use egui::Checkbox; + +use crate::constants::*; +use egui::{Color32, ComboBox, Label, RichText, Ui}; +use log::*; +impl P2pool { + pub(super) fn simple(&mut self, ui: &mut Ui, width: f32, height: f32, ping: &Arc>) { + // [Node] + let height = height / 6.5; + ui.spacing_mut().slider_width = width - 8.0; + ui.spacing_mut().icon_width = width / 25.0; + + // [Auto-select] if we haven't already. + // Using [Arc>] as an intermediary here + // saves me the hassle of wrapping [state: State] completely + // and [.lock().unwrap()]ing it everywhere. + // Two atomic bools = enough to represent this data + debug!("P2Pool Tab | Running [auto-select] check"); + if self.auto_select { + let mut ping = lock!(ping); + // If we haven't auto_selected yet, auto-select and turn it off + if ping.pinged && !ping.auto_selected { + self.node = ping.fastest.to_string(); + ping.auto_selected = true; + } + drop(ping); + } + + ui.vertical(|ui| { + ui.horizontal(|ui| { + debug!("P2Pool Tab | Rendering [Ping List]"); + // [Ping List] + let mut ms = 0; + let mut color = Color32::LIGHT_GRAY; + if lock!(ping).pinged { + for data in lock!(ping).nodes.iter() { + if data.ip == self.node { + ms = data.ms; + color = data.color; + break; + } + } + } + debug!("P2Pool Tab | Rendering [ComboBox] of Remote Nodes"); + let ip_location = format_ip_location(&self.node, false); + let text = RichText::new(format!(" ⏺ {}ms | {}", ms, ip_location)).color(color); + ComboBox::from_id_source("remote_nodes") + .selected_text(text) + .width(width) + .show_ui(ui, |ui| { + for data in lock!(ping).nodes.iter() { + let ms = format_ms(data.ms); + let ip_location = format_ip_location(data.ip, true); + let text = RichText::new(format!(" ⏺ {} | {}", ms, ip_location)) + .color(data.color); + ui.selectable_value(&mut self.node, data.ip.to_string(), text); + } + }); + }); + + ui.add_space(5.0); + + debug!("P2Pool Tab | Rendering [Select fastest ... Ping] buttons"); + ui.horizontal(|ui| { + let width = (width / 5.0) - 6.0; + // [Select random node] + if ui + .add_sized([width, height], Button::new("Select random node")) + .on_hover_text(P2POOL_SELECT_RANDOM) + .clicked() + { + self.node = RemoteNode::get_random(&self.node); + } + // [Select fastest node] + if ui + .add_sized([width, height], Button::new("Select fastest node")) + .on_hover_text(P2POOL_SELECT_FASTEST) + .clicked() + && lock!(ping).pinged + { + self.node = lock!(ping).fastest.to_string(); + } + // [Ping Button] + ui.add_enabled_ui(!lock!(ping).pinging, |ui| { + if ui + .add_sized([width, height], Button::new("Ping remote nodes")) + .on_hover_text(P2POOL_PING) + .clicked() + { + Ping::spawn_thread(ping); + } + }); + // [Last <-] + if ui + .add_sized([width, height], Button::new("⬅ Last")) + .on_hover_text(P2POOL_SELECT_LAST) + .clicked() + { + let ping = lock!(ping); + match ping.pinged { + true => self.node = RemoteNode::get_last_from_ping(&self.node, &ping.nodes), + false => self.node = RemoteNode::get_last(&self.node), + } + drop(ping); + } + // [Next ->] + if ui + .add_sized([width, height], Button::new("Next ➡")) + .on_hover_text(P2POOL_SELECT_NEXT) + .clicked() + { + let ping = lock!(ping); + match ping.pinged { + true => self.node = RemoteNode::get_next_from_ping(&self.node, &ping.nodes), + false => self.node = RemoteNode::get_next(&self.node), + } + drop(ping); + } + }); + + ui.vertical(|ui| { + let height = height / 2.0; + let pinging = lock!(ping).pinging; + ui.set_enabled(pinging); + let prog = lock!(ping).prog.round(); + let msg = RichText::new(format!("{} ... {}%", lock!(ping).msg, prog)); + let height = height / 1.25; + ui.add_space(5.0); + ui.add_sized([width, height], Label::new(msg)); + ui.add_space(5.0); + if pinging { + ui.add_sized([width, height], Spinner::new().size(height)); + } else { + ui.add_sized([width, height], Label::new("...")); + } + ui.add_sized([width, height], ProgressBar::new(prog.round() / 100.0)); + ui.add_space(5.0); + }); + }); + + debug!("P2Pool Tab | Rendering [Auto-*] buttons"); + ui.group(|ui| { + ui.horizontal(|ui| { + let width = (width / 3.0) - (SPACE * 1.75); + // [Auto-node] + ui.add_sized( + [width, height], + Checkbox::new(&mut self.auto_select, "Auto-select"), + ) + .on_hover_text(P2POOL_AUTO_SELECT); + ui.separator(); + // [Auto-node] + ui.add_sized( + [width, height], + Checkbox::new(&mut self.auto_ping, "Auto-ping"), + ) + .on_hover_text(P2POOL_AUTO_NODE); + ui.separator(); + // [Backup host] + ui.add_sized( + [width, height], + Checkbox::new(&mut self.backup_host, "Backup host"), + ) + .on_hover_text(P2POOL_BACKUP_HOST_SIMPLE); + }) + }); + + debug!("P2Pool Tab | Rendering warning text"); + ui.add_sized( + [width, height / 2.0], + Hyperlink::from_label_and_url( + "WARNING: It is recommended to run/use your own Monero Node (hover for details)", + "https://github.com/hinto-janai/gupax#running-a-local-monero-node", + ), + ) + .on_hover_text(P2POOL_COMMUNITY_NODE_WARNING); + } +} diff --git a/src/app/panels/middle/status/benchmarks.rs b/src/app/panels/middle/status/benchmarks.rs new file mode 100644 index 0000000..622eae7 --- /dev/null +++ b/src/app/panels/middle/status/benchmarks.rs @@ -0,0 +1,221 @@ +use std::sync::{Arc, Mutex}; + +use crate::{ + app::Benchmark, disk::state::Status, helper::xmrig::PubXmrigApi, utils::human::HumanNumber, +}; +use egui::{Hyperlink, ProgressBar, Spinner}; + +use crate::utils::macros::lock; + +use crate::constants::*; +use egui::{Label, RichText}; +use log::*; +impl Status { + pub(super) fn benchmarks( + &mut self, + width: f32, + height: f32, + ui: &mut egui::Ui, + benchmarks: &[Benchmark], + xmrig_alive: bool, + xmrig_api: &Arc>, + ) { + debug!("Status Tab | Rendering [Benchmarks]"); + let text = height / 20.0; + let double = text * 2.0; + let log = height / 3.0; + + // [0], The user's CPU (most likely). + let cpu = &benchmarks[0]; + ui.horizontal(|ui| { + let width = (width / 2.0) - (SPACE * 1.666); + let min_height = log; + ui.group(|ui| { + ui.vertical(|ui| { + ui.set_min_height(min_height); + ui.add_sized( + [width, text], + Label::new(RichText::new("Your CPU").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_CPU); + ui.add_sized([width, text], Label::new(cpu.cpu.as_str())); + ui.add_sized( + [width, text], + Label::new(RichText::new("Total Benchmarks").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_BENCHMARKS); + ui.add_sized([width, text], Label::new(format!("{}", cpu.benchmarks))); + ui.add_sized( + [width, text], + Label::new(RichText::new("Rank").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_RANK); + ui.add_sized( + [width, text], + Label::new(format!("{}/{}", cpu.rank, &benchmarks.len())), + ); + }) + }); + ui.group(|ui| { + ui.vertical(|ui| { + ui.set_min_height(min_height); + ui.add_sized( + [width, text], + Label::new(RichText::new("High Hashrate").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_HIGH); + ui.add_sized( + [width, text], + Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.high))), + ); + ui.add_sized( + [width, text], + Label::new(RichText::new("Average Hashrate").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_AVERAGE); + ui.add_sized( + [width, text], + Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.average))), + ); + ui.add_sized( + [width, text], + Label::new(RichText::new("Low Hashrate").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_LOW); + ui.add_sized( + [width, text], + Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.low))), + ); + }) + }) + }); + + // User's CPU hashrate comparison (if XMRig is alive). + ui.scope(|ui| { + if xmrig_alive { + let api = lock!(xmrig_api); + let percent = (api.hashrate_raw / cpu.high) * 100.0; + let human = HumanNumber::to_percent(percent); + if percent > 100.0 { + ui.add_sized( + [width, double], + Label::new(format!( + "Your CPU's is faster than the highest benchmark! It is [{}] faster @ {}!", + human, api.hashrate + )), + ); + ui.add_sized([width, text], ProgressBar::new(1.0)); + } else if api.hashrate_raw == 0.0 { + ui.add_sized([width, text], Label::new("Measuring hashrate...")); + ui.add_sized([width, text], Spinner::new().size(text)); + ui.add_sized([width, text], ProgressBar::new(0.0)); + } else { + ui.add_sized( + [width, double], + Label::new(format!( + "Your CPU's hashrate is [{}] of the highest benchmark @ {}", + human, api.hashrate + )), + ); + ui.add_sized([width, text], ProgressBar::new(percent / 100.0)); + } + } else { + ui.set_enabled(xmrig_alive); + ui.add_sized( + [width, double], + Label::new("XMRig is offline. Hashrate cannot be determined."), + ); + ui.add_sized([width, text], ProgressBar::new(0.0)); + } + }); + + // Comparison + ui.group(|ui| { + ui.add_sized( + [width, text], + Hyperlink::from_label_and_url("Other CPUs", "https://xmrig.com/benchmark"), + ) + .on_hover_text(STATUS_SUBMENU_OTHER_CPUS); + }); + + egui::ScrollArea::both() + .scroll_bar_visibility( + egui::containers::scroll_area::ScrollBarVisibility::AlwaysVisible, + ) + .max_width(width) + .max_height(height) + .auto_shrink([false; 2]) + .show_viewport(ui, |ui, _| { + let width = width / 20.0; + let (cpu, bar, high, average, low, rank, bench) = ( + width * 10.0, + width * 3.0, + width * 2.0, + width * 2.0, + width * 2.0, + width, + width * 2.0, + ); + ui.group(|ui| { + ui.horizontal(|ui| { + ui.add_sized([cpu, double], Label::new("CPU")) + .on_hover_text(STATUS_SUBMENU_OTHER_CPU); + ui.separator(); + ui.add_sized([bar, double], Label::new("Relative")) + .on_hover_text(STATUS_SUBMENU_OTHER_RELATIVE); + ui.separator(); + ui.add_sized([high, double], Label::new("High")) + .on_hover_text(STATUS_SUBMENU_OTHER_HIGH); + ui.separator(); + ui.add_sized([average, double], Label::new("Average")) + .on_hover_text(STATUS_SUBMENU_OTHER_AVERAGE); + ui.separator(); + ui.add_sized([low, double], Label::new("Low")) + .on_hover_text(STATUS_SUBMENU_OTHER_LOW); + ui.separator(); + ui.add_sized([rank, double], Label::new("Rank")) + .on_hover_text(STATUS_SUBMENU_OTHER_RANK); + ui.separator(); + ui.add_sized([bench, double], Label::new("Benchmarks")) + .on_hover_text(STATUS_SUBMENU_OTHER_BENCHMARKS); + }); + }); + + for benchmark in benchmarks[1..].iter() { + ui.group(|ui| { + ui.horizontal(|ui| { + ui.add_sized([cpu, text], Label::new(benchmark.cpu.as_str())); + ui.separator(); + ui.add_sized([bar, text], ProgressBar::new(benchmark.percent / 100.0)) + .on_hover_text(HumanNumber::to_percent(benchmark.percent).as_str()); + ui.separator(); + ui.add_sized( + [high, text], + Label::new(HumanNumber::to_hashrate(benchmark.high).as_str()), + ); + ui.separator(); + ui.add_sized( + [average, text], + Label::new(HumanNumber::to_hashrate(benchmark.average).as_str()), + ); + ui.separator(); + ui.add_sized( + [low, text], + Label::new(HumanNumber::to_hashrate(benchmark.low).as_str()), + ); + ui.separator(); + ui.add_sized( + [rank, text], + Label::new(HumanNumber::from_u16(benchmark.rank).as_str()), + ); + ui.separator(); + ui.add_sized( + [bench, text], + Label::new(HumanNumber::from_u16(benchmark.benchmarks).as_str()), + ); + }) + }); + } + }); + } +} diff --git a/src/app/panels/middle/status/mod.rs b/src/app/panels/middle/status/mod.rs new file mode 100644 index 0000000..8c4c02b --- /dev/null +++ b/src/app/panels/middle/status/mod.rs @@ -0,0 +1,83 @@ +// Gupax - GUI Uniting P2Pool And XMRig +// +// Copyright (c) 2022-2023 hinto-janai +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{ + app::Benchmark, + disk::{gupax_p2pool_api::GupaxP2poolApi, state::Status, status::*}, + helper::{ + p2pool::{ImgP2pool, PubP2poolApi}, + xmrig::{ImgXmrig, PubXmrigApi}, + Sys, + }, +}; +use std::sync::{Arc, Mutex}; + +mod benchmarks; +mod p2pool; +mod processes; + +impl Status { + #[inline(always)] // called once + #[allow(clippy::too_many_arguments)] + pub fn show( + &mut self, + sys: &Arc>, + p2pool_api: &Arc>, + xmrig_api: &Arc>, + p2pool_img: &Arc>, + xmrig_img: &Arc>, + p2pool_alive: bool, + xmrig_alive: bool, + max_threads: usize, + gupax_p2pool_api: &Arc>, + benchmarks: &[Benchmark], + width: f32, + height: f32, + _ctx: &egui::Context, + ui: &mut egui::Ui, + ) { + //---------------------------------------------------------------------------------------------------- [Processes] + if self.submenu == Submenu::Processes { + self.processes( + sys, + width, + height, + ui, + p2pool_alive, + p2pool_api, + xmrig_alive, + xmrig_api, + p2pool_img, + xmrig_img, + max_threads, + ); + //---------------------------------------------------------------------------------------------------- [P2Pool] + } else if self.submenu == Submenu::P2pool { + self.p2pool( + width, + height, + ui, + gupax_p2pool_api, + p2pool_alive, + p2pool_api, + ); + //---------------------------------------------------------------------------------------------------- [Benchmarks] + } else if self.submenu == Submenu::Benchmarks { + self.benchmarks(width, height, ui, benchmarks, xmrig_alive, xmrig_api) + } + } +} diff --git a/src/app/panels/middle/status/p2pool.rs b/src/app/panels/middle/status/p2pool.rs new file mode 100644 index 0000000..4cffe85 --- /dev/null +++ b/src/app/panels/middle/status/p2pool.rs @@ -0,0 +1,427 @@ +use std::sync::{Arc, Mutex}; + +use egui::{Label, RichText, SelectableLabel, Slider, TextEdit}; + +use crate::{ + disk::{ + gupax_p2pool_api::GupaxP2poolApi, + state::Status, + status::{Hash, PayoutView}, + }, + helper::p2pool::PubP2poolApi, + utils::{constants::*, human::HumanNumber, macros::lock}, +}; + +impl Status { + pub fn p2pool( + &mut self, + width: f32, + height: f32, + ui: &mut egui::Ui, + gupax_p2pool_api: &Arc>, + p2pool_alive: bool, + p2pool_api: &Arc>, + ) { + let api = lock!(gupax_p2pool_api); + let text = height / 25.0; + let log = height / 2.8; + // Payout Text + PayoutView buttons + ui.group(|ui| { + ui.horizontal(|ui| { + let width = (width / 3.0) - (SPACE * 4.0); + ui.add_sized( + [width, text], + Label::new( + RichText::new(format!("Total Payouts: {}", api.payout)) + .underline() + .color(LIGHT_GRAY), + ), + ) + .on_hover_text(STATUS_SUBMENU_PAYOUT); + ui.separator(); + ui.add_sized( + [width, text], + Label::new( + RichText::new(format!("Total XMR: {}", api.xmr)) + .underline() + .color(LIGHT_GRAY), + ), + ) + .on_hover_text(STATUS_SUBMENU_XMR); + let width = width / 4.0; + ui.separator(); + if ui + .add_sized( + [width, text], + SelectableLabel::new(self.payout_view == PayoutView::Latest, "Latest"), + ) + .on_hover_text(STATUS_SUBMENU_LATEST) + .clicked() + { + self.payout_view = PayoutView::Latest; + } + ui.separator(); + if ui + .add_sized( + [width, text], + SelectableLabel::new(self.payout_view == PayoutView::Oldest, "Oldest"), + ) + .on_hover_text(STATUS_SUBMENU_OLDEST) + .clicked() + { + self.payout_view = PayoutView::Oldest; + } + ui.separator(); + if ui + .add_sized( + [width, text], + SelectableLabel::new(self.payout_view == PayoutView::Biggest, "Biggest"), + ) + .on_hover_text(STATUS_SUBMENU_BIGGEST) + .clicked() + { + self.payout_view = PayoutView::Biggest; + } + ui.separator(); + if ui + .add_sized( + [width, text], + SelectableLabel::new(self.payout_view == PayoutView::Smallest, "Smallest"), + ) + .on_hover_text(STATUS_SUBMENU_SMALLEST) + .clicked() + { + self.payout_view = PayoutView::Smallest; + } + }); + ui.separator(); + // Actual logs + egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| { + egui::ScrollArea::vertical() + .stick_to_bottom(self.payout_view == PayoutView::Oldest) + .max_width(width) + .max_height(log) + .auto_shrink([false; 2]) + .show_viewport(ui, |ui, _| { + ui.style_mut().override_text_style = + Some(egui::TextStyle::Name("MonospaceLarge".into())); + match self.payout_view { + PayoutView::Latest => ui.add_sized( + [width, log], + TextEdit::multiline(&mut api.log_rev.as_str()), + ), + PayoutView::Oldest => ui.add_sized( + [width, log], + TextEdit::multiline(&mut api.log.as_str()), + ), + PayoutView::Biggest => ui.add_sized( + [width, log], + TextEdit::multiline(&mut api.payout_high.as_str()), + ), + PayoutView::Smallest => ui.add_sized( + [width, log], + TextEdit::multiline(&mut api.payout_low.as_str()), + ), + }; + }); + }); + }); + drop(api); + // Payout/Share Calculator + let button = (width / 20.0) - (SPACE * 1.666); + ui.group(|ui| { + ui.horizontal(|ui| { + ui.set_min_width(width - SPACE); + if ui + .add_sized( + [button * 2.0, text], + SelectableLabel::new(!self.manual_hash, "Automatic"), + ) + .on_hover_text(STATUS_SUBMENU_AUTOMATIC) + .clicked() + { + self.manual_hash = false; + } + ui.separator(); + if ui + .add_sized( + [button * 2.0, text], + SelectableLabel::new(self.manual_hash, "Manual"), + ) + .on_hover_text(STATUS_SUBMENU_MANUAL) + .clicked() + { + self.manual_hash = true; + } + ui.separator(); + ui.set_enabled(self.manual_hash); + if ui + .add_sized( + [button, text], + SelectableLabel::new(self.hash_metric == Hash::Hash, "Hash"), + ) + .on_hover_text(STATUS_SUBMENU_HASH) + .clicked() + { + self.hash_metric = Hash::Hash; + } + ui.separator(); + if ui + .add_sized( + [button, text], + SelectableLabel::new(self.hash_metric == Hash::Kilo, "Kilo"), + ) + .on_hover_text(STATUS_SUBMENU_KILO) + .clicked() + { + self.hash_metric = Hash::Kilo; + } + ui.separator(); + if ui + .add_sized( + [button, text], + SelectableLabel::new(self.hash_metric == Hash::Mega, "Mega"), + ) + .on_hover_text(STATUS_SUBMENU_MEGA) + .clicked() + { + self.hash_metric = Hash::Mega; + } + ui.separator(); + if ui + .add_sized( + [button, text], + SelectableLabel::new(self.hash_metric == Hash::Giga, "Giga"), + ) + .on_hover_text(STATUS_SUBMENU_GIGA) + .clicked() + { + self.hash_metric = Hash::Giga; + } + ui.separator(); + ui.spacing_mut().slider_width = button * 11.5; + ui.add_sized( + [button * 14.0, text], + Slider::new(&mut self.hashrate, 1.0..=1_000.0), + ); + }) + }); + // Actual stats + ui.set_enabled(p2pool_alive); + let text = height / 25.0; + let width = (width / 3.0) - (SPACE * 1.666); + let min_height = ui.available_height() / 1.3; + let api = lock!(p2pool_api); + ui.horizontal(|ui| { + ui.group(|ui| { + ui.vertical(|ui| { + ui.set_min_height(min_height); + ui.add_sized( + [width, text], + Label::new(RichText::new("Monero Difficulty").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_MONERO_DIFFICULTY); + ui.add_sized([width, text], Label::new(api.monero_difficulty.as_str())); + ui.add_sized( + [width, text], + Label::new(RichText::new("Monero Hashrate").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_MONERO_HASHRATE); + ui.add_sized([width, text], Label::new(api.monero_hashrate.as_str())); + ui.add_sized( + [width, text], + Label::new(RichText::new("P2Pool Difficulty").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL_DIFFICULTY); + ui.add_sized([width, text], Label::new(api.p2pool_difficulty.as_str())); + ui.add_sized( + [width, text], + Label::new(RichText::new("P2Pool Hashrate").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL_HASHRATE); + ui.add_sized([width, text], Label::new(api.p2pool_hashrate.as_str())); + }) + }); + ui.group(|ui| { + ui.vertical(|ui| { + ui.set_min_height(min_height); + if self.manual_hash { + let hashrate = + Hash::convert_to_hash(self.hashrate, self.hash_metric) as u64; + let p2pool_share_mean = PubP2poolApi::calculate_share_or_block_time( + hashrate, + api.p2pool_difficulty_u64, + ); + let solo_block_mean = PubP2poolApi::calculate_share_or_block_time( + hashrate, + api.monero_difficulty_u64, + ); + ui.add_sized( + [width, text], + Label::new( + RichText::new("Manually Inputted Hashrate") + .underline() + .color(BONE), + ), + ); + ui.add_sized( + [width, text], + Label::new(format!("{} H/s", HumanNumber::from_u64(hashrate))), + ); + ui.add_sized( + [width, text], + Label::new(RichText::new("P2Pool Block Mean").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL_BLOCK_MEAN); + ui.add_sized([width, text], Label::new(api.p2pool_block_mean.to_string())); + ui.add_sized( + [width, text], + Label::new( + RichText::new("Your P2Pool Share Mean") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL_SHARE_MEAN); + ui.add_sized([width, text], Label::new(p2pool_share_mean.to_string())); + ui.add_sized( + [width, text], + Label::new( + RichText::new("Your Solo Block Mean") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_SUBMENU_SOLO_BLOCK_MEAN); + ui.add_sized([width, text], Label::new(solo_block_mean.to_string())); + } else { + ui.add_sized( + [width, text], + Label::new( + RichText::new("Your P2Pool Hashrate") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_HASHRATE); + ui.add_sized( + [width, text], + Label::new(format!("{} H/s", api.hashrate_1h)), + ); + ui.add_sized( + [width, text], + Label::new(RichText::new("P2Pool Block Mean").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL_BLOCK_MEAN); + ui.add_sized([width, text], Label::new(api.p2pool_block_mean.to_string())); + ui.add_sized( + [width, text], + Label::new( + RichText::new("Your P2Pool Share Mean") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL_SHARE_MEAN); + ui.add_sized([width, text], Label::new(api.p2pool_share_mean.to_string())); + ui.add_sized( + [width, text], + Label::new( + RichText::new("Your Solo Block Mean") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_SUBMENU_SOLO_BLOCK_MEAN); + ui.add_sized([width, text], Label::new(api.solo_block_mean.to_string())); + } + }) + }); + ui.group(|ui| { + ui.vertical(|ui| { + ui.set_min_height(min_height); + if self.manual_hash { + let hashrate = + Hash::convert_to_hash(self.hashrate, self.hash_metric) as u64; + let user_p2pool_percent = + PubP2poolApi::calculate_dominance(hashrate, api.p2pool_hashrate_u64); + let user_monero_percent = + PubP2poolApi::calculate_dominance(hashrate, api.monero_hashrate_u64); + ui.add_sized( + [width, text], + Label::new(RichText::new("P2Pool Miners").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL_MINERS); + ui.add_sized([width, text], Label::new(api.miners.as_str())); + ui.add_sized( + [width, text], + Label::new(RichText::new("P2Pool Dominance").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL_DOMINANCE); + ui.add_sized([width, text], Label::new(api.p2pool_percent.as_str())); + ui.add_sized( + [width, text], + Label::new( + RichText::new("Your P2Pool Dominance") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_DOMINANCE); + ui.add_sized([width, text], Label::new(user_p2pool_percent.as_str())); + ui.add_sized( + [width, text], + Label::new( + RichText::new("Your Monero Dominance") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_MONERO_DOMINANCE); + ui.add_sized([width, text], Label::new(user_monero_percent.as_str())); + } else { + ui.add_sized( + [width, text], + Label::new(RichText::new("P2Pool Miners").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL_MINERS); + ui.add_sized([width, text], Label::new(api.miners.as_str())); + ui.add_sized( + [width, text], + Label::new(RichText::new("P2Pool Dominance").underline().color(BONE)), + ) + .on_hover_text(STATUS_SUBMENU_P2POOL_DOMINANCE); + ui.add_sized([width, text], Label::new(api.p2pool_percent.as_str())); + ui.add_sized( + [width, text], + Label::new( + RichText::new("Your P2Pool Dominance") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_DOMINANCE); + ui.add_sized([width, text], Label::new(api.user_p2pool_percent.as_str())); + ui.add_sized( + [width, text], + Label::new( + RichText::new("Your Monero Dominance") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_SUBMENU_YOUR_MONERO_DOMINANCE); + ui.add_sized([width, text], Label::new(api.user_monero_percent.as_str())); + } + }) + }); + }); + // Tick bar + ui.add_sized( + [ui.available_width(), text], + Label::new(api.calculate_tick_bar()), + ) + .on_hover_text(STATUS_SUBMENU_PROGRESS_BAR); + drop(api); + } +} diff --git a/src/app/panels/middle/status/processes.rs b/src/app/panels/middle/status/processes.rs new file mode 100644 index 0000000..28b3350 --- /dev/null +++ b/src/app/panels/middle/status/processes.rs @@ -0,0 +1,303 @@ +use std::sync::{Arc, Mutex}; + +use crate::disk::state::Status; +use crate::helper::p2pool::{ImgP2pool, PubP2poolApi}; +use crate::helper::xmrig::{ImgXmrig, PubXmrigApi}; +use crate::helper::Sys; +use crate::utils::macros::lock; +use egui::TextStyle; + +use crate::constants::*; +use egui::{Label, RichText, TextStyle::*}; +use log::*; +impl Status { + #[allow(clippy::too_many_arguments)] + pub(super) fn processes( + &mut self, + sys: &Arc>, + width: f32, + height: f32, + ui: &mut egui::Ui, + p2pool_alive: bool, + p2pool_api: &Arc>, + xmrig_alive: bool, + xmrig_api: &Arc>, + p2pool_img: &Arc>, + xmrig_img: &Arc>, + max_threads: usize, + ) { + let width = (width / 3.0) - (SPACE * 1.666); + let min_height = height - SPACE; + let height = height / 25.0; + ui.horizontal(|ui| { + // [Gupax] + ui.group(|ui| { + ui.vertical(|ui| { + debug!("Status Tab | Rendering [Gupax]"); + ui.set_min_height(min_height); + ui.add_sized( + [width, height], + Label::new( + RichText::new("[Gupax]") + .color(LIGHT_GRAY) + .text_style(TextStyle::Name("MonospaceLarge".into())), + ), + ) + .on_hover_text("Gupax is online"); + let sys = lock!(sys); + ui.add_sized( + [width, height], + Label::new(RichText::new("Uptime").underline().color(BONE)), + ) + .on_hover_text(STATUS_GUPAX_UPTIME); + ui.add_sized([width, height], Label::new(sys.gupax_uptime.to_string())); + ui.add_sized( + [width, height], + Label::new(RichText::new("Gupax CPU").underline().color(BONE)), + ) + .on_hover_text(STATUS_GUPAX_CPU_USAGE); + ui.add_sized([width, height], Label::new(sys.gupax_cpu_usage.to_string())); + ui.add_sized( + [width, height], + Label::new(RichText::new("Gupax Memory").underline().color(BONE)), + ) + .on_hover_text(STATUS_GUPAX_MEMORY_USAGE); + ui.add_sized( + [width, height], + Label::new(sys.gupax_memory_used_mb.to_string()), + ); + ui.add_sized( + [width, height], + Label::new(RichText::new("System CPU").underline().color(BONE)), + ) + .on_hover_text(STATUS_GUPAX_SYSTEM_CPU_USAGE); + ui.add_sized( + [width, height], + Label::new(sys.system_cpu_usage.to_string()), + ); + ui.add_sized( + [width, height], + Label::new(RichText::new("System Memory").underline().color(BONE)), + ) + .on_hover_text(STATUS_GUPAX_SYSTEM_MEMORY); + ui.add_sized([width, height], Label::new(sys.system_memory.to_string())); + ui.add_sized( + [width, height], + Label::new(RichText::new("System CPU Model").underline().color(BONE)), + ) + .on_hover_text(STATUS_GUPAX_SYSTEM_CPU_MODEL); + ui.add_sized( + [width, height], + Label::new(sys.system_cpu_model.to_string()), + ); + drop(sys); + }) + }); + // [P2Pool] + ui.group(|ui| { + ui.vertical(|ui| { + debug!("Status Tab | Rendering [P2Pool]"); + ui.set_enabled(p2pool_alive); + ui.set_min_height(min_height); + ui.add_sized( + [width, height], + Label::new( + RichText::new("[P2Pool]") + .color(LIGHT_GRAY) + .text_style(TextStyle::Name("MonospaceLarge".into())), + ), + ) + .on_hover_text("P2Pool is online") + .on_disabled_hover_text("P2Pool is offline"); + ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into())); + let height = height / 1.4; + let api = lock!(p2pool_api); + ui.add_sized( + [width, height], + Label::new(RichText::new("Uptime").underline().color(BONE)), + ) + .on_hover_text(STATUS_P2POOL_UPTIME); + ui.add_sized([width, height], Label::new(format!("{}", api.uptime))); + ui.add_sized( + [width, height], + Label::new(RichText::new("Shares Found").underline().color(BONE)), + ) + .on_hover_text(STATUS_P2POOL_SHARES); + ui.add_sized([width, height], Label::new(format!("{}", api.shares_found))); + ui.add_sized( + [width, height], + Label::new(RichText::new("Payouts").underline().color(BONE)), + ) + .on_hover_text(STATUS_P2POOL_PAYOUTS); + ui.add_sized( + [width, height], + Label::new(format!("Total: {}", api.payouts)), + ); + ui.add_sized( + [width, height], + Label::new(format!( + "[{:.7}/hour]\n[{:.7}/day]\n[{:.7}/month]", + api.payouts_hour, api.payouts_day, api.payouts_month + )), + ); + ui.add_sized( + [width, height], + Label::new(RichText::new("XMR Mined").underline().color(BONE)), + ) + .on_hover_text(STATUS_P2POOL_XMR); + ui.add_sized( + [width, height], + Label::new(format!("Total: {:.13} XMR", api.xmr)), + ); + ui.add_sized( + [width, height], + Label::new(format!( + "[{:.7}/hour]\n[{:.7}/day]\n[{:.7}/month]", + api.xmr_hour, api.xmr_day, api.xmr_month + )), + ); + ui.add_sized( + [width, height], + Label::new( + RichText::new("Hashrate (15m/1h/24h)") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_P2POOL_HASHRATE); + ui.add_sized( + [width, height], + Label::new(format!( + "[{} H/s] [{} H/s] [{} H/s]", + api.hashrate_15m, api.hashrate_1h, api.hashrate_24h + )), + ); + ui.add_sized( + [width, height], + Label::new(RichText::new("Miners Connected").underline().color(BONE)), + ) + .on_hover_text(STATUS_P2POOL_CONNECTIONS); + ui.add_sized([width, height], Label::new(format!("{}", api.connections))); + ui.add_sized( + [width, height], + Label::new(RichText::new("Effort").underline().color(BONE)), + ) + .on_hover_text(STATUS_P2POOL_EFFORT); + ui.add_sized( + [width, height], + Label::new(format!( + "[Average: {}] [Current: {}]", + api.average_effort, api.current_effort + )), + ); + let img = lock!(p2pool_img); + ui.add_sized( + [width, height], + Label::new(RichText::new("Monero Node").underline().color(BONE)), + ) + .on_hover_text(STATUS_P2POOL_MONERO_NODE); + ui.add_sized( + [width, height], + Label::new(format!( + "[IP: {}]\n[RPC: {}] [ZMQ: {}]", + &img.host, &img.rpc, &img.zmq + )), + ); + ui.add_sized( + [width, height], + Label::new(RichText::new("Sidechain").underline().color(BONE)), + ) + .on_hover_text(STATUS_P2POOL_POOL); + ui.add_sized([width, height], Label::new(&img.mini)); + ui.add_sized( + [width, height], + Label::new(RichText::new("Address").underline().color(BONE)), + ) + .on_hover_text(STATUS_P2POOL_ADDRESS); + ui.add_sized([width, height], Label::new(&img.address)); + drop(img); + drop(api); + }) + }); + // [XMRig] + ui.group(|ui| { + ui.vertical(|ui| { + debug!("Status Tab | Rendering [XMRig]"); + ui.set_enabled(xmrig_alive); + ui.set_min_height(min_height); + ui.add_sized( + [width, height], + Label::new( + RichText::new("[XMRig]") + .color(LIGHT_GRAY) + .text_style(TextStyle::Name("MonospaceLarge".into())), + ), + ) + .on_hover_text("XMRig is online") + .on_disabled_hover_text("XMRig is offline"); + let api = lock!(xmrig_api); + ui.add_sized( + [width, height], + Label::new(RichText::new("Uptime").underline().color(BONE)), + ) + .on_hover_text(STATUS_XMRIG_UPTIME); + ui.add_sized([width, height], Label::new(format!("{}", api.uptime))); + ui.add_sized( + [width, height], + Label::new( + RichText::new("CPU Load (10s/60s/15m)") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_XMRIG_CPU); + ui.add_sized([width, height], Label::new(format!("{}", api.resources))); + ui.add_sized( + [width, height], + Label::new( + RichText::new("Hashrate (10s/60s/15m)") + .underline() + .color(BONE), + ), + ) + .on_hover_text(STATUS_XMRIG_HASHRATE); + ui.add_sized([width, height], Label::new(format!("{}", api.hashrate))); + ui.add_sized( + [width, height], + Label::new(RichText::new("Difficulty").underline().color(BONE)), + ) + .on_hover_text(STATUS_XMRIG_DIFFICULTY); + ui.add_sized([width, height], Label::new(format!("{}", api.diff))); + ui.add_sized( + [width, height], + Label::new(RichText::new("Shares").underline().color(BONE)), + ) + .on_hover_text(STATUS_XMRIG_SHARES); + ui.add_sized( + [width, height], + Label::new(format!( + "[Accepted: {}] [Rejected: {}]", + api.accepted, api.rejected + )), + ); + ui.add_sized( + [width, height], + Label::new(RichText::new("Pool").underline().color(BONE)), + ) + .on_hover_text(STATUS_XMRIG_POOL); + ui.add_sized([width, height], Label::new(&lock!(xmrig_img).url)); + ui.add_sized( + [width, height], + Label::new(RichText::new("Threads").underline().color(BONE)), + ) + .on_hover_text(STATUS_XMRIG_THREADS); + ui.add_sized( + [width, height], + Label::new(format!("{}/{}", &lock!(xmrig_img).threads, max_threads)), + ); + drop(api); + }) + }); + }); + } +} diff --git a/src/xmrig.rs b/src/app/panels/middle/xmrig.rs similarity index 98% rename from src/xmrig.rs rename to src/app/panels/middle/xmrig.rs index 00de655..b655711 100644 --- a/src/xmrig.rs +++ b/src/app/panels/middle/xmrig.rs @@ -15,8 +15,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use crate::disk::pool::Pool; +use crate::disk::state::Xmrig; +use crate::helper::xmrig::PubXmrigApi; +use crate::helper::Process; use crate::regex::REGEXES; -use crate::{constants::*, disk::*, macros::*, Process, PubXmrigApi, Regexes}; +use crate::utils::regex::Regexes; +use crate::{constants::*, macros::*}; use egui::{ Button, Checkbox, ComboBox, Label, RichText, SelectableLabel, Slider, TextEdit, TextStyle::*, }; @@ -24,7 +29,7 @@ use log::*; use std::sync::{Arc, Mutex}; -impl crate::disk::Xmrig { +impl Xmrig { #[inline(always)] // called once #[allow(clippy::too_many_arguments)] pub fn show( diff --git a/src/xvb.rs b/src/app/panels/middle/xvb.rs similarity index 96% rename from src/xvb.rs rename to src/app/panels/middle/xvb.rs index 030837d..5244627 100644 --- a/src/xvb.rs +++ b/src/app/panels/middle/xvb.rs @@ -2,7 +2,7 @@ use egui::{Hyperlink, Image}; use crate::constants::{BYTES_XVB, SPACE}; -impl crate::disk::Xvb { +impl crate::disk::state::Xvb { #[inline(always)] // called once pub fn show(width: f32, height: f32, _ctx: &egui::Context, ui: &mut egui::Ui) { let website_height = height / 10.0; diff --git a/src/app/panels/mod.rs b/src/app/panels/mod.rs new file mode 100644 index 0000000..2984de5 --- /dev/null +++ b/src/app/panels/mod.rs @@ -0,0 +1,4 @@ +pub mod bottom; +pub mod middle; +pub mod quit_error; +pub mod top; diff --git a/src/app/panels/quit_error.rs b/src/app/panels/quit_error.rs new file mode 100644 index 0000000..cef526f --- /dev/null +++ b/src/app/panels/quit_error.rs @@ -0,0 +1,362 @@ +use std::process::exit; + +use crate::app::keys::KeyPressed; +use crate::disk::node::Node; +use crate::disk::state::State; +use crate::utils::constants::*; +use crate::utils::errors::ErrorState; +use crate::utils::ferris::*; +use crate::utils::macros::{arc_mut, flip, lock, lock2}; +use crate::utils::resets::{reset_nodes, reset_state}; +use crate::utils::sudo::SudoState; +use egui::TextStyle::Name; +use egui::*; + +impl crate::app::App { + pub(in crate::app) fn quit_error_panel( + &mut self, + ctx: &egui::Context, + p2pool_is_alive: bool, + xmrig_is_alive: bool, + key: &KeyPressed, + ) { + CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered(|ui| { + // Set width/height/font + let width = self.width; + let height = self.height / 4.0; + ui.style_mut().override_text_style = Some(Name("MonospaceLarge".into())); + + // Display ferris + use crate::utils::errors::ErrorButtons; + use crate::utils::errors::ErrorButtons::*; + use crate::utils::errors::ErrorFerris; + use crate::utils::errors::ErrorFerris::*; + let ferris = match self.error_state.ferris { + Happy => Image::from_bytes("bytes://happy.png", FERRIS_HAPPY), + Cute => Image::from_bytes("bytes://cute.png", FERRIS_CUTE), + Oops => Image::from_bytes("bytes://oops.png", FERRIS_OOPS), + Error => Image::from_bytes("bytes://error.png", FERRIS_ERROR), + Panic => Image::from_bytes("bytes://panic.png", FERRIS_PANIC), + ErrorFerris::Sudo => Image::from_bytes("bytes://panic.png", FERRIS_SUDO), + }; + + match self.error_state.buttons { + ErrorButtons::Debug => ui.add_sized( + [width, height / 4.0], + Label::new("--- Debug Info ---\n\nPress [ESC] to quit"), + ), + _ => ui.add_sized(Vec2::new(width, height), ferris), + }; + + // Error/Quit screen + match self.error_state.buttons { + StayQuit => { + let mut text = "".to_string(); + if *lock2!(self.update, updating) { + text = format!( + "{}\nUpdate is in progress...! Quitting may cause file corruption!", + text + ); + } + if p2pool_is_alive { + text = format!("{}\nP2Pool is online...!", text); + } + if xmrig_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)) + } + ResetState => { + ui.add_sized( + [width, height], + Label::new(format!( + "--- Gupax has encountered an error! ---\n{}", + &self.error_state.msg + )), + ); + ui.add_sized( + [width, height], + Label::new("Reset Gupax state? (Your settings)"), + ) + } + ResetNode => { + ui.add_sized( + [width, height], + Label::new(format!( + "--- Gupax has encountered an error! ---\n{}", + &self.error_state.msg + )), + ); + ui.add_sized([width, height], Label::new("Reset the manual node list?")) + } + ErrorButtons::Sudo => { + let text = format!( + "Why does XMRig need admin privilege?\n{}", + XMRIG_ADMIN_REASON + ); + let height = height / 4.0; + ui.add_sized( + [width, height], + Label::new(format!( + "--- Gupax needs sudo/admin privilege for XMRig! ---\n{}", + &self.error_state.msg + )), + ); + ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into())); + ui.add_sized([width / 2.0, height], Label::new(text)); + ui.add_sized( + [width, height], + Hyperlink::from_label_and_url( + "Click here for more info.", + "https://xmrig.com/docs/miner/randomx-optimization-guide", + ), + ) + } + Debug => { + egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| { + let width = ui.available_width(); + let height = ui.available_height(); + egui::ScrollArea::vertical() + .max_width(width) + .max_height(height) + .auto_shrink([false; 2]) + .show_viewport(ui, |ui, _| { + ui.add_sized( + [width - 20.0, height], + TextEdit::multiline(&mut self.error_state.msg.as_str()), + ); + }); + }); + ui.label("") + } + _ => { + match self.error_state.ferris { + Panic => ui.add_sized( + [width, height], + Label::new("--- Gupax has encountered an unrecoverable error! ---"), + ), + Happy => ui.add_sized([width, height], Label::new("--- Success! ---")), + _ => ui.add_sized( + [width, height], + Label::new("--- Gupax has encountered an error! ---"), + ), + }; + let height = height / 2.0; + // Show GitHub rant link for Windows admin problems. + if cfg!(windows) && self.error_state.buttons == ErrorButtons::WindowsAdmin { + ui.add_sized([width, height], Hyperlink::from_label_and_url( + "[Why does Gupax need to be Admin? (on Windows)]", + "https://github.com/hinto-janai/gupax/tree/main/src#why-does-gupax-need-to-be-admin-on-windows" + )); + ui.add_sized([width, height], Label::new(&self.error_state.msg)) + } else { + ui.add_sized([width, height], Label::new(&self.error_state.msg)) + } + } + }; + let height = ui.available_height(); + + match self.error_state.buttons { + YesNo => { + if ui + .add_sized([width, height / 2.0], Button::new("Yes")) + .clicked() + { + self.error_state.reset() + } + // If [Esc] was pressed, assume [No] + if key.is_esc() + || ui + .add_sized([width, height / 2.0], Button::new("No")) + .clicked() + { + exit(0); + } + } + StayQuit => { + // If [Esc] was pressed, assume [Stay] + if key.is_esc() + || ui + .add_sized([width, height / 2.0], Button::new("Stay")) + .clicked() + { + self.error_state = ErrorState::new(); + } + if ui + .add_sized([width, height / 2.0], Button::new("Quit")) + .clicked() + { + if self.state.gupax.save_before_quit { + self.save_before_quit(); + } + exit(0); + } + } + // This code handles the [state.toml/node.toml] resetting, [panic!]'ing if it errors once more + // Another error after this either means an IO error or permission error, which Gupax can't fix. + // [Yes/No] buttons + ResetState => { + if ui + .add_sized([width, height / 2.0], Button::new("Yes")) + .clicked() + { + match reset_state(&self.state_path) { + Ok(_) => match State::get(&self.state_path) { + Ok(s) => { + self.state = s; + self.og = arc_mut!(self.state.clone()); + self.error_state.set( + "State read OK", + ErrorFerris::Happy, + ErrorButtons::Okay, + ); + } + Err(e) => self.error_state.set( + format!("State read fail: {}", e), + ErrorFerris::Panic, + ErrorButtons::Quit, + ), + }, + Err(e) => self.error_state.set( + format!("State reset fail: {}", e), + ErrorFerris::Panic, + ErrorButtons::Quit, + ), + }; + } + if key.is_esc() + || ui + .add_sized([width, height / 2.0], Button::new("No")) + .clicked() + { + self.error_state.reset() + } + } + ResetNode => { + if ui + .add_sized([width, height / 2.0], Button::new("Yes")) + .clicked() + { + match reset_nodes(&self.node_path) { + Ok(_) => match Node::get(&self.node_path) { + Ok(s) => { + self.node_vec = s; + self.og_node_vec = self.node_vec.clone(); + self.error_state.set( + "Node read OK", + ErrorFerris::Happy, + ErrorButtons::Okay, + ); + } + Err(e) => self.error_state.set( + format!("Node read fail: {}", e), + ErrorFerris::Panic, + ErrorButtons::Quit, + ), + }, + Err(e) => self.error_state.set( + format!("Node reset fail: {}", e), + ErrorFerris::Panic, + ErrorButtons::Quit, + ), + }; + } + if key.is_esc() + || ui + .add_sized([width, height / 2.0], Button::new("No")) + .clicked() + { + self.error_state.reset() + } + } + ErrorButtons::Sudo => { + let sudo_width = width / 10.0; + let height = ui.available_height() / 4.0; + let mut sudo = lock!(self.sudo); + let hide = sudo.hide; + if sudo.testing { + ui.add_sized([width, height], Spinner::new().size(height)); + ui.set_enabled(false); + } else { + ui.add_sized([width, height], Label::new(&sudo.msg)); + } + ui.add_space(height); + let height = ui.available_height() / 5.0; + // Password input box with a hider. + ui.horizontal(|ui| { + let response = ui.add_sized( + [sudo_width * 8.0, height], + TextEdit::hint_text( + TextEdit::singleline(&mut sudo.pass).password(hide), + PASSWORD_TEXT, + ), + ); + let box_width = (ui.available_width() / 2.0) - 5.0; + if (response.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter))) + || ui + .add_sized([box_width, height], Button::new("Enter")) + .on_hover_text(PASSWORD_ENTER) + .clicked() + { + response.request_focus(); + if !sudo.testing { + SudoState::test_sudo( + self.sudo.clone(), + &self.helper.clone(), + &self.state.xmrig, + &self.state.gupax.absolute_xmrig_path, + ); + } + } + let color = if hide { BLACK } else { BRIGHT_YELLOW }; + if ui + .add_sized( + [box_width, height], + Button::new(RichText::new("👁").color(color)), + ) + .on_hover_text(PASSWORD_HIDE) + .clicked() + { + flip!(sudo.hide); + } + }); + if (key.is_esc() && !sudo.testing) + || ui + .add_sized([width, height * 4.0], Button::new("Leave")) + .on_hover_text(PASSWORD_LEAVE) + .clicked() + { + self.error_state.reset(); + }; + // If [test_sudo()] finished, reset error state. + if sudo.success { + self.error_state.reset(); + } + } + crate::app::ErrorButtons::Okay | crate::app::ErrorButtons::WindowsAdmin => { + if key.is_esc() + || ui.add_sized([width, height], Button::new("Okay")).clicked() + { + self.error_state.reset(); + } + } + Debug => { + if key.is_esc() { + self.error_state.reset(); + } + } + Quit => { + if ui.add_sized([width, height], Button::new("Quit")).clicked() { + exit(1); + } + } + } + }) + }); + } +} diff --git a/src/app/panels/top.rs b/src/app/panels/top.rs new file mode 100644 index 0000000..a38144f --- /dev/null +++ b/src/app/panels/top.rs @@ -0,0 +1,79 @@ +use egui::TextStyle::Name; +use egui::{SelectableLabel, TopBottomPanel}; +use log::debug; + +use crate::{app::Tab, utils::constants::SPACE}; + +impl crate::app::App { + pub fn top_panel(&mut self, ctx: &egui::Context) { + debug!("App | Rendering TOP tabs"); + TopBottomPanel::top("top").show(ctx, |ui| { + let width = (self.width - (SPACE * 11.0)) / 6.0; + let height = self.height / 15.0; + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.style_mut().override_text_style = Some(Name("Tab".into())); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::About, "About"), + ) + .clicked() + { + self.tab = Tab::About; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::Status, "Status"), + ) + .clicked() + { + self.tab = Tab::Status; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::Gupax, "Gupax"), + ) + .clicked() + { + self.tab = Tab::Gupax; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::P2pool, "P2Pool"), + ) + .clicked() + { + self.tab = Tab::P2pool; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::Xmrig, "XMRig"), + ) + .clicked() + { + self.tab = Tab::Xmrig; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::Xvb, "XvB"), + ) + .clicked() + { + self.tab = Tab::Xvb; + } + }); + ui.add_space(4.0); + }); + } +} diff --git a/src/app/quit.rs b/src/app/quit.rs new file mode 100644 index 0000000..6bd590d --- /dev/null +++ b/src/app/quit.rs @@ -0,0 +1,48 @@ +use log::info; + +use crate::errors::ErrorButtons; +use crate::errors::ErrorFerris; + +use super::App; + +impl App { + pub(super) fn quit(&mut self, ctx: &egui::Context) { + // If closing. + // Used to be `eframe::App::on_close_event(&mut self) -> bool`. + let close_signal = ctx.input(|input| { + use egui::viewport::ViewportCommand; + + if !input.viewport().close_requested() { + return None; + } + info!("quit"); + if self.state.gupax.ask_before_quit { + // If we're already on the [ask_before_quit] screen and + // the user tried to exit again, exit. + if self.error_state.quit_twice { + if self.state.gupax.save_before_quit { + self.save_before_quit(); + } + return Some(ViewportCommand::Close); + } + // Else, set the error + self.error_state + .set("", ErrorFerris::Oops, ErrorButtons::StayQuit); + self.error_state.quit_twice = true; + Some(ViewportCommand::CancelClose) + // Else, just quit. + } else { + if self.state.gupax.save_before_quit { + self.save_before_quit(); + } + Some(ViewportCommand::Close) + } + }); + // This will either: + // 1. Cancel a close signal + // 2. Close the program + if let Some(cmd) = close_signal { + ctx.send_viewport_cmd(cmd); + } + } +} diff --git a/src/app/resize.rs b/src/app/resize.rs new file mode 100644 index 0000000..ec71453 --- /dev/null +++ b/src/app/resize.rs @@ -0,0 +1,77 @@ +use crate::inits::init_text_styles; +use crate::SPACE; +use egui::Color32; +use log::debug; +use log::info; + +use super::App; + +impl App { + pub fn resize(&mut self, ctx: &egui::Context) { + // This resizes fonts/buttons/etc globally depending on the width. + // This is separate from the [self.width != available_width] logic above + // because placing [init_text_styles()] above would mean calling it 60x a second + // while the user was readjusting the frame. It's a pretty heavy operation and looks + // buggy when calling it that many times. Looking for a [must_resize] in addition to + // checking if the user is hovering over the app means that we only have call it once. + debug!("App | Checking if we need to resize"); + if self.must_resize && ctx.is_pointer_over_area() { + self.resizing = true; + self.must_resize = false; + } + // This (ab)uses [Area] and [TextEdit] to overlay a full black layer over whatever UI we had before. + // It incrementally becomes more opaque until [self.alpha] >= 250, when we just switch to pure black (no alpha). + // When black, we're safe to [init_text_styles()], and then incrementally go transparent, until we remove the layer. + if self.resizing { + egui::Area::new("resize_layer") + .order(egui::Order::Foreground) + .anchor(egui::Align2::CENTER_CENTER, (0.0, 0.0)) + .show(ctx, |ui| { + if self.alpha < 250 { + egui::Frame::none() + .fill(Color32::from_rgba_premultiplied(0, 0, 0, self.alpha)) + .show(ui, |ui| { + ui.add_sized( + [ui.available_width() + SPACE, ui.available_height() + SPACE], + egui::TextEdit::multiline(&mut ""), + ); + }); + ctx.request_repaint(); + self.alpha += 10; + } else { + egui::Frame::none() + .fill(Color32::from_rgb(0, 0, 0)) + .show(ui, |ui| { + ui.add_sized( + [ui.available_width() + SPACE, ui.available_height() + SPACE], + egui::TextEdit::multiline(&mut ""), + ); + }); + ctx.request_repaint(); + info!( + "App | Resizing frame to match new internal resolution: [{}x{}]", + self.width, self.height + ); + init_text_styles(ctx, self.width, self.state.gupax.selected_scale); + self.resizing = false; + } + }); + } else if self.alpha != 0 { + egui::Area::new("resize_layer") + .order(egui::Order::Foreground) + .anchor(egui::Align2::CENTER_CENTER, (0.0, 0.0)) + .show(ctx, |ui| { + egui::Frame::none() + .fill(Color32::from_rgba_premultiplied(0, 0, 0, self.alpha)) + .show(ui, |ui| { + ui.add_sized( + [ui.available_width() + SPACE, ui.available_height() + SPACE], + egui::TextEdit::multiline(&mut ""), + ); + }) + }); + self.alpha -= 10; + ctx.request_repaint(); + } + } +} diff --git a/src/components/gupax.rs b/src/components/gupax.rs new file mode 100644 index 0000000..aa0c3dc --- /dev/null +++ b/src/components/gupax.rs @@ -0,0 +1,112 @@ +// Gupax - GUI Uniting P2Pool And XMRig +// +// Copyright (c) 2022-2023 hinto-janai +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{ + disk::state::*, + utils::macros::{arc_mut, lock}, +}; +use log::*; +use serde::{Deserialize, Serialize}; +use std::{ + sync::{Arc, Mutex}, + thread, +}; + +//---------------------------------------------------------------------------------------------------- FileWindow +// Struct for writing/reading the path state. +// The opened file picker is started in a new +// thread so main() needs to be in sync. +pub struct FileWindow { + pub thread: bool, // Is there already a FileWindow thread? + pub picked_p2pool: bool, // Did the user pick a path for p2pool? + pub picked_xmrig: bool, // Did the user pick a path for xmrig? + pub p2pool_path: String, // The picked p2pool path + pub xmrig_path: String, // The picked p2pool path +} + +impl FileWindow { + pub fn new() -> Arc> { + arc_mut!(Self { + thread: false, + picked_p2pool: false, + picked_xmrig: false, + p2pool_path: String::new(), + xmrig_path: String::new(), + }) + } +} + +#[derive(Debug, Clone)] +pub enum FileType { + P2pool, + Xmrig, +} + +//---------------------------------------------------------------------------------------------------- Ratio Lock +// Enum for the lock ratio in the advanced tab. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] +pub enum Ratio { + Width, + Height, + None, +} + +//---------------------------------------------------------------------------------------------------- Gupax +impl Gupax { + // Checks if a path is a valid path to a file. + pub fn path_is_file(path: &str) -> bool { + let path = path.to_string(); + match crate::disk::into_absolute_path(path) { + Ok(path) => path.is_file(), + _ => false, + } + } + + #[cold] + #[inline(never)] + pub fn spawn_file_window_thread(file_window: &Arc>, file_type: FileType) { + use FileType::*; + let name = match file_type { + P2pool => "P2Pool", + Xmrig => "XMRig", + }; + let file_window = file_window.clone(); + lock!(file_window).thread = true; + thread::spawn(move || { + match rfd::FileDialog::new() + .set_title(format!("Select {} Binary for Gupax", name)) + .pick_file() + { + Some(path) => { + info!("Gupax | Path selected for {} ... {}", name, path.display()); + match file_type { + P2pool => { + lock!(file_window).p2pool_path = path.display().to_string(); + lock!(file_window).picked_p2pool = true; + } + Xmrig => { + lock!(file_window).xmrig_path = path.display().to_string(); + lock!(file_window).picked_xmrig = true; + } + }; + } + None => info!("Gupax | No path selected for {}", name), + }; + lock!(file_window).thread = false; + }); + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..cb94ae2 --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,3 @@ +pub mod gupax; +pub mod node; +pub mod update; diff --git a/src/node.rs b/src/components/node.rs similarity index 97% rename from src/node.rs rename to src/components/node.rs index d950abb..64db6db 100644 --- a/src/node.rs +++ b/src/components/node.rs @@ -15,6 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use crate::components::update::Pkg; use crate::{constants::*, macros::*}; use egui::Color32; use hyper::{client::HttpConnector, Body, Client, Request}; @@ -409,7 +410,7 @@ impl Ping { let client: Client = Client::builder().build(HttpConnector::new()); // Random User Agent - let rand_user_agent = crate::Pkg::get_user_agent(); + let rand_user_agent = Pkg::get_user_agent(); // Handle vector let mut handles = Vec::with_capacity(REMOTE_NODE_LENGTH); let node_vec = arc_mut!(Vec::with_capacity(REMOTE_NODE_LENGTH)); @@ -506,13 +507,17 @@ impl Ping { lock!(node_vec).push(NodeData { ip, ms, color }); } } - -//---------------------------------------------------------------------------------------------------- TESTS +//---------------------------------------------------------------------------------------------------- NODE #[cfg(test)] mod test { + use crate::components::node::{ + format_ip, REMOTE_NODES, REMOTE_NODE_LENGTH, REMOTE_NODE_MAX_CHARS, + }; + use crate::components::update::Pkg; + #[test] fn validate_node_ips() { - for (ip, location, rpc, zmq) in crate::REMOTE_NODES { + for (ip, location, rpc, zmq) in REMOTE_NODES { assert!(ip.len() < 255); assert!(ip.is_ascii()); assert!(!location.is_empty()); @@ -524,8 +529,8 @@ mod test { #[test] fn spacing() { - for (ip, _, _, _) in crate::REMOTE_NODES { - assert!(crate::format_ip(ip).len() <= crate::REMOTE_NODE_MAX_CHARS); + for (ip, _, _, _) in REMOTE_NODES { + assert!(format_ip(ip).len() <= REMOTE_NODE_MAX_CHARS); } } @@ -534,7 +539,6 @@ mod test { #[tokio::test] #[ignore] async fn full_ping() { - use crate::{REMOTE_NODES, REMOTE_NODE_LENGTH}; use hyper::{client::HttpConnector, Client, Request}; use serde::{Deserialize, Serialize}; @@ -548,7 +552,7 @@ mod test { let client: Client = Client::builder().build(HttpConnector::new()); // Random User Agent - let rand_user_agent = crate::Pkg::get_user_agent(); + let rand_user_agent = Pkg::get_user_agent(); // Only fail this test if >50% of nodes fail. const HALF_REMOTE_NODES: usize = REMOTE_NODE_LENGTH / 2; diff --git a/src/update.rs b/src/components/update.rs similarity index 99% rename from src/update.rs rename to src/components/update.rs index 22e18a7..fbce211 100644 --- a/src/update.rs +++ b/src/components/update.rs @@ -24,9 +24,17 @@ // b. auto-update at startup //---------------------------------------------------------------------------------------------------- Imports +use crate::components::update::Name::*; use crate::{ - constants::GUPAX_VERSION, disk::*, macros::*, update::Name::*, ErrorButtons, ErrorFerris, - ErrorState, Restart, + app::Restart, + constants::GUPAX_VERSION, + disk::{ + state::{State, Version}, + *, + }, + macros::*, + miscs::get_exe_dir, + utils::errors::{ErrorButtons, ErrorFerris, ErrorState}, }; use anyhow::{anyhow, Error}; use arti_client::TorClient; @@ -310,7 +318,7 @@ impl Update { .take(10) .map(char::from) .collect(); - let base = crate::get_exe_dir()?; + let base = get_exe_dir()?; #[cfg(target_os = "windows")] let tmp_dir = format!("{}{}{}{}", base, r"\gupax_update_", rand_string, r"\"); #[cfg(target_family = "unix")] @@ -365,7 +373,7 @@ impl Update { // code only needs to be edited once, here. pub fn spawn_thread( og: &Arc>, - gupax: &crate::disk::Gupax, + gupax: &crate::disk::state::Gupax, state_path: &Path, update: &Arc>, error_state: &mut ErrorState, diff --git a/src/disk.rs b/src/disk.rs deleted file mode 100644 index 38e981f..0000000 --- a/src/disk.rs +++ /dev/null @@ -1,1653 +0,0 @@ -// Gupax - GUI Uniting P2Pool And XMRig -// -// Copyright (c) 2022-2023 hinto-janai -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -// This handles reading/writing the disk files: -// - [state.toml] -> [App] state -// - [nodes.toml] -> [Manual Nodes] list -// The TOML format is used. This struct hierarchy -// directly translates into the TOML parser: -// State/ -// ├─ Gupax/ -// │ ├─ ... -// ├─ P2pool/ -// │ ├─ ... -// ├─ Xmrig/ -// │ ├─ ... -// ├─ Version/ -// ├─ ... - -use crate::{constants::*, gupax::Ratio, human::*, macros::*, xmr::*, Tab}; -use figment::providers::{Format, Toml}; -use figment::Figment; -use log::*; -use serde::{Deserialize, Serialize}; -#[cfg(target_family = "unix")] -use std::os::unix::fs::PermissionsExt; -use std::path::Path; -use std::{ - fmt::Display, - fmt::Write, - fs, - path::PathBuf, - result::Result, - sync::{Arc, Mutex}, -}; - -//---------------------------------------------------------------------------------------------------- Const -// State file -const ERROR: &str = "Disk error"; -const PATH_ERROR: &str = "PATH for state directory could not be not found"; - -#[cfg(target_os = "windows")] -const DIRECTORY: &str = r#"Gupax\"#; -#[cfg(target_os = "macos")] -const DIRECTORY: &str = "Gupax/"; -#[cfg(target_os = "linux")] -const DIRECTORY: &str = "gupax/"; - -// File names -pub const STATE_TOML: &str = "state.toml"; -pub const NODE_TOML: &str = "node.toml"; -pub const POOL_TOML: &str = "pool.toml"; - -// P2Pool API -// Lives within the Gupax OS data directory. -// ~/.local/share/gupax/p2pool/ -// ├─ payout_log // Raw log lines of payouts received -// ├─ payout // Single [u64] representing total payouts -// ├─ xmr // Single [u64] representing total XMR mined in atomic units -#[cfg(target_os = "windows")] -pub const GUPAX_P2POOL_API_DIRECTORY: &str = r"p2pool\"; -#[cfg(target_family = "unix")] -pub const GUPAX_P2POOL_API_DIRECTORY: &str = "p2pool/"; -pub const GUPAX_P2POOL_API_LOG: &str = "log"; -pub const GUPAX_P2POOL_API_PAYOUT: &str = "payout"; -pub const GUPAX_P2POOL_API_XMR: &str = "xmr"; -pub const GUPAX_P2POOL_API_FILE_ARRAY: [&str; 3] = [ - GUPAX_P2POOL_API_LOG, - GUPAX_P2POOL_API_PAYOUT, - GUPAX_P2POOL_API_XMR, -]; - -#[cfg(target_os = "windows")] -pub const DEFAULT_P2POOL_PATH: &str = r"P2Pool\p2pool.exe"; -#[cfg(target_os = "macos")] -pub const DEFAULT_P2POOL_PATH: &str = "p2pool/p2pool"; -#[cfg(target_os = "windows")] -pub const DEFAULT_XMRIG_PATH: &str = r"XMRig\xmrig.exe"; -#[cfg(target_os = "macos")] -pub const DEFAULT_XMRIG_PATH: &str = "xmrig/xmrig"; - -// Default to [/usr/bin/] for Linux distro builds. -#[cfg(target_os = "linux")] -#[cfg(not(feature = "distro"))] -pub const DEFAULT_P2POOL_PATH: &str = "p2pool/p2pool"; -#[cfg(target_os = "linux")] -#[cfg(not(feature = "distro"))] -pub const DEFAULT_XMRIG_PATH: &str = "xmrig/xmrig"; -#[cfg(target_os = "linux")] -#[cfg(feature = "distro")] -pub const DEFAULT_P2POOL_PATH: &str = "/usr/bin/p2pool"; -#[cfg(target_os = "linux")] -#[cfg(feature = "distro")] -pub const DEFAULT_XMRIG_PATH: &str = "/usr/bin/xmrig"; - -//---------------------------------------------------------------------------------------------------- General functions for all [File]'s -// get_file_path() | Return absolute path to OS data path + filename -// read_to_string() | Convert the file at a given path into a [String] -// create_new() | Write a default TOML Struct into the appropriate file (in OS data path) -// into_absolute_path() | Convert relative -> absolute path - -pub fn get_gupax_data_path() -> Result { - // Get OS data folder - // Linux | $XDG_DATA_HOME or $HOME/.local/share/gupax | /home/alice/.local/state/gupax - // macOS | $HOME/Library/Application Support/Gupax | /Users/Alice/Library/Application Support/Gupax - // Windows | {FOLDERID_RoamingAppData}\Gupax | C:\Users\Alice\AppData\Roaming\Gupax - match dirs::data_dir() { - Some(mut path) => { - path.push(DIRECTORY); - info!("OS | Data path ... {}", path.display()); - create_gupax_dir(&path)?; - let mut gupax_p2pool_dir = path.clone(); - gupax_p2pool_dir.push(GUPAX_P2POOL_API_DIRECTORY); - create_gupax_p2pool_dir(&gupax_p2pool_dir)?; - Ok(path) - } - None => { - error!("OS | Data path ... FAIL"); - Err(TomlError::Path(PATH_ERROR.to_string())) - } - } -} - -pub fn set_unix_750_perms(path: &PathBuf) -> Result<(), TomlError> { - #[cfg(target_os = "windows")] - return Ok(()); - #[cfg(target_family = "unix")] - match fs::set_permissions(path, fs::Permissions::from_mode(0o750)) { - Ok(_) => { - info!( - "OS | Unix 750 permissions on path [{}] ... OK", - path.display() - ); - Ok(()) - } - Err(e) => { - error!( - "OS | Unix 750 permissions on path [{}] ... FAIL ... {}", - path.display(), - e - ); - Err(TomlError::Io(e)) - } - } -} - -pub fn set_unix_660_perms(path: &PathBuf) -> Result<(), TomlError> { - #[cfg(target_os = "windows")] - return Ok(()); - #[cfg(target_family = "unix")] - match fs::set_permissions(path, fs::Permissions::from_mode(0o660)) { - Ok(_) => { - info!( - "OS | Unix 660 permissions on path [{}] ... OK", - path.display() - ); - Ok(()) - } - Err(e) => { - error!( - "OS | Unix 660 permissions on path [{}] ... FAIL ... {}", - path.display(), - e - ); - Err(TomlError::Io(e)) - } - } -} - -pub fn get_gupax_p2pool_path(os_data_path: &Path) -> PathBuf { - let mut gupax_p2pool_dir = os_data_path.to_path_buf(); - gupax_p2pool_dir.push(GUPAX_P2POOL_API_DIRECTORY); - gupax_p2pool_dir -} - -pub fn create_gupax_dir(path: &PathBuf) -> Result<(), TomlError> { - // Create Gupax directory - match fs::create_dir_all(path) { - Ok(_) => info!("OS | Create data path ... OK"), - Err(e) => { - error!("OS | Create data path ... FAIL ... {}", e); - return Err(TomlError::Io(e)); - } - } - set_unix_750_perms(path) -} - -pub fn create_gupax_p2pool_dir(path: &PathBuf) -> Result<(), TomlError> { - // Create Gupax directory - match fs::create_dir_all(path) { - Ok(_) => { - info!( - "OS | Create Gupax-P2Pool API path [{}] ... OK", - path.display() - ); - Ok(()) - } - Err(e) => { - error!( - "OS | Create Gupax-P2Pool API path [{}] ... FAIL ... {}", - path.display(), - e - ); - Err(TomlError::Io(e)) - } - } -} - -// Convert a [File] path to a [String] -pub fn read_to_string(file: File, path: &PathBuf) -> Result { - match fs::read_to_string(path) { - Ok(string) => { - info!("{:?} | Read ... OK", file); - Ok(string) - } - Err(err) => { - warn!("{:?} | Read ... FAIL", file); - Err(TomlError::Io(err)) - } - } -} - -// Write str to console with [info!] surrounded by "---" -pub fn print_dash(toml: &str) { - info!("{}", HORIZONTAL); - for i in toml.lines() { - info!("{}", i); - } - info!("{}", HORIZONTAL); -} - -// Turn relative paths into absolute paths -pub fn into_absolute_path(path: String) -> Result { - let path = PathBuf::from(path); - if path.is_relative() { - let mut dir = std::env::current_exe()?; - dir.pop(); - dir.push(path); - Ok(dir) - } else { - Ok(path) - } -} - -//---------------------------------------------------------------------------------------------------- [State] Impl -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -impl State { - pub fn new() -> Self { - let max_threads = benri::threads!(); - let current_threads = if max_threads == 1 { 1 } else { max_threads / 2 }; - Self { - status: Status::default(), - gupax: Gupax::default(), - p2pool: P2pool::default(), - xmrig: Xmrig::with_threads(max_threads, current_threads), - xvb: Xvb::default(), - version: arc_mut!(Version::default()), - } - } - - pub fn update_absolute_path(&mut self) -> Result<(), TomlError> { - self.gupax.absolute_p2pool_path = into_absolute_path(self.gupax.p2pool_path.clone())?; - self.gupax.absolute_xmrig_path = into_absolute_path(self.gupax.xmrig_path.clone())?; - Ok(()) - } - - // Convert [&str] to [State] - pub fn from_str(string: &str) -> Result { - match toml::de::from_str(string) { - Ok(state) => { - info!("State | Parse ... OK"); - print_dash(string); - Ok(state) - } - Err(err) => { - warn!("State | String -> State ... FAIL ... {}", err); - Err(TomlError::Deserialize(err)) - } - } - } - - // Convert [State] to [String] - pub fn to_string(&self) -> Result { - match toml::ser::to_string(self) { - Ok(s) => Ok(s), - Err(e) => { - error!("State | Couldn't serialize default file: {}", e); - Err(TomlError::Serialize(e)) - } - } - } - - // Combination of multiple functions: - // 1. Attempt to read file from path into [String] - // |_ Create a default file if not found - // 2. Deserialize [String] into a proper [Struct] - // |_ Attempt to merge if deserialization fails - pub fn get(path: &PathBuf) -> Result { - // Read - let file = File::State; - let string = match read_to_string(file, path) { - Ok(string) => string, - // Create - _ => { - Self::create_new(path)?; - match read_to_string(file, path) { - Ok(s) => s, - Err(e) => return Err(e), - } - } - }; - // Deserialize, attempt merge if failed - match Self::from_str(&string) { - Ok(s) => Ok(s), - Err(_) => { - warn!("State | Attempting merge..."); - match Self::merge(&string) { - Ok(mut new) => { - Self::save(&mut new, path)?; - Ok(new) - } - Err(e) => Err(e), - } - } - } - } - - // Completely overwrite current [state.toml] - // with a new default version, and return [Self]. - pub fn create_new(path: &PathBuf) -> Result { - info!("State | Creating new default..."); - let new = Self::new(); - let string = Self::to_string(&new)?; - fs::write(path, string)?; - info!("State | Write ... OK"); - Ok(new) - } - - // Save [State] onto disk file [gupax.toml] - pub fn save(&mut self, path: &PathBuf) -> Result<(), TomlError> { - info!("State | Saving to disk..."); - // Convert path to absolute - self.gupax.absolute_p2pool_path = into_absolute_path(self.gupax.p2pool_path.clone())?; - self.gupax.absolute_xmrig_path = into_absolute_path(self.gupax.xmrig_path.clone())?; - let string = match toml::ser::to_string(&self) { - Ok(string) => { - info!("State | Parse ... OK"); - print_dash(&string); - string - } - Err(err) => { - error!("State | Couldn't parse TOML into string ... FAIL"); - return Err(TomlError::Serialize(err)); - } - }; - match fs::write(path, string) { - Ok(_) => { - info!("State | Save ... OK"); - Ok(()) - } - Err(err) => { - error!("State | Couldn't overwrite TOML file ... FAIL"); - Err(TomlError::Io(err)) - } - } - } - - // Take [String] as input, merge it with whatever the current [default] is, - // leaving behind old keys+values and updating [default] with old valid ones. - pub fn merge(old: &str) -> Result { - let default = toml::ser::to_string(&Self::new()).unwrap(); - let new: Self = match Figment::from(Toml::string(&default)) - .merge(Toml::string(old)) - .extract() - { - Ok(new) => { - info!("State | TOML merge ... OK"); - new - } - Err(err) => { - error!("State | Couldn't merge default + old TOML"); - return Err(TomlError::Merge(err)); - } - }; - Ok(new) - } -} - -//---------------------------------------------------------------------------------------------------- [Node] Impl -impl Node { - pub fn localhost() -> Self { - Self { - ip: "localhost".to_string(), - rpc: "18081".to_string(), - zmq: "18083".to_string(), - } - } - - pub fn new_vec() -> Vec<(String, Self)> { - vec![("Local Monero Node".to_string(), Self::localhost())] - } - - pub fn new_tuple() -> (String, Self) { - ("Local Monero Node".to_string(), Self::localhost()) - } - - // Convert [String] to [Node] Vec - pub fn from_str_to_vec(string: &str) -> Result, TomlError> { - let nodes: toml::map::Map = match toml::de::from_str(string) { - Ok(map) => { - info!("Node | Parse ... OK"); - map - } - Err(err) => { - error!("Node | String parse ... FAIL ... {}", err); - return Err(TomlError::Deserialize(err)); - } - }; - let size = nodes.keys().len(); - let mut vec = Vec::with_capacity(size); - for (key, values) in nodes.iter() { - let ip = match values.get("ip") { - Some(ip) => match ip.as_str() { - Some(ip) => ip.to_string(), - None => { - error!("Node | [None] at [ip] parse"); - return Err(TomlError::Parse("[None] at [ip] parse")); - } - }, - None => { - error!("Node | [None] at [ip] parse"); - return Err(TomlError::Parse("[None] at [ip] parse")); - } - }; - let rpc = match values.get("rpc") { - Some(rpc) => match rpc.as_str() { - Some(rpc) => rpc.to_string(), - None => { - error!("Node | [None] at [rpc] parse"); - return Err(TomlError::Parse("[None] at [rpc] parse")); - } - }, - None => { - error!("Node | [None] at [rpc] parse"); - return Err(TomlError::Parse("[None] at [rpc] parse")); - } - }; - let zmq = match values.get("zmq") { - Some(zmq) => match zmq.as_str() { - Some(zmq) => zmq.to_string(), - None => { - error!("Node | [None] at [zmq] parse"); - return Err(TomlError::Parse("[None] at [zmq] parse")); - } - }, - None => { - error!("Node | [None] at [zmq] parse"); - return Err(TomlError::Parse("[None] at [zmq] parse")); - } - }; - let node = Node { ip, rpc, zmq }; - vec.push((key.clone(), node)); - } - Ok(vec) - } - - // Convert [Vec<(String, Self)>] into [String] - // that can be written as a proper TOML file - pub fn to_string(vec: &[(String, Self)]) -> Result { - let mut toml = String::new(); - for (key, value) in vec.iter() { - write!( - toml, - "[\'{}\']\nip = {:#?}\nrpc = {:#?}\nzmq = {:#?}\n\n", - key, value.ip, value.rpc, value.zmq, - )?; - } - Ok(toml) - } - - // Combination of multiple functions: - // 1. Attempt to read file from path into [String] - // |_ Create a default file if not found - // 2. Deserialize [String] into a proper [Struct] - // |_ Attempt to merge if deserialization fails - pub fn get(path: &PathBuf) -> Result, TomlError> { - // Read - let file = File::Node; - let string = match read_to_string(file, path) { - Ok(string) => string, - // Create - _ => { - Self::create_new(path)?; - read_to_string(file, path)? - } - }; - // Deserialize, attempt merge if failed - Self::from_str_to_vec(&string) - } - - // Completely overwrite current [node.toml] - // with a new default version, and return [Vec]. - pub fn create_new(path: &PathBuf) -> Result, TomlError> { - info!("Node | Creating new default..."); - let new = Self::new_vec(); - let string = Self::to_string(&Self::new_vec())?; - fs::write(path, string)?; - info!("Node | Write ... OK"); - Ok(new) - } - - // Save [Node] onto disk file [node.toml] - pub fn save(vec: &[(String, Self)], path: &PathBuf) -> Result<(), TomlError> { - info!("Node | Saving to disk ... [{}]", path.display()); - let string = Self::to_string(vec)?; - match fs::write(path, string) { - Ok(_) => { - info!("Node | Save ... OK"); - Ok(()) - } - Err(err) => { - error!("Node | Couldn't overwrite file"); - Err(TomlError::Io(err)) - } - } - } - - // pub fn merge(old: &String) -> Result { - // info!("Node | Starting TOML merge..."); - // let default = match toml::ser::to_string(&Self::new()) { - // Ok(string) => { info!("Node | Default TOML parse ... OK"); string }, - // Err(err) => { error!("Node | Couldn't parse default TOML into string"); return Err(TomlError::Serialize(err)) }, - // }; - // let mut new: Self = match Figment::new().merge(Toml::string(&old)).merge(Toml::string(&default)).extract() { - // Ok(new) => { info!("Node | TOML merge ... OK"); new }, - // Err(err) => { error!("Node | Couldn't merge default + old TOML"); return Err(TomlError::Merge(err)) }, - // }; - // // Attempt save - // Self::save(&mut new)?; - // Ok(new) - // } -} - -//---------------------------------------------------------------------------------------------------- [Pool] impl -impl Pool { - pub fn p2pool() -> Self { - Self { - rig: GUPAX_VERSION_UNDERSCORE.to_string(), - ip: "localhost".to_string(), - port: "3333".to_string(), - } - } - - pub fn new_vec() -> Vec<(String, Self)> { - vec![("Local P2Pool".to_string(), Self::p2pool())] - } - - pub fn new_tuple() -> (String, Self) { - ("Local P2Pool".to_string(), Self::p2pool()) - } - - pub fn from_str_to_vec(string: &str) -> Result, TomlError> { - let pools: toml::map::Map = match toml::de::from_str(string) { - Ok(map) => { - info!("Pool | Parse ... OK"); - map - } - Err(err) => { - error!("Pool | String parse ... FAIL ... {}", err); - return Err(TomlError::Deserialize(err)); - } - }; - let size = pools.keys().len(); - let mut vec = Vec::with_capacity(size); - // We have to do [.as_str()] -> [.to_string()] to get rid of the \"...\" that gets added on. - for (key, values) in pools.iter() { - let rig = match values.get("rig") { - Some(rig) => match rig.as_str() { - Some(rig) => rig.to_string(), - None => { - error!("Pool | [None] at [rig] parse"); - return Err(TomlError::Parse("[None] at [rig] parse")); - } - }, - None => { - error!("Pool | [None] at [rig] parse"); - return Err(TomlError::Parse("[None] at [rig] parse")); - } - }; - let ip = match values.get("ip") { - Some(ip) => match ip.as_str() { - Some(ip) => ip.to_string(), - None => { - error!("Pool | [None] at [ip] parse"); - return Err(TomlError::Parse("[None] at [ip] parse")); - } - }, - None => { - error!("Pool | [None] at [ip] parse"); - return Err(TomlError::Parse("[None] at [ip] parse")); - } - }; - let port = match values.get("port") { - Some(port) => match port.as_str() { - Some(port) => port.to_string(), - None => { - error!("Pool | [None] at [port] parse"); - return Err(TomlError::Parse("[None] at [port] parse")); - } - }, - None => { - error!("Pool | [None] at [port] parse"); - return Err(TomlError::Parse("[None] at [port] parse")); - } - }; - let pool = Pool { rig, ip, port }; - vec.push((key.clone(), pool)); - } - Ok(vec) - } - - pub fn to_string(vec: &[(String, Self)]) -> Result { - let mut toml = String::new(); - for (key, value) in vec.iter() { - write!( - toml, - "[\'{}\']\nrig = {:#?}\nip = {:#?}\nport = {:#?}\n\n", - key, value.rig, value.ip, value.port, - )?; - } - Ok(toml) - } - - pub fn get(path: &PathBuf) -> Result, TomlError> { - // Read - let file = File::Pool; - let string = match read_to_string(file, path) { - Ok(string) => string, - // Create - _ => { - Self::create_new(path)?; - read_to_string(file, path)? - } - }; - // Deserialize - Self::from_str_to_vec(&string) - } - - pub fn create_new(path: &PathBuf) -> Result, TomlError> { - info!("Pool | Creating new default..."); - let new = Self::new_vec(); - let string = Self::to_string(&Self::new_vec())?; - fs::write(path, string)?; - info!("Pool | Write ... OK"); - Ok(new) - } - - pub fn save(vec: &[(String, Self)], path: &PathBuf) -> Result<(), TomlError> { - info!("Pool | Saving to disk ... [{}]", path.display()); - let string = Self::to_string(vec)?; - match fs::write(path, string) { - Ok(_) => { - info!("Pool | Save ... OK"); - Ok(()) - } - Err(err) => { - error!("Pool | Couldn't overwrite file"); - Err(TomlError::Io(err)) - } - } - } -} - -//---------------------------------------------------------------------------------------------------- Gupax-P2Pool API -#[derive(Clone, Debug)] -pub struct GupaxP2poolApi { - pub log: String, // Log file only containing full payout lines - pub log_rev: String, // Same as above but reversed based off lines - pub payout: HumanNumber, // Human-friendly display of payout count - pub payout_u64: u64, // [u64] version of above - pub payout_ord: PayoutOrd, // Ordered Vec of payouts, see [PayoutOrd] - pub payout_low: String, // A pre-allocated/computed [String] of the above Vec from low payout to high - pub payout_high: String, // Same as above but high -> low - pub xmr: AtomicUnit, // XMR stored as atomic units - pub path_log: PathBuf, // Path to [log] - pub path_payout: PathBuf, // Path to [payout] - pub path_xmr: PathBuf, // Path to [xmr] -} - -impl Default for GupaxP2poolApi { - fn default() -> Self { - Self::new() - } -} - -impl GupaxP2poolApi { - //---------------------------------------------------------------------------------------------------- Init, these pretty much only get called once - pub fn new() -> Self { - Self { - log: String::new(), - log_rev: String::new(), - payout: HumanNumber::unknown(), - payout_u64: 0, - payout_ord: PayoutOrd::new(), - payout_low: String::new(), - payout_high: String::new(), - xmr: AtomicUnit::new(), - path_xmr: PathBuf::new(), - path_payout: PathBuf::new(), - path_log: PathBuf::new(), - } - } - - pub fn fill_paths(&mut self, gupax_p2pool_dir: &Path) { - let mut path_log = gupax_p2pool_dir.to_path_buf(); - let mut path_payout = gupax_p2pool_dir.to_path_buf(); - let mut path_xmr = gupax_p2pool_dir.to_path_buf(); - path_log.push(GUPAX_P2POOL_API_LOG); - path_payout.push(GUPAX_P2POOL_API_PAYOUT); - path_xmr.push(GUPAX_P2POOL_API_XMR); - *self = Self { - path_log, - path_payout, - path_xmr, - ..std::mem::take(self) - }; - } - - pub fn create_all_files(gupax_p2pool_dir: &Path) -> Result<(), TomlError> { - use std::io::Write; - for file in GUPAX_P2POOL_API_FILE_ARRAY { - let mut path = gupax_p2pool_dir.to_path_buf(); - path.push(file); - if path.exists() { - info!( - "GupaxP2poolApi | [{}] already exists, skipping...", - path.display() - ); - continue; - } - match std::fs::File::create(&path) { - Ok(mut f) => { - match file { - GUPAX_P2POOL_API_PAYOUT | GUPAX_P2POOL_API_XMR => writeln!(f, "0")?, - _ => (), - } - info!("GupaxP2poolApi | [{}] create ... OK", path.display()); - } - Err(e) => { - warn!( - "GupaxP2poolApi | [{}] create ... FAIL: {}", - path.display(), - e - ); - return Err(TomlError::Io(e)); - } - } - } - Ok(()) - } - - pub fn read_all_files_and_update(&mut self) -> Result<(), TomlError> { - let payout_u64 = match read_to_string(File::Payout, &self.path_payout)? - .trim() - .parse::() - { - Ok(o) => o, - Err(e) => { - warn!("GupaxP2poolApi | [payout] parse error: {}", e); - return Err(TomlError::Parse("payout")); - } - }; - let xmr = match read_to_string(File::Xmr, &self.path_xmr)? - .trim() - .parse::() - { - Ok(o) => AtomicUnit::from_u64(o), - Err(e) => { - warn!("GupaxP2poolApi | [xmr] parse error: {}", e); - return Err(TomlError::Parse("xmr")); - } - }; - let payout = HumanNumber::from_u64(payout_u64); - let log = read_to_string(File::Log, &self.path_log)?; - self.payout_ord.update_from_payout_log(&log); - self.update_payout_strings(); - *self = Self { - log, - payout, - payout_u64, - xmr, - ..std::mem::take(self) - }; - self.update_log_rev(); - Ok(()) - } - - // Completely delete the [p2pool] folder and create defaults. - pub fn create_new(path: &PathBuf) -> Result<(), TomlError> { - info!( - "GupaxP2poolApi | Deleting old folder at [{}]...", - path.display() - ); - std::fs::remove_dir_all(path)?; - info!( - "GupaxP2poolApi | Creating new default folder at [{}]...", - path.display() - ); - create_gupax_p2pool_dir(path)?; - Self::create_all_files(path)?; - Ok(()) - } - - //---------------------------------------------------------------------------------------------------- Live, functions that actually update/write live stats - pub fn update_log_rev(&mut self) { - let mut log_rev = String::with_capacity(self.log.len()); - for line in self.log.lines().rev() { - log_rev.push_str(line); - log_rev.push('\n'); - } - self.log_rev = log_rev; - } - - pub fn format_payout(date: &str, atomic_unit: &AtomicUnit, block: &HumanNumber) -> String { - format!("{} | {} XMR | Block {}", date, atomic_unit, block) - } - - pub fn append_log(&mut self, formatted_log_line: &str) { - self.log.push_str(formatted_log_line); - self.log.push('\n'); - } - - pub fn append_head_log_rev(&mut self, formatted_log_line: &str) { - self.log_rev = format!("{}\n{}", formatted_log_line, self.log_rev); - } - - pub fn update_payout_low(&mut self) { - self.payout_ord.sort_payout_low_to_high(); - self.payout_low = self.payout_ord.to_string(); - } - - pub fn update_payout_high(&mut self) { - self.payout_ord.sort_payout_high_to_low(); - self.payout_high = self.payout_ord.to_string(); - } - - pub fn update_payout_strings(&mut self) { - self.update_payout_low(); - self.update_payout_high(); - } - - // Takes the (date, atomic_unit, block) and updates [self] and the [PayoutOrd] - pub fn add_payout( - &mut self, - formatted_log_line: &str, - date: String, - atomic_unit: AtomicUnit, - block: HumanNumber, - ) { - self.append_log(formatted_log_line); - self.append_head_log_rev(formatted_log_line); - self.payout_u64 += 1; - self.payout = HumanNumber::from_u64(self.payout_u64); - self.xmr = self.xmr.add_self(atomic_unit); - self.payout_ord.push(date, atomic_unit, block); - self.update_payout_strings(); - } - - pub fn write_to_all_files(&self, formatted_log_line: &str) -> Result<(), TomlError> { - Self::disk_overwrite(&self.payout_u64.to_string(), &self.path_payout)?; - Self::disk_overwrite(&self.xmr.to_string(), &self.path_xmr)?; - Self::disk_append(formatted_log_line, &self.path_log)?; - Ok(()) - } - - pub fn disk_append(formatted_log_line: &str, path: &PathBuf) -> Result<(), TomlError> { - use std::io::Write; - let mut file = match fs::OpenOptions::new().append(true).create(true).open(path) { - Ok(f) => f, - Err(e) => { - error!( - "GupaxP2poolApi | Append [{}] ... FAIL: {}", - path.display(), - e - ); - return Err(TomlError::Io(e)); - } - }; - match writeln!(file, "{}", formatted_log_line) { - Ok(_) => { - debug!("GupaxP2poolApi | Append [{}] ... OK", path.display()); - Ok(()) - } - Err(e) => { - error!( - "GupaxP2poolApi | Append [{}] ... FAIL: {}", - path.display(), - e - ); - Err(TomlError::Io(e)) - } - } - } - - pub fn disk_overwrite(string: &str, path: &PathBuf) -> Result<(), TomlError> { - use std::io::Write; - let mut file = match fs::OpenOptions::new() - .write(true) - .truncate(true) - .create(true) - .open(path) - { - Ok(f) => f, - Err(e) => { - error!( - "GupaxP2poolApi | Overwrite [{}] ... FAIL: {}", - path.display(), - e - ); - return Err(TomlError::Io(e)); - } - }; - match writeln!(file, "{}", string) { - Ok(_) => { - debug!("GupaxP2poolApi | Overwrite [{}] ... OK", path.display()); - Ok(()) - } - Err(e) => { - error!( - "GupaxP2poolApi | Overwrite [{}] ... FAIL: {}", - path.display(), - e - ); - Err(TomlError::Io(e)) - } - } - } -} - -//---------------------------------------------------------------------------------------------------- Custom Error [TomlError] -#[derive(Debug)] -pub enum TomlError { - Io(std::io::Error), - Path(String), - Serialize(toml::ser::Error), - Deserialize(toml::de::Error), - Merge(figment::Error), - Format(std::fmt::Error), - Parse(&'static str), -} - -impl Display for TomlError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - use TomlError::*; - match self { - Io(err) => write!(f, "{}: IO | {}", ERROR, err), - Path(err) => write!(f, "{}: Path | {}", ERROR, err), - Serialize(err) => write!(f, "{}: Serialize | {}", ERROR, err), - Deserialize(err) => write!(f, "{}: Deserialize | {}", ERROR, err), - Merge(err) => write!(f, "{}: Merge | {}", ERROR, err), - Format(err) => write!(f, "{}: Format | {}", ERROR, err), - Parse(err) => write!(f, "{}: Parse | {}", ERROR, err), - } - } -} - -impl From for TomlError { - fn from(err: std::io::Error) -> Self { - TomlError::Io(err) - } -} - -impl From for TomlError { - fn from(err: std::fmt::Error) -> Self { - TomlError::Format(err) - } -} - -//---------------------------------------------------------------------------------------------------- [File] Enum (for matching which file) -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] -pub enum File { - // State files - State, // state.toml | Gupax state - Node, // node.toml | P2Pool manual node selector - Pool, // pool.toml | XMRig manual pool selector - - // Gupax-P2Pool API - Log, // log | Raw log lines of P2Pool payouts received - Payout, // payout | Single [u64] representing total payouts - Xmr, // xmr | Single [u64] representing total XMR mined in atomic units -} - -//---------------------------------------------------------------------------------------------------- [Submenu] enum for [Status] tab -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] -pub enum Submenu { - Processes, - P2pool, - Benchmarks, -} - -impl Default for Submenu { - fn default() -> Self { - Self::Processes - } -} - -impl Display for Submenu { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - use Submenu::*; - match self { - P2pool => write!(f, "P2Pool"), - _ => write!(f, "{:?}", self), - } - } -} - -//---------------------------------------------------------------------------------------------------- [PayoutView] enum for [Status/P2Pool] tab -// The enum buttons for selecting which "view" to sort the payout log in. -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] -pub enum PayoutView { - Latest, // Shows the most recent logs first - Oldest, // Shows the oldest logs first - Biggest, // Shows highest to lowest payouts - Smallest, // Shows lowest to highest payouts -} - -impl PayoutView { - fn new() -> Self { - Self::Latest - } -} - -impl Default for PayoutView { - fn default() -> Self { - Self::new() - } -} - -impl Display for PayoutView { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - -//---------------------------------------------------------------------------------------------------- [Hash] enum for [Status/P2Pool] -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] -#[allow(clippy::enum_variant_names)] -pub enum Hash { - Hash, - Kilo, - Mega, - Giga, -} - -impl Default for Hash { - fn default() -> Self { - Self::Hash - } -} - -impl Hash { - pub fn convert_to_hash(f: f64, from: Self) -> f64 { - match from { - Self::Hash => f, - Self::Kilo => f * 1_000.0, - Self::Mega => f * 1_000_000.0, - Self::Giga => f * 1_000_000_000.0, - } - } - - pub fn convert(f: f64, og: Self, new: Self) -> f64 { - match og { - Self::Hash => match new { - Self::Hash => f, - Self::Kilo => f / 1_000.0, - Self::Mega => f / 1_000_000.0, - Self::Giga => f / 1_000_000_000.0, - }, - Self::Kilo => match new { - Self::Hash => f * 1_000.0, - Self::Kilo => f, - Self::Mega => f / 1_000.0, - Self::Giga => f / 1_000_000.0, - }, - Self::Mega => match new { - Self::Hash => f * 1_000_000.0, - Self::Kilo => f * 1_000.0, - Self::Mega => f, - Self::Giga => f / 1_000.0, - }, - Self::Giga => match new { - Self::Hash => f * 1_000_000_000.0, - Self::Kilo => f * 1_000_000.0, - Self::Mega => f * 1_000.0, - Self::Giga => f, - }, - } - } -} - -impl Display for Hash { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Hash::Hash => write!(f, "Hash"), - _ => write!(f, "{:?}hash", self), - } - } -} - -//---------------------------------------------------------------------------------------------------- [Node] Struct -#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] -pub struct Node { - pub ip: String, - pub rpc: String, - pub zmq: String, -} - -//---------------------------------------------------------------------------------------------------- [Pool] Struct -#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] -pub struct Pool { - pub rig: String, - pub ip: String, - pub port: String, -} - -//---------------------------------------------------------------------------------------------------- [State] Struct -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct State { - pub status: Status, - pub gupax: Gupax, - pub p2pool: P2pool, - pub xmrig: Xmrig, - pub xvb: Xvb, - pub version: Arc>, -} - -#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] -pub struct Status { - pub submenu: Submenu, - pub payout_view: PayoutView, - pub monero_enabled: bool, - pub manual_hash: bool, - pub hashrate: f64, - pub hash_metric: Hash, -} - -#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] -pub struct Gupax { - pub simple: bool, - pub auto_update: bool, - pub auto_p2pool: bool, - pub auto_xmrig: bool, - // pub auto_monero: bool, - pub ask_before_quit: bool, - pub save_before_quit: bool, - pub update_via_tor: bool, - pub p2pool_path: String, - pub xmrig_path: String, - pub absolute_p2pool_path: PathBuf, - pub absolute_xmrig_path: PathBuf, - pub selected_width: u16, - pub selected_height: u16, - pub selected_scale: f32, - pub tab: Tab, - pub ratio: Ratio, -} - -#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] -pub struct P2pool { - pub simple: bool, - pub mini: bool, - pub auto_ping: bool, - pub auto_select: bool, - pub backup_host: bool, - pub out_peers: u16, - pub in_peers: u16, - pub log_level: u8, - pub node: String, - pub arguments: String, - pub address: String, - pub name: String, - pub ip: String, - pub rpc: String, - pub zmq: String, - pub selected_index: usize, - pub selected_name: String, - pub selected_ip: String, - pub selected_rpc: String, - pub selected_zmq: String, -} - -#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] -pub struct Xmrig { - pub simple: bool, - pub pause: u8, - pub simple_rig: String, - pub arguments: String, - pub tls: bool, - pub keepalive: bool, - pub max_threads: usize, - pub current_threads: usize, - pub address: String, - pub api_ip: String, - pub api_port: String, - pub name: String, - pub rig: String, - pub ip: String, - pub port: String, - pub selected_index: usize, - pub selected_name: String, - pub selected_rig: String, - pub selected_ip: String, - pub selected_port: String, -} - -#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize, Default)] -pub struct Xvb { - pub token: u32, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Version { - pub gupax: String, - pub p2pool: String, - pub xmrig: String, -} - -//---------------------------------------------------------------------------------------------------- [State] Defaults -impl Default for Status { - fn default() -> Self { - Self { - submenu: Submenu::default(), - payout_view: PayoutView::default(), - monero_enabled: false, - manual_hash: false, - hashrate: 1.0, - hash_metric: Hash::default(), - } - } -} - -impl Default for Gupax { - fn default() -> Self { - Self { - simple: true, - auto_update: true, - auto_p2pool: false, - auto_xmrig: false, - ask_before_quit: true, - save_before_quit: true, - update_via_tor: true, - p2pool_path: DEFAULT_P2POOL_PATH.to_string(), - xmrig_path: DEFAULT_XMRIG_PATH.to_string(), - absolute_p2pool_path: into_absolute_path(DEFAULT_P2POOL_PATH.to_string()).unwrap(), - absolute_xmrig_path: into_absolute_path(DEFAULT_XMRIG_PATH.to_string()).unwrap(), - selected_width: APP_DEFAULT_WIDTH as u16, - selected_height: APP_DEFAULT_HEIGHT as u16, - selected_scale: APP_DEFAULT_SCALE, - ratio: Ratio::Width, - tab: Tab::About, - } - } -} - -impl Default for P2pool { - fn default() -> Self { - Self { - simple: true, - mini: true, - auto_ping: true, - auto_select: true, - backup_host: true, - out_peers: 10, - in_peers: 10, - log_level: 3, - node: crate::RemoteNode::new().to_string(), - arguments: String::new(), - address: String::with_capacity(96), - name: "Local Monero Node".to_string(), - ip: "localhost".to_string(), - rpc: "18081".to_string(), - zmq: "18083".to_string(), - selected_index: 0, - selected_name: "Local Monero Node".to_string(), - selected_ip: "localhost".to_string(), - selected_rpc: "18081".to_string(), - selected_zmq: "18083".to_string(), - } - } -} -impl Xmrig { - fn with_threads(max_threads: usize, current_threads: usize) -> Self { - let xmrig = Self::default(); - Self { - max_threads, - current_threads, - ..xmrig - } - } -} -impl Default for Xmrig { - fn default() -> Self { - Self { - simple: true, - pause: 0, - simple_rig: String::with_capacity(30), - arguments: String::with_capacity(300), - address: String::with_capacity(96), - name: "Local P2Pool".to_string(), - rig: GUPAX_VERSION_UNDERSCORE.to_string(), - ip: "localhost".to_string(), - port: "3333".to_string(), - selected_index: 0, - selected_name: "Local P2Pool".to_string(), - selected_ip: "localhost".to_string(), - selected_rig: GUPAX_VERSION_UNDERSCORE.to_string(), - selected_port: "3333".to_string(), - api_ip: "localhost".to_string(), - api_port: "18088".to_string(), - tls: false, - keepalive: false, - current_threads: 1, - max_threads: 1, - } - } -} - -impl Default for Version { - fn default() -> Self { - Self { - gupax: GUPAX_VERSION.to_string(), - p2pool: P2POOL_VERSION.to_string(), - xmrig: XMRIG_VERSION.to_string(), - } - } -} - -//---------------------------------------------------------------------------------------------------- TESTS -#[cfg(test)] -mod test { - #[test] - fn serde_default_state() { - let state = crate::State::new(); - let string = crate::State::to_string(&state).unwrap(); - crate::State::from_str(&string).unwrap(); - } - #[test] - fn serde_default_node() { - let node = crate::Node::new_vec(); - let string = crate::Node::to_string(&node).unwrap(); - crate::Node::from_str_to_vec(&string).unwrap(); - } - #[test] - fn serde_default_pool() { - let pool = crate::Pool::new_vec(); - let string = crate::Pool::to_string(&pool).unwrap(); - crate::Pool::from_str_to_vec(&string).unwrap(); - } - - #[test] - fn serde_custom_state() { - let state = r#" - [gupax] - simple = true - auto_update = true - auto_p2pool = false - auto_xmrig = false - ask_before_quit = true - save_before_quit = true - update_via_tor = true - p2pool_path = "p2pool/p2pool" - xmrig_path = "xmrig/xmrig" - absolute_p2pool_path = "/home/hinto/p2pool/p2pool" - absolute_xmrig_path = "/home/hinto/xmrig/xmrig" - selected_width = 1280 - selected_height = 960 - selected_scale = 0.0 - tab = "About" - ratio = "Width" - - [status] - submenu = "P2pool" - payout_view = "Oldest" - monero_enabled = true - manual_hash = false - hashrate = 1241.23 - hash_metric = "Hash" - - [p2pool] - simple = true - mini = true - auto_ping = true - auto_select = true - backup_host = true - out_peers = 10 - in_peers = 450 - log_level = 3 - node = "Seth" - arguments = "" - address = "44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW" - name = "Local Monero Node" - ip = "192.168.1.123" - rpc = "18089" - zmq = "18083" - selected_index = 0 - selected_name = "Local Monero Node" - selected_ip = "192.168.1.123" - selected_rpc = "18089" - selected_zmq = "18083" - - [xmrig] - simple = true - pause = 0 - simple_rig = "" - arguments = "" - tls = false - keepalive = false - max_threads = 32 - current_threads = 16 - address = "" - api_ip = "localhost" - api_port = "18088" - name = "linux" - rig = "Gupax" - ip = "192.168.1.122" - port = "3333" - selected_index = 1 - selected_name = "linux" - selected_rig = "Gupax" - selected_ip = "192.168.1.122" - selected_port = "3333" - - [xvb] - token = 0 - [version] - gupax = "v1.3.0" - p2pool = "v2.5" - xmrig = "v6.18.0" - "#; - let state = crate::State::from_str(state).unwrap(); - crate::State::to_string(&state).unwrap(); - } - - #[test] - fn serde_custom_node() { - let node = r#" - ['Local Monero Node'] - ip = "localhost" - rpc = "18081" - zmq = "18083" - - ['asdf-_. ._123'] - ip = "localhost" - rpc = "11" - zmq = "1234" - - ['aaa bbb'] - ip = "192.168.2.333" - rpc = "1" - zmq = "65535" - "#; - let node = crate::Node::from_str_to_vec(node).unwrap(); - crate::Node::to_string(&node).unwrap(); - } - - #[test] - fn serde_custom_pool() { - let pool = r#" - ['Local P2Pool'] - rig = "Gupax_v1.0.0" - ip = "localhost" - port = "3333" - - ['aaa xx .. -'] - rig = "Gupax" - ip = "192.168.22.22" - port = "1" - - [' a'] - rig = "Gupax_v1.0.0" - ip = "127.0.0.1" - port = "65535" - "#; - let pool = crate::Pool::from_str_to_vec(pool).unwrap(); - crate::Pool::to_string(&pool).unwrap(); - } - - // Make sure we keep the user's old values that are still - // valid but discard the ones that don't exist anymore. - #[test] - fn merge_state() { - let bad_state = r#" - [gupax] - SETTING_THAT_DOESNT_EXIST_ANYMORE = 123123 - simple = false - auto_update = true - auto_p2pool = false - auto_xmrig = false - ask_before_quit = true - save_before_quit = true - update_via_tor = true - p2pool_path = "p2pool/p2pool" - xmrig_path = "xmrig/xmrig" - absolute_p2pool_path = "" - absolute_xmrig_path = "" - selected_width = 0 - selected_height = 0 - tab = "About" - ratio = "Width" - - [p2pool] - SETTING_THAT_DOESNT_EXIST_ANYMORE = "String" - simple = true - mini = true - auto_ping = true - auto_select = true - out_peers = 10 - in_peers = 450 - log_level = 6 - node = "Seth" - arguments = "" - address = "44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW" - name = "Local Monero Node" - ip = "localhost" - rpc = "18081" - zmq = "18083" - selected_index = 0 - selected_name = "Local Monero Node" - selected_ip = "localhost" - selected_rpc = "18081" - selected_zmq = "18083" - - [xmrig] - SETTING_THAT_DOESNT_EXIST_ANYMORE = true - simple = true - pause = 0 - simple_rig = "" - arguments = "" - tls = false - keepalive = false - max_threads = 32 - current_threads = 16 - address = "" - api_ip = "localhost" - api_port = "18088" - name = "Local P2Pool" - rig = "Gupax_v1.0.0" - ip = "localhost" - port = "3333" - selected_index = 0 - selected_name = "Local P2Pool" - selected_rig = "Gupax_v1.0.0" - selected_ip = "localhost" - selected_port = "3333" - - [xvb] - token = 0 - [version] - gupax = "v1.0.0" - p2pool = "v2.5" - xmrig = "v6.18.0" - "#.to_string(); - let merged_state = crate::State::merge(&bad_state).unwrap(); - let merged_state = crate::State::to_string(&merged_state).unwrap(); - println!("{}", merged_state); - assert!(merged_state.contains("simple = false")); - assert!(merged_state.contains("in_peers = 450")); - assert!(merged_state.contains("log_level = 6")); - assert!(merged_state.contains(r#"node = "Seth""#)); - assert!(!merged_state.contains("SETTING_THAT_DOESNT_EXIST_ANYMORE")); - assert!(merged_state.contains("44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW")); - assert!(merged_state.contains("backup_host = true")); - } - - #[test] - fn create_and_serde_gupax_p2pool_api() { - use crate::disk::GupaxP2poolApi; - use crate::xmr::AtomicUnit; - use crate::xmr::PayoutOrd; - - // Get API dir, fill paths. - let mut api = GupaxP2poolApi::new(); - let mut path = crate::disk::get_gupax_data_path().unwrap(); - path.push(crate::disk::GUPAX_P2POOL_API_DIRECTORY); - GupaxP2poolApi::fill_paths(&mut api, &path); - println!("{:#?}", api); - - // Create, write some fake data. - GupaxP2poolApi::create_all_files(&path).unwrap(); - api.log = "NOTICE 2022-01-27 01:30:23.1377 P2Pool You received a payout of 0.000000000001 XMR in block 2642816".to_string(); - api.payout_u64 = 1; - api.xmr = AtomicUnit::from_u64(2); - let (date, atomic_unit, block) = PayoutOrd::parse_raw_payout_line(&api.log); - let formatted_log_line = GupaxP2poolApi::format_payout(&date, &atomic_unit, &block); - GupaxP2poolApi::write_to_all_files(&api, &formatted_log_line).unwrap(); - println!("AFTER WRITE: {:#?}", api); - - // Read - GupaxP2poolApi::read_all_files_and_update(&mut api).unwrap(); - println!("AFTER READ: {:#?}", api); - - // Assert that the file read mutated the internal struct correctly. - assert_eq!(api.payout_u64, 1); - assert_eq!(api.xmr.to_u64(), 2); - assert!(!api.payout_ord.is_empty()); - assert!(api - .log - .contains("2022-01-27 01:30:23.1377 | 0.000000000001 XMR | Block 2,642,816")); - } - - #[test] - fn convert_hash() { - use crate::disk::Hash; - let hash = 1.0; - assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Hash), 1.0); - assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Kilo), 0.001); - assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Mega), 0.000_001); - assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Giga), 0.000_000_001); - let hash = 1.0; - assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Hash), 1_000.0); - assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Kilo), 1.0); - assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Mega), 0.001); - assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Giga), 0.000_001); - let hash = 1.0; - assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Hash), 1_000_000.0); - assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Kilo), 1_000.0); - assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Mega), 1.0); - assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Giga), 0.001); - let hash = 1.0; - assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Hash), 1_000_000_000.0); - assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Kilo), 1_000_000.0); - assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Mega), 1_000.0); - assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Giga), 1.0); - } -} diff --git a/src/disk/consts.rs b/src/disk/consts.rs new file mode 100644 index 0000000..315511b --- /dev/null +++ b/src/disk/consts.rs @@ -0,0 +1,58 @@ +//---------------------------------------------------------------------------------------------------- Const +// State file +pub const ERROR: &str = "Disk error"; +pub const PATH_ERROR: &str = "PATH for state directory could not be not found"; + +#[cfg(target_os = "windows")] +pub const DIRECTORY: &str = r#"Gupax\"#; +#[cfg(target_os = "macos")] +pub const DIRECTORY: &str = "Gupax/"; +#[cfg(target_os = "linux")] +pub const DIRECTORY: &str = "gupax/"; + +// File names +pub const STATE_TOML: &str = "state.toml"; +pub const NODE_TOML: &str = "node.toml"; +pub const POOL_TOML: &str = "pool.toml"; + +// P2Pool API +// Lives within the Gupax OS data directory. +// ~/.local/share/gupax/p2pool/ +// ├─ payout_log // Raw log lines of payouts received +// ├─ payout // Single [u64] representing total payouts +// ├─ xmr // Single [u64] representing total XMR mined in atomic units +#[cfg(target_os = "windows")] +pub const GUPAX_P2POOL_API_DIRECTORY: &str = r"p2pool\"; +#[cfg(target_family = "unix")] +pub const GUPAX_P2POOL_API_DIRECTORY: &str = "p2pool/"; +pub const GUPAX_P2POOL_API_LOG: &str = "log"; +pub const GUPAX_P2POOL_API_PAYOUT: &str = "payout"; +pub const GUPAX_P2POOL_API_XMR: &str = "xmr"; +pub const GUPAX_P2POOL_API_FILE_ARRAY: [&str; 3] = [ + GUPAX_P2POOL_API_LOG, + GUPAX_P2POOL_API_PAYOUT, + GUPAX_P2POOL_API_XMR, +]; + +#[cfg(target_os = "windows")] +pub const DEFAULT_P2POOL_PATH: &str = r"P2Pool\p2pool.exe"; +#[cfg(target_os = "macos")] +pub const DEFAULT_P2POOL_PATH: &str = "p2pool/p2pool"; +#[cfg(target_os = "windows")] +pub const DEFAULT_XMRIG_PATH: &str = r"XMRig\xmrig.exe"; +#[cfg(target_os = "macos")] +pub const DEFAULT_XMRIG_PATH: &str = "xmrig/xmrig"; + +// Default to [/usr/bin/] for Linux distro builds. +#[cfg(target_os = "linux")] +#[cfg(not(feature = "distro"))] +pub const DEFAULT_P2POOL_PATH: &str = "p2pool/p2pool"; +#[cfg(target_os = "linux")] +#[cfg(not(feature = "distro"))] +pub const DEFAULT_XMRIG_PATH: &str = "xmrig/xmrig"; +#[cfg(target_os = "linux")] +#[cfg(feature = "distro")] +pub const DEFAULT_P2POOL_PATH: &str = "/usr/bin/p2pool"; +#[cfg(target_os = "linux")] +#[cfg(feature = "distro")] +pub const DEFAULT_XMRIG_PATH: &str = "/usr/bin/xmrig"; diff --git a/src/disk/errors.rs b/src/disk/errors.rs new file mode 100644 index 0000000..93b0a5b --- /dev/null +++ b/src/disk/errors.rs @@ -0,0 +1,39 @@ +use super::*; +//---------------------------------------------------------------------------------------------------- Custom Error [TomlError] +#[derive(Debug)] +pub enum TomlError { + Io(std::io::Error), + Path(String), + Serialize(toml::ser::Error), + Deserialize(toml::de::Error), + Merge(figment::Error), + Format(std::fmt::Error), + Parse(&'static str), +} + +impl Display for TomlError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use TomlError::*; + match self { + Io(err) => write!(f, "{}: IO | {}", ERROR, err), + Path(err) => write!(f, "{}: Path | {}", ERROR, err), + Serialize(err) => write!(f, "{}: Serialize | {}", ERROR, err), + Deserialize(err) => write!(f, "{}: Deserialize | {}", ERROR, err), + Merge(err) => write!(f, "{}: Merge | {}", ERROR, err), + Format(err) => write!(f, "{}: Format | {}", ERROR, err), + Parse(err) => write!(f, "{}: Parse | {}", ERROR, err), + } + } +} + +impl From for TomlError { + fn from(err: std::io::Error) -> Self { + TomlError::Io(err) + } +} + +impl From for TomlError { + fn from(err: std::fmt::Error) -> Self { + TomlError::Format(err) + } +} diff --git a/src/disk/gupax_p2pool_api.rs b/src/disk/gupax_p2pool_api.rs new file mode 100644 index 0000000..949ba0f --- /dev/null +++ b/src/disk/gupax_p2pool_api.rs @@ -0,0 +1,266 @@ +use super::*; +//---------------------------------------------------------------------------------------------------- Gupax-P2Pool API +#[derive(Clone, Debug)] +pub struct GupaxP2poolApi { + pub log: String, // Log file only containing full payout lines + pub log_rev: String, // Same as above but reversed based off lines + pub payout: HumanNumber, // Human-friendly display of payout count + pub payout_u64: u64, // [u64] version of above + pub payout_ord: PayoutOrd, // Ordered Vec of payouts, see [PayoutOrd] + pub payout_low: String, // A pre-allocated/computed [String] of the above Vec from low payout to high + pub payout_high: String, // Same as above but high -> low + pub xmr: AtomicUnit, // XMR stored as atomic units + pub path_log: PathBuf, // Path to [log] + pub path_payout: PathBuf, // Path to [payout] + pub path_xmr: PathBuf, // Path to [xmr] +} + +impl Default for GupaxP2poolApi { + fn default() -> Self { + Self::new() + } +} + +impl GupaxP2poolApi { + //---------------------------------------------------------------------------------------------------- Init, these pretty much only get called once + pub fn new() -> Self { + Self { + log: String::new(), + log_rev: String::new(), + payout: HumanNumber::unknown(), + payout_u64: 0, + payout_ord: PayoutOrd::new(), + payout_low: String::new(), + payout_high: String::new(), + xmr: AtomicUnit::new(), + path_xmr: PathBuf::new(), + path_payout: PathBuf::new(), + path_log: PathBuf::new(), + } + } + + pub fn fill_paths(&mut self, gupax_p2pool_dir: &Path) { + let mut path_log = gupax_p2pool_dir.to_path_buf(); + let mut path_payout = gupax_p2pool_dir.to_path_buf(); + let mut path_xmr = gupax_p2pool_dir.to_path_buf(); + path_log.push(GUPAX_P2POOL_API_LOG); + path_payout.push(GUPAX_P2POOL_API_PAYOUT); + path_xmr.push(GUPAX_P2POOL_API_XMR); + *self = Self { + path_log, + path_payout, + path_xmr, + ..std::mem::take(self) + }; + } + + pub fn create_all_files(gupax_p2pool_dir: &Path) -> Result<(), TomlError> { + use std::io::Write; + for file in GUPAX_P2POOL_API_FILE_ARRAY { + let mut path = gupax_p2pool_dir.to_path_buf(); + path.push(file); + if path.exists() { + info!( + "GupaxP2poolApi | [{}] already exists, skipping...", + path.display() + ); + continue; + } + match std::fs::File::create(&path) { + Ok(mut f) => { + match file { + GUPAX_P2POOL_API_PAYOUT | GUPAX_P2POOL_API_XMR => writeln!(f, "0")?, + _ => (), + } + info!("GupaxP2poolApi | [{}] create ... OK", path.display()); + } + Err(e) => { + warn!( + "GupaxP2poolApi | [{}] create ... FAIL: {}", + path.display(), + e + ); + return Err(TomlError::Io(e)); + } + } + } + Ok(()) + } + + pub fn read_all_files_and_update(&mut self) -> Result<(), TomlError> { + let payout_u64 = match read_to_string(File::Payout, &self.path_payout)? + .trim() + .parse::() + { + Ok(o) => o, + Err(e) => { + warn!("GupaxP2poolApi | [payout] parse error: {}", e); + return Err(TomlError::Parse("payout")); + } + }; + let xmr = match read_to_string(File::Xmr, &self.path_xmr)? + .trim() + .parse::() + { + Ok(o) => AtomicUnit::from_u64(o), + Err(e) => { + warn!("GupaxP2poolApi | [xmr] parse error: {}", e); + return Err(TomlError::Parse("xmr")); + } + }; + let payout = HumanNumber::from_u64(payout_u64); + let log = read_to_string(File::Log, &self.path_log)?; + self.payout_ord.update_from_payout_log(&log); + self.update_payout_strings(); + *self = Self { + log, + payout, + payout_u64, + xmr, + ..std::mem::take(self) + }; + self.update_log_rev(); + Ok(()) + } + + // Completely delete the [p2pool] folder and create defaults. + pub fn create_new(path: &PathBuf) -> Result<(), TomlError> { + info!( + "GupaxP2poolApi | Deleting old folder at [{}]...", + path.display() + ); + std::fs::remove_dir_all(path)?; + info!( + "GupaxP2poolApi | Creating new default folder at [{}]...", + path.display() + ); + create_gupax_p2pool_dir(path)?; + Self::create_all_files(path)?; + Ok(()) + } + + //---------------------------------------------------------------------------------------------------- Live, functions that actually update/write live stats + pub fn update_log_rev(&mut self) { + let mut log_rev = String::with_capacity(self.log.len()); + for line in self.log.lines().rev() { + log_rev.push_str(line); + log_rev.push('\n'); + } + self.log_rev = log_rev; + } + + pub fn format_payout(date: &str, atomic_unit: &AtomicUnit, block: &HumanNumber) -> String { + format!("{} | {} XMR | Block {}", date, atomic_unit, block) + } + + pub fn append_log(&mut self, formatted_log_line: &str) { + self.log.push_str(formatted_log_line); + self.log.push('\n'); + } + + pub fn append_head_log_rev(&mut self, formatted_log_line: &str) { + self.log_rev = format!("{}\n{}", formatted_log_line, self.log_rev); + } + + pub fn update_payout_low(&mut self) { + self.payout_ord.sort_payout_low_to_high(); + self.payout_low = self.payout_ord.to_string(); + } + + pub fn update_payout_high(&mut self) { + self.payout_ord.sort_payout_high_to_low(); + self.payout_high = self.payout_ord.to_string(); + } + + pub fn update_payout_strings(&mut self) { + self.update_payout_low(); + self.update_payout_high(); + } + + // Takes the (date, atomic_unit, block) and updates [self] and the [PayoutOrd] + pub fn add_payout( + &mut self, + formatted_log_line: &str, + date: String, + atomic_unit: AtomicUnit, + block: HumanNumber, + ) { + self.append_log(formatted_log_line); + self.append_head_log_rev(formatted_log_line); + self.payout_u64 += 1; + self.payout = HumanNumber::from_u64(self.payout_u64); + self.xmr = self.xmr.add_self(atomic_unit); + self.payout_ord.push(date, atomic_unit, block); + self.update_payout_strings(); + } + + pub fn write_to_all_files(&self, formatted_log_line: &str) -> Result<(), TomlError> { + Self::disk_overwrite(&self.payout_u64.to_string(), &self.path_payout)?; + Self::disk_overwrite(&self.xmr.to_string(), &self.path_xmr)?; + Self::disk_append(formatted_log_line, &self.path_log)?; + Ok(()) + } + + pub fn disk_append(formatted_log_line: &str, path: &PathBuf) -> Result<(), TomlError> { + use std::io::Write; + let mut file = match fs::OpenOptions::new().append(true).create(true).open(path) { + Ok(f) => f, + Err(e) => { + error!( + "GupaxP2poolApi | Append [{}] ... FAIL: {}", + path.display(), + e + ); + return Err(TomlError::Io(e)); + } + }; + match writeln!(file, "{}", formatted_log_line) { + Ok(_) => { + debug!("GupaxP2poolApi | Append [{}] ... OK", path.display()); + Ok(()) + } + Err(e) => { + error!( + "GupaxP2poolApi | Append [{}] ... FAIL: {}", + path.display(), + e + ); + Err(TomlError::Io(e)) + } + } + } + + pub fn disk_overwrite(string: &str, path: &PathBuf) -> Result<(), TomlError> { + use std::io::Write; + let mut file = match fs::OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(path) + { + Ok(f) => f, + Err(e) => { + error!( + "GupaxP2poolApi | Overwrite [{}] ... FAIL: {}", + path.display(), + e + ); + return Err(TomlError::Io(e)); + } + }; + match writeln!(file, "{}", string) { + Ok(_) => { + debug!("GupaxP2poolApi | Overwrite [{}] ... OK", path.display()); + Ok(()) + } + Err(e) => { + error!( + "GupaxP2poolApi | Overwrite [{}] ... FAIL: {}", + path.display(), + e + ); + Err(TomlError::Io(e)) + } + } + } +} diff --git a/src/disk/mod.rs b/src/disk/mod.rs new file mode 100644 index 0000000..e4ae0ef --- /dev/null +++ b/src/disk/mod.rs @@ -0,0 +1,222 @@ +// Gupax - GUI Uniting P2Pool And XMRig +// +// Copyright (c) 2022-2023 hinto-janai +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// This handles reading/writing the disk files: +// - [state.toml] -> [App] state +// - [nodes.toml] -> [Manual Nodes] list +// The TOML format is used. This struct hierarchy +// directly translates into the TOML parser: +// State/ +// ├─ Gupax/ +// │ ├─ ... +// ├─ P2pool/ +// │ ├─ ... +// ├─ Xmrig/ +// │ ├─ ... +// ├─ Version/ +// ├─ ... + +use crate::disk::consts::*; +use crate::{app::Tab, components::gupax::Ratio, constants::*, human::*, macros::*, xmr::*}; +use figment::providers::{Format, Toml}; +use figment::Figment; +use log::*; +use serde::{Deserialize, Serialize}; +#[cfg(target_family = "unix")] +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use std::{ + fmt::Display, + fmt::Write, + fs, + path::PathBuf, + result::Result, + sync::{Arc, Mutex}, +}; + +use self::errors::TomlError; + +pub mod consts; +pub mod errors; +pub mod gupax_p2pool_api; +pub mod node; +pub mod pool; +pub mod state; +pub mod status; +pub mod tests; +//---------------------------------------------------------------------------------------------------- General functions for all [File]'s +// get_file_path() | Return absolute path to OS data path + filename +// read_to_string() | Convert the file at a given path into a [String] +// create_new() | Write a default TOML Struct into the appropriate file (in OS data path) +// into_absolute_path() | Convert relative -> absolute path + +pub fn get_gupax_data_path() -> Result { + // Get OS data folder + // Linux | $XDG_DATA_HOME or $HOME/.local/share/gupax | /home/alice/.local/state/gupax + // macOS | $HOME/Library/Application Support/Gupax | /Users/Alice/Library/Application Support/Gupax + // Windows | {FOLDERID_RoamingAppData}\Gupax | C:\Users\Alice\AppData\Roaming\Gupax + match dirs::data_dir() { + Some(mut path) => { + path.push(DIRECTORY); + info!("OS | Data path ... {}", path.display()); + create_gupax_dir(&path)?; + let mut gupax_p2pool_dir = path.clone(); + gupax_p2pool_dir.push(GUPAX_P2POOL_API_DIRECTORY); + create_gupax_p2pool_dir(&gupax_p2pool_dir)?; + Ok(path) + } + None => { + error!("OS | Data path ... FAIL"); + Err(TomlError::Path(PATH_ERROR.to_string())) + } + } +} + +pub fn set_unix_750_perms(path: &PathBuf) -> Result<(), TomlError> { + #[cfg(target_os = "windows")] + return Ok(()); + #[cfg(target_family = "unix")] + match fs::set_permissions(path, fs::Permissions::from_mode(0o750)) { + Ok(_) => { + info!( + "OS | Unix 750 permissions on path [{}] ... OK", + path.display() + ); + Ok(()) + } + Err(e) => { + error!( + "OS | Unix 750 permissions on path [{}] ... FAIL ... {}", + path.display(), + e + ); + Err(TomlError::Io(e)) + } + } +} + +pub fn set_unix_660_perms(path: &PathBuf) -> Result<(), TomlError> { + #[cfg(target_os = "windows")] + return Ok(()); + #[cfg(target_family = "unix")] + match fs::set_permissions(path, fs::Permissions::from_mode(0o660)) { + Ok(_) => { + info!( + "OS | Unix 660 permissions on path [{}] ... OK", + path.display() + ); + Ok(()) + } + Err(e) => { + error!( + "OS | Unix 660 permissions on path [{}] ... FAIL ... {}", + path.display(), + e + ); + Err(TomlError::Io(e)) + } + } +} + +pub fn get_gupax_p2pool_path(os_data_path: &Path) -> PathBuf { + let mut gupax_p2pool_dir = os_data_path.to_path_buf(); + gupax_p2pool_dir.push(GUPAX_P2POOL_API_DIRECTORY); + gupax_p2pool_dir +} + +pub fn create_gupax_dir(path: &PathBuf) -> Result<(), TomlError> { + // Create Gupax directory + match fs::create_dir_all(path) { + Ok(_) => info!("OS | Create data path ... OK"), + Err(e) => { + error!("OS | Create data path ... FAIL ... {}", e); + return Err(TomlError::Io(e)); + } + } + set_unix_750_perms(path) +} + +pub fn create_gupax_p2pool_dir(path: &PathBuf) -> Result<(), TomlError> { + // Create Gupax directory + match fs::create_dir_all(path) { + Ok(_) => { + info!( + "OS | Create Gupax-P2Pool API path [{}] ... OK", + path.display() + ); + Ok(()) + } + Err(e) => { + error!( + "OS | Create Gupax-P2Pool API path [{}] ... FAIL ... {}", + path.display(), + e + ); + Err(TomlError::Io(e)) + } + } +} + +// Convert a [File] path to a [String] +pub fn read_to_string(file: File, path: &PathBuf) -> Result { + match fs::read_to_string(path) { + Ok(string) => { + info!("{:?} | Read ... OK", file); + Ok(string) + } + Err(err) => { + warn!("{:?} | Read ... FAIL", file); + Err(TomlError::Io(err)) + } + } +} + +// Write str to console with [info!] surrounded by "---" +pub fn print_dash(toml: &str) { + info!("{}", HORIZONTAL); + for i in toml.lines() { + info!("{}", i); + } + info!("{}", HORIZONTAL); +} + +// Turn relative paths into absolute paths +pub fn into_absolute_path(path: String) -> Result { + let path = PathBuf::from(path); + if path.is_relative() { + let mut dir = std::env::current_exe()?; + dir.pop(); + dir.push(path); + Ok(dir) + } else { + Ok(path) + } +} + +//---------------------------------------------------------------------------------------------------- [File] Enum (for matching which file) +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] +pub enum File { + // State files + State, // state.toml | Gupax state + Node, // node.toml | P2Pool manual node selector + Pool, // pool.toml | XMRig manual pool selector + + // Gupax-P2Pool API + Log, // log | Raw log lines of P2Pool payouts received + Payout, // payout | Single [u64] representing total payouts + Xmr, // xmr | Single [u64] representing total XMR mined in atomic units +} diff --git a/src/disk/node.rs b/src/disk/node.rs new file mode 100644 index 0000000..1c90c7d --- /dev/null +++ b/src/disk/node.rs @@ -0,0 +1,164 @@ +use crate::disk::TomlError; +use crate::disk::*; +use serde::{Deserialize, Serialize}; +//---------------------------------------------------------------------------------------------------- [Node] Impl +impl Node { + pub fn localhost() -> Self { + Self { + ip: "localhost".to_string(), + rpc: "18081".to_string(), + zmq: "18083".to_string(), + } + } + + pub fn new_vec() -> Vec<(String, Self)> { + vec![("Local Monero Node".to_string(), Self::localhost())] + } + + pub fn new_tuple() -> (String, Self) { + ("Local Monero Node".to_string(), Self::localhost()) + } + + // Convert [String] to [Node] Vec + pub fn from_str_to_vec(string: &str) -> Result, TomlError> { + let nodes: toml::map::Map = match toml::de::from_str(string) { + Ok(map) => { + info!("Node | Parse ... OK"); + map + } + Err(err) => { + error!("Node | String parse ... FAIL ... {}", err); + return Err(TomlError::Deserialize(err)); + } + }; + let size = nodes.keys().len(); + let mut vec = Vec::with_capacity(size); + for (key, values) in nodes.iter() { + let ip = match values.get("ip") { + Some(ip) => match ip.as_str() { + Some(ip) => ip.to_string(), + None => { + error!("Node | [None] at [ip] parse"); + return Err(TomlError::Parse("[None] at [ip] parse")); + } + }, + None => { + error!("Node | [None] at [ip] parse"); + return Err(TomlError::Parse("[None] at [ip] parse")); + } + }; + let rpc = match values.get("rpc") { + Some(rpc) => match rpc.as_str() { + Some(rpc) => rpc.to_string(), + None => { + error!("Node | [None] at [rpc] parse"); + return Err(TomlError::Parse("[None] at [rpc] parse")); + } + }, + None => { + error!("Node | [None] at [rpc] parse"); + return Err(TomlError::Parse("[None] at [rpc] parse")); + } + }; + let zmq = match values.get("zmq") { + Some(zmq) => match zmq.as_str() { + Some(zmq) => zmq.to_string(), + None => { + error!("Node | [None] at [zmq] parse"); + return Err(TomlError::Parse("[None] at [zmq] parse")); + } + }, + None => { + error!("Node | [None] at [zmq] parse"); + return Err(TomlError::Parse("[None] at [zmq] parse")); + } + }; + let node = Node { ip, rpc, zmq }; + vec.push((key.clone(), node)); + } + Ok(vec) + } + + // Convert [Vec<(String, Self)>] into [String] + // that can be written as a proper TOML file + pub fn to_string(vec: &[(String, Self)]) -> Result { + let mut toml = String::new(); + for (key, value) in vec.iter() { + write!( + toml, + "[\'{}\']\nip = {:#?}\nrpc = {:#?}\nzmq = {:#?}\n\n", + key, value.ip, value.rpc, value.zmq, + )?; + } + Ok(toml) + } + + // Combination of multiple functions: + // 1. Attempt to read file from path into [String] + // |_ Create a default file if not found + // 2. Deserialize [String] into a proper [Struct] + // |_ Attempt to merge if deserialization fails + pub fn get(path: &PathBuf) -> Result, TomlError> { + // Read + let file = File::Node; + let string = match read_to_string(file, path) { + Ok(string) => string, + // Create + _ => { + Self::create_new(path)?; + read_to_string(file, path)? + } + }; + // Deserialize, attempt merge if failed + Self::from_str_to_vec(&string) + } + + // Completely overwrite current [node.toml] + // with a new default version, and return [Vec]. + pub fn create_new(path: &PathBuf) -> Result, TomlError> { + info!("Node | Creating new default..."); + let new = Self::new_vec(); + let string = Self::to_string(&Self::new_vec())?; + fs::write(path, string)?; + info!("Node | Write ... OK"); + Ok(new) + } + + // Save [Node] onto disk file [node.toml] + pub fn save(vec: &[(String, Self)], path: &PathBuf) -> Result<(), TomlError> { + info!("Node | Saving to disk ... [{}]", path.display()); + let string = Self::to_string(vec)?; + match fs::write(path, string) { + Ok(_) => { + info!("Node | Save ... OK"); + Ok(()) + } + Err(err) => { + error!("Node | Couldn't overwrite file"); + Err(TomlError::Io(err)) + } + } + } + + // pub fn merge(old: &String) -> Result { + // info!("Node | Starting TOML merge..."); + // let default = match toml::ser::to_string(&Self::new()) { + // Ok(string) => { info!("Node | Default TOML parse ... OK"); string }, + // Err(err) => { error!("Node | Couldn't parse default TOML into string"); return Err(TomlError::Serialize(err)) }, + // }; + // let mut new: Self = match Figment::new().merge(Toml::string(&old)).merge(Toml::string(&default)).extract() { + // Ok(new) => { info!("Node | TOML merge ... OK"); new }, + // Err(err) => { error!("Node | Couldn't merge default + old TOML"); return Err(TomlError::Merge(err)) }, + // }; + // // Attempt save + // Self::save(&mut new)?; + // Ok(new) + // } +} +//---------------------------------------------------------------------------------------------------- [Node] Struct +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] +pub struct Node { + pub ip: String, + pub rpc: String, + pub zmq: String, +} diff --git a/src/disk/pool.rs b/src/disk/pool.rs new file mode 100644 index 0000000..a75c586 --- /dev/null +++ b/src/disk/pool.rs @@ -0,0 +1,137 @@ +use super::*; +//---------------------------------------------------------------------------------------------------- [Pool] impl +impl Pool { + pub fn p2pool() -> Self { + Self { + rig: GUPAX_VERSION_UNDERSCORE.to_string(), + ip: "localhost".to_string(), + port: "3333".to_string(), + } + } + + pub fn new_vec() -> Vec<(String, Self)> { + vec![("Local P2Pool".to_string(), Self::p2pool())] + } + + pub fn new_tuple() -> (String, Self) { + ("Local P2Pool".to_string(), Self::p2pool()) + } + + pub fn from_str_to_vec(string: &str) -> Result, TomlError> { + let pools: toml::map::Map = match toml::de::from_str(string) { + Ok(map) => { + info!("Pool | Parse ... OK"); + map + } + Err(err) => { + error!("Pool | String parse ... FAIL ... {}", err); + return Err(TomlError::Deserialize(err)); + } + }; + let size = pools.keys().len(); + let mut vec = Vec::with_capacity(size); + // We have to do [.as_str()] -> [.to_string()] to get rid of the \"...\" that gets added on. + for (key, values) in pools.iter() { + let rig = match values.get("rig") { + Some(rig) => match rig.as_str() { + Some(rig) => rig.to_string(), + None => { + error!("Pool | [None] at [rig] parse"); + return Err(TomlError::Parse("[None] at [rig] parse")); + } + }, + None => { + error!("Pool | [None] at [rig] parse"); + return Err(TomlError::Parse("[None] at [rig] parse")); + } + }; + let ip = match values.get("ip") { + Some(ip) => match ip.as_str() { + Some(ip) => ip.to_string(), + None => { + error!("Pool | [None] at [ip] parse"); + return Err(TomlError::Parse("[None] at [ip] parse")); + } + }, + None => { + error!("Pool | [None] at [ip] parse"); + return Err(TomlError::Parse("[None] at [ip] parse")); + } + }; + let port = match values.get("port") { + Some(port) => match port.as_str() { + Some(port) => port.to_string(), + None => { + error!("Pool | [None] at [port] parse"); + return Err(TomlError::Parse("[None] at [port] parse")); + } + }, + None => { + error!("Pool | [None] at [port] parse"); + return Err(TomlError::Parse("[None] at [port] parse")); + } + }; + let pool = Pool { rig, ip, port }; + vec.push((key.clone(), pool)); + } + Ok(vec) + } + + pub fn to_string(vec: &[(String, Self)]) -> Result { + let mut toml = String::new(); + for (key, value) in vec.iter() { + write!( + toml, + "[\'{}\']\nrig = {:#?}\nip = {:#?}\nport = {:#?}\n\n", + key, value.rig, value.ip, value.port, + )?; + } + Ok(toml) + } + + pub fn get(path: &PathBuf) -> Result, TomlError> { + // Read + let file = File::Pool; + let string = match read_to_string(file, path) { + Ok(string) => string, + // Create + _ => { + Self::create_new(path)?; + read_to_string(file, path)? + } + }; + // Deserialize + Self::from_str_to_vec(&string) + } + + pub fn create_new(path: &PathBuf) -> Result, TomlError> { + info!("Pool | Creating new default..."); + let new = Self::new_vec(); + let string = Self::to_string(&Self::new_vec())?; + fs::write(path, string)?; + info!("Pool | Write ... OK"); + Ok(new) + } + + pub fn save(vec: &[(String, Self)], path: &PathBuf) -> Result<(), TomlError> { + info!("Pool | Saving to disk ... [{}]", path.display()); + let string = Self::to_string(vec)?; + match fs::write(path, string) { + Ok(_) => { + info!("Pool | Save ... OK"); + Ok(()) + } + Err(err) => { + error!("Pool | Couldn't overwrite file"); + Err(TomlError::Io(err)) + } + } + } +} +//---------------------------------------------------------------------------------------------------- [Pool] Struct +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] +pub struct Pool { + pub rig: String, + pub ip: String, + pub port: String, +} diff --git a/src/disk/state.rs b/src/disk/state.rs new file mode 100644 index 0000000..4e684b8 --- /dev/null +++ b/src/disk/state.rs @@ -0,0 +1,362 @@ +use super::*; +use crate::{components::node::RemoteNode, disk::status::*}; +//---------------------------------------------------------------------------------------------------- [State] Impl +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +impl State { + pub fn new() -> Self { + let max_threads = benri::threads!(); + let current_threads = if max_threads == 1 { 1 } else { max_threads / 2 }; + Self { + status: Status::default(), + gupax: Gupax::default(), + p2pool: P2pool::default(), + xmrig: Xmrig::with_threads(max_threads, current_threads), + xvb: Xvb::default(), + version: arc_mut!(Version::default()), + } + } + + pub fn update_absolute_path(&mut self) -> Result<(), TomlError> { + self.gupax.absolute_p2pool_path = into_absolute_path(self.gupax.p2pool_path.clone())?; + self.gupax.absolute_xmrig_path = into_absolute_path(self.gupax.xmrig_path.clone())?; + Ok(()) + } + + // Convert [&str] to [State] + pub fn from_str(string: &str) -> Result { + match toml::de::from_str(string) { + Ok(state) => { + info!("State | Parse ... OK"); + print_dash(string); + Ok(state) + } + Err(err) => { + warn!("State | String -> State ... FAIL ... {}", err); + Err(TomlError::Deserialize(err)) + } + } + } + + // Convert [State] to [String] + pub fn to_string(&self) -> Result { + match toml::ser::to_string(self) { + Ok(s) => Ok(s), + Err(e) => { + error!("State | Couldn't serialize default file: {}", e); + Err(TomlError::Serialize(e)) + } + } + } + + // Combination of multiple functions: + // 1. Attempt to read file from path into [String] + // |_ Create a default file if not found + // 2. Deserialize [String] into a proper [Struct] + // |_ Attempt to merge if deserialization fails + pub fn get(path: &PathBuf) -> Result { + // Read + let file = File::State; + let string = match read_to_string(file, path) { + Ok(string) => string, + // Create + _ => { + Self::create_new(path)?; + match read_to_string(file, path) { + Ok(s) => s, + Err(e) => return Err(e), + } + } + }; + // Deserialize, attempt merge if failed + match Self::from_str(&string) { + Ok(s) => Ok(s), + Err(_) => { + warn!("State | Attempting merge..."); + match Self::merge(&string) { + Ok(mut new) => { + Self::save(&mut new, path)?; + Ok(new) + } + Err(e) => Err(e), + } + } + } + } + + // Completely overwrite current [state.toml] + // with a new default version, and return [Self]. + pub fn create_new(path: &PathBuf) -> Result { + info!("State | Creating new default..."); + let new = Self::new(); + let string = Self::to_string(&new)?; + fs::write(path, string)?; + info!("State | Write ... OK"); + Ok(new) + } + + // Save [State] onto disk file [gupax.toml] + pub fn save(&mut self, path: &PathBuf) -> Result<(), TomlError> { + info!("State | Saving to disk..."); + // Convert path to absolute + self.gupax.absolute_p2pool_path = into_absolute_path(self.gupax.p2pool_path.clone())?; + self.gupax.absolute_xmrig_path = into_absolute_path(self.gupax.xmrig_path.clone())?; + let string = match toml::ser::to_string(&self) { + Ok(string) => { + info!("State | Parse ... OK"); + print_dash(&string); + string + } + Err(err) => { + error!("State | Couldn't parse TOML into string ... FAIL"); + return Err(TomlError::Serialize(err)); + } + }; + match fs::write(path, string) { + Ok(_) => { + info!("State | Save ... OK"); + Ok(()) + } + Err(err) => { + error!("State | Couldn't overwrite TOML file ... FAIL"); + Err(TomlError::Io(err)) + } + } + } + + // Take [String] as input, merge it with whatever the current [default] is, + // leaving behind old keys+values and updating [default] with old valid ones. + pub fn merge(old: &str) -> Result { + let default = toml::ser::to_string(&Self::new()).unwrap(); + let new: Self = match Figment::from(Toml::string(&default)) + .merge(Toml::string(old)) + .extract() + { + Ok(new) => { + info!("State | TOML merge ... OK"); + new + } + Err(err) => { + error!("State | Couldn't merge default + old TOML"); + return Err(TomlError::Merge(err)); + } + }; + Ok(new) + } +} +//---------------------------------------------------------------------------------------------------- [State] Struct +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct State { + pub status: Status, + pub gupax: Gupax, + pub p2pool: P2pool, + pub xmrig: Xmrig, + pub xvb: Xvb, + pub version: Arc>, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct Status { + pub submenu: Submenu, + pub payout_view: PayoutView, + pub monero_enabled: bool, + pub manual_hash: bool, + pub hashrate: f64, + pub hash_metric: Hash, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct Gupax { + pub simple: bool, + pub auto_update: bool, + pub auto_p2pool: bool, + pub auto_xmrig: bool, + // pub auto_monero: bool, + pub ask_before_quit: bool, + pub save_before_quit: bool, + pub update_via_tor: bool, + pub p2pool_path: String, + pub xmrig_path: String, + pub absolute_p2pool_path: PathBuf, + pub absolute_xmrig_path: PathBuf, + pub selected_width: u16, + pub selected_height: u16, + pub selected_scale: f32, + pub tab: Tab, + pub ratio: Ratio, +} + +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] +pub struct P2pool { + pub simple: bool, + pub mini: bool, + pub auto_ping: bool, + pub auto_select: bool, + pub backup_host: bool, + pub out_peers: u16, + pub in_peers: u16, + pub log_level: u8, + pub node: String, + pub arguments: String, + pub address: String, + pub name: String, + pub ip: String, + pub rpc: String, + pub zmq: String, + pub selected_index: usize, + pub selected_name: String, + pub selected_ip: String, + pub selected_rpc: String, + pub selected_zmq: String, +} + +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] +pub struct Xmrig { + pub simple: bool, + pub pause: u8, + pub simple_rig: String, + pub arguments: String, + pub tls: bool, + pub keepalive: bool, + pub max_threads: usize, + pub current_threads: usize, + pub address: String, + pub api_ip: String, + pub api_port: String, + pub name: String, + pub rig: String, + pub ip: String, + pub port: String, + pub selected_index: usize, + pub selected_name: String, + pub selected_rig: String, + pub selected_ip: String, + pub selected_port: String, +} + +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize, Default)] +pub struct Xvb { + pub token_confirmed: String, + pub token_inserted: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Version { + pub gupax: String, + pub p2pool: String, + pub xmrig: String, +} + +//---------------------------------------------------------------------------------------------------- [State] Defaults +impl Default for Status { + fn default() -> Self { + Self { + submenu: Submenu::default(), + payout_view: PayoutView::default(), + monero_enabled: false, + manual_hash: false, + hashrate: 1.0, + hash_metric: Hash::default(), + } + } +} + +impl Default for Gupax { + fn default() -> Self { + Self { + simple: true, + auto_update: true, + auto_p2pool: false, + auto_xmrig: false, + ask_before_quit: true, + save_before_quit: true, + update_via_tor: true, + p2pool_path: DEFAULT_P2POOL_PATH.to_string(), + xmrig_path: DEFAULT_XMRIG_PATH.to_string(), + absolute_p2pool_path: into_absolute_path(DEFAULT_P2POOL_PATH.to_string()).unwrap(), + absolute_xmrig_path: into_absolute_path(DEFAULT_XMRIG_PATH.to_string()).unwrap(), + selected_width: APP_DEFAULT_WIDTH as u16, + selected_height: APP_DEFAULT_HEIGHT as u16, + selected_scale: APP_DEFAULT_SCALE, + ratio: Ratio::Width, + tab: Tab::About, + } + } +} + +impl Default for P2pool { + fn default() -> Self { + Self { + simple: true, + mini: true, + auto_ping: true, + auto_select: true, + backup_host: true, + out_peers: 10, + in_peers: 10, + log_level: 3, + node: RemoteNode::new().to_string(), + arguments: String::new(), + address: String::with_capacity(96), + name: "Local Monero Node".to_string(), + ip: "localhost".to_string(), + rpc: "18081".to_string(), + zmq: "18083".to_string(), + selected_index: 0, + selected_name: "Local Monero Node".to_string(), + selected_ip: "localhost".to_string(), + selected_rpc: "18081".to_string(), + selected_zmq: "18083".to_string(), + } + } +} +impl Xmrig { + fn with_threads(max_threads: usize, current_threads: usize) -> Self { + let xmrig = Self::default(); + Self { + max_threads, + current_threads, + ..xmrig + } + } +} +impl Default for Xmrig { + fn default() -> Self { + Self { + simple: true, + pause: 0, + simple_rig: String::with_capacity(30), + arguments: String::with_capacity(300), + address: String::with_capacity(96), + name: "Local P2Pool".to_string(), + rig: GUPAX_VERSION_UNDERSCORE.to_string(), + ip: "localhost".to_string(), + port: "3333".to_string(), + selected_index: 0, + selected_name: "Local P2Pool".to_string(), + selected_ip: "localhost".to_string(), + selected_rig: GUPAX_VERSION_UNDERSCORE.to_string(), + selected_port: "3333".to_string(), + api_ip: "localhost".to_string(), + api_port: "18088".to_string(), + tls: false, + keepalive: false, + current_threads: 1, + max_threads: 1, + } + } +} + +impl Default for Version { + fn default() -> Self { + Self { + gupax: GUPAX_VERSION.to_string(), + p2pool: P2POOL_VERSION.to_string(), + xmrig: XMRIG_VERSION.to_string(), + } + } +} diff --git a/src/disk/status.rs b/src/disk/status.rs new file mode 100644 index 0000000..83e40ac --- /dev/null +++ b/src/disk/status.rs @@ -0,0 +1,117 @@ +use super::*; +//---------------------------------------------------------------------------------------------------- [Submenu] enum for [Status] tab +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] +pub enum Submenu { + Processes, + P2pool, + Benchmarks, +} + +impl Default for Submenu { + fn default() -> Self { + Self::Processes + } +} + +impl Display for Submenu { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use Submenu::*; + match self { + P2pool => write!(f, "P2Pool"), + _ => write!(f, "{:?}", self), + } + } +} + +//---------------------------------------------------------------------------------------------------- [PayoutView] enum for [Status/P2Pool] tab +// The enum buttons for selecting which "view" to sort the payout log in. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] +pub enum PayoutView { + Latest, // Shows the most recent logs first + Oldest, // Shows the oldest logs first + Biggest, // Shows highest to lowest payouts + Smallest, // Shows lowest to highest payouts +} + +impl PayoutView { + fn new() -> Self { + Self::Latest + } +} + +impl Default for PayoutView { + fn default() -> Self { + Self::new() + } +} + +impl Display for PayoutView { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +//---------------------------------------------------------------------------------------------------- [Hash] enum for [Status/P2Pool] +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] +#[allow(clippy::enum_variant_names)] +pub enum Hash { + Hash, + Kilo, + Mega, + Giga, +} + +impl Default for Hash { + fn default() -> Self { + Self::Hash + } +} + +impl Hash { + pub fn convert_to_hash(f: f64, from: Self) -> f64 { + match from { + Self::Hash => f, + Self::Kilo => f * 1_000.0, + Self::Mega => f * 1_000_000.0, + Self::Giga => f * 1_000_000_000.0, + } + } + + pub fn convert(f: f64, og: Self, new: Self) -> f64 { + match og { + Self::Hash => match new { + Self::Hash => f, + Self::Kilo => f / 1_000.0, + Self::Mega => f / 1_000_000.0, + Self::Giga => f / 1_000_000_000.0, + }, + Self::Kilo => match new { + Self::Hash => f * 1_000.0, + Self::Kilo => f, + Self::Mega => f / 1_000.0, + Self::Giga => f / 1_000_000.0, + }, + Self::Mega => match new { + Self::Hash => f * 1_000_000.0, + Self::Kilo => f * 1_000.0, + Self::Mega => f, + Self::Giga => f / 1_000.0, + }, + Self::Giga => match new { + Self::Hash => f * 1_000_000_000.0, + Self::Kilo => f * 1_000_000.0, + Self::Mega => f * 1_000.0, + Self::Giga => f, + }, + } + } +} + +impl Display for Hash { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Hash::Hash => write!(f, "Hash"), + _ => write!(f, "{:?}hash", self), + } + } +} diff --git a/src/disk/tests.rs b/src/disk/tests.rs new file mode 100644 index 0000000..6b6ef83 --- /dev/null +++ b/src/disk/tests.rs @@ -0,0 +1,302 @@ +//---------------------------------------------------------------------------------------------------- TESTS +#[cfg(test)] +mod test { + use crate::disk::node::Node; + use crate::disk::pool::Pool; + use crate::disk::state::State; + #[test] + fn serde_default_state() { + let state = State::new(); + let string = State::to_string(&state).unwrap(); + State::from_str(&string).unwrap(); + } + #[test] + fn serde_default_node() { + let node = Node::new_vec(); + let string = Node::to_string(&node).unwrap(); + Node::from_str_to_vec(&string).unwrap(); + } + #[test] + fn serde_default_pool() { + let pool = Pool::new_vec(); + let string = Pool::to_string(&pool).unwrap(); + Pool::from_str_to_vec(&string).unwrap(); + } + + #[test] + fn serde_custom_state() { + let state = r#" + [gupax] + simple = true + auto_update = true + auto_p2pool = false + auto_xmrig = false + ask_before_quit = true + save_before_quit = true + update_via_tor = true + p2pool_path = "p2pool/p2pool" + xmrig_path = "xmrig/xmrig" + absolute_p2pool_path = "/home/hinto/p2pool/p2pool" + absolute_xmrig_path = "/home/hinto/xmrig/xmrig" + selected_width = 1280 + selected_height = 960 + selected_scale = 0.0 + tab = "About" + ratio = "Width" + + [status] + submenu = "P2pool" + payout_view = "Oldest" + monero_enabled = true + manual_hash = false + hashrate = 1241.23 + hash_metric = "Hash" + + [p2pool] + simple = true + mini = true + auto_ping = true + auto_select = true + backup_host = true + out_peers = 10 + in_peers = 450 + log_level = 3 + node = "Seth" + arguments = "" + address = "44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW" + name = "Local Monero Node" + ip = "192.168.1.123" + rpc = "18089" + zmq = "18083" + selected_index = 0 + selected_name = "Local Monero Node" + selected_ip = "192.168.1.123" + selected_rpc = "18089" + selected_zmq = "18083" + + [xmrig] + simple = true + pause = 0 + simple_rig = "" + arguments = "" + tls = false + keepalive = false + max_threads = 32 + current_threads = 16 + address = "" + api_ip = "localhost" + api_port = "18088" + name = "linux" + rig = "Gupax" + ip = "192.168.1.122" + port = "3333" + selected_index = 1 + selected_name = "linux" + selected_rig = "Gupax" + selected_ip = "192.168.1.122" + selected_port = "3333" + + [xvb] + token_confirmed = "" + token_inserted = "" + [version] + gupax = "v1.3.0" + p2pool = "v2.5" + xmrig = "v6.18.0" + "#; + let state = State::from_str(state).unwrap(); + State::to_string(&state).unwrap(); + } + + #[test] + fn serde_custom_node() { + let node = r#" + ['Local Monero Node'] + ip = "localhost" + rpc = "18081" + zmq = "18083" + + ['asdf-_. ._123'] + ip = "localhost" + rpc = "11" + zmq = "1234" + + ['aaa bbb'] + ip = "192.168.2.333" + rpc = "1" + zmq = "65535" + "#; + let node = Node::from_str_to_vec(node).unwrap(); + Node::to_string(&node).unwrap(); + } + + #[test] + fn serde_custom_pool() { + let pool = r#" + ['Local P2Pool'] + rig = "Gupax_v1.0.0" + ip = "localhost" + port = "3333" + + ['aaa xx .. -'] + rig = "Gupax" + ip = "192.168.22.22" + port = "1" + + [' a'] + rig = "Gupax_v1.0.0" + ip = "127.0.0.1" + port = "65535" + "#; + let pool = Pool::from_str_to_vec(pool).unwrap(); + Pool::to_string(&pool).unwrap(); + } + + // Make sure we keep the user's old values that are still + // valid but discard the ones that don't exist anymore. + #[test] + fn merge_state() { + let bad_state = r#" + [gupax] + SETTING_THAT_DOESNT_EXIST_ANYMORE = 123123 + simple = false + auto_update = true + auto_p2pool = false + auto_xmrig = false + ask_before_quit = true + save_before_quit = true + update_via_tor = true + p2pool_path = "p2pool/p2pool" + xmrig_path = "xmrig/xmrig" + absolute_p2pool_path = "" + absolute_xmrig_path = "" + selected_width = 0 + selected_height = 0 + tab = "About" + ratio = "Width" + + [p2pool] + SETTING_THAT_DOESNT_EXIST_ANYMORE = "String" + simple = true + mini = true + auto_ping = true + auto_select = true + out_peers = 10 + in_peers = 450 + log_level = 6 + node = "Seth" + arguments = "" + address = "44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW" + name = "Local Monero Node" + ip = "localhost" + rpc = "18081" + zmq = "18083" + selected_index = 0 + selected_name = "Local Monero Node" + selected_ip = "localhost" + selected_rpc = "18081" + selected_zmq = "18083" + + [xmrig] + SETTING_THAT_DOESNT_EXIST_ANYMORE = true + simple = true + pause = 0 + simple_rig = "" + arguments = "" + tls = false + keepalive = false + max_threads = 32 + current_threads = 16 + address = "" + api_ip = "localhost" + api_port = "18088" + name = "Local P2Pool" + rig = "Gupax_v1.0.0" + ip = "localhost" + port = "3333" + selected_index = 0 + selected_name = "Local P2Pool" + selected_rig = "Gupax_v1.0.0" + selected_ip = "localhost" + selected_port = "3333" + + [xvb] + token = "" + [version] + gupax = "v1.0.0" + p2pool = "v2.5" + xmrig = "v6.18.0" + "#.to_string(); + let merged_state = State::merge(&bad_state).unwrap(); + let merged_state = State::to_string(&merged_state).unwrap(); + println!("{}", merged_state); + assert!(merged_state.contains("simple = false")); + assert!(merged_state.contains("in_peers = 450")); + assert!(merged_state.contains("log_level = 6")); + assert!(merged_state.contains(r#"node = "Seth""#)); + assert!(!merged_state.contains("SETTING_THAT_DOESNT_EXIST_ANYMORE")); + assert!(merged_state.contains("44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW")); + assert!(merged_state.contains("backup_host = true")); + } + + #[test] + fn create_and_serde_gupax_p2pool_api() { + use crate::disk::gupax_p2pool_api::GupaxP2poolApi; + use crate::xmr::AtomicUnit; + use crate::xmr::PayoutOrd; + + // Get API dir, fill paths. + let mut api = GupaxP2poolApi::new(); + let mut path = crate::disk::get_gupax_data_path().unwrap(); + path.push(crate::disk::GUPAX_P2POOL_API_DIRECTORY); + GupaxP2poolApi::fill_paths(&mut api, &path); + println!("{:#?}", api); + + // Create, write some fake data. + GupaxP2poolApi::create_all_files(&path).unwrap(); + api.log = "NOTICE 2022-01-27 01:30:23.1377 P2Pool You received a payout of 0.000000000001 XMR in block 2642816".to_string(); + api.payout_u64 = 1; + api.xmr = AtomicUnit::from_u64(2); + let (date, atomic_unit, block) = PayoutOrd::parse_raw_payout_line(&api.log); + let formatted_log_line = GupaxP2poolApi::format_payout(&date, &atomic_unit, &block); + GupaxP2poolApi::write_to_all_files(&api, &formatted_log_line).unwrap(); + println!("AFTER WRITE: {:#?}", api); + + // Read + GupaxP2poolApi::read_all_files_and_update(&mut api).unwrap(); + println!("AFTER READ: {:#?}", api); + + // Assert that the file read mutated the internal struct correctly. + assert_eq!(api.payout_u64, 1); + assert_eq!(api.xmr.to_u64(), 2); + assert!(!api.payout_ord.is_empty()); + assert!(api + .log + .contains("2022-01-27 01:30:23.1377 | 0.000000000001 XMR | Block 2,642,816")); + } + + #[test] + fn convert_hash() { + use crate::disk::status::Hash; + let hash = 1.0; + assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Hash), 1.0); + assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Kilo), 0.001); + assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Mega), 0.000_001); + assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Giga), 0.000_000_001); + let hash = 1.0; + assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Hash), 1_000.0); + assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Kilo), 1.0); + assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Mega), 0.001); + assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Giga), 0.000_001); + let hash = 1.0; + assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Hash), 1_000_000.0); + assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Kilo), 1_000.0); + assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Mega), 1.0); + assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Giga), 0.001); + let hash = 1.0; + assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Hash), 1_000_000_000.0); + assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Kilo), 1_000_000.0); + assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Mega), 1_000.0); + assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Giga), 1.0); + } +} diff --git a/src/free.rs b/src/free.rs deleted file mode 100644 index 706fcbf..0000000 --- a/src/free.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Free functions. - -//---------------------------------------------------------------------------------------------------- Use -use crate::constants::*; - -//---------------------------------------------------------------------------------------------------- -#[cold] -#[inline(never)] -// Clamp the scaling resolution `f32` to a known good `f32`. -pub fn clamp_scale(scale: f32) -> f32 { - // Make sure it is finite. - if !scale.is_finite() { - return APP_DEFAULT_SCALE; - } - - // Clamp between valid range. - scale.clamp(APP_MIN_SCALE, APP_MAX_SCALE) -} diff --git a/src/helper.rs b/src/helper.rs deleted file mode 100644 index 4d9df1c..0000000 --- a/src/helper.rs +++ /dev/null @@ -1,2942 +0,0 @@ -// Gupax - GUI Uniting P2Pool And XMRig -// -// Copyright (c) 2022-2023 hinto-janai -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -// This file represents the "helper" thread, which is the full separate thread -// that runs alongside the main [App] GUI thread. It exists for the entire duration -// of Gupax so that things can be handled without locking up the GUI thread. -// -// This thread is a continual 1 second loop, collecting available jobs on the -// way down and (if possible) asynchronously executing them at the very end. -// -// The main GUI thread will interface with this thread by mutating the Arc's -// found here, e.g: User clicks [Stop P2Pool] -> Arc is set -// indicating to this thread during its loop: "I should stop P2Pool!", e.g: -// -// if lock!(p2pool).signal == ProcessSignal::Stop { -// stop_p2pool(), -// } -// -// This also includes all things related to handling the child processes (P2Pool/XMRig): -// piping their stdout/stderr/stdin, accessing their APIs (HTTP + disk files), etc. - -//---------------------------------------------------------------------------------------------------- Import -use crate::regex::{P2POOL_REGEX, XMRIG_REGEX}; -use crate::{constants::*, human::*, macros::*, xmr::*, GupaxP2poolApi, RemoteNode, SudoState}; -use log::*; -use serde::{Deserialize, Serialize}; -use std::path::Path; -use std::{ - fmt::Write, - path::PathBuf, - process::Stdio, - sync::{Arc, Mutex}, - thread, - time::*, -}; - -//---------------------------------------------------------------------------------------------------- Constants -// The max amount of bytes of process output we are willing to -// hold in memory before it's too much and we need to reset. -const MAX_GUI_OUTPUT_BYTES: usize = 500_000; -// Just a little leeway so a reset will go off before the [String] allocates more memory. -const GUI_OUTPUT_LEEWAY: usize = MAX_GUI_OUTPUT_BYTES - 1000; - -// Some constants for generating hashrate/difficulty. -const MONERO_BLOCK_TIME_IN_SECONDS: u64 = 120; -const P2POOL_BLOCK_TIME_IN_SECONDS: u64 = 10; - -//---------------------------------------------------------------------------------------------------- [Helper] Struct -// A meta struct holding all the data that gets processed in this thread -pub struct Helper { - pub instant: Instant, // Gupax start as an [Instant] - pub uptime: HumanTime, // Gupax uptime formatting for humans - pub pub_sys: Arc>, // The public API for [sysinfo] that the [Status] tab reads from - pub p2pool: Arc>, // P2Pool process state - pub xmrig: Arc>, // XMRig process state - pub gui_api_p2pool: Arc>, // P2Pool API state (for GUI thread) - pub gui_api_xmrig: Arc>, // XMRig API state (for GUI thread) - pub img_p2pool: Arc>, // A static "image" of the data P2Pool started with - pub img_xmrig: Arc>, // A static "image" of the data XMRig started with - pub_api_p2pool: Arc>, // P2Pool API state (for Helper/P2Pool thread) - pub_api_xmrig: Arc>, // XMRig API state (for Helper/XMRig thread) - pub gupax_p2pool_api: Arc>, // -} - -// The communication between the data here and the GUI thread goes as follows: -// [GUI] <---> [Helper] <---> [Watchdog] <---> [Private Data only available here] -// -// Both [GUI] and [Helper] own their separate [Pub*Api] structs. -// Since P2Pool & XMRig will be updating their information out of sync, -// it's the helpers job to lock everything, and move the watchdog [Pub*Api]s -// on a 1-second interval into the [GUI]'s [Pub*Api] struct, atomically. - -//---------------------------------------------------------------------------------------------------- -#[derive(Debug, Clone)] -pub struct Sys { - pub gupax_uptime: String, - pub gupax_cpu_usage: String, - pub gupax_memory_used_mb: String, - pub system_cpu_model: String, - pub system_memory: String, - pub system_cpu_usage: String, -} - -impl Sys { - pub fn new() -> Self { - Self { - gupax_uptime: "0 seconds".to_string(), - gupax_cpu_usage: "???%".to_string(), - gupax_memory_used_mb: "??? megabytes".to_string(), - system_cpu_usage: "???%".to_string(), - system_memory: "???GB / ???GB".to_string(), - system_cpu_model: "???".to_string(), - } - } -} -impl Default for Sys { - fn default() -> Self { - Self::new() - } -} - -//---------------------------------------------------------------------------------------------------- [Process] Struct -// This holds all the state of a (child) process. -// The main GUI thread will use this to display console text, online state, etc. -#[derive(Debug)] -pub struct Process { - pub name: ProcessName, // P2Pool or XMRig? - pub state: ProcessState, // The state of the process (alive, dead, etc) - pub signal: ProcessSignal, // Did the user click [Start/Stop/Restart]? - // STDIN Problem: - // - User can input many many commands in 1 second - // - The process loop only processes every 1 second - // - If there is only 1 [String] holding the user input, - // the user could overwrite their last input before - // the loop even has a chance to process their last command - // STDIN Solution: - // - When the user inputs something, push it to a [Vec] - // - In the process loop, loop over every [Vec] element and - // send each one individually to the process stdin - // - pub input: Vec, - - // 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. - // 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]. - // "parse" contains the output that will be parsed, then tossed out. "pub" will be written to - // the same as parse, but it will be [swap()]'d by the "helper" thread into the GUIs [String]. - // The "helper" thread synchronizes this swap so that the data in here is moved there - // roughly once a second. GUI thread never touches this. - output_parse: Arc>, - output_pub: Arc>, - - // Start time of process. - start: std::time::Instant, -} - -//---------------------------------------------------------------------------------------------------- [Process] Impl -impl Process { - pub fn new(name: ProcessName, _args: String, _path: PathBuf) -> Self { - Self { - name, - state: ProcessState::Dead, - signal: ProcessSignal::None, - start: Instant::now(), - // stdin: Option::None, - // child: Option::None, - output_parse: arc_mut!(String::with_capacity(500)), - output_pub: arc_mut!(String::with_capacity(500)), - input: vec![String::new()], - } - } - - // Borrow a [&str], return an owned split collection - #[inline] - pub fn parse_args(args: &str) -> Vec { - args.split_whitespace().map(|s| s.to_owned()).collect() - } - - #[inline] - // Convenience functions - pub fn is_alive(&self) -> bool { - self.state == ProcessState::Alive - || self.state == ProcessState::Middle - || self.state == ProcessState::Syncing - || self.state == ProcessState::NotMining - } - - #[inline] - pub fn is_waiting(&self) -> bool { - self.state == ProcessState::Middle || self.state == ProcessState::Waiting - } - - #[inline] - pub fn is_syncing(&self) -> bool { - self.state == ProcessState::Syncing - } - - #[inline] - pub fn is_not_mining(&self) -> bool { - self.state == ProcessState::NotMining - } -} - -//---------------------------------------------------------------------------------------------------- [Process*] Enum -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -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 ([re]starting/stopping), YELLOW! - Waiting, // Process was successfully killed by a restart, and is ready to be started again, YELLOW! - - // Only for P2Pool, ORANGE. - Syncing, - - // Only for XMRig, ORANGE. - NotMining, -} - -impl Default for ProcessState { - fn default() -> Self { - Self::Dead - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum ProcessSignal { - None, - Start, - Stop, - Restart, -} - -impl Default for ProcessSignal { - fn default() -> Self { - Self::None - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum ProcessName { - P2pool, - Xmrig, -} - -impl std::fmt::Display for ProcessState { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{:#?}", self) - } -} -impl std::fmt::Display for ProcessSignal { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{:#?}", self) - } -} -impl std::fmt::Display for ProcessName { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match *self { - ProcessName::P2pool => write!(f, "P2Pool"), - ProcessName::Xmrig => write!(f, "XMRig"), - } - } -} - -//---------------------------------------------------------------------------------------------------- [Helper] -impl Helper { - //---------------------------------------------------------------------------------------------------- General Functions - #[allow(clippy::too_many_arguments)] - pub fn new( - instant: std::time::Instant, - pub_sys: Arc>, - p2pool: Arc>, - xmrig: Arc>, - gui_api_p2pool: Arc>, - gui_api_xmrig: Arc>, - img_p2pool: Arc>, - img_xmrig: Arc>, - gupax_p2pool_api: Arc>, - ) -> Self { - Self { - instant, - pub_sys, - uptime: HumanTime::into_human(instant.elapsed()), - pub_api_p2pool: arc_mut!(PubP2poolApi::new()), - pub_api_xmrig: arc_mut!(PubXmrigApi::new()), - // These are created when initializing [App], since it needs a handle to it as well - p2pool, - xmrig, - gui_api_p2pool, - gui_api_xmrig, - img_p2pool, - img_xmrig, - gupax_p2pool_api, - } - } - - #[cold] - #[inline(never)] - fn read_pty_xmrig( - output_parse: Arc>, - output_pub: Arc>, - reader: Box, - ) { - use std::io::BufRead; - let mut stdout = std::io::BufReader::new(reader).lines(); - - // Run a ANSI escape sequence filter for the first few lines. - let mut i = 0; - while let Some(Ok(line)) = stdout.next() { - let line = strip_ansi_escapes::strip_str(line); - if let Err(e) = writeln!(lock!(output_parse), "{}", line) { - error!("XMRig PTY Parse | Output error: {}", e); - } - if let Err(e) = writeln!(lock!(output_pub), "{}", line) { - error!("XMRig PTY Pub | Output error: {}", e); - } - if i > 20 { - break; - } else { - i += 1; - } - } - - while let Some(Ok(line)) = stdout.next() { - // println!("{}", line); // For debugging. - if let Err(e) = writeln!(lock!(output_parse), "{}", line) { - error!("XMRig PTY Parse | Output error: {}", e); - } - if let Err(e) = writeln!(lock!(output_pub), "{}", line) { - error!("XMRig PTY Pub | Output error: {}", e); - } - } - } - - #[cold] - #[inline(never)] - fn read_pty_p2pool( - output_parse: Arc>, - output_pub: Arc>, - reader: Box, - gupax_p2pool_api: Arc>, - ) { - use std::io::BufRead; - let mut stdout = std::io::BufReader::new(reader).lines(); - - // Run a ANSI escape sequence filter for the first few lines. - let mut i = 0; - while let Some(Ok(line)) = stdout.next() { - let line = strip_ansi_escapes::strip_str(line); - if let Err(e) = writeln!(lock!(output_parse), "{}", line) { - error!("P2Pool PTY Parse | Output error: {}", e); - } - if let Err(e) = writeln!(lock!(output_pub), "{}", line) { - error!("P2Pool PTY Pub | Output error: {}", e); - } - if i > 20 { - break; - } else { - i += 1; - } - } - - while let Some(Ok(line)) = stdout.next() { - // println!("{}", line); // For debugging. - if P2POOL_REGEX.payout.is_match(&line) { - debug!("P2Pool PTY | Found payout, attempting write: {}", line); - let (date, atomic_unit, block) = PayoutOrd::parse_raw_payout_line(&line); - let formatted_log_line = GupaxP2poolApi::format_payout(&date, &atomic_unit, &block); - GupaxP2poolApi::add_payout( - &mut lock!(gupax_p2pool_api), - &formatted_log_line, - date, - atomic_unit, - block, - ); - if let Err(e) = GupaxP2poolApi::write_to_all_files( - &lock!(gupax_p2pool_api), - &formatted_log_line, - ) { - error!("P2Pool PTY GupaxP2poolApi | Write error: {}", e); - } - } - if let Err(e) = writeln!(lock!(output_parse), "{}", line) { - error!("P2Pool PTY Parse | Output error: {}", e); - } - if let Err(e) = writeln!(lock!(output_pub), "{}", line) { - error!("P2Pool PTY Pub | Output error: {}", e); - } - } - } - - // Reset output if larger than max bytes. - // This will also append a message showing it was reset. - fn check_reset_gui_output(output: &mut String, name: ProcessName) { - let len = output.len(); - if len > GUI_OUTPUT_LEEWAY { - info!( - "{} Watchdog | Output is nearing {} bytes, resetting!", - name, MAX_GUI_OUTPUT_BYTES - ); - let text = format!("{}\n{} GUI log is exceeding the maximum: {} bytes!\nI've reset the logs for you!\n{}\n\n\n\n", HORI_CONSOLE, name, MAX_GUI_OUTPUT_BYTES, HORI_CONSOLE); - output.clear(); - output.push_str(&text); - debug!("{} Watchdog | Resetting GUI output ... OK", name); - } else { - debug!( - "{} Watchdog | GUI output reset not needed! Current byte length ... {}", - name, len - ); - } - } - - // Read P2Pool/XMRig's API file to a [String]. - fn path_to_string( - path: &Path, - name: ProcessName, - ) -> std::result::Result { - match std::fs::read_to_string(path) { - Ok(s) => Ok(s), - Err(e) => { - warn!("{} API | [{}] read error: {}", name, path.display(), e); - Err(e) - } - } - } - - //---------------------------------------------------------------------------------------------------- P2Pool specific - #[cold] - #[inline(never)] - // Just sets some signals for the watchdog thread to pick up on. - pub fn stop_p2pool(helper: &Arc>) { - info!("P2Pool | Attempting to stop..."); - lock2!(helper, p2pool).signal = ProcessSignal::Stop; - lock2!(helper, p2pool).state = ProcessState::Middle; - } - - #[cold] - #[inline(never)] - // 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: &Path, - backup_hosts: Option>, - ) { - info!("P2Pool | Attempting to restart..."); - lock2!(helper, p2pool).signal = ProcessSignal::Restart; - lock2!(helper, p2pool).state = ProcessState::Middle; - - let helper = Arc::clone(helper); - let state = state.clone(); - let path = path.to_path_buf(); - // This thread lives to wait, start p2pool then die. - thread::spawn(move || { - while lock2!(helper, p2pool).is_alive() { - warn!("P2Pool | Want to restart but process is still alive, waiting..."); - sleep!(1000); - } - // Ok, process is not alive, start the new one! - info!("P2Pool | Old process seems dead, starting new one!"); - Self::start_p2pool(&helper, &state, &path, backup_hosts); - }); - info!("P2Pool | Restart ... OK"); - } - - #[cold] - #[inline(never)] - // 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: &Path, - backup_hosts: Option>, - ) { - lock2!(helper, p2pool).state = ProcessState::Middle; - - let (args, api_path_local, api_path_network, api_path_pool) = - Self::build_p2pool_args_and_mutate_img(helper, state, path, backup_hosts); - - // Print arguments & user settings to console - crate::disk::print_dash(&format!( - "P2Pool | Launch arguments: {:#?} | Local API Path: {:#?} | Network API Path: {:#?} | Pool API Path: {:#?}", - args, - api_path_local, - api_path_network, - api_path_pool, - )); - - // Spawn watchdog thread - let process = Arc::clone(&lock!(helper).p2pool); - let gui_api = Arc::clone(&lock!(helper).gui_api_p2pool); - let pub_api = Arc::clone(&lock!(helper).pub_api_p2pool); - let gupax_p2pool_api = Arc::clone(&lock!(helper).gupax_p2pool_api); - let path = path.to_path_buf(); - thread::spawn(move || { - Self::spawn_p2pool_watchdog( - process, - gui_api, - pub_api, - args, - path, - api_path_local, - api_path_network, - api_path_pool, - gupax_p2pool_api, - ); - }); - } - - // Takes in a 95-char Monero address, returns the first and last - // 6 characters separated with dots like so: [4abcde...abcdef] - fn head_tail_of_monero_address(address: &str) -> String { - if address.len() < 95 { - return "???".to_string(); - } - let head = &address[0..6]; - let tail = &address[89..95]; - head.to_owned() + "..." + tail - } - - #[cold] - #[inline(never)] - // Takes in some [State/P2pool] and parses it to build the actual command arguments. - // Returns the [Vec] of actual arguments, and mutates the [ImgP2pool] for the main GUI thread - // It returns a value... and mutates a deeply nested passed argument... this is some pretty bad code... - pub fn build_p2pool_args_and_mutate_img( - helper: &Arc>, - state: &crate::disk::P2pool, - path: &Path, - backup_hosts: Option>, - ) -> (Vec, PathBuf, PathBuf, PathBuf) { - let mut args = Vec::with_capacity(500); - let path = path.to_path_buf(); - let mut api_path = path; - api_path.pop(); - - // [Simple] - if state.simple { - // Build the p2pool argument - let (ip, rpc, zmq) = RemoteNode::get_ip_rpc_zmq(&state.node); // Get: (IP, RPC, ZMQ) - args.push("--wallet".to_string()); - args.push(state.address.clone()); // Wallet address - args.push("--host".to_string()); - args.push(ip.to_string()); // IP Address - args.push("--rpc-port".to_string()); - args.push(rpc.to_string()); // RPC Port - args.push("--zmq-port".to_string()); - args.push(zmq.to_string()); // ZMQ Port - args.push("--data-api".to_string()); - args.push(api_path.display().to_string()); // API Path - args.push("--local-api".to_string()); // Enable API - args.push("--no-color".to_string()); // Remove color escape sequences, Gupax terminal can't parse it :( - args.push("--mini".to_string()); // P2Pool Mini - args.push("--light-mode".to_string()); // Assume user is not using P2Pool to mine. - - // Push other nodes if `backup_host`. - if let Some(nodes) = backup_hosts { - for node in nodes { - if (node.ip.as_str(), node.rpc.as_str(), node.zmq.as_str()) != (ip, rpc, zmq) { - args.push("--host".to_string()); - args.push(node.ip.to_string()); - args.push("--rpc-port".to_string()); - args.push(node.rpc.to_string()); - args.push("--zmq-port".to_string()); - args.push(node.zmq.to_string()); - } - } - } - - *lock2!(helper, img_p2pool) = ImgP2pool { - mini: "P2Pool Mini".to_string(), - address: Self::head_tail_of_monero_address(&state.address), - host: ip.to_string(), - rpc: rpc.to_string(), - zmq: zmq.to_string(), - out_peers: "10".to_string(), - in_peers: "10".to_string(), - }; - - // [Advanced] - } else { - // Overriding command arguments - if !state.arguments.is_empty() { - // This parses the input and attempts to fill out - // the [ImgP2pool]... This is pretty bad code... - let mut last = ""; - let lock = lock!(helper); - let mut p2pool_image = lock!(lock.img_p2pool); - let mut mini = false; - for arg in state.arguments.split_whitespace() { - match last { - "--mini" => { - mini = true; - p2pool_image.mini = "P2Pool Mini".to_string(); - } - "--wallet" => p2pool_image.address = Self::head_tail_of_monero_address(arg), - "--host" => p2pool_image.host = arg.to_string(), - "--rpc-port" => p2pool_image.rpc = arg.to_string(), - "--zmq-port" => p2pool_image.zmq = arg.to_string(), - "--out-peers" => p2pool_image.out_peers = arg.to_string(), - "--in-peers" => p2pool_image.in_peers = arg.to_string(), - "--data-api" => api_path = PathBuf::from(arg), - _ => (), - } - if !mini { - p2pool_image.mini = "P2Pool Main".to_string(); - } - let arg = if arg == "localhost" { "127.0.0.1" } else { arg }; - args.push(arg.to_string()); - last = arg; - } - // Else, build the argument - } else { - let ip = if state.ip == "localhost" { - "127.0.0.1" - } else { - &state.ip - }; - args.push("--wallet".to_string()); - args.push(state.address.clone()); // Wallet - args.push("--host".to_string()); - args.push(ip.to_string()); // IP - args.push("--rpc-port".to_string()); - args.push(state.rpc.to_string()); // RPC - args.push("--zmq-port".to_string()); - args.push(state.zmq.to_string()); // ZMQ - args.push("--loglevel".to_string()); - args.push(state.log_level.to_string()); // Log Level - args.push("--out-peers".to_string()); - args.push(state.out_peers.to_string()); // Out Peers - args.push("--in-peers".to_string()); - args.push(state.in_peers.to_string()); // In Peers - args.push("--data-api".to_string()); - args.push(api_path.display().to_string()); // API Path - args.push("--local-api".to_string()); // Enable API - args.push("--no-color".to_string()); // Remove color escape sequences - args.push("--light-mode".to_string()); // Assume user is not using P2Pool to mine. - if state.mini { - args.push("--mini".to_string()); - }; // Mini - - // Push other nodes if `backup_host`. - if let Some(nodes) = backup_hosts { - for node in nodes { - let ip = if node.ip == "localhost" { - "127.0.0.1" - } else { - &node.ip - }; - if (node.ip.as_str(), node.rpc.as_str(), node.zmq.as_str()) - != (ip, &state.rpc, &state.zmq) - { - args.push("--host".to_string()); - args.push(node.ip.to_string()); - args.push("--rpc-port".to_string()); - args.push(node.rpc.to_string()); - args.push("--zmq-port".to_string()); - args.push(node.zmq.to_string()); - } - } - } - - *lock2!(helper, img_p2pool) = ImgP2pool { - mini: if state.mini { - "P2Pool Mini".to_string() - } else { - "P2Pool Main".to_string() - }, - address: Self::head_tail_of_monero_address(&state.address), - host: state.selected_ip.to_string(), - rpc: state.selected_rpc.to_string(), - zmq: state.selected_zmq.to_string(), - out_peers: state.out_peers.to_string(), - in_peers: state.in_peers.to_string(), - }; - } - } - let mut api_path_local = api_path.clone(); - let mut api_path_network = api_path.clone(); - let mut api_path_pool = api_path.clone(); - api_path_local.push(P2POOL_API_PATH_LOCAL); - api_path_network.push(P2POOL_API_PATH_NETWORK); - api_path_pool.push(P2POOL_API_PATH_POOL); - (args, api_path_local, api_path_network, api_path_pool) - } - - #[cold] - #[inline(never)] - // The P2Pool watchdog. Spawns 1 OS thread for reading a PTY (STDOUT+STDERR), and combines the [Child] with a PTY so STDIN actually works. - #[allow(clippy::too_many_arguments)] - fn spawn_p2pool_watchdog( - process: Arc>, - gui_api: Arc>, - pub_api: Arc>, - args: Vec, - path: std::path::PathBuf, - api_path_local: std::path::PathBuf, - api_path_network: std::path::PathBuf, - api_path_pool: std::path::PathBuf, - gupax_p2pool_api: Arc>, - ) { - // 1a. Create PTY - debug!("P2Pool | Creating PTY..."); - let pty = portable_pty::native_pty_system(); - let pair = pty - .openpty(portable_pty::PtySize { - rows: 100, - cols: 1000, - pixel_width: 0, - pixel_height: 0, - }) - .unwrap(); - // 1b. Create command - debug!("P2Pool | Creating command..."); - let mut cmd = portable_pty::CommandBuilder::new(path.as_path()); - cmd.args(args); - cmd.env("NO_COLOR", "true"); - cmd.cwd(path.as_path().parent().unwrap()); - // 1c. Create child - debug!("P2Pool | Creating child..."); - let child_pty = arc_mut!(pair.slave.spawn_command(cmd).unwrap()); - drop(pair.slave); - - // 2. Set process state - debug!("P2Pool | Setting process state..."); - let mut lock = lock!(process); - lock.state = ProcessState::Syncing; - lock.signal = ProcessSignal::None; - lock.start = Instant::now(); - let reader = pair.master.try_clone_reader().unwrap(); // Get STDOUT/STDERR before moving the PTY - let mut stdin = pair.master.take_writer().unwrap(); - drop(lock); - - // 3. Spawn PTY read thread - debug!("P2Pool | Spawning PTY read thread..."); - let output_parse = Arc::clone(&lock!(process).output_parse); - let output_pub = Arc::clone(&lock!(process).output_pub); - let gupax_p2pool_api = Arc::clone(&gupax_p2pool_api); - thread::spawn(move || { - Self::read_pty_p2pool(output_parse, output_pub, reader, gupax_p2pool_api); - }); - let output_parse = Arc::clone(&lock!(process).output_parse); - let output_pub = Arc::clone(&lock!(process).output_pub); - - debug!("P2Pool | Cleaning old [local] API files..."); - // Attempt to remove stale API file - match std::fs::remove_file(&api_path_local) { - Ok(_) => info!("P2Pool | Attempting to remove stale API file ... OK"), - Err(e) => warn!( - "P2Pool | Attempting to remove stale API file ... FAIL ... {}", - e - ), - } - // Attempt to create a default empty one. - use std::io::Write; - if std::fs::File::create(&api_path_local).is_ok() { - let text = r#"{"hashrate_15m":0,"hashrate_1h":0,"hashrate_24h":0,"shares_found":0,"average_effort":0.0,"current_effort":0.0,"connections":0}"#; - match std::fs::write(&api_path_local, text) { - Ok(_) => info!("P2Pool | Creating default empty API file ... OK"), - Err(e) => warn!( - "P2Pool | Creating default empty API file ... FAIL ... {}", - e - ), - } - } - let start = lock!(process).start; - - // Reset stats before loop - *lock!(pub_api) = PubP2poolApi::new(); - *lock!(gui_api) = PubP2poolApi::new(); - - // 4. Loop as watchdog - info!("P2Pool | Entering watchdog mode... woof!"); - loop { - // Set timer - let now = Instant::now(); - debug!("P2Pool Watchdog | ----------- Start of loop -----------"); - lock!(gui_api).tick += 1; - - // Check if the process is secretly died without us knowing :) - if let Ok(Some(code)) = lock!(child_pty).try_wait() { - debug!("P2Pool Watchdog | Process secretly died! Getting exit status"); - let exit_status = match code.success() { - true => { - lock!(process).state = ProcessState::Dead; - "Successful" - } - false => { - lock!(process).state = ProcessState::Failed; - "Failed" - } - }; - let uptime = HumanTime::into_human(start.elapsed()); - info!( - "P2Pool Watchdog | Stopped ... Uptime was: [{}], Exit status: [{}]", - uptime, exit_status - ); - // This is written directly into the GUI, because sometimes the 900ms event loop can't catch it. - if let Err(e) = writeln!( - lock!(gui_api).output, - "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n", - HORI_CONSOLE, - uptime, - exit_status, - HORI_CONSOLE - ) { - error!( - "P2Pool Watchdog | GUI Uptime/Exit status write failed: {}", - e - ); - } - lock!(process).signal = ProcessSignal::None; - debug!("P2Pool Watchdog | Secret dead process reap OK, breaking"); - break; - } - - // Check SIGNAL - if lock!(process).signal == ProcessSignal::Stop { - debug!("P2Pool Watchdog | Stop SIGNAL caught"); - // This actually sends a SIGHUP to p2pool (closes the PTY, hangs up on p2pool) - if let Err(e) = lock!(child_pty).kill() { - error!("P2Pool Watchdog | Kill error: {}", e); - } - // Wait to get the exit status - let exit_status = match lock!(child_pty).wait() { - Ok(e) => { - if e.success() { - lock!(process).state = ProcessState::Dead; - "Successful" - } else { - lock!(process).state = ProcessState::Failed; - "Failed" - } - } - _ => { - lock!(process).state = ProcessState::Failed; - "Unknown Error" - } - }; - let uptime = HumanTime::into_human(start.elapsed()); - info!( - "P2Pool Watchdog | Stopped ... Uptime was: [{}], Exit status: [{}]", - uptime, exit_status - ); - // This is written directly into the GUI API, because sometimes the 900ms event loop can't catch it. - if let Err(e) = writeln!( - lock!(gui_api).output, - "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n", - HORI_CONSOLE, - uptime, - exit_status, - HORI_CONSOLE - ) { - error!( - "P2Pool Watchdog | GUI Uptime/Exit status write failed: {}", - e - ); - } - lock!(process).signal = ProcessSignal::None; - debug!("P2Pool Watchdog | Stop SIGNAL done, breaking"); - break; - // Check RESTART - } else if lock!(process).signal == ProcessSignal::Restart { - debug!("P2Pool Watchdog | Restart SIGNAL caught"); - // This actually sends a SIGHUP to p2pool (closes the PTY, hangs up on p2pool) - if let Err(e) = lock!(child_pty).kill() { - error!("P2Pool Watchdog | Kill error: {}", e); - } - // Wait to get the exit status - let exit_status = match lock!(child_pty).wait() { - Ok(e) => { - if e.success() { - "Successful" - } else { - "Failed" - } - } - _ => "Unknown Error", - }; - let uptime = HumanTime::into_human(start.elapsed()); - info!( - "P2Pool Watchdog | Stopped ... Uptime was: [{}], Exit status: [{}]", - uptime, exit_status - ); - // This is written directly into the GUI API, because sometimes the 900ms event loop can't catch it. - if let Err(e) = writeln!( - lock!(gui_api).output, - "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n", - HORI_CONSOLE, - uptime, - exit_status, - HORI_CONSOLE - ) { - error!( - "P2Pool Watchdog | GUI Uptime/Exit status write failed: {}", - e - ); - } - lock!(process).state = ProcessState::Waiting; - debug!("P2Pool Watchdog | Restart SIGNAL done, breaking"); - break; - } - - // Check vector of user input - let mut lock = lock!(process); - if !lock.input.is_empty() { - let input = std::mem::take(&mut lock.input); - for line in input { - if line.is_empty() { - continue; - } - debug!( - "P2Pool Watchdog | User input not empty, writing to STDIN: [{}]", - line - ); - // Windows terminals (or at least the PTY abstraction I'm using, portable_pty) - // requires a [\r\n] to end a line, whereas Unix is okay with just a [\n]. - // - // I have literally read all of [portable_pty]'s source code, dug into Win32 APIs, - // even rewrote some of the actual PTY code in order to understand why STDIN doesn't work - // on Windows. It's because of a fucking missing [\r]. Another reason to hate Windows :D - // - // XMRig did actually work before though, since it reads STDIN directly without needing a newline. - #[cfg(target_os = "windows")] - if let Err(e) = write!(stdin, "{}\r\n", line) { - error!("P2Pool Watchdog | STDIN error: {}", e); - } - #[cfg(target_family = "unix")] - if let Err(e) = writeln!(stdin, "{}", line) { - error!("P2Pool Watchdog | STDIN error: {}", e); - } - // Flush. - if let Err(e) = stdin.flush() { - error!("P2Pool Watchdog | STDIN flush error: {}", e); - } - } - } - drop(lock); - - // Check if logs need resetting - debug!("P2Pool Watchdog | Attempting GUI log reset check"); - let mut lock = lock!(gui_api); - Self::check_reset_gui_output(&mut lock.output, ProcessName::P2pool); - drop(lock); - - // Always update from output - debug!("P2Pool Watchdog | Starting [update_from_output()]"); - PubP2poolApi::update_from_output( - &pub_api, - &output_parse, - &output_pub, - start.elapsed(), - &process, - ); - - // Read [local] API - debug!("P2Pool Watchdog | Attempting [local] API file read"); - if let Ok(string) = Self::path_to_string(&api_path_local, ProcessName::P2pool) { - // Deserialize - if let Ok(local_api) = PrivP2poolLocalApi::from_str(&string) { - // Update the structs. - PubP2poolApi::update_from_local(&pub_api, local_api); - } - } - // If more than 1 minute has passed, read the other API files. - if lock!(gui_api).tick >= 60 { - debug!("P2Pool Watchdog | Attempting [network] & [pool] API file read"); - if let (Ok(network_api), Ok(pool_api)) = ( - Self::path_to_string(&api_path_network, ProcessName::P2pool), - Self::path_to_string(&api_path_pool, ProcessName::P2pool), - ) { - if let (Ok(network_api), Ok(pool_api)) = ( - PrivP2poolNetworkApi::from_str(&network_api), - PrivP2poolPoolApi::from_str(&pool_api), - ) { - PubP2poolApi::update_from_network_pool(&pub_api, network_api, pool_api); - lock!(gui_api).tick = 0; - } - } - } - - // 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 - if elapsed < 900 { - let sleep = (900 - elapsed) as u64; - debug!( - "P2Pool Watchdog | END OF LOOP - Tick: [{}/60] - Sleeping for [{}]ms...", - lock!(gui_api).tick, - sleep - ); - sleep!(sleep); - } else { - debug!( - "P2Pool Watchdog | END OF LOOP - Tick: [{}/60] Not sleeping!", - lock!(gui_api).tick - ); - } - } - - // 5. If loop broke, we must be done here. - info!("P2Pool Watchdog | Watchdog thread exiting... Goodbye!"); - } - - //---------------------------------------------------------------------------------------------------- XMRig specific, most functions are very similar to P2Pool's - #[cold] - #[inline(never)] - // If processes are started with [sudo] on macOS, they must also - // be killed with [sudo] (even if I have a direct handle to it as the - // parent process...!). This is only needed on macOS, not Linux. - fn sudo_kill(pid: u32, sudo: &Arc>) -> bool { - // Spawn [sudo] to execute [kill] on the given [pid] - let mut child = std::process::Command::new("sudo") - .args(["--stdin", "kill", "-9", &pid.to_string()]) - .stdin(Stdio::piped()) - .spawn() - .unwrap(); - - // Write the [sudo] password to STDIN. - let mut stdin = child.stdin.take().unwrap(); - use std::io::Write; - if let Err(e) = writeln!(stdin, "{}\n", lock!(sudo).pass) { - error!("Sudo Kill | STDIN error: {}", e); - } - - // Return exit code of [sudo/kill]. - child.wait().unwrap().success() - } - - #[cold] - #[inline(never)] - // Just sets some signals for the watchdog thread to pick up on. - pub fn stop_xmrig(helper: &Arc>) { - info!("XMRig | Attempting to stop..."); - lock2!(helper, xmrig).signal = ProcessSignal::Stop; - lock2!(helper, xmrig).state = ProcessState::Middle; - } - - #[cold] - #[inline(never)] - // The "restart frontend" to a "frontend" function. - // Basically calls to kill the current xmrig, waits a little, then starts the below function in a a new thread, then exit. - pub fn restart_xmrig( - helper: &Arc>, - state: &crate::disk::Xmrig, - path: &Path, - sudo: Arc>, - ) { - info!("XMRig | Attempting to restart..."); - lock2!(helper, xmrig).signal = ProcessSignal::Restart; - lock2!(helper, xmrig).state = ProcessState::Middle; - - let helper = Arc::clone(helper); - let state = state.clone(); - let path = path.to_path_buf(); - // This thread lives to wait, start xmrig then die. - thread::spawn(move || { - while lock2!(helper, xmrig).state != ProcessState::Waiting { - warn!("XMRig | Want to restart but process is still alive, waiting..."); - sleep!(1000); - } - // Ok, process is not alive, start the new one! - info!("XMRig | Old process seems dead, starting new one!"); - Self::start_xmrig(&helper, &state, &path, sudo); - }); - info!("XMRig | Restart ... OK"); - } - - #[cold] - #[inline(never)] - pub fn start_xmrig( - helper: &Arc>, - state: &crate::disk::Xmrig, - path: &Path, - sudo: Arc>, - ) { - lock2!(helper, xmrig).state = ProcessState::Middle; - - let (args, api_ip_port) = Self::build_xmrig_args_and_mutate_img(helper, state, path); - - // Print arguments & user settings to console - crate::disk::print_dash(&format!("XMRig | Launch arguments: {:#?}", args)); - info!("XMRig | Using path: [{}]", path.display()); - - // Spawn watchdog thread - let process = Arc::clone(&lock!(helper).xmrig); - let gui_api = Arc::clone(&lock!(helper).gui_api_xmrig); - let pub_api = Arc::clone(&lock!(helper).pub_api_xmrig); - let path = path.to_path_buf(); - thread::spawn(move || { - Self::spawn_xmrig_watchdog(process, gui_api, pub_api, args, path, sudo, api_ip_port); - }); - } - - #[cold] - #[inline(never)] - // Takes in some [State/Xmrig] and parses it to build the actual command arguments. - // Returns the [Vec] of actual arguments, and mutates the [ImgXmrig] for the main GUI thread - // It returns a value... and mutates a deeply nested passed argument... this is some pretty bad code... - pub fn build_xmrig_args_and_mutate_img( - helper: &Arc>, - state: &crate::disk::Xmrig, - path: &std::path::Path, - ) -> (Vec, String) { - let mut args = Vec::with_capacity(500); - let mut api_ip = String::with_capacity(15); - let mut api_port = String::with_capacity(5); - let path = path.to_path_buf(); - // The actual binary we're executing is [sudo], technically - // the XMRig path is just an argument to sudo, so add it. - // Before that though, add the ["--prompt"] flag and set it - // to emptyness so that it doesn't show up in the output. - if cfg!(unix) { - args.push(r#"--prompt="#.to_string()); - args.push("--".to_string()); - args.push(path.display().to_string()); - } - - // [Simple] - if state.simple { - // Build the xmrig argument - let rig = if state.simple_rig.is_empty() { - GUPAX_VERSION_UNDERSCORE.to_string() - } else { - state.simple_rig.clone() - }; // Rig name - args.push("--url".to_string()); - args.push("127.0.0.1:3333".to_string()); // Local P2Pool (the default) - args.push("--threads".to_string()); - args.push(state.current_threads.to_string()); // Threads - args.push("--user".to_string()); - args.push(rig); // Rig name - args.push("--no-color".to_string()); // No color - args.push("--http-host".to_string()); - args.push("127.0.0.1".to_string()); // HTTP API IP - args.push("--http-port".to_string()); - args.push("18088".to_string()); // HTTP API Port - if state.pause != 0 { - args.push("--pause-on-active".to_string()); - args.push(state.pause.to_string()); - } // Pause on active - *lock2!(helper, img_xmrig) = ImgXmrig { - threads: state.current_threads.to_string(), - url: "127.0.0.1:3333 (Local P2Pool)".to_string(), - }; - api_ip = "127.0.0.1".to_string(); - api_port = "18088".to_string(); - - // [Advanced] - } else { - // Overriding command arguments - if !state.arguments.is_empty() { - // This parses the input and attempts to fill out - // the [ImgXmrig]... This is pretty bad code... - let mut last = ""; - let lock = lock!(helper); - let mut xmrig_image = lock!(lock.img_xmrig); - for arg in state.arguments.split_whitespace() { - match last { - "--threads" => xmrig_image.threads = arg.to_string(), - "--url" => xmrig_image.url = arg.to_string(), - "--http-host" => { - api_ip = if arg == "localhost" { - "127.0.0.1".to_string() - } else { - arg.to_string() - } - } - "--http-port" => api_port = arg.to_string(), - _ => (), - } - args.push(if arg == "localhost" { - "127.0.0.1".to_string() - } else { - arg.to_string() - }); - last = arg; - } - // Else, build the argument - } else { - // XMRig doesn't understand [localhost] - let ip = if state.ip == "localhost" || state.ip.is_empty() { - "127.0.0.1" - } else { - &state.ip - }; - api_ip = if state.api_ip == "localhost" || state.api_ip.is_empty() { - "127.0.0.1".to_string() - } else { - state.api_ip.to_string() - }; - api_port = if state.api_port.is_empty() { - "18088".to_string() - } else { - state.api_port.to_string() - }; - let url = format!("{}:{}", ip, state.port); // Combine IP:Port into one string - args.push("--user".to_string()); - args.push(state.address.clone()); // Wallet - args.push("--threads".to_string()); - args.push(state.current_threads.to_string()); // Threads - args.push("--rig-id".to_string()); - args.push(state.rig.to_string()); // Rig ID - args.push("--url".to_string()); - args.push(url.clone()); // IP/Port - args.push("--http-host".to_string()); - args.push(api_ip.to_string()); // HTTP API IP - args.push("--http-port".to_string()); - args.push(api_port.to_string()); // HTTP API Port - args.push("--no-color".to_string()); // No color escape codes - if state.tls { - args.push("--tls".to_string()); - } // TLS - if state.keepalive { - args.push("--keepalive".to_string()); - } // Keepalive - if state.pause != 0 { - args.push("--pause-on-active".to_string()); - args.push(state.pause.to_string()); - } // Pause on active - *lock2!(helper, img_xmrig) = ImgXmrig { - url, - threads: state.current_threads.to_string(), - }; - } - } - (args, format!("{}:{}", api_ip, api_port)) - } - - // We actually spawn [sudo] on Unix, with XMRig being the argument. - #[cfg(target_family = "unix")] - fn create_xmrig_cmd_unix(args: Vec, path: PathBuf) -> portable_pty::CommandBuilder { - let mut cmd = portable_pty::cmdbuilder::CommandBuilder::new("sudo"); - cmd.args(args); - cmd.cwd(path.as_path().parent().unwrap()); - cmd - } - - // Gupax should be admin on Windows, so just spawn XMRig normally. - #[cfg(target_os = "windows")] - fn create_xmrig_cmd_windows(args: Vec, path: PathBuf) -> portable_pty::CommandBuilder { - let mut cmd = portable_pty::cmdbuilder::CommandBuilder::new(path.clone()); - cmd.args(args); - cmd.cwd(path.as_path().parent().unwrap()); - cmd - } - - #[cold] - #[inline(never)] - // The XMRig watchdog. Spawns 1 OS thread for reading a PTY (STDOUT+STDERR), and combines the [Child] with a PTY so STDIN actually works. - // This isn't actually async, a tokio runtime is unfortunately needed because [Hyper] is an async library (HTTP API calls) - #[tokio::main] - #[allow(clippy::await_holding_lock)] - async fn spawn_xmrig_watchdog( - process: Arc>, - gui_api: Arc>, - pub_api: Arc>, - args: Vec, - path: std::path::PathBuf, - sudo: Arc>, - mut api_ip_port: String, - ) { - // 1a. Create PTY - debug!("XMRig | Creating PTY..."); - let pty = portable_pty::native_pty_system(); - let pair = pty - .openpty(portable_pty::PtySize { - rows: 100, - cols: 1000, - pixel_width: 0, - pixel_height: 0, - }) - .unwrap(); - // 1b. Create command - debug!("XMRig | Creating command..."); - #[cfg(target_os = "windows")] - let cmd = Self::create_xmrig_cmd_windows(args, path); - #[cfg(target_family = "unix")] - let cmd = Self::create_xmrig_cmd_unix(args, path); - // 1c. Create child - debug!("XMRig | Creating child..."); - let child_pty = arc_mut!(pair.slave.spawn_command(cmd).unwrap()); - drop(pair.slave); - - let mut stdin = pair.master.take_writer().unwrap(); - - // 2. Input [sudo] pass, wipe, then drop. - if cfg!(unix) { - debug!("XMRig | Inputting [sudo] and wiping..."); - // a) Sleep to wait for [sudo]'s non-echo prompt (on Unix). - // this prevents users pass from showing up in the STDOUT. - sleep!(3000); - if let Err(e) = writeln!(stdin, "{}", lock!(sudo).pass) { - error!("XMRig | Sudo STDIN error: {}", e); - }; - SudoState::wipe(&sudo); - - // b) Reset GUI STDOUT just in case. - debug!("XMRig | Clearing GUI output..."); - lock!(gui_api).output.clear(); - } - - // 3. Set process state - debug!("XMRig | Setting process state..."); - let mut lock = lock!(process); - lock.state = ProcessState::NotMining; - lock.signal = ProcessSignal::None; - lock.start = Instant::now(); - let reader = pair.master.try_clone_reader().unwrap(); // Get STDOUT/STDERR before moving the PTY - drop(lock); - - // 4. Spawn PTY read thread - debug!("XMRig | Spawning PTY read thread..."); - let output_parse = Arc::clone(&lock!(process).output_parse); - let output_pub = Arc::clone(&lock!(process).output_pub); - thread::spawn(move || { - Self::read_pty_xmrig(output_parse, output_pub, reader); - }); - let output_parse = Arc::clone(&lock!(process).output_parse); - let output_pub = Arc::clone(&lock!(process).output_pub); - - let client: hyper::Client = - hyper::Client::builder().build(hyper::client::HttpConnector::new()); - let start = lock!(process).start; - let api_uri = { - if !api_ip_port.ends_with('/') { - api_ip_port.push('/'); - } - "http://".to_owned() + &api_ip_port + XMRIG_API_URI - }; - info!("XMRig | Final API URI: {}", api_uri); - - // Reset stats before loop - *lock!(pub_api) = PubXmrigApi::new(); - *lock!(gui_api) = PubXmrigApi::new(); - - // 5. Loop as watchdog - info!("XMRig | Entering watchdog mode... woof!"); - loop { - // Set timer - let now = Instant::now(); - debug!("XMRig Watchdog | ----------- Start of loop -----------"); - - // Check if the process secretly died without us knowing :) - if let Ok(Some(code)) = lock!(child_pty).try_wait() { - debug!("XMRig Watchdog | Process secretly died on us! Getting exit status..."); - let exit_status = match code.success() { - true => { - lock!(process).state = ProcessState::Dead; - "Successful" - } - false => { - lock!(process).state = ProcessState::Failed; - "Failed" - } - }; - let uptime = HumanTime::into_human(start.elapsed()); - info!( - "XMRig | Stopped ... Uptime was: [{}], Exit status: [{}]", - uptime, exit_status - ); - if let Err(e) = writeln!( - lock!(gui_api).output, - "{}\nXMRig stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n", - HORI_CONSOLE, - uptime, - exit_status, - HORI_CONSOLE - ) { - error!( - "XMRig Watchdog | GUI Uptime/Exit status write failed: {}", - e - ); - } - lock!(process).signal = ProcessSignal::None; - debug!("XMRig Watchdog | Secret dead process reap OK, breaking"); - break; - } - - // Stop on [Stop/Restart] SIGNAL - let signal = lock!(process).signal; - if signal == ProcessSignal::Stop || signal == ProcessSignal::Restart { - debug!("XMRig Watchdog | Stop/Restart SIGNAL caught"); - // macOS requires [sudo] again to kill [XMRig] - if cfg!(target_os = "macos") { - // If we're at this point, that means the user has - // entered their [sudo] pass again, after we wiped it. - // So, we should be able to find it in our [Arc>]. - Self::sudo_kill(lock!(child_pty).process_id().unwrap(), &sudo); - // And... wipe it again (only if we're stopping full). - // If we're restarting, the next start will wipe it for us. - if signal != ProcessSignal::Restart { - SudoState::wipe(&sudo); - } - } else if let Err(e) = lock!(child_pty).kill() { - error!("XMRig Watchdog | Kill error: {}", e); - } - let exit_status = match lock!(child_pty).wait() { - Ok(e) => { - let mut process = lock!(process); - if e.success() { - if process.signal == ProcessSignal::Stop { - process.state = ProcessState::Dead; - } - "Successful" - } else { - if process.signal == ProcessSignal::Stop { - process.state = ProcessState::Failed; - } - "Failed" - } - } - _ => { - let mut process = lock!(process); - if process.signal == ProcessSignal::Stop { - process.state = ProcessState::Failed; - } - "Unknown Error" - } - }; - let uptime = HumanTime::into_human(start.elapsed()); - info!( - "XMRig | Stopped ... Uptime was: [{}], Exit status: [{}]", - uptime, exit_status - ); - if let Err(e) = writeln!( - lock!(gui_api).output, - "{}\nXMRig stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n", - HORI_CONSOLE, - uptime, - exit_status, - HORI_CONSOLE - ) { - error!( - "XMRig Watchdog | GUI Uptime/Exit status write failed: {}", - e - ); - } - let mut process = lock!(process); - match process.signal { - ProcessSignal::Stop => process.signal = ProcessSignal::None, - ProcessSignal::Restart => process.state = ProcessState::Waiting, - _ => (), - } - debug!("XMRig Watchdog | Stop/Restart SIGNAL done, breaking"); - break; - } - - // Check vector of user input - { - let mut lock = lock!(process); - if !lock.input.is_empty() { - let input = std::mem::take(&mut lock.input); - for line in input { - if line.is_empty() { - continue; - } - debug!( - "XMRig Watchdog | User input not empty, writing to STDIN: [{}]", - line - ); - #[cfg(target_os = "windows")] - if let Err(e) = write!(stdin, "{}\r\n", line) { - error!("XMRig Watchdog | STDIN error: {}", e); - } - #[cfg(target_family = "unix")] - if let Err(e) = writeln!(stdin, "{}", line) { - error!("XMRig Watchdog | STDIN error: {}", e); - } - // Flush. - if let Err(e) = stdin.flush() { - error!("XMRig Watchdog | STDIN flush error: {}", e); - } - } - } - } - // Check if logs need resetting - debug!("XMRig Watchdog | Attempting GUI log reset check"); - { - let mut lock = lock!(gui_api); - Self::check_reset_gui_output(&mut lock.output, ProcessName::Xmrig); - } - // Always update from output - debug!("XMRig Watchdog | Starting [update_from_output()]"); - PubXmrigApi::update_from_output( - &pub_api, - &output_pub, - &output_parse, - start.elapsed(), - &process, - ); - - // Send an HTTP API request - debug!("XMRig Watchdog | Attempting HTTP API request..."); - if let Ok(priv_api) = PrivXmrigApi::request_xmrig_api(client.clone(), &api_uri).await { - debug!("XMRig Watchdog | HTTP API request OK, attempting [update_from_priv()]"); - PubXmrigApi::update_from_priv(&pub_api, priv_api); - } else { - warn!( - "XMRig Watchdog | Could not send HTTP API request to: {}", - api_uri - ); - } - - // 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 - if elapsed < 900 { - let sleep = (900 - elapsed) as u64; - debug!( - "XMRig Watchdog | END OF LOOP - Sleeping for [{}]ms...", - sleep - ); - sleep!(sleep); - } else { - debug!("XMRig Watchdog | END OF LOOP - Not sleeping!"); - } - } - - // 5. If loop broke, we must be done here. - info!("XMRig Watchdog | Watchdog thread exiting... Goodbye!"); - } - - //---------------------------------------------------------------------------------------------------- The "helper" - #[inline(always)] // called once - fn update_pub_sys_from_sysinfo( - sysinfo: &sysinfo::System, - pub_sys: &mut Sys, - pid: &sysinfo::Pid, - helper: &Helper, - max_threads: usize, - ) { - let gupax_uptime = helper.uptime.to_string(); - let cpu = &sysinfo.cpus()[0]; - let gupax_cpu_usage = format!( - "{:.2}%", - sysinfo.process(*pid).unwrap().cpu_usage() / (max_threads as f32) - ); - let gupax_memory_used_mb = - HumanNumber::from_u64(sysinfo.process(*pid).unwrap().memory() / 1_000_000); - let gupax_memory_used_mb = format!("{} megabytes", gupax_memory_used_mb); - let system_cpu_model = format!("{} ({}MHz)", cpu.brand(), cpu.frequency()); - let system_memory = { - let used = (sysinfo.used_memory() as f64) / 1_000_000_000.0; - let total = (sysinfo.total_memory() as f64) / 1_000_000_000.0; - format!("{:.3} GB / {:.3} GB", used, total) - }; - let system_cpu_usage = { - let mut total: f32 = 0.0; - for cpu in sysinfo.cpus() { - total += cpu.cpu_usage(); - } - format!("{:.2}%", total / (max_threads as f32)) - }; - *pub_sys = Sys { - gupax_uptime, - gupax_cpu_usage, - gupax_memory_used_mb, - system_cpu_usage, - system_memory, - system_cpu_model, - }; - } - - #[cold] - #[inline(never)] - // The "helper" thread. Syncs data between threads here and the GUI. - #[allow(clippy::await_holding_lock)] - pub fn spawn_helper( - helper: &Arc>, - mut sysinfo: sysinfo::System, - pid: sysinfo::Pid, - max_threads: usize, - ) { - // The ordering of these locks is _very_ important. They MUST be in sync with how the main GUI thread locks stuff - // or a deadlock will occur given enough time. They will eventually both want to lock the [Arc] the other - // thread is already locking. Yes, I figured this out the hard way, hence the vast amount of debug!() messages. - // Example of different order (BAD!): - // - // GUI Main -> locks [p2pool] first - // Helper -> locks [gui_api_p2pool] first - // GUI Status Tab -> tries to lock [gui_api_p2pool] -> CAN'T - // Helper -> tries to lock [p2pool] -> CAN'T - // - // These two threads are now in a deadlock because both - // are trying to access locks the other one already has. - // - // The locking order here must be in the same chronological - // order as the main GUI thread (top to bottom). - - let helper = Arc::clone(helper); - let lock = lock!(helper); - let p2pool = Arc::clone(&lock.p2pool); - let xmrig = Arc::clone(&lock.xmrig); - let pub_sys = Arc::clone(&lock.pub_sys); - let gui_api_p2pool = Arc::clone(&lock.gui_api_p2pool); - let gui_api_xmrig = Arc::clone(&lock.gui_api_xmrig); - let pub_api_p2pool = Arc::clone(&lock.pub_api_p2pool); - let pub_api_xmrig = Arc::clone(&lock.pub_api_xmrig); - drop(lock); - - let sysinfo_cpu = sysinfo::CpuRefreshKind::everything(); - let sysinfo_processes = sysinfo::ProcessRefreshKind::new().with_cpu(); - - thread::spawn(move || { - info!("Helper | Hello from helper thread! Entering loop where I will spend the rest of my days..."); - // Begin loop - loop { - // 1. Loop init timestamp - let start = Instant::now(); - debug!("Helper | ----------- Start of loop -----------"); - - // Ignore the invasive [debug!()] messages on the right side of the code. - // The reason why they are there are so that it's extremely easy to track - // down the culprit of an [Arc] deadlock. I know, they're ugly. - - // 2. Lock... EVERYTHING! - let mut lock = lock!(helper); - debug!("Helper | Locking (1/8) ... [helper]"); - let p2pool = lock!(p2pool); - debug!("Helper | Locking (2/8) ... [p2pool]"); - let xmrig = lock!(xmrig); - debug!("Helper | Locking (3/8) ... [xmrig]"); - let mut lock_pub_sys = lock!(pub_sys); - debug!("Helper | Locking (4/8) ... [pub_sys]"); - let mut gui_api_p2pool = lock!(gui_api_p2pool); - debug!("Helper | Locking (5/8) ... [gui_api_p2pool]"); - let mut gui_api_xmrig = lock!(gui_api_xmrig); - debug!("Helper | Locking (6/8) ... [gui_api_xmrig]"); - let mut pub_api_p2pool = lock!(pub_api_p2pool); - debug!("Helper | Locking (7/8) ... [pub_api_p2pool]"); - let mut pub_api_xmrig = lock!(pub_api_xmrig); - debug!("Helper | Locking (8/8) ... [pub_api_xmrig]"); - // Calculate Gupax's uptime always. - lock.uptime = HumanTime::into_human(lock.instant.elapsed()); - // If [P2Pool] is alive... - if p2pool.is_alive() { - debug!("Helper | P2Pool is alive! Running [combine_gui_pub_api()]"); - PubP2poolApi::combine_gui_pub_api(&mut gui_api_p2pool, &mut pub_api_p2pool); - } else { - debug!("Helper | P2Pool is dead! Skipping..."); - } - // If [XMRig] is alive... - if xmrig.is_alive() { - debug!("Helper | XMRig is alive! Running [combine_gui_pub_api()]"); - PubXmrigApi::combine_gui_pub_api(&mut gui_api_xmrig, &mut pub_api_xmrig); - } else { - debug!("Helper | XMRig is dead! Skipping..."); - } - - // 2. Selectively refresh [sysinfo] for only what we need (better performance). - sysinfo.refresh_cpu_specifics(sysinfo_cpu); - debug!("Helper | Sysinfo refresh (1/3) ... [cpu]"); - sysinfo.refresh_processes_specifics(sysinfo_processes); - debug!("Helper | Sysinfo refresh (2/3) ... [processes]"); - sysinfo.refresh_memory(); - debug!("Helper | Sysinfo refresh (3/3) ... [memory]"); - debug!("Helper | Sysinfo OK, running [update_pub_sys_from_sysinfo()]"); - Self::update_pub_sys_from_sysinfo( - &sysinfo, - &mut lock_pub_sys, - &pid, - &lock, - max_threads, - ); - - // 3. Drop... (almost) EVERYTHING... IN REVERSE! - drop(lock_pub_sys); - debug!("Helper | Unlocking (1/8) ... [pub_sys]"); - drop(xmrig); - debug!("Helper | Unlocking (2/8) ... [xmrig]"); - drop(p2pool); - debug!("Helper | Unlocking (3/8) ... [p2pool]"); - drop(pub_api_xmrig); - debug!("Helper | Unlocking (4/8) ... [pub_api_xmrig]"); - drop(pub_api_p2pool); - debug!("Helper | Unlocking (5/8) ... [pub_api_p2pool]"); - drop(gui_api_xmrig); - debug!("Helper | Unlocking (6/8) ... [gui_api_xmrig]"); - drop(gui_api_p2pool); - debug!("Helper | Unlocking (7/8) ... [gui_api_p2pool]"); - drop(lock); - debug!("Helper | Unlocking (8/8) ... [helper]"); - - // 4. Calculate if we should sleep or not. - // If we should sleep, how long? - let elapsed = start.elapsed().as_millis(); - if elapsed < 1000 { - // Casting from u128 to u64 should be safe here, because [elapsed] - // is less than 1000, meaning it can fit into a u64 easy. - let sleep = (1000 - elapsed) as u64; - debug!("Helper | END OF LOOP - Sleeping for [{}]ms...", sleep); - sleep!(sleep); - } else { - debug!("Helper | END OF LOOP - Not sleeping!"); - } - - // 5. End loop - } - }); - } -} - -//---------------------------------------------------------------------------------------------------- [ImgP2pool] -// A static "image" of data that P2Pool started with. -// This is just a snapshot of the user data when they initially started P2Pool. -// Created by [start_p2pool()] and return to the main GUI thread where it will store it. -// No need for an [Arc] since the Helper thread doesn't need this information. -#[derive(Debug, Clone)] -pub struct ImgP2pool { - pub mini: String, // Did the user start on the mini-chain? - pub address: String, // What address is the current p2pool paying out to? (This gets shortened to [4xxxxx...xxxxxx]) - pub host: String, // What monerod are we using? - pub rpc: String, // What is the RPC port? - pub zmq: String, // What is the ZMQ port? - pub out_peers: String, // How many out-peers? - pub in_peers: String, // How many in-peers? -} - -impl Default for ImgP2pool { - fn default() -> Self { - Self::new() - } -} - -impl ImgP2pool { - pub fn new() -> Self { - Self { - mini: String::from("???"), - address: String::from("???"), - host: String::from("???"), - rpc: String::from("???"), - zmq: String::from("???"), - out_peers: String::from("???"), - in_peers: String::from("???"), - } - } -} - -//---------------------------------------------------------------------------------------------------- Public P2Pool API -// Helper/GUI threads both have a copy of this, Helper updates -// the GUI's version on a 1-second interval from the private data. -#[derive(Debug, Clone, PartialEq)] -pub struct PubP2poolApi { - // Output - pub output: String, - // Uptime - pub uptime: HumanTime, - // These are manually parsed from the STDOUT. - pub payouts: u128, - pub payouts_hour: f64, - pub payouts_day: f64, - pub payouts_month: f64, - pub xmr: f64, - pub xmr_hour: f64, - pub xmr_day: f64, - pub xmr_month: f64, - // Local API - pub hashrate_15m: HumanNumber, - pub hashrate_1h: HumanNumber, - pub hashrate_24h: HumanNumber, - pub shares_found: HumanNumber, - pub average_effort: HumanNumber, - pub current_effort: HumanNumber, - pub connections: HumanNumber, - // The API needs a raw ints to go off of and - // there's not a good way to access it without doing weird - // [Arc] shenanigans, so some raw ints are stored here. - pub user_p2pool_hashrate_u64: u64, - pub p2pool_difficulty_u64: u64, - pub monero_difficulty_u64: u64, - pub p2pool_hashrate_u64: u64, - pub monero_hashrate_u64: u64, - // Tick. Every loop this gets incremented. - // At 60, it indicated we should read the below API files. - pub tick: u8, - // Network API - pub monero_difficulty: HumanNumber, // e.g: [15,000,000] - pub monero_hashrate: HumanNumber, // e.g: [1.000 GH/s] - pub hash: String, // Current block hash - pub height: HumanNumber, - pub reward: AtomicUnit, - // Pool API - pub p2pool_difficulty: HumanNumber, - pub p2pool_hashrate: HumanNumber, - pub miners: HumanNumber, // Current amount of miners on P2Pool sidechain - // Mean (calculated in functions, not serialized) - pub solo_block_mean: HumanTime, // Time it would take the user to find a solo block - pub p2pool_block_mean: HumanTime, // Time it takes the P2Pool sidechain to find a block - pub p2pool_share_mean: HumanTime, // Time it would take the user to find a P2Pool share - // Percent - pub p2pool_percent: HumanNumber, // Percentage of P2Pool hashrate capture of overall Monero hashrate. - pub user_p2pool_percent: HumanNumber, // How much percent the user's hashrate accounts for in P2Pool. - pub user_monero_percent: HumanNumber, // How much percent the user's hashrate accounts for in all of Monero hashrate. -} - -impl Default for PubP2poolApi { - fn default() -> Self { - Self::new() - } -} - -impl PubP2poolApi { - pub fn new() -> Self { - Self { - output: String::new(), - uptime: HumanTime::new(), - payouts: 0, - payouts_hour: 0.0, - payouts_day: 0.0, - payouts_month: 0.0, - xmr: 0.0, - xmr_hour: 0.0, - xmr_day: 0.0, - xmr_month: 0.0, - hashrate_15m: HumanNumber::unknown(), - hashrate_1h: HumanNumber::unknown(), - hashrate_24h: HumanNumber::unknown(), - shares_found: HumanNumber::unknown(), - average_effort: HumanNumber::unknown(), - current_effort: HumanNumber::unknown(), - connections: HumanNumber::unknown(), - tick: 0, - user_p2pool_hashrate_u64: 0, - p2pool_difficulty_u64: 0, - monero_difficulty_u64: 0, - p2pool_hashrate_u64: 0, - monero_hashrate_u64: 0, - monero_difficulty: HumanNumber::unknown(), - monero_hashrate: HumanNumber::unknown(), - hash: String::from("???"), - height: HumanNumber::unknown(), - reward: AtomicUnit::new(), - p2pool_difficulty: HumanNumber::unknown(), - p2pool_hashrate: HumanNumber::unknown(), - miners: HumanNumber::unknown(), - solo_block_mean: HumanTime::new(), - p2pool_block_mean: HumanTime::new(), - p2pool_share_mean: HumanTime::new(), - p2pool_percent: HumanNumber::unknown(), - user_p2pool_percent: HumanNumber::unknown(), - user_monero_percent: HumanNumber::unknown(), - } - } - - #[inline] - // The issue with just doing [gui_api = pub_api] is that values get overwritten. - // This doesn't matter for any of the values EXCEPT for the output, so we must - // manually append it instead of overwriting. - // This is used in the "helper" thread. - fn combine_gui_pub_api(gui_api: &mut Self, pub_api: &mut Self) { - let mut output = std::mem::take(&mut gui_api.output); - let buf = std::mem::take(&mut pub_api.output); - if !buf.is_empty() { - output.push_str(&buf); - } - *gui_api = Self { - output, - tick: std::mem::take(&mut gui_api.tick), - ..pub_api.clone() - }; - } - - #[inline] - // 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) -> (u128 /* payout count */, f64 /* total xmr */) { - let iter = P2POOL_REGEX.payout.find_iter(output); - let mut sum: f64 = 0.0; - let mut count: u128 = 0; - for i in iter { - if let Some(word) = P2POOL_REGEX.payout_float.find(i.as_str()) { - match word.as_str().parse::() { - Ok(num) => { - sum += num; - count += 1; - } - Err(e) => error!("P2Pool | Total XMR sum calculation error: [{}]", e), - } - } - } - (count, sum) - } - - // Mutate "watchdog"'s [PubP2poolApi] with data the process output. - fn update_from_output( - public: &Arc>, - output_parse: &Arc>, - output_pub: &Arc>, - elapsed: std::time::Duration, - process: &Arc>, - ) { - // 1. Take the process's current output buffer and combine it with Pub (if not empty) - let mut output_pub = lock!(output_pub); - if !output_pub.is_empty() { - lock!(public) - .output - .push_str(&std::mem::take(&mut *output_pub)); - } - - // 2. Parse the full STDOUT - let mut output_parse = lock!(output_parse); - let (payouts_new, xmr_new) = Self::calc_payouts_and_xmr(&output_parse); - // Check for "SYNCHRONIZED" only if we aren't already. - if lock!(process).state == ProcessState::Syncing { - // How many times the word was captured. - let synchronized_captures = P2POOL_REGEX.synchronized.find_iter(&output_parse).count(); - - // If P2Pool receives shares before syncing, it will start mining on its own sidechain. - // In this instance, we technically are "synced" on block 1 and P2Pool will print "SYNCHRONIZED" - // although, that doesn't necessarily mean we're synced on main/mini-chain. - // - // So, if we find a `next block = 1`, that means we - // must look for at least 2 instances of "SYNCHRONIZED", - // one for the sidechain, one for main/mini. - if P2POOL_REGEX.next_height_1.is_match(&output_parse) { - if synchronized_captures > 1 { - lock!(process).state = ProcessState::Alive; - } - } else if synchronized_captures > 0 { - // if there is no `next block = 1`, fallback to - // just finding 1 instance of "SYNCHRONIZED". - lock!(process).state = ProcessState::Alive; - } - } - // 3. Throw away [output_parse] - output_parse.clear(); - drop(output_parse); - // 4. Add to current values - let mut public = lock!(public); - let (payouts, xmr) = (public.payouts + payouts_new, public.xmr + xmr_new); - - // 5. Calculate hour/day/month given elapsed time - let elapsed_as_secs_f64 = elapsed.as_secs_f64(); - // Payouts - let per_sec = (payouts as f64) / elapsed_as_secs_f64; - let payouts_hour = (per_sec * 60.0) * 60.0; - let payouts_day = payouts_hour * 24.0; - let payouts_month = payouts_day * 30.0; - // Total XMR - let per_sec = xmr / elapsed_as_secs_f64; - let xmr_hour = (per_sec * 60.0) * 60.0; - let xmr_day = xmr_hour * 24.0; - let xmr_month = xmr_day * 30.0; - - if payouts_new != 0 { - debug!( - "P2Pool Watchdog | New [Payout] found in output ... {}", - payouts_new - ); - debug!("P2Pool Watchdog | Total [Payout] should be ... {}", payouts); - debug!( - "P2Pool Watchdog | Correct [Payout per] should be ... [{}/hour, {}/day, {}/month]", - payouts_hour, payouts_day, payouts_month - ); - } - if xmr_new != 0.0 { - debug!( - "P2Pool Watchdog | New [XMR mined] found in output ... {}", - xmr_new - ); - debug!("P2Pool Watchdog | Total [XMR mined] should be ... {}", xmr); - debug!("P2Pool Watchdog | Correct [XMR mined per] should be ... [{}/hour, {}/day, {}/month]", xmr_hour, xmr_day, xmr_month); - } - - // 6. Mutate the struct with the new info - *public = Self { - uptime: HumanTime::into_human(elapsed), - payouts, - xmr, - payouts_hour, - payouts_day, - payouts_month, - xmr_hour, - xmr_day, - xmr_month, - ..std::mem::take(&mut *public) - }; - } - - // Mutate [PubP2poolApi] with data from a [PrivP2poolLocalApi] and the process output. - fn update_from_local(public: &Arc>, local: PrivP2poolLocalApi) { - let mut public = lock!(public); - *public = Self { - hashrate_15m: HumanNumber::from_u64(local.hashrate_15m), - hashrate_1h: HumanNumber::from_u64(local.hashrate_1h), - hashrate_24h: HumanNumber::from_u64(local.hashrate_24h), - shares_found: HumanNumber::from_u64(local.shares_found), - average_effort: HumanNumber::to_percent(local.average_effort), - current_effort: HumanNumber::to_percent(local.current_effort), - connections: HumanNumber::from_u32(local.connections), - user_p2pool_hashrate_u64: local.hashrate_1h, - ..std::mem::take(&mut *public) - }; - } - - // Mutate [PubP2poolApi] with data from a [PrivP2pool(Network|Pool)Api]. - fn update_from_network_pool( - public: &Arc>, - net: PrivP2poolNetworkApi, - pool: PrivP2poolPoolApi, - ) { - let user_hashrate = lock!(public).user_p2pool_hashrate_u64; // The user's total P2Pool hashrate - let monero_difficulty = net.difficulty; - let monero_hashrate = monero_difficulty / MONERO_BLOCK_TIME_IN_SECONDS; - let p2pool_hashrate = pool.pool_statistics.hashRate; - let p2pool_difficulty = p2pool_hashrate * P2POOL_BLOCK_TIME_IN_SECONDS; - // These [0] checks prevent dividing by 0 (it [panic!()]s) - let p2pool_block_mean; - let user_p2pool_percent; - if p2pool_hashrate == 0 { - p2pool_block_mean = HumanTime::new(); - user_p2pool_percent = HumanNumber::unknown(); - } else { - p2pool_block_mean = HumanTime::into_human(std::time::Duration::from_secs( - monero_difficulty / p2pool_hashrate, - )); - let f = (user_hashrate as f64 / p2pool_hashrate as f64) * 100.0; - user_p2pool_percent = HumanNumber::from_f64_to_percent_6_point(f); - }; - let p2pool_percent; - let user_monero_percent; - if monero_hashrate == 0 { - p2pool_percent = HumanNumber::unknown(); - user_monero_percent = HumanNumber::unknown(); - } else { - let f = (p2pool_hashrate as f64 / monero_hashrate as f64) * 100.0; - p2pool_percent = HumanNumber::from_f64_to_percent_6_point(f); - let f = (user_hashrate as f64 / monero_hashrate as f64) * 100.0; - user_monero_percent = HumanNumber::from_f64_to_percent_6_point(f); - }; - let solo_block_mean; - let p2pool_share_mean; - if user_hashrate == 0 { - solo_block_mean = HumanTime::new(); - p2pool_share_mean = HumanTime::new(); - } else { - solo_block_mean = HumanTime::into_human(std::time::Duration::from_secs( - monero_difficulty / user_hashrate, - )); - p2pool_share_mean = HumanTime::into_human(std::time::Duration::from_secs( - p2pool_difficulty / user_hashrate, - )); - } - let mut public = lock!(public); - *public = Self { - p2pool_difficulty_u64: p2pool_difficulty, - monero_difficulty_u64: monero_difficulty, - p2pool_hashrate_u64: p2pool_hashrate, - monero_hashrate_u64: monero_hashrate, - monero_difficulty: HumanNumber::from_u64(monero_difficulty), - monero_hashrate: HumanNumber::from_u64_to_gigahash_3_point(monero_hashrate), - hash: net.hash, - height: HumanNumber::from_u32(net.height), - reward: AtomicUnit::from_u64(net.reward), - p2pool_difficulty: HumanNumber::from_u64(p2pool_difficulty), - p2pool_hashrate: HumanNumber::from_u64_to_megahash_3_point(p2pool_hashrate), - miners: HumanNumber::from_u32(pool.pool_statistics.miners), - solo_block_mean, - p2pool_block_mean, - p2pool_share_mean, - p2pool_percent, - user_p2pool_percent, - user_monero_percent, - ..std::mem::take(&mut *public) - }; - } - - #[inline] - pub fn calculate_share_or_block_time(hashrate: u64, difficulty: u64) -> HumanTime { - if hashrate == 0 { - HumanTime::new() - } else { - HumanTime::from_u64(difficulty / hashrate) - } - } - - #[inline] - pub fn calculate_dominance(my_hashrate: u64, global_hashrate: u64) -> HumanNumber { - if global_hashrate == 0 { - HumanNumber::unknown() - } else { - let f = (my_hashrate as f64 / global_hashrate as f64) * 100.0; - HumanNumber::from_f64_to_percent_6_point(f) - } - } - - pub const fn calculate_tick_bar(&self) -> &'static str { - // The stars are reduced by one because it takes a frame to render the stats. - // We want 0 stars at the same time stats are rendered, so it looks a little off here. - match self.tick { - 1 => "[ ]", - 2 => "[* ]", - 3 => "[** ]", - 4 => "[*** ]", - 5 => "[**** ]", - 6 => "[***** ]", - 7 => "[****** ]", - 8 => "[******* ]", - 9 => "[******** ]", - 10 => "[********* ]", - 11 => "[********** ]", - 12 => "[*********** ]", - 13 => "[************ ]", - 14 => "[************* ]", - 15 => "[************** ]", - 16 => "[*************** ]", - 17 => "[**************** ]", - 18 => "[***************** ]", - 19 => "[****************** ]", - 20 => "[******************* ]", - 21 => "[******************** ]", - 22 => "[********************* ]", - 23 => "[********************** ]", - 24 => "[*********************** ]", - 25 => "[************************ ]", - 26 => "[************************* ]", - 27 => "[************************** ]", - 28 => "[*************************** ]", - 29 => "[**************************** ]", - 30 => "[***************************** ]", - 31 => "[****************************** ]", - 32 => "[******************************* ]", - 33 => "[******************************** ]", - 34 => "[********************************* ]", - 35 => "[********************************** ]", - 36 => "[*********************************** ]", - 37 => "[************************************ ]", - 38 => "[************************************* ]", - 39 => "[************************************** ]", - 40 => "[*************************************** ]", - 41 => "[**************************************** ]", - 42 => "[***************************************** ]", - 43 => "[****************************************** ]", - 44 => "[******************************************* ]", - 45 => "[******************************************** ]", - 46 => "[********************************************* ]", - 47 => "[********************************************** ]", - 48 => "[*********************************************** ]", - 49 => "[************************************************ ]", - 50 => "[************************************************* ]", - 51 => "[************************************************** ]", - 52 => "[*************************************************** ]", - 53 => "[**************************************************** ]", - 54 => "[***************************************************** ]", - 55 => "[****************************************************** ]", - 56 => "[******************************************************* ]", - 57 => "[******************************************************** ]", - 58 => "[********************************************************* ]", - 59 => "[********************************************************** ]", - 60 => "[*********************************************************** ]", - _ => "[************************************************************]", - } - } -} - -//---------------------------------------------------------------------------------------------------- Private P2Pool "Local" Api -// This matches directly to P2Pool's [local/stratum] 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, Clone, Copy)] -struct PrivP2poolLocalApi { - hashrate_15m: u64, - hashrate_1h: u64, - hashrate_24h: u64, - shares_found: u64, - average_effort: f32, - current_effort: f32, - connections: u32, // This is a `uint32_t` in `p2pool` -} - -impl Default for PrivP2poolLocalApi { - fn default() -> Self { - Self::new() - } -} - -impl PrivP2poolLocalApi { - fn new() -> Self { - Self { - hashrate_15m: 0, - hashrate_1h: 0, - hashrate_24h: 0, - shares_found: 0, - average_effort: 0.0, - current_effort: 0.0, - connections: 0, - } - } - - // Deserialize the above [String] into a [PrivP2poolApi] - fn from_str(string: &str) -> std::result::Result { - match serde_json::from_str::(string) { - Ok(a) => Ok(a), - Err(e) => { - warn!("P2Pool Local API | Could not deserialize API data: {}", e); - Err(e) - } - } - } -} - -//---------------------------------------------------------------------------------------------------- Private P2Pool "Network" API -// This matches P2Pool's [network/stats] JSON API file. -#[derive(Debug, Serialize, Deserialize, Clone)] -struct PrivP2poolNetworkApi { - difficulty: u64, - hash: String, - height: u32, - reward: u64, - timestamp: u32, -} - -impl Default for PrivP2poolNetworkApi { - fn default() -> Self { - Self::new() - } -} - -impl PrivP2poolNetworkApi { - fn new() -> Self { - Self { - difficulty: 0, - hash: String::from("???"), - height: 0, - reward: 0, - timestamp: 0, - } - } - - fn from_str(string: &str) -> std::result::Result { - match serde_json::from_str::(string) { - Ok(a) => Ok(a), - Err(e) => { - warn!("P2Pool Network API | Could not deserialize API data: {}", e); - Err(e) - } - } - } -} - -//---------------------------------------------------------------------------------------------------- Private P2Pool "Pool" API -// This matches P2Pool's [pool/stats] JSON API file. -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] -struct PrivP2poolPoolApi { - pool_statistics: PoolStatistics, -} - -impl Default for PrivP2poolPoolApi { - fn default() -> Self { - Self::new() - } -} - -impl PrivP2poolPoolApi { - fn new() -> Self { - Self { - pool_statistics: PoolStatistics::new(), - } - } - - fn from_str(string: &str) -> std::result::Result { - match serde_json::from_str::(string) { - Ok(a) => Ok(a), - Err(e) => { - warn!("P2Pool Pool API | Could not deserialize API data: {}", e); - Err(e) - } - } - } -} - -#[allow(non_snake_case)] -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] -struct PoolStatistics { - hashRate: u64, - miners: u32, -} -impl Default for PoolStatistics { - fn default() -> Self { - Self::new() - } -} -impl PoolStatistics { - fn new() -> Self { - Self { - hashRate: 0, - miners: 0, - } - } -} - -//---------------------------------------------------------------------------------------------------- [ImgXmrig] -#[derive(Debug, Clone)] -pub struct ImgXmrig { - pub threads: String, - pub url: String, -} - -impl Default for ImgXmrig { - fn default() -> Self { - Self::new() - } -} - -impl ImgXmrig { - pub fn new() -> Self { - Self { - threads: "???".to_string(), - url: "???".to_string(), - } - } -} - -//---------------------------------------------------------------------------------------------------- Public XMRig API -#[derive(Debug, Clone)] -pub struct PubXmrigApi { - pub output: String, - pub uptime: HumanTime, - pub worker_id: String, - pub resources: HumanNumber, - pub hashrate: HumanNumber, - pub diff: HumanNumber, - pub accepted: HumanNumber, - pub rejected: HumanNumber, - - pub hashrate_raw: f32, -} - -impl Default for PubXmrigApi { - fn default() -> Self { - Self::new() - } -} - -impl PubXmrigApi { - pub fn new() -> Self { - Self { - output: String::new(), - uptime: HumanTime::new(), - worker_id: "???".to_string(), - resources: HumanNumber::unknown(), - hashrate: HumanNumber::unknown(), - diff: HumanNumber::unknown(), - accepted: HumanNumber::unknown(), - rejected: HumanNumber::unknown(), - hashrate_raw: 0.0, - } - } - - #[inline] - fn combine_gui_pub_api(gui_api: &mut Self, pub_api: &mut Self) { - let output = std::mem::take(&mut gui_api.output); - let buf = std::mem::take(&mut pub_api.output); - *gui_api = Self { - output, - ..std::mem::take(pub_api) - }; - if !buf.is_empty() { - gui_api.output.push_str(&buf); - } - } - - // This combines the buffer from the PTY thread [output_pub] - // with the actual [PubApiXmrig] output field. - fn update_from_output( - public: &Arc>, - output_parse: &Arc>, - output_pub: &Arc>, - elapsed: std::time::Duration, - process: &Arc>, - ) { - // 1. Take the process's current output buffer and combine it with Pub (if not empty) - let mut output_pub = lock!(output_pub); - - { - let mut public = lock!(public); - if !output_pub.is_empty() { - public.output.push_str(&std::mem::take(&mut *output_pub)); - } - // Update uptime - public.uptime = HumanTime::into_human(elapsed); - } - - // 2. Check for "new job"/"no active...". - let mut output_parse = lock!(output_parse); - if XMRIG_REGEX.new_job.is_match(&output_parse) { - lock!(process).state = ProcessState::Alive; - } else if XMRIG_REGEX.not_mining.is_match(&output_parse) { - lock!(process).state = ProcessState::NotMining; - } - - // 3. Throw away [output_parse] - output_parse.clear(); - drop(output_parse); - } - - // Formats raw private data into ready-to-print human readable version. - fn update_from_priv(public: &Arc>, private: PrivXmrigApi) { - let mut public = lock!(public); - let hashrate_raw = match private.hashrate.total.first() { - Some(Some(h)) => *h, - _ => 0.0, - }; - - *public = Self { - worker_id: private.worker_id, - resources: HumanNumber::from_load(private.resources.load_average), - hashrate: HumanNumber::from_hashrate(private.hashrate.total), - diff: HumanNumber::from_u128(private.connection.diff), - accepted: HumanNumber::from_u128(private.connection.accepted), - rejected: HumanNumber::from_u128(private.connection.rejected), - hashrate_raw, - ..std::mem::take(&mut *public) - } - } -} - -//---------------------------------------------------------------------------------------------------- Private XMRig API -// This matches to some JSON stats in the HTTP call [summary], -// 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, Clone)] -struct PrivXmrigApi { - worker_id: String, - resources: Resources, - connection: Connection, - hashrate: Hashrate, -} - -impl PrivXmrigApi { - fn new() -> Self { - Self { - worker_id: String::new(), - resources: Resources::new(), - connection: Connection::new(), - hashrate: Hashrate::new(), - } - } - - #[inline] - // Send an HTTP request to XMRig's API, serialize it into [Self] and return it - async fn request_xmrig_api( - client: hyper::Client, - api_uri: &str, - ) -> std::result::Result { - let request = hyper::Request::builder() - .method("GET") - .uri(api_uri) - .body(hyper::Body::empty())?; - let response = tokio::time::timeout( - std::time::Duration::from_millis(500), - client.request(request), - ) - .await?; - let body = hyper::body::to_bytes(response?.body_mut()).await?; - Ok(serde_json::from_slice::(&body)?) - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] -struct Resources { - load_average: [Option; 3], -} -impl Resources { - fn new() -> Self { - Self { - load_average: [Some(0.0), Some(0.0), Some(0.0)], - } - } -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -struct Connection { - diff: u128, - accepted: u128, - rejected: u128, -} -impl Connection { - fn new() -> Self { - Self { - diff: 0, - accepted: 0, - rejected: 0, - } - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] -struct Hashrate { - total: [Option; 3], -} -impl Hashrate { - fn new() -> Self { - Self { - total: [Some(0.0), Some(0.0), Some(0.0)], - } - } -} - -//---------------------------------------------------------------------------------------------------- TESTS -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn reset_gui_output() { - let max = crate::helper::GUI_OUTPUT_LEEWAY; - let mut string = String::with_capacity(max); - for _ in 0..=max { - string.push('0'); - } - crate::Helper::check_reset_gui_output(&mut string, crate::ProcessName::P2pool); - // Some text gets added, so just check for less than 500 bytes. - assert!(string.len() < 500); - } - - #[test] - fn combine_gui_pub_p2pool_api() { - use crate::helper::PubP2poolApi; - let mut gui_api = PubP2poolApi::new(); - let mut pub_api = PubP2poolApi::new(); - pub_api.payouts = 1; - pub_api.payouts_hour = 2.0; - pub_api.payouts_day = 3.0; - pub_api.payouts_month = 4.0; - pub_api.xmr = 1.0; - pub_api.xmr_hour = 2.0; - pub_api.xmr_day = 3.0; - pub_api.xmr_month = 4.0; - println!("BEFORE - GUI_API: {:#?}\nPUB_API: {:#?}", gui_api, pub_api); - assert_ne!(gui_api, pub_api); - PubP2poolApi::combine_gui_pub_api(&mut gui_api, &mut pub_api); - println!("AFTER - GUI_API: {:#?}\nPUB_API: {:#?}", gui_api, pub_api); - assert_eq!(gui_api, pub_api); - pub_api.xmr = 2.0; - PubP2poolApi::combine_gui_pub_api(&mut gui_api, &mut pub_api); - assert_eq!(gui_api, pub_api); - assert_eq!(gui_api.xmr, 2.0); - assert_eq!(pub_api.xmr, 2.0); - } - - #[test] - fn calc_payouts_and_xmr_from_output_p2pool() { - use crate::helper::PubP2poolApi; - use std::sync::{Arc, Mutex}; - let public = Arc::new(Mutex::new(PubP2poolApi::new())); - let output_parse = Arc::new(Mutex::new(String::from( - r#"payout of 5.000000000001 XMR in block 1111 - payout of 5.000000000001 XMR in block 1112 - payout of 5.000000000001 XMR in block 1113"#, - ))); - let output_pub = Arc::new(Mutex::new(String::new())); - let elapsed = std::time::Duration::from_secs(60); - let process = Arc::new(Mutex::new(Process::new( - ProcessName::P2pool, - "".to_string(), - PathBuf::new(), - ))); - PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); - let public = public.lock().unwrap(); - println!("{:#?}", public); - assert_eq!(public.payouts, 3); - assert_eq!(public.payouts_hour, 180.0); - assert_eq!(public.payouts_day, 4320.0); - assert_eq!(public.payouts_month, 129600.0); - assert_eq!(public.xmr, 15.000000000003); - assert_eq!(public.xmr_hour, 900.00000000018); - assert_eq!(public.xmr_day, 21600.00000000432); - assert_eq!(public.xmr_month, 648000.0000001296); - } - - #[test] - fn set_p2pool_synchronized() { - use crate::helper::PubP2poolApi; - use std::sync::{Arc, Mutex}; - let public = Arc::new(Mutex::new(PubP2poolApi::new())); - let output_parse = Arc::new(Mutex::new(String::from( - r#"payout of 5.000000000001 XMR in block 1111 - NOTICE 2021-12-27 21:42:17.2008 SideChain SYNCHRONIZED - payout of 5.000000000001 XMR in block 1113"#, - ))); - let output_pub = Arc::new(Mutex::new(String::new())); - let elapsed = std::time::Duration::from_secs(60); - let process = Arc::new(Mutex::new(Process::new( - ProcessName::P2pool, - "".to_string(), - PathBuf::new(), - ))); - - // It only gets checked if we're `Syncing`. - process.lock().unwrap().state = ProcessState::Syncing; - PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); - println!("{:#?}", process); - assert!(process.lock().unwrap().state == ProcessState::Alive); - } - - #[test] - fn p2pool_synchronized_false_positive() { - use crate::helper::PubP2poolApi; - use std::sync::{Arc, Mutex}; - let public = Arc::new(Mutex::new(PubP2poolApi::new())); - - // The SideChain that is "SYNCHRONIZED" in this output is - // probably not main/mini, but the sidechain started on height 1, - // so this should _not_ trigger alive state. - let output_parse = Arc::new(Mutex::new(String::from( - r#"payout of 5.000000000001 XMR in block 1111 - SideChain new chain tip: next height = 1 - NOTICE 2021-12-27 21:42:17.2008 SideChain SYNCHRONIZED - payout of 5.000000000001 XMR in block 1113"#, - ))); - let output_pub = Arc::new(Mutex::new(String::new())); - let elapsed = std::time::Duration::from_secs(60); - let process = Arc::new(Mutex::new(Process::new( - ProcessName::P2pool, - "".to_string(), - PathBuf::new(), - ))); - - // It only gets checked if we're `Syncing`. - process.lock().unwrap().state = ProcessState::Syncing; - PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); - println!("{:#?}", process); - assert!(process.lock().unwrap().state == ProcessState::Syncing); // still syncing - } - - #[test] - fn p2pool_synchronized_double_synchronized() { - use crate::helper::PubP2poolApi; - use std::sync::{Arc, Mutex}; - let public = Arc::new(Mutex::new(PubP2poolApi::new())); - - // The 1st SideChain that is "SYNCHRONIZED" in this output is - // the sidechain started on height 1, but there is another one - // which means the real main/mini is probably synced, - // so this _should_ trigger alive state. - let output_parse = Arc::new(Mutex::new(String::from( - r#"payout of 5.000000000001 XMR in block 1111 - SideChain new chain tip: next height = 1 - NOTICE 2021-12-27 21:42:17.2008 SideChain SYNCHRONIZED - payout of 5.000000000001 XMR in block 1113 - NOTICE 2021-12-27 21:42:17.2100 SideChain SYNCHRONIZED"#, - ))); - let output_pub = Arc::new(Mutex::new(String::new())); - let elapsed = std::time::Duration::from_secs(60); - let process = Arc::new(Mutex::new(Process::new( - ProcessName::P2pool, - "".to_string(), - PathBuf::new(), - ))); - - // It only gets checked if we're `Syncing`. - process.lock().unwrap().state = ProcessState::Syncing; - PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); - println!("{:#?}", process); - assert!(process.lock().unwrap().state == ProcessState::Alive); - } - - #[test] - fn update_pub_p2pool_from_local_network_pool() { - use crate::helper::PoolStatistics; - use crate::helper::PrivP2poolLocalApi; - use crate::helper::PrivP2poolNetworkApi; - use crate::helper::PrivP2poolPoolApi; - use crate::helper::PubP2poolApi; - use std::sync::{Arc, Mutex}; - let public = Arc::new(Mutex::new(PubP2poolApi::new())); - let local = PrivP2poolLocalApi { - hashrate_15m: 10_000, - hashrate_1h: 20_000, - hashrate_24h: 30_000, - shares_found: 1000, - average_effort: 100.000, - current_effort: 200.000, - connections: 1234, - }; - let network = PrivP2poolNetworkApi { - difficulty: 300_000_000_000, - hash: "asdf".to_string(), - height: 1234, - reward: 2345, - timestamp: 3456, - }; - let pool = PrivP2poolPoolApi { - pool_statistics: PoolStatistics { - hashRate: 1_000_000, // 1 MH/s - miners: 1_000, - }, - }; - // Update Local - PubP2poolApi::update_from_local(&public, local); - let p = public.lock().unwrap(); - println!("AFTER LOCAL: {:#?}", p); - assert_eq!(p.hashrate_15m.to_string(), "10,000"); - assert_eq!(p.hashrate_1h.to_string(), "20,000"); - assert_eq!(p.hashrate_24h.to_string(), "30,000"); - assert_eq!(p.shares_found.to_string(), "1,000"); - assert_eq!(p.average_effort.to_string(), "100.00%"); - assert_eq!(p.current_effort.to_string(), "200.00%"); - assert_eq!(p.connections.to_string(), "1,234"); - assert_eq!(p.user_p2pool_hashrate_u64, 20000); - drop(p); - // Update Network + Pool - PubP2poolApi::update_from_network_pool(&public, network, pool); - let p = public.lock().unwrap(); - println!("AFTER NETWORK+POOL: {:#?}", p); - assert_eq!(p.monero_difficulty.to_string(), "300,000,000,000"); - assert_eq!(p.monero_hashrate.to_string(), "2.500 GH/s"); - assert_eq!(p.hash.to_string(), "asdf"); - assert_eq!(p.height.to_string(), "1,234"); - assert_eq!(p.reward.to_u64(), 2345); - assert_eq!(p.p2pool_difficulty.to_string(), "10,000,000"); - assert_eq!(p.p2pool_hashrate.to_string(), "1.000 MH/s"); - assert_eq!(p.miners.to_string(), "1,000"); - assert_eq!( - p.solo_block_mean.to_string(), - "5 months, 21 days, 9 hours, 52 minutes" - ); - assert_eq!( - p.p2pool_block_mean.to_string(), - "3 days, 11 hours, 20 minutes" - ); - assert_eq!(p.p2pool_share_mean.to_string(), "8 minutes, 20 seconds"); - assert_eq!(p.p2pool_percent.to_string(), "0.040000%"); - assert_eq!(p.user_p2pool_percent.to_string(), "2.000000%"); - assert_eq!(p.user_monero_percent.to_string(), "0.000800%"); - drop(p); - } - - #[test] - fn set_xmrig_mining() { - use crate::helper::PubXmrigApi; - use std::sync::{Arc, Mutex}; - let public = Arc::new(Mutex::new(PubXmrigApi::new())); - let output_parse = Arc::new(Mutex::new(String::from( - "[2022-02-12 12:49:30.311] net no active pools, stop mining", - ))); - let output_pub = Arc::new(Mutex::new(String::new())); - let elapsed = std::time::Duration::from_secs(60); - let process = Arc::new(Mutex::new(Process::new( - ProcessName::Xmrig, - "".to_string(), - PathBuf::new(), - ))); - - process.lock().unwrap().state = ProcessState::Alive; - PubXmrigApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); - println!("{:#?}", process); - assert!(process.lock().unwrap().state == ProcessState::NotMining); - - let output_parse = Arc::new(Mutex::new(String::from("[2022-02-12 12:49:30.311] net new job from 192.168.2.1:3333 diff 402K algo rx/0 height 2241142 (11 tx)"))); - PubXmrigApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); - assert!(process.lock().unwrap().state == ProcessState::Alive); - } - - #[test] - fn serde_priv_p2pool_local_api() { - let data = r#"{ - "hashrate_15m": 12, - "hashrate_1h": 11111, - "hashrate_24h": 468967, - "total_hashes": 2019283840922394082390, - "shares_found": 289037, - "average_effort": 915.563, - "current_effort": 129.297, - "connections": 123, - "incoming_connections": 96 - }"#; - let priv_api = crate::helper::PrivP2poolLocalApi::from_str(data).unwrap(); - let json = serde_json::ser::to_string_pretty(&priv_api).unwrap(); - println!("{}", json); - let data_after_ser = r#"{ - "hashrate_15m": 12, - "hashrate_1h": 11111, - "hashrate_24h": 468967, - "shares_found": 289037, - "average_effort": 915.563, - "current_effort": 129.297, - "connections": 123 -}"#; - assert_eq!(data_after_ser, json) - } - - #[test] - fn serde_priv_p2pool_network_api() { - let data = r#"{ - "difficulty": 319028180924, - "hash": "22ae1b83d727bb2ff4efc17b485bc47bc8bf5e29a7b3af65baf42213ac70a39b", - "height": 2776576, - "reward": 600499860000, - "timestamp": 1670953659 - }"#; - let priv_api = crate::helper::PrivP2poolNetworkApi::from_str(data).unwrap(); - let json = serde_json::ser::to_string_pretty(&priv_api).unwrap(); - println!("{}", json); - let data_after_ser = r#"{ - "difficulty": 319028180924, - "hash": "22ae1b83d727bb2ff4efc17b485bc47bc8bf5e29a7b3af65baf42213ac70a39b", - "height": 2776576, - "reward": 600499860000, - "timestamp": 1670953659 -}"#; - assert_eq!(data_after_ser, json) - } - - #[test] - fn serde_priv_p2pool_pool_api() { - let data = r#"{ - "pool_list": ["pplns"], - "pool_statistics": { - "hashRate": 10225772, - "miners": 713, - "totalHashes": 487463929193948, - "lastBlockFoundTime": 1670453228, - "lastBlockFound": 2756570, - "totalBlocksFound": 4 - } - }"#; - let priv_api = crate::helper::PrivP2poolPoolApi::from_str(data).unwrap(); - let json = serde_json::ser::to_string_pretty(&priv_api).unwrap(); - println!("{}", json); - let data_after_ser = r#"{ - "pool_statistics": { - "hashRate": 10225772, - "miners": 713 - } -}"#; - assert_eq!(data_after_ser, json) - } - - #[test] - fn serde_priv_xmrig_api() { - let data = r#"{ - "id": "6226e3sd0cd1a6es", - "worker_id": "hinto", - "uptime": 123, - "restricted": true, - "resources": { - "memory": { - "free": 123, - "total": 123123, - "resident_set_memory": 123123123 - }, - "load_average": [10.97, 10.58, 10.47], - "hardware_concurrency": 12 - }, - "features": ["api", "asm", "http", "hwloc", "tls", "opencl", "cuda"], - "results": { - "diff_current": 123, - "shares_good": 123, - "shares_total": 123, - "avg_time": 123, - "avg_time_ms": 123, - "hashes_total": 123, - "best": [123, 123, 123, 13, 123, 123, 123, 123, 123, 123], - "error_log": [] - }, - "algo": "rx/0", - "connection": { - "pool": "localhost:3333", - "ip": "127.0.0.1", - "uptime": 123, - "uptime_ms": 123, - "ping": 0, - "failures": 0, - "tls": null, - "tls-fingerprint": null, - "algo": "rx/0", - "diff": 123, - "accepted": 123, - "rejected": 123, - "avg_time": 123, - "avg_time_ms": 123, - "hashes_total": 123, - "error_log": [] - }, - "version": "6.18.0", - "kind": "miner", - "ua": "XMRig/6.18.0 (Linux x86_64) libuv/2.0.0-dev gcc/10.2.1", - "cpu": { - "brand": "blah blah blah", - "family": 1, - "model": 2, - "stepping": 0, - "proc_info": 123, - "aes": true, - "avx2": true, - "x64": true, - "64_bit": true, - "l2": 123123, - "l3": 123123, - "cores": 12, - "threads": 24, - "packages": 1, - "nodes": 1, - "backend": "hwloc/2.8.0a1-git", - "msr": "ryzen_19h", - "assembly": "ryzen", - "arch": "x86_64", - "flags": ["aes", "vaes", "avx", "avx2", "bmi2", "osxsave", "pdpe1gb", "sse2", "ssse3", "sse4.1", "popcnt", "cat_l3"] - }, - "donate_level": 0, - "paused": false, - "algorithms": ["cn/1", "cn/2", "cn/r", "cn/fast", "cn/half", "cn/xao", "cn/rto", "cn/rwz", "cn/zls", "cn/double", "cn/ccx", "cn-lite/1", "cn-heavy/0", "cn-heavy/tube", "cn-heavy/xhv", "cn-pico", "cn-pico/tlo", "cn/upx2", "rx/0", "rx/wow", "rx/arq", "rx/graft", "rx/sfx", "rx/keva", "argon2/chukwa", "argon2/chukwav2", "argon2/ninja", "astrobwt", "astrobwt/v2", "ghostrider"], - "hashrate": { - "total": [111.11, 111.11, 111.11], - "highest": 111.11, - "threads": [ - [111.11, 111.11, 111.11] - ] - }, - "hugepages": true - }"#; - use crate::helper::PrivXmrigApi; - let priv_api = serde_json::from_str::(data).unwrap(); - let json = serde_json::ser::to_string_pretty(&priv_api).unwrap(); - println!("{}", json); - let data_after_ser = r#"{ - "worker_id": "hinto", - "resources": { - "load_average": [ - 10.97, - 10.58, - 10.47 - ] - }, - "connection": { - "diff": 123, - "accepted": 123, - "rejected": 123 - }, - "hashrate": { - "total": [ - 111.11, - 111.11, - 111.11 - ] - } -}"#; - assert_eq!(data_after_ser, json) - } -} diff --git a/src/helper/mod.rs b/src/helper/mod.rs new file mode 100644 index 0000000..798c063 --- /dev/null +++ b/src/helper/mod.rs @@ -0,0 +1,955 @@ +// Gupax - GUI Uniting P2Pool And XMRig +// +// Copyright (c) 2022-2023 hinto-janai +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// This file represents the "helper" thread, which is the full separate thread +// that runs alongside the main [App] GUI thread. It exists for the entire duration +// of Gupax so that things can be handled without locking up the GUI thread. +// +// This thread is a continual 1 second loop, collecting available jobs on the +// way down and (if possible) asynchronously executing them at the very end. +// +// The main GUI thread will interface with this thread by mutating the Arc's +// found here, e.g: User clicks [Stop P2Pool] -> Arc is set +// indicating to this thread during its loop: "I should stop P2Pool!", e.g: +// +// if lock!(p2pool).signal == ProcessSignal::Stop { +// stop_p2pool(), +// } +// +// This also includes all things related to handling the child processes (P2Pool/XMRig): +// piping their stdout/stderr/stdin, accessing their APIs (HTTP + disk files), etc. + +//---------------------------------------------------------------------------------------------------- Import +use crate::helper::{ + p2pool::{ImgP2pool, PubP2poolApi}, + xmrig::{ImgXmrig, PubXmrigApi}, +}; +use crate::{constants::*, disk::gupax_p2pool_api::GupaxP2poolApi, human::*, macros::*}; +use log::*; +use std::path::Path; +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, + thread, + time::*, +}; +pub mod p2pool; +pub mod xmrig; + +//---------------------------------------------------------------------------------------------------- Constants +// The max amount of bytes of process output we are willing to +// hold in memory before it's too much and we need to reset. +const MAX_GUI_OUTPUT_BYTES: usize = 500_000; +// Just a little leeway so a reset will go off before the [String] allocates more memory. +const GUI_OUTPUT_LEEWAY: usize = MAX_GUI_OUTPUT_BYTES - 1000; + +// Some constants for generating hashrate/difficulty. +const MONERO_BLOCK_TIME_IN_SECONDS: u64 = 120; +const P2POOL_BLOCK_TIME_IN_SECONDS: u64 = 10; + +//---------------------------------------------------------------------------------------------------- [Helper] Struct +// A meta struct holding all the data that gets processed in this thread +pub struct Helper { + pub instant: Instant, // Gupax start as an [Instant] + pub uptime: HumanTime, // Gupax uptime formatting for humans + pub pub_sys: Arc>, // The public API for [sysinfo] that the [Status] tab reads from + pub p2pool: Arc>, // P2Pool process state + pub xmrig: Arc>, // XMRig process state + pub gui_api_p2pool: Arc>, // P2Pool API state (for GUI thread) + pub gui_api_xmrig: Arc>, // XMRig API state (for GUI thread) + pub img_p2pool: Arc>, // A static "image" of the data P2Pool started with + pub img_xmrig: Arc>, // A static "image" of the data XMRig started with + pub_api_p2pool: Arc>, // P2Pool API state (for Helper/P2Pool thread) + pub_api_xmrig: Arc>, // XMRig API state (for Helper/XMRig thread) + pub gupax_p2pool_api: Arc>, // +} + +// The communication between the data here and the GUI thread goes as follows: +// [GUI] <---> [Helper] <---> [Watchdog] <---> [Private Data only available here] +// +// Both [GUI] and [Helper] own their separate [Pub*Api] structs. +// Since P2Pool & XMRig will be updating their information out of sync, +// it's the helpers job to lock everything, and move the watchdog [Pub*Api]s +// on a 1-second interval into the [GUI]'s [Pub*Api] struct, atomically. + +//---------------------------------------------------------------------------------------------------- +#[derive(Debug, Clone)] +pub struct Sys { + pub gupax_uptime: String, + pub gupax_cpu_usage: String, + pub gupax_memory_used_mb: String, + pub system_cpu_model: String, + pub system_memory: String, + pub system_cpu_usage: String, +} + +impl Sys { + pub fn new() -> Self { + Self { + gupax_uptime: "0 seconds".to_string(), + gupax_cpu_usage: "???%".to_string(), + gupax_memory_used_mb: "??? megabytes".to_string(), + system_cpu_usage: "???%".to_string(), + system_memory: "???GB / ???GB".to_string(), + system_cpu_model: "???".to_string(), + } + } +} +impl Default for Sys { + fn default() -> Self { + Self::new() + } +} + +//---------------------------------------------------------------------------------------------------- [Process] Struct +// This holds all the state of a (child) process. +// The main GUI thread will use this to display console text, online state, etc. +#[derive(Debug)] +pub struct Process { + pub name: ProcessName, // P2Pool or XMRig? + pub state: ProcessState, // The state of the process (alive, dead, etc) + pub signal: ProcessSignal, // Did the user click [Start/Stop/Restart]? + // STDIN Problem: + // - User can input many many commands in 1 second + // - The process loop only processes every 1 second + // - If there is only 1 [String] holding the user input, + // the user could overwrite their last input before + // the loop even has a chance to process their last command + // STDIN Solution: + // - When the user inputs something, push it to a [Vec] + // - In the process loop, loop over every [Vec] element and + // send each one individually to the process stdin + // + pub input: Vec, + + // 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. + // 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]. + // "parse" contains the output that will be parsed, then tossed out. "pub" will be written to + // the same as parse, but it will be [swap()]'d by the "helper" thread into the GUIs [String]. + // The "helper" thread synchronizes this swap so that the data in here is moved there + // roughly once a second. GUI thread never touches this. + output_parse: Arc>, + output_pub: Arc>, + + // Start time of process. + start: std::time::Instant, +} + +//---------------------------------------------------------------------------------------------------- [Process] Impl +impl Process { + pub fn new(name: ProcessName, _args: String, _path: PathBuf) -> Self { + Self { + name, + state: ProcessState::Dead, + signal: ProcessSignal::None, + start: Instant::now(), + // stdin: Option::None, + // child: Option::None, + output_parse: arc_mut!(String::with_capacity(500)), + output_pub: arc_mut!(String::with_capacity(500)), + input: vec![String::new()], + } + } + + // Borrow a [&str], return an owned split collection + #[inline] + pub fn parse_args(args: &str) -> Vec { + args.split_whitespace().map(|s| s.to_owned()).collect() + } + + #[inline] + // Convenience functions + pub fn is_alive(&self) -> bool { + self.state == ProcessState::Alive + || self.state == ProcessState::Middle + || self.state == ProcessState::Syncing + || self.state == ProcessState::NotMining + } + + #[inline] + pub fn is_waiting(&self) -> bool { + self.state == ProcessState::Middle || self.state == ProcessState::Waiting + } + + #[inline] + pub fn is_syncing(&self) -> bool { + self.state == ProcessState::Syncing + } + + #[inline] + pub fn is_not_mining(&self) -> bool { + self.state == ProcessState::NotMining + } +} + +//---------------------------------------------------------------------------------------------------- [Process*] Enum +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +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 ([re]starting/stopping), YELLOW! + Waiting, // Process was successfully killed by a restart, and is ready to be started again, YELLOW! + + // Only for P2Pool, ORANGE. + Syncing, + + // Only for XMRig, ORANGE. + NotMining, +} + +impl Default for ProcessState { + fn default() -> Self { + Self::Dead + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum ProcessSignal { + None, + Start, + Stop, + Restart, +} + +impl Default for ProcessSignal { + fn default() -> Self { + Self::None + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum ProcessName { + P2pool, + Xmrig, +} + +impl std::fmt::Display for ProcessState { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:#?}", self) + } +} +impl std::fmt::Display for ProcessSignal { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:#?}", self) + } +} +impl std::fmt::Display for ProcessName { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + ProcessName::P2pool => write!(f, "P2Pool"), + ProcessName::Xmrig => write!(f, "XMRig"), + } + } +} + +//---------------------------------------------------------------------------------------------------- [Helper] +impl Helper { + //---------------------------------------------------------------------------------------------------- General Functions + #[allow(clippy::too_many_arguments)] + pub fn new( + instant: std::time::Instant, + pub_sys: Arc>, + p2pool: Arc>, + xmrig: Arc>, + gui_api_p2pool: Arc>, + gui_api_xmrig: Arc>, + img_p2pool: Arc>, + img_xmrig: Arc>, + gupax_p2pool_api: Arc>, + ) -> Self { + Self { + instant, + pub_sys, + uptime: HumanTime::into_human(instant.elapsed()), + pub_api_p2pool: arc_mut!(PubP2poolApi::new()), + pub_api_xmrig: arc_mut!(PubXmrigApi::new()), + // These are created when initializing [App], since it needs a handle to it as well + p2pool, + xmrig, + gui_api_p2pool, + gui_api_xmrig, + img_p2pool, + img_xmrig, + gupax_p2pool_api, + } + } + + // Reset output if larger than max bytes. + // This will also append a message showing it was reset. + fn check_reset_gui_output(output: &mut String, name: ProcessName) { + let len = output.len(); + if len > GUI_OUTPUT_LEEWAY { + info!( + "{} Watchdog | Output is nearing {} bytes, resetting!", + name, MAX_GUI_OUTPUT_BYTES + ); + let text = format!("{}\n{} GUI log is exceeding the maximum: {} bytes!\nI've reset the logs for you!\n{}\n\n\n\n", HORI_CONSOLE, name, MAX_GUI_OUTPUT_BYTES, HORI_CONSOLE); + output.clear(); + output.push_str(&text); + debug!("{} Watchdog | Resetting GUI output ... OK", name); + } else { + debug!( + "{} Watchdog | GUI output reset not needed! Current byte length ... {}", + name, len + ); + } + } + + // Read P2Pool/XMRig's API file to a [String]. + fn path_to_string( + path: &Path, + name: ProcessName, + ) -> std::result::Result { + match std::fs::read_to_string(path) { + Ok(s) => Ok(s), + Err(e) => { + warn!("{} API | [{}] read error: {}", name, path.display(), e); + Err(e) + } + } + } + //---------------------------------------------------------------------------------------------------- The "helper" + #[inline(always)] // called once + fn update_pub_sys_from_sysinfo( + sysinfo: &sysinfo::System, + pub_sys: &mut Sys, + pid: &sysinfo::Pid, + helper: &Helper, + max_threads: usize, + ) { + let gupax_uptime = helper.uptime.to_string(); + let cpu = &sysinfo.cpus()[0]; + let gupax_cpu_usage = format!( + "{:.2}%", + sysinfo.process(*pid).unwrap().cpu_usage() / (max_threads as f32) + ); + let gupax_memory_used_mb = + HumanNumber::from_u64(sysinfo.process(*pid).unwrap().memory() / 1_000_000); + let gupax_memory_used_mb = format!("{} megabytes", gupax_memory_used_mb); + let system_cpu_model = format!("{} ({}MHz)", cpu.brand(), cpu.frequency()); + let system_memory = { + let used = (sysinfo.used_memory() as f64) / 1_000_000_000.0; + let total = (sysinfo.total_memory() as f64) / 1_000_000_000.0; + format!("{:.3} GB / {:.3} GB", used, total) + }; + let system_cpu_usage = { + let mut total: f32 = 0.0; + for cpu in sysinfo.cpus() { + total += cpu.cpu_usage(); + } + format!("{:.2}%", total / (max_threads as f32)) + }; + *pub_sys = Sys { + gupax_uptime, + gupax_cpu_usage, + gupax_memory_used_mb, + system_cpu_usage, + system_memory, + system_cpu_model, + }; + } + + #[cold] + #[inline(never)] + // The "helper" thread. Syncs data between threads here and the GUI. + #[allow(clippy::await_holding_lock)] + pub fn spawn_helper( + helper: &Arc>, + mut sysinfo: sysinfo::System, + pid: sysinfo::Pid, + max_threads: usize, + ) { + // The ordering of these locks is _very_ important. They MUST be in sync with how the main GUI thread locks stuff + // or a deadlock will occur given enough time. They will eventually both want to lock the [Arc] the other + // thread is already locking. Yes, I figured this out the hard way, hence the vast amount of debug!() messages. + // Example of different order (BAD!): + // + // GUI Main -> locks [p2pool] first + // Helper -> locks [gui_api_p2pool] first + // GUI Status Tab -> tries to lock [gui_api_p2pool] -> CAN'T + // Helper -> tries to lock [p2pool] -> CAN'T + // + // These two threads are now in a deadlock because both + // are trying to access locks the other one already has. + // + // The locking order here must be in the same chronological + // order as the main GUI thread (top to bottom). + + let helper = Arc::clone(helper); + let lock = lock!(helper); + let p2pool = Arc::clone(&lock.p2pool); + let xmrig = Arc::clone(&lock.xmrig); + let pub_sys = Arc::clone(&lock.pub_sys); + let gui_api_p2pool = Arc::clone(&lock.gui_api_p2pool); + let gui_api_xmrig = Arc::clone(&lock.gui_api_xmrig); + let pub_api_p2pool = Arc::clone(&lock.pub_api_p2pool); + let pub_api_xmrig = Arc::clone(&lock.pub_api_xmrig); + drop(lock); + + let sysinfo_cpu = sysinfo::CpuRefreshKind::everything(); + let sysinfo_processes = sysinfo::ProcessRefreshKind::new().with_cpu(); + + thread::spawn(move || { + info!("Helper | Hello from helper thread! Entering loop where I will spend the rest of my days..."); + // Begin loop + loop { + // 1. Loop init timestamp + let start = Instant::now(); + debug!("Helper | ----------- Start of loop -----------"); + + // Ignore the invasive [debug!()] messages on the right side of the code. + // The reason why they are there are so that it's extremely easy to track + // down the culprit of an [Arc] deadlock. I know, they're ugly. + + // 2. Lock... EVERYTHING! + let mut lock = lock!(helper); + debug!("Helper | Locking (1/9) ... [helper]"); + let p2pool = lock!(p2pool); + debug!("Helper | Locking (2/9) ... [p2pool]"); + let xmrig = lock!(xmrig); + debug!("Helper | Locking (3/9) ... [xmrig]"); + let mut lock_pub_sys = lock!(pub_sys); + debug!("Helper | Locking (5/9) ... [pub_sys]"); + let mut gui_api_p2pool = lock!(gui_api_p2pool); + debug!("Helper | Locking (6/9) ... [gui_api_p2pool]"); + let mut gui_api_xmrig = lock!(gui_api_xmrig); + debug!("Helper | Locking (7/9) ... [gui_api_xmrig]"); + let mut pub_api_p2pool = lock!(pub_api_p2pool); + debug!("Helper | Locking (8/9) ... [pub_api_p2pool]"); + let mut pub_api_xmrig = lock!(pub_api_xmrig); + debug!("Helper | Locking (9/9) ... [pub_api_xmrig]"); + // Calculate Gupax's uptime always. + lock.uptime = HumanTime::into_human(lock.instant.elapsed()); + // If [P2Pool] is alive... + if p2pool.is_alive() { + debug!("Helper | P2Pool is alive! Running [combine_gui_pub_api()]"); + PubP2poolApi::combine_gui_pub_api(&mut gui_api_p2pool, &mut pub_api_p2pool); + } else { + debug!("Helper | P2Pool is dead! Skipping..."); + } + // If [XMRig] is alive... + if xmrig.is_alive() { + debug!("Helper | XMRig is alive! Running [combine_gui_pub_api()]"); + PubXmrigApi::combine_gui_pub_api(&mut gui_api_xmrig, &mut pub_api_xmrig); + } else { + debug!("Helper | XMRig is dead! Skipping..."); + } + + // 2. Selectively refresh [sysinfo] for only what we need (better performance). + sysinfo.refresh_cpu_specifics(sysinfo_cpu); + debug!("Helper | Sysinfo refresh (1/3) ... [cpu]"); + sysinfo.refresh_processes_specifics(sysinfo_processes); + debug!("Helper | Sysinfo refresh (2/3) ... [processes]"); + sysinfo.refresh_memory(); + debug!("Helper | Sysinfo refresh (3/3) ... [memory]"); + debug!("Helper | Sysinfo OK, running [update_pub_sys_from_sysinfo()]"); + Self::update_pub_sys_from_sysinfo( + &sysinfo, + &mut lock_pub_sys, + &pid, + &lock, + max_threads, + ); + + // 3. Drop... (almost) EVERYTHING... IN REVERSE! + drop(lock_pub_sys); + debug!("Helper | Unlocking (1/9) ... [pub_sys]"); + drop(xmrig); + debug!("Helper | Unlocking (2/9) ... [xmrig]"); + drop(p2pool); + debug!("Helper | Unlocking (3/9) ... [p2pool]"); + drop(pub_api_xmrig); + debug!("Helper | Unlocking (4/9) ... [pub_api_xmrig]"); + drop(pub_api_p2pool); + debug!("Helper | Unlocking (5/9) ... [pub_api_p2pool]"); + drop(gui_api_xmrig); + debug!("Helper | Unlocking (6/9) ... [gui_api_xmrig]"); + drop(gui_api_p2pool); + debug!("Helper | Unlocking (7/9) ... [gui_api_p2pool]"); + drop(lock); + debug!("Helper | Unlocking (8/9) ... [helper]"); + + // 4. Calculate if we should sleep or not. + // If we should sleep, how long? + let elapsed = start.elapsed().as_millis(); + if elapsed < 1000 { + // Casting from u128 to u64 should be safe here, because [elapsed] + // is less than 1000, meaning it can fit into a u64 easy. + let sleep = (1000 - elapsed) as u64; + debug!("Helper | END OF LOOP - Sleeping for [{}]ms...", sleep); + sleep!(sleep); + } else { + debug!("Helper | END OF LOOP - Not sleeping!"); + } + + // 5. End loop + } + }); + } +} + +//---------------------------------------------------------------------------------------------------- TESTS +#[cfg(test)] +mod test { + use crate::helper::p2pool::{PrivP2poolLocalApi, PrivP2poolNetworkApi}; + + use super::*; + + #[test] + fn reset_gui_output() { + let max = crate::helper::GUI_OUTPUT_LEEWAY; + let mut string = String::with_capacity(max); + for _ in 0..=max { + string.push('0'); + } + Helper::check_reset_gui_output(&mut string, ProcessName::P2pool); + // Some text gets added, so just check for less than 500 bytes. + assert!(string.len() < 500); + } + + #[test] + fn combine_gui_pub_p2pool_api() { + use crate::helper::PubP2poolApi; + let mut gui_api = PubP2poolApi::new(); + let mut pub_api = PubP2poolApi::new(); + pub_api.payouts = 1; + pub_api.payouts_hour = 2.0; + pub_api.payouts_day = 3.0; + pub_api.payouts_month = 4.0; + pub_api.xmr = 1.0; + pub_api.xmr_hour = 2.0; + pub_api.xmr_day = 3.0; + pub_api.xmr_month = 4.0; + println!("BEFORE - GUI_API: {:#?}\nPUB_API: {:#?}", gui_api, pub_api); + assert_ne!(gui_api, pub_api); + PubP2poolApi::combine_gui_pub_api(&mut gui_api, &mut pub_api); + println!("AFTER - GUI_API: {:#?}\nPUB_API: {:#?}", gui_api, pub_api); + assert_eq!(gui_api, pub_api); + pub_api.xmr = 2.0; + PubP2poolApi::combine_gui_pub_api(&mut gui_api, &mut pub_api); + assert_eq!(gui_api, pub_api); + assert_eq!(gui_api.xmr, 2.0); + assert_eq!(pub_api.xmr, 2.0); + } + + #[test] + fn calc_payouts_and_xmr_from_output_p2pool() { + use crate::helper::PubP2poolApi; + use std::sync::{Arc, Mutex}; + let public = Arc::new(Mutex::new(PubP2poolApi::new())); + let output_parse = Arc::new(Mutex::new(String::from( + r#"payout of 5.000000000001 XMR in block 1111 + payout of 5.000000000001 XMR in block 1112 + payout of 5.000000000001 XMR in block 1113"#, + ))); + let output_pub = Arc::new(Mutex::new(String::new())); + let elapsed = std::time::Duration::from_secs(60); + let process = Arc::new(Mutex::new(Process::new( + ProcessName::P2pool, + "".to_string(), + PathBuf::new(), + ))); + PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); + let public = public.lock().unwrap(); + println!("{:#?}", public); + assert_eq!(public.payouts, 3); + assert_eq!(public.payouts_hour, 180.0); + assert_eq!(public.payouts_day, 4320.0); + assert_eq!(public.payouts_month, 129600.0); + assert_eq!(public.xmr, 15.000000000003); + assert_eq!(public.xmr_hour, 900.00000000018); + assert_eq!(public.xmr_day, 21600.00000000432); + assert_eq!(public.xmr_month, 648000.0000001296); + } + + #[test] + fn set_p2pool_synchronized() { + use crate::helper::PubP2poolApi; + use std::sync::{Arc, Mutex}; + let public = Arc::new(Mutex::new(PubP2poolApi::new())); + let output_parse = Arc::new(Mutex::new(String::from( + r#"payout of 5.000000000001 XMR in block 1111 + NOTICE 2021-12-27 21:42:17.2008 SideChain SYNCHRONIZED + payout of 5.000000000001 XMR in block 1113"#, + ))); + let output_pub = Arc::new(Mutex::new(String::new())); + let elapsed = std::time::Duration::from_secs(60); + let process = Arc::new(Mutex::new(Process::new( + ProcessName::P2pool, + "".to_string(), + PathBuf::new(), + ))); + + // It only gets checked if we're `Syncing`. + process.lock().unwrap().state = ProcessState::Syncing; + PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); + println!("{:#?}", process); + assert!(process.lock().unwrap().state == ProcessState::Alive); + } + + #[test] + fn p2pool_synchronized_false_positive() { + use crate::helper::PubP2poolApi; + use std::sync::{Arc, Mutex}; + let public = Arc::new(Mutex::new(PubP2poolApi::new())); + + // The SideChain that is "SYNCHRONIZED" in this output is + // probably not main/mini, but the sidechain started on height 1, + // so this should _not_ trigger alive state. + let output_parse = Arc::new(Mutex::new(String::from( + r#"payout of 5.000000000001 XMR in block 1111 + SideChain new chain tip: next height = 1 + NOTICE 2021-12-27 21:42:17.2008 SideChain SYNCHRONIZED + payout of 5.000000000001 XMR in block 1113"#, + ))); + let output_pub = Arc::new(Mutex::new(String::new())); + let elapsed = std::time::Duration::from_secs(60); + let process = Arc::new(Mutex::new(Process::new( + ProcessName::P2pool, + "".to_string(), + PathBuf::new(), + ))); + + // It only gets checked if we're `Syncing`. + process.lock().unwrap().state = ProcessState::Syncing; + PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); + println!("{:#?}", process); + assert!(process.lock().unwrap().state == ProcessState::Syncing); // still syncing + } + + #[test] + fn p2pool_synchronized_double_synchronized() { + use crate::helper::PubP2poolApi; + use std::sync::{Arc, Mutex}; + let public = Arc::new(Mutex::new(PubP2poolApi::new())); + + // The 1st SideChain that is "SYNCHRONIZED" in this output is + // the sidechain started on height 1, but there is another one + // which means the real main/mini is probably synced, + // so this _should_ trigger alive state. + let output_parse = Arc::new(Mutex::new(String::from( + r#"payout of 5.000000000001 XMR in block 1111 + SideChain new chain tip: next height = 1 + NOTICE 2021-12-27 21:42:17.2008 SideChain SYNCHRONIZED + payout of 5.000000000001 XMR in block 1113 + NOTICE 2021-12-27 21:42:17.2100 SideChain SYNCHRONIZED"#, + ))); + let output_pub = Arc::new(Mutex::new(String::new())); + let elapsed = std::time::Duration::from_secs(60); + let process = Arc::new(Mutex::new(Process::new( + ProcessName::P2pool, + "".to_string(), + PathBuf::new(), + ))); + + // It only gets checked if we're `Syncing`. + process.lock().unwrap().state = ProcessState::Syncing; + PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); + println!("{:#?}", process); + assert!(process.lock().unwrap().state == ProcessState::Alive); + } + + #[test] + fn update_pub_p2pool_from_local_network_pool() { + use crate::helper::p2pool::PoolStatistics; + use crate::helper::p2pool::PrivP2poolLocalApi; + use crate::helper::p2pool::PrivP2poolNetworkApi; + use crate::helper::p2pool::PrivP2poolPoolApi; + use crate::helper::PubP2poolApi; + use std::sync::{Arc, Mutex}; + let public = Arc::new(Mutex::new(PubP2poolApi::new())); + let local = PrivP2poolLocalApi { + hashrate_15m: 10_000, + hashrate_1h: 20_000, + hashrate_24h: 30_000, + shares_found: 1000, + average_effort: 100.000, + current_effort: 200.000, + connections: 1234, + }; + let network = PrivP2poolNetworkApi { + difficulty: 300_000_000_000, + hash: "asdf".to_string(), + height: 1234, + reward: 2345, + timestamp: 3456, + }; + let pool = PrivP2poolPoolApi { + pool_statistics: PoolStatistics { + hashRate: 1_000_000, // 1 MH/s + miners: 1_000, + }, + }; + // Update Local + PubP2poolApi::update_from_local(&public, local); + let p = public.lock().unwrap(); + println!("AFTER LOCAL: {:#?}", p); + assert_eq!(p.hashrate_15m.to_string(), "10,000"); + assert_eq!(p.hashrate_1h.to_string(), "20,000"); + assert_eq!(p.hashrate_24h.to_string(), "30,000"); + assert_eq!(p.shares_found.to_string(), "1,000"); + assert_eq!(p.average_effort.to_string(), "100.00%"); + assert_eq!(p.current_effort.to_string(), "200.00%"); + assert_eq!(p.connections.to_string(), "1,234"); + assert_eq!(p.user_p2pool_hashrate_u64, 20000); + drop(p); + // Update Network + Pool + PubP2poolApi::update_from_network_pool(&public, network, pool); + let p = public.lock().unwrap(); + println!("AFTER NETWORK+POOL: {:#?}", p); + assert_eq!(p.monero_difficulty.to_string(), "300,000,000,000"); + assert_eq!(p.monero_hashrate.to_string(), "2.500 GH/s"); + assert_eq!(p.hash.to_string(), "asdf"); + assert_eq!(p.height.to_string(), "1,234"); + assert_eq!(p.reward.to_u64(), 2345); + assert_eq!(p.p2pool_difficulty.to_string(), "10,000,000"); + assert_eq!(p.p2pool_hashrate.to_string(), "1.000 MH/s"); + assert_eq!(p.miners.to_string(), "1,000"); + assert_eq!( + p.solo_block_mean.to_string(), + "5 months, 21 days, 9 hours, 52 minutes" + ); + assert_eq!( + p.p2pool_block_mean.to_string(), + "3 days, 11 hours, 20 minutes" + ); + assert_eq!(p.p2pool_share_mean.to_string(), "8 minutes, 20 seconds"); + assert_eq!(p.p2pool_percent.to_string(), "0.040000%"); + assert_eq!(p.user_p2pool_percent.to_string(), "2.000000%"); + assert_eq!(p.user_monero_percent.to_string(), "0.000800%"); + drop(p); + } + + #[test] + fn set_xmrig_mining() { + use crate::helper::PubXmrigApi; + use std::sync::{Arc, Mutex}; + let public = Arc::new(Mutex::new(PubXmrigApi::new())); + let output_parse = Arc::new(Mutex::new(String::from( + "[2022-02-12 12:49:30.311] net no active pools, stop mining", + ))); + let output_pub = Arc::new(Mutex::new(String::new())); + let elapsed = std::time::Duration::from_secs(60); + let process = Arc::new(Mutex::new(Process::new( + ProcessName::Xmrig, + "".to_string(), + PathBuf::new(), + ))); + + process.lock().unwrap().state = ProcessState::Alive; + PubXmrigApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); + println!("{:#?}", process); + assert!(process.lock().unwrap().state == ProcessState::NotMining); + + let output_parse = Arc::new(Mutex::new(String::from("[2022-02-12 12:49:30.311] net new job from 192.168.2.1:3333 diff 402K algo rx/0 height 2241142 (11 tx)"))); + PubXmrigApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process); + assert!(process.lock().unwrap().state == ProcessState::Alive); + } + + #[test] + fn serde_priv_p2pool_local_api() { + let data = r#"{ + "hashrate_15m": 12, + "hashrate_1h": 11111, + "hashrate_24h": 468967, + "total_hashes": 2019283840922394082390, + "shares_found": 289037, + "average_effort": 915.563, + "current_effort": 129.297, + "connections": 123, + "incoming_connections": 96 + }"#; + let priv_api = PrivP2poolLocalApi::from_str(data).unwrap(); + let json = serde_json::ser::to_string_pretty(&priv_api).unwrap(); + println!("{}", json); + let data_after_ser = r#"{ + "hashrate_15m": 12, + "hashrate_1h": 11111, + "hashrate_24h": 468967, + "shares_found": 289037, + "average_effort": 915.563, + "current_effort": 129.297, + "connections": 123 +}"#; + assert_eq!(data_after_ser, json) + } + + #[test] + fn serde_priv_p2pool_network_api() { + let data = r#"{ + "difficulty": 319028180924, + "hash": "22ae1b83d727bb2ff4efc17b485bc47bc8bf5e29a7b3af65baf42213ac70a39b", + "height": 2776576, + "reward": 600499860000, + "timestamp": 1670953659 + }"#; + let priv_api = PrivP2poolNetworkApi::from_str(data).unwrap(); + let json = serde_json::ser::to_string_pretty(&priv_api).unwrap(); + println!("{}", json); + let data_after_ser = r#"{ + "difficulty": 319028180924, + "hash": "22ae1b83d727bb2ff4efc17b485bc47bc8bf5e29a7b3af65baf42213ac70a39b", + "height": 2776576, + "reward": 600499860000, + "timestamp": 1670953659 +}"#; + assert_eq!(data_after_ser, json) + } + + #[test] + fn serde_priv_p2pool_pool_api() { + let data = r#"{ + "pool_list": ["pplns"], + "pool_statistics": { + "hashRate": 10225772, + "miners": 713, + "totalHashes": 487463929193948, + "lastBlockFoundTime": 1670453228, + "lastBlockFound": 2756570, + "totalBlocksFound": 4 + } + }"#; + let priv_api = crate::helper::p2pool::PrivP2poolPoolApi::from_str(data).unwrap(); + let json = serde_json::ser::to_string_pretty(&priv_api).unwrap(); + println!("{}", json); + let data_after_ser = r#"{ + "pool_statistics": { + "hashRate": 10225772, + "miners": 713 + } +}"#; + assert_eq!(data_after_ser, json) + } + + #[test] + fn serde_priv_xmrig_api() { + let data = r#"{ + "id": "6226e3sd0cd1a6es", + "worker_id": "hinto", + "uptime": 123, + "restricted": true, + "resources": { + "memory": { + "free": 123, + "total": 123123, + "resident_set_memory": 123123123 + }, + "load_average": [10.97, 10.58, 10.47], + "hardware_concurrency": 12 + }, + "features": ["api", "asm", "http", "hwloc", "tls", "opencl", "cuda"], + "results": { + "diff_current": 123, + "shares_good": 123, + "shares_total": 123, + "avg_time": 123, + "avg_time_ms": 123, + "hashes_total": 123, + "best": [123, 123, 123, 13, 123, 123, 123, 123, 123, 123], + "error_log": [] + }, + "algo": "rx/0", + "connection": { + "pool": "localhost:3333", + "ip": "127.0.0.1", + "uptime": 123, + "uptime_ms": 123, + "ping": 0, + "failures": 0, + "tls": null, + "tls-fingerprint": null, + "algo": "rx/0", + "diff": 123, + "accepted": 123, + "rejected": 123, + "avg_time": 123, + "avg_time_ms": 123, + "hashes_total": 123, + "error_log": [] + }, + "version": "6.18.0", + "kind": "miner", + "ua": "XMRig/6.18.0 (Linux x86_64) libuv/2.0.0-dev gcc/10.2.1", + "cpu": { + "brand": "blah blah blah", + "family": 1, + "model": 2, + "stepping": 0, + "proc_info": 123, + "aes": true, + "avx2": true, + "x64": true, + "64_bit": true, + "l2": 123123, + "l3": 123123, + "cores": 12, + "threads": 24, + "packages": 1, + "nodes": 1, + "backend": "hwloc/2.8.0a1-git", + "msr": "ryzen_19h", + "assembly": "ryzen", + "arch": "x86_64", + "flags": ["aes", "vaes", "avx", "avx2", "bmi2", "osxsave", "pdpe1gb", "sse2", "ssse3", "sse4.1", "popcnt", "cat_l3"] + }, + "donate_level": 0, + "paused": false, + "algorithms": ["cn/1", "cn/2", "cn/r", "cn/fast", "cn/half", "cn/xao", "cn/rto", "cn/rwz", "cn/zls", "cn/double", "cn/ccx", "cn-lite/1", "cn-heavy/0", "cn-heavy/tube", "cn-heavy/xhv", "cn-pico", "cn-pico/tlo", "cn/upx2", "rx/0", "rx/wow", "rx/arq", "rx/graft", "rx/sfx", "rx/keva", "argon2/chukwa", "argon2/chukwav2", "argon2/ninja", "astrobwt", "astrobwt/v2", "ghostrider"], + "hashrate": { + "total": [111.11, 111.11, 111.11], + "highest": 111.11, + "threads": [ + [111.11, 111.11, 111.11] + ] + }, + "hugepages": true + }"#; + use crate::helper::xmrig::PrivXmrigApi; + let priv_api = serde_json::from_str::(data).unwrap(); + let json = serde_json::ser::to_string_pretty(&priv_api).unwrap(); + println!("{}", json); + let data_after_ser = r#"{ + "worker_id": "hinto", + "resources": { + "load_average": [ + 10.97, + 10.58, + 10.47 + ] + }, + "connection": { + "diff": 123, + "accepted": 123, + "rejected": 123 + }, + "hashrate": { + "total": [ + 111.11, + 111.11, + 111.11 + ] + } +}"#; + assert_eq!(data_after_ser, json) + } +} diff --git a/src/helper/p2pool.rs b/src/helper/p2pool.rs new file mode 100644 index 0000000..4855c3e --- /dev/null +++ b/src/helper/p2pool.rs @@ -0,0 +1,1252 @@ +use super::Helper; +use super::Process; +use crate::components::node::RemoteNode; +use crate::disk::state::P2pool; +use crate::helper::ProcessName; +use crate::helper::ProcessSignal; +use crate::helper::ProcessState; +use crate::regex::P2POOL_REGEX; +use crate::{ + constants::*, + disk::{gupax_p2pool_api::GupaxP2poolApi, node::Node}, + helper::{MONERO_BLOCK_TIME_IN_SECONDS, P2POOL_BLOCK_TIME_IN_SECONDS}, + human::*, + macros::*, + xmr::*, +}; +use log::*; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use std::{ + fmt::Write, + path::PathBuf, + sync::{Arc, Mutex}, + thread, + time::*, +}; +impl Helper { + #[cold] + #[inline(never)] + fn read_pty_p2pool( + output_parse: Arc>, + output_pub: Arc>, + reader: Box, + gupax_p2pool_api: Arc>, + ) { + use std::io::BufRead; + let mut stdout = std::io::BufReader::new(reader).lines(); + + // Run a ANSI escape sequence filter for the first few lines. + let mut i = 0; + while let Some(Ok(line)) = stdout.next() { + let line = strip_ansi_escapes::strip_str(line); + if let Err(e) = writeln!(lock!(output_parse), "{}", line) { + error!("P2Pool PTY Parse | Output error: {}", e); + } + if let Err(e) = writeln!(lock!(output_pub), "{}", line) { + error!("P2Pool PTY Pub | Output error: {}", e); + } + if i > 20 { + break; + } else { + i += 1; + } + } + + while let Some(Ok(line)) = stdout.next() { + // println!("{}", line); // For debugging. + if P2POOL_REGEX.payout.is_match(&line) { + debug!("P2Pool PTY | Found payout, attempting write: {}", line); + let (date, atomic_unit, block) = PayoutOrd::parse_raw_payout_line(&line); + let formatted_log_line = GupaxP2poolApi::format_payout(&date, &atomic_unit, &block); + GupaxP2poolApi::add_payout( + &mut lock!(gupax_p2pool_api), + &formatted_log_line, + date, + atomic_unit, + block, + ); + if let Err(e) = GupaxP2poolApi::write_to_all_files( + &lock!(gupax_p2pool_api), + &formatted_log_line, + ) { + error!("P2Pool PTY GupaxP2poolApi | Write error: {}", e); + } + } + if let Err(e) = writeln!(lock!(output_parse), "{}", line) { + error!("P2Pool PTY Parse | Output error: {}", e); + } + if let Err(e) = writeln!(lock!(output_pub), "{}", line) { + error!("P2Pool PTY Pub | Output error: {}", e); + } + } + } + //---------------------------------------------------------------------------------------------------- P2Pool specific + #[cold] + #[inline(never)] + // Just sets some signals for the watchdog thread to pick up on. + pub fn stop_p2pool(helper: &Arc>) { + info!("P2Pool | Attempting to stop..."); + lock2!(helper, p2pool).signal = ProcessSignal::Stop; + lock2!(helper, p2pool).state = ProcessState::Middle; + } + + #[cold] + #[inline(never)] + // 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: &P2pool, + path: &Path, + backup_hosts: Option>, + ) { + info!("P2Pool | Attempting to restart..."); + lock2!(helper, p2pool).signal = ProcessSignal::Restart; + lock2!(helper, p2pool).state = ProcessState::Middle; + + let helper = Arc::clone(helper); + let state = state.clone(); + let path = path.to_path_buf(); + // This thread lives to wait, start p2pool then die. + thread::spawn(move || { + while lock2!(helper, p2pool).is_alive() { + warn!("P2Pool | Want to restart but process is still alive, waiting..."); + sleep!(1000); + } + // Ok, process is not alive, start the new one! + info!("P2Pool | Old process seems dead, starting new one!"); + Self::start_p2pool(&helper, &state, &path, backup_hosts); + }); + info!("P2Pool | Restart ... OK"); + } + + #[cold] + #[inline(never)] + // The "frontend" function that parses the arguments, and spawns either the [Simple] or [Advanced] P2Pool watchdog thread. + pub fn start_p2pool( + helper: &Arc>, + state: &P2pool, + path: &Path, + backup_hosts: Option>, + ) { + lock2!(helper, p2pool).state = ProcessState::Middle; + + let (args, api_path_local, api_path_network, api_path_pool) = + Self::build_p2pool_args_and_mutate_img(helper, state, path, backup_hosts); + + // Print arguments & user settings to console + crate::disk::print_dash(&format!( + "P2Pool | Launch arguments: {:#?} | Local API Path: {:#?} | Network API Path: {:#?} | Pool API Path: {:#?}", + args, + api_path_local, + api_path_network, + api_path_pool, + )); + + // Spawn watchdog thread + let process = Arc::clone(&lock!(helper).p2pool); + let gui_api = Arc::clone(&lock!(helper).gui_api_p2pool); + let pub_api = Arc::clone(&lock!(helper).pub_api_p2pool); + let gupax_p2pool_api = Arc::clone(&lock!(helper).gupax_p2pool_api); + let path = path.to_path_buf(); + thread::spawn(move || { + Self::spawn_p2pool_watchdog( + process, + gui_api, + pub_api, + args, + path, + api_path_local, + api_path_network, + api_path_pool, + gupax_p2pool_api, + ); + }); + } + + // Takes in a 95-char Monero address, returns the first and last + // 6 characters separated with dots like so: [4abcde...abcdef] + fn head_tail_of_monero_address(address: &str) -> String { + if address.len() < 95 { + return "???".to_string(); + } + let head = &address[0..6]; + let tail = &address[89..95]; + head.to_owned() + "..." + tail + } + + #[cold] + #[inline(never)] + // Takes in some [State/P2pool] and parses it to build the actual command arguments. + // Returns the [Vec] of actual arguments, and mutates the [ImgP2pool] for the main GUI thread + // It returns a value... and mutates a deeply nested passed argument... this is some pretty bad code... + pub fn build_p2pool_args_and_mutate_img( + helper: &Arc>, + state: &P2pool, + path: &Path, + backup_hosts: Option>, + ) -> (Vec, PathBuf, PathBuf, PathBuf) { + let mut args = Vec::with_capacity(500); + let path = path.to_path_buf(); + let mut api_path = path; + api_path.pop(); + + // [Simple] + if state.simple { + // Build the p2pool argument + let (ip, rpc, zmq) = RemoteNode::get_ip_rpc_zmq(&state.node); // Get: (IP, RPC, ZMQ) + args.push("--wallet".to_string()); + args.push(state.address.clone()); // Wallet address + args.push("--host".to_string()); + args.push(ip.to_string()); // IP Address + args.push("--rpc-port".to_string()); + args.push(rpc.to_string()); // RPC Port + args.push("--zmq-port".to_string()); + args.push(zmq.to_string()); // ZMQ Port + args.push("--data-api".to_string()); + args.push(api_path.display().to_string()); // API Path + args.push("--local-api".to_string()); // Enable API + args.push("--no-color".to_string()); // Remove color escape sequences, Gupax terminal can't parse it :( + args.push("--mini".to_string()); // P2Pool Mini + args.push("--light-mode".to_string()); // Assume user is not using P2Pool to mine. + + // Push other nodes if `backup_host`. + if let Some(nodes) = backup_hosts { + for node in nodes { + if (node.ip.as_str(), node.rpc.as_str(), node.zmq.as_str()) != (ip, rpc, zmq) { + args.push("--host".to_string()); + args.push(node.ip.to_string()); + args.push("--rpc-port".to_string()); + args.push(node.rpc.to_string()); + args.push("--zmq-port".to_string()); + args.push(node.zmq.to_string()); + } + } + } + + *lock2!(helper, img_p2pool) = ImgP2pool { + mini: "P2Pool Mini".to_string(), + address: Self::head_tail_of_monero_address(&state.address), + host: ip.to_string(), + rpc: rpc.to_string(), + zmq: zmq.to_string(), + out_peers: "10".to_string(), + in_peers: "10".to_string(), + }; + + // [Advanced] + } else { + // Overriding command arguments + if !state.arguments.is_empty() { + // This parses the input and attempts to fill out + // the [ImgP2pool]... This is pretty bad code... + let mut last = ""; + let lock = lock!(helper); + let mut p2pool_image = lock!(lock.img_p2pool); + let mut mini = false; + for arg in state.arguments.split_whitespace() { + match last { + "--mini" => { + mini = true; + p2pool_image.mini = "P2Pool Mini".to_string(); + } + "--wallet" => p2pool_image.address = Self::head_tail_of_monero_address(arg), + "--host" => p2pool_image.host = arg.to_string(), + "--rpc-port" => p2pool_image.rpc = arg.to_string(), + "--zmq-port" => p2pool_image.zmq = arg.to_string(), + "--out-peers" => p2pool_image.out_peers = arg.to_string(), + "--in-peers" => p2pool_image.in_peers = arg.to_string(), + "--data-api" => api_path = PathBuf::from(arg), + _ => (), + } + if !mini { + p2pool_image.mini = "P2Pool Main".to_string(); + } + let arg = if arg == "localhost" { "127.0.0.1" } else { arg }; + args.push(arg.to_string()); + last = arg; + } + // Else, build the argument + } else { + let ip = if state.ip == "localhost" { + "127.0.0.1" + } else { + &state.ip + }; + args.push("--wallet".to_string()); + args.push(state.address.clone()); // Wallet + args.push("--host".to_string()); + args.push(ip.to_string()); // IP + args.push("--rpc-port".to_string()); + args.push(state.rpc.to_string()); // RPC + args.push("--zmq-port".to_string()); + args.push(state.zmq.to_string()); // ZMQ + args.push("--loglevel".to_string()); + args.push(state.log_level.to_string()); // Log Level + args.push("--out-peers".to_string()); + args.push(state.out_peers.to_string()); // Out Peers + args.push("--in-peers".to_string()); + args.push(state.in_peers.to_string()); // In Peers + args.push("--data-api".to_string()); + args.push(api_path.display().to_string()); // API Path + args.push("--local-api".to_string()); // Enable API + args.push("--no-color".to_string()); // Remove color escape sequences + args.push("--light-mode".to_string()); // Assume user is not using P2Pool to mine. + if state.mini { + args.push("--mini".to_string()); + }; // Mini + + // Push other nodes if `backup_host`. + if let Some(nodes) = backup_hosts { + for node in nodes { + let ip = if node.ip == "localhost" { + "127.0.0.1" + } else { + &node.ip + }; + if (node.ip.as_str(), node.rpc.as_str(), node.zmq.as_str()) + != (ip, &state.rpc, &state.zmq) + { + args.push("--host".to_string()); + args.push(node.ip.to_string()); + args.push("--rpc-port".to_string()); + args.push(node.rpc.to_string()); + args.push("--zmq-port".to_string()); + args.push(node.zmq.to_string()); + } + } + } + + *lock2!(helper, img_p2pool) = ImgP2pool { + mini: if state.mini { + "P2Pool Mini".to_string() + } else { + "P2Pool Main".to_string() + }, + address: Self::head_tail_of_monero_address(&state.address), + host: state.selected_ip.to_string(), + rpc: state.selected_rpc.to_string(), + zmq: state.selected_zmq.to_string(), + out_peers: state.out_peers.to_string(), + in_peers: state.in_peers.to_string(), + }; + } + } + let mut api_path_local = api_path.clone(); + let mut api_path_network = api_path.clone(); + let mut api_path_pool = api_path.clone(); + api_path_local.push(P2POOL_API_PATH_LOCAL); + api_path_network.push(P2POOL_API_PATH_NETWORK); + api_path_pool.push(P2POOL_API_PATH_POOL); + (args, api_path_local, api_path_network, api_path_pool) + } + + #[cold] + #[inline(never)] + // The P2Pool watchdog. Spawns 1 OS thread for reading a PTY (STDOUT+STDERR), and combines the [Child] with a PTY so STDIN actually works. + #[allow(clippy::too_many_arguments)] + fn spawn_p2pool_watchdog( + process: Arc>, + gui_api: Arc>, + pub_api: Arc>, + args: Vec, + path: std::path::PathBuf, + api_path_local: std::path::PathBuf, + api_path_network: std::path::PathBuf, + api_path_pool: std::path::PathBuf, + gupax_p2pool_api: Arc>, + ) { + // 1a. Create PTY + debug!("P2Pool | Creating PTY..."); + let pty = portable_pty::native_pty_system(); + let pair = pty + .openpty(portable_pty::PtySize { + rows: 100, + cols: 1000, + pixel_width: 0, + pixel_height: 0, + }) + .unwrap(); + // 1b. Create command + debug!("P2Pool | Creating command..."); + let mut cmd = portable_pty::CommandBuilder::new(path.as_path()); + cmd.args(args); + cmd.env("NO_COLOR", "true"); + cmd.cwd(path.as_path().parent().unwrap()); + // 1c. Create child + debug!("P2Pool | Creating child..."); + let child_pty = arc_mut!(pair.slave.spawn_command(cmd).unwrap()); + drop(pair.slave); + + // 2. Set process state + debug!("P2Pool | Setting process state..."); + let mut lock = lock!(process); + lock.state = ProcessState::Syncing; + lock.signal = ProcessSignal::None; + lock.start = Instant::now(); + let reader = pair.master.try_clone_reader().unwrap(); // Get STDOUT/STDERR before moving the PTY + let mut stdin = pair.master.take_writer().unwrap(); + drop(lock); + + // 3. Spawn PTY read thread + debug!("P2Pool | Spawning PTY read thread..."); + let output_parse = Arc::clone(&lock!(process).output_parse); + let output_pub = Arc::clone(&lock!(process).output_pub); + let gupax_p2pool_api = Arc::clone(&gupax_p2pool_api); + thread::spawn(move || { + Self::read_pty_p2pool(output_parse, output_pub, reader, gupax_p2pool_api); + }); + let output_parse = Arc::clone(&lock!(process).output_parse); + let output_pub = Arc::clone(&lock!(process).output_pub); + + debug!("P2Pool | Cleaning old [local] API files..."); + // Attempt to remove stale API file + match std::fs::remove_file(&api_path_local) { + Ok(_) => info!("P2Pool | Attempting to remove stale API file ... OK"), + Err(e) => warn!( + "P2Pool | Attempting to remove stale API file ... FAIL ... {}", + e + ), + } + // Attempt to create a default empty one. + use std::io::Write; + if std::fs::File::create(&api_path_local).is_ok() { + let text = r#"{"hashrate_15m":0,"hashrate_1h":0,"hashrate_24h":0,"shares_found":0,"average_effort":0.0,"current_effort":0.0,"connections":0}"#; + match std::fs::write(&api_path_local, text) { + Ok(_) => info!("P2Pool | Creating default empty API file ... OK"), + Err(e) => warn!( + "P2Pool | Creating default empty API file ... FAIL ... {}", + e + ), + } + } + let start = lock!(process).start; + + // Reset stats before loop + *lock!(pub_api) = PubP2poolApi::new(); + *lock!(gui_api) = PubP2poolApi::new(); + + // 4. Loop as watchdog + info!("P2Pool | Entering watchdog mode... woof!"); + loop { + // Set timer + let now = Instant::now(); + debug!("P2Pool Watchdog | ----------- Start of loop -----------"); + lock!(gui_api).tick += 1; + + // Check if the process is secretly died without us knowing :) + if let Ok(Some(code)) = lock!(child_pty).try_wait() { + debug!("P2Pool Watchdog | Process secretly died! Getting exit status"); + let exit_status = match code.success() { + true => { + lock!(process).state = ProcessState::Dead; + "Successful" + } + false => { + lock!(process).state = ProcessState::Failed; + "Failed" + } + }; + let uptime = HumanTime::into_human(start.elapsed()); + info!( + "P2Pool Watchdog | Stopped ... Uptime was: [{}], Exit status: [{}]", + uptime, exit_status + ); + // This is written directly into the GUI, because sometimes the 900ms event loop can't catch it. + if let Err(e) = writeln!( + lock!(gui_api).output, + "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n", + HORI_CONSOLE, + uptime, + exit_status, + HORI_CONSOLE + ) { + error!( + "P2Pool Watchdog | GUI Uptime/Exit status write failed: {}", + e + ); + } + lock!(process).signal = ProcessSignal::None; + debug!("P2Pool Watchdog | Secret dead process reap OK, breaking"); + break; + } + + // Check SIGNAL + if lock!(process).signal == ProcessSignal::Stop { + debug!("P2Pool Watchdog | Stop SIGNAL caught"); + // This actually sends a SIGHUP to p2pool (closes the PTY, hangs up on p2pool) + if let Err(e) = lock!(child_pty).kill() { + error!("P2Pool Watchdog | Kill error: {}", e); + } + // Wait to get the exit status + let exit_status = match lock!(child_pty).wait() { + Ok(e) => { + if e.success() { + lock!(process).state = ProcessState::Dead; + "Successful" + } else { + lock!(process).state = ProcessState::Failed; + "Failed" + } + } + _ => { + lock!(process).state = ProcessState::Failed; + "Unknown Error" + } + }; + let uptime = HumanTime::into_human(start.elapsed()); + info!( + "P2Pool Watchdog | Stopped ... Uptime was: [{}], Exit status: [{}]", + uptime, exit_status + ); + // This is written directly into the GUI API, because sometimes the 900ms event loop can't catch it. + if let Err(e) = writeln!( + lock!(gui_api).output, + "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n", + HORI_CONSOLE, + uptime, + exit_status, + HORI_CONSOLE + ) { + error!( + "P2Pool Watchdog | GUI Uptime/Exit status write failed: {}", + e + ); + } + lock!(process).signal = ProcessSignal::None; + debug!("P2Pool Watchdog | Stop SIGNAL done, breaking"); + break; + // Check RESTART + } else if lock!(process).signal == ProcessSignal::Restart { + debug!("P2Pool Watchdog | Restart SIGNAL caught"); + // This actually sends a SIGHUP to p2pool (closes the PTY, hangs up on p2pool) + if let Err(e) = lock!(child_pty).kill() { + error!("P2Pool Watchdog | Kill error: {}", e); + } + // Wait to get the exit status + let exit_status = match lock!(child_pty).wait() { + Ok(e) => { + if e.success() { + "Successful" + } else { + "Failed" + } + } + _ => "Unknown Error", + }; + let uptime = HumanTime::into_human(start.elapsed()); + info!( + "P2Pool Watchdog | Stopped ... Uptime was: [{}], Exit status: [{}]", + uptime, exit_status + ); + // This is written directly into the GUI API, because sometimes the 900ms event loop can't catch it. + if let Err(e) = writeln!( + lock!(gui_api).output, + "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n", + HORI_CONSOLE, + uptime, + exit_status, + HORI_CONSOLE + ) { + error!( + "P2Pool Watchdog | GUI Uptime/Exit status write failed: {}", + e + ); + } + lock!(process).state = ProcessState::Waiting; + debug!("P2Pool Watchdog | Restart SIGNAL done, breaking"); + break; + } + + // Check vector of user input + let mut lock = lock!(process); + if !lock.input.is_empty() { + let input = std::mem::take(&mut lock.input); + for line in input { + if line.is_empty() { + continue; + } + debug!( + "P2Pool Watchdog | User input not empty, writing to STDIN: [{}]", + line + ); + // Windows terminals (or at least the PTY abstraction I'm using, portable_pty) + // requires a [\r\n] to end a line, whereas Unix is okay with just a [\n]. + // + // I have literally read all of [portable_pty]'s source code, dug into Win32 APIs, + // even rewrote some of the actual PTY code in order to understand why STDIN doesn't work + // on Windows. It's because of a fucking missing [\r]. Another reason to hate Windows :D + // + // XMRig did actually work before though, since it reads STDIN directly without needing a newline. + #[cfg(target_os = "windows")] + if let Err(e) = write!(stdin, "{}\r\n", line) { + error!("P2Pool Watchdog | STDIN error: {}", e); + } + #[cfg(target_family = "unix")] + if let Err(e) = writeln!(stdin, "{}", line) { + error!("P2Pool Watchdog | STDIN error: {}", e); + } + // Flush. + if let Err(e) = stdin.flush() { + error!("P2Pool Watchdog | STDIN flush error: {}", e); + } + } + } + drop(lock); + + // Check if logs need resetting + debug!("P2Pool Watchdog | Attempting GUI log reset check"); + let mut lock = lock!(gui_api); + Self::check_reset_gui_output(&mut lock.output, ProcessName::P2pool); + drop(lock); + + // Always update from output + debug!("P2Pool Watchdog | Starting [update_from_output()]"); + PubP2poolApi::update_from_output( + &pub_api, + &output_parse, + &output_pub, + start.elapsed(), + &process, + ); + + // Read [local] API + debug!("P2Pool Watchdog | Attempting [local] API file read"); + if let Ok(string) = Self::path_to_string(&api_path_local, ProcessName::P2pool) { + // Deserialize + if let Ok(local_api) = PrivP2poolLocalApi::from_str(&string) { + // Update the structs. + PubP2poolApi::update_from_local(&pub_api, local_api); + } + } + // If more than 1 minute has passed, read the other API files. + if lock!(gui_api).tick >= 60 { + debug!("P2Pool Watchdog | Attempting [network] & [pool] API file read"); + if let (Ok(network_api), Ok(pool_api)) = ( + Self::path_to_string(&api_path_network, ProcessName::P2pool), + Self::path_to_string(&api_path_pool, ProcessName::P2pool), + ) { + if let (Ok(network_api), Ok(pool_api)) = ( + PrivP2poolNetworkApi::from_str(&network_api), + PrivP2poolPoolApi::from_str(&pool_api), + ) { + PubP2poolApi::update_from_network_pool(&pub_api, network_api, pool_api); + lock!(gui_api).tick = 0; + } + } + } + + // 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 + if elapsed < 900 { + let sleep = (900 - elapsed) as u64; + debug!( + "P2Pool Watchdog | END OF LOOP - Tick: [{}/60] - Sleeping for [{}]ms...", + lock!(gui_api).tick, + sleep + ); + sleep!(sleep); + } else { + debug!( + "P2Pool Watchdog | END OF LOOP - Tick: [{}/60] Not sleeping!", + lock!(gui_api).tick + ); + } + } + + // 5. If loop broke, we must be done here. + info!("P2Pool Watchdog | Watchdog thread exiting... Goodbye!"); + } +} +//---------------------------------------------------------------------------------------------------- [ImgP2pool] +// A static "image" of data that P2Pool started with. +// This is just a snapshot of the user data when they initially started P2Pool. +// Created by [start_p2pool()] and return to the main GUI thread where it will store it. +// No need for an [Arc] since the Helper thread doesn't need this information. +#[derive(Debug, Clone)] +pub struct ImgP2pool { + pub mini: String, // Did the user start on the mini-chain? + pub address: String, // What address is the current p2pool paying out to? (This gets shortened to [4xxxxx...xxxxxx]) + pub host: String, // What monerod are we using? + pub rpc: String, // What is the RPC port? + pub zmq: String, // What is the ZMQ port? + pub out_peers: String, // How many out-peers? + pub in_peers: String, // How many in-peers? +} + +impl Default for ImgP2pool { + fn default() -> Self { + Self::new() + } +} + +impl ImgP2pool { + pub fn new() -> Self { + Self { + mini: String::from("???"), + address: String::from("???"), + host: String::from("???"), + rpc: String::from("???"), + zmq: String::from("???"), + out_peers: String::from("???"), + in_peers: String::from("???"), + } + } +} + +//---------------------------------------------------------------------------------------------------- Public P2Pool API +// Helper/GUI threads both have a copy of this, Helper updates +// the GUI's version on a 1-second interval from the private data. +#[derive(Debug, Clone, PartialEq)] +pub struct PubP2poolApi { + // Output + pub output: String, + // Uptime + pub uptime: HumanTime, + // These are manually parsed from the STDOUT. + pub payouts: u128, + pub payouts_hour: f64, + pub payouts_day: f64, + pub payouts_month: f64, + pub xmr: f64, + pub xmr_hour: f64, + pub xmr_day: f64, + pub xmr_month: f64, + // Local API + pub hashrate_15m: HumanNumber, + pub hashrate_1h: HumanNumber, + pub hashrate_24h: HumanNumber, + pub shares_found: HumanNumber, + pub average_effort: HumanNumber, + pub current_effort: HumanNumber, + pub connections: HumanNumber, + // The API needs a raw ints to go off of and + // there's not a good way to access it without doing weird + // [Arc] shenanigans, so some raw ints are stored here. + pub user_p2pool_hashrate_u64: u64, + pub p2pool_difficulty_u64: u64, + pub monero_difficulty_u64: u64, + pub p2pool_hashrate_u64: u64, + pub monero_hashrate_u64: u64, + // Tick. Every loop this gets incremented. + // At 60, it indicated we should read the below API files. + pub tick: u8, + // Network API + pub monero_difficulty: HumanNumber, // e.g: [15,000,000] + pub monero_hashrate: HumanNumber, // e.g: [1.000 GH/s] + pub hash: String, // Current block hash + pub height: HumanNumber, + pub reward: AtomicUnit, + // Pool API + pub p2pool_difficulty: HumanNumber, + pub p2pool_hashrate: HumanNumber, + pub miners: HumanNumber, // Current amount of miners on P2Pool sidechain + // Mean (calculated in functions, not serialized) + pub solo_block_mean: HumanTime, // Time it would take the user to find a solo block + pub p2pool_block_mean: HumanTime, // Time it takes the P2Pool sidechain to find a block + pub p2pool_share_mean: HumanTime, // Time it would take the user to find a P2Pool share + // Percent + pub p2pool_percent: HumanNumber, // Percentage of P2Pool hashrate capture of overall Monero hashrate. + pub user_p2pool_percent: HumanNumber, // How much percent the user's hashrate accounts for in P2Pool. + pub user_monero_percent: HumanNumber, // How much percent the user's hashrate accounts for in all of Monero hashrate. +} + +impl Default for PubP2poolApi { + fn default() -> Self { + Self::new() + } +} + +impl PubP2poolApi { + pub fn new() -> Self { + Self { + output: String::new(), + uptime: HumanTime::new(), + payouts: 0, + payouts_hour: 0.0, + payouts_day: 0.0, + payouts_month: 0.0, + xmr: 0.0, + xmr_hour: 0.0, + xmr_day: 0.0, + xmr_month: 0.0, + hashrate_15m: HumanNumber::unknown(), + hashrate_1h: HumanNumber::unknown(), + hashrate_24h: HumanNumber::unknown(), + shares_found: HumanNumber::unknown(), + average_effort: HumanNumber::unknown(), + current_effort: HumanNumber::unknown(), + connections: HumanNumber::unknown(), + tick: 0, + user_p2pool_hashrate_u64: 0, + p2pool_difficulty_u64: 0, + monero_difficulty_u64: 0, + p2pool_hashrate_u64: 0, + monero_hashrate_u64: 0, + monero_difficulty: HumanNumber::unknown(), + monero_hashrate: HumanNumber::unknown(), + hash: String::from("???"), + height: HumanNumber::unknown(), + reward: AtomicUnit::new(), + p2pool_difficulty: HumanNumber::unknown(), + p2pool_hashrate: HumanNumber::unknown(), + miners: HumanNumber::unknown(), + solo_block_mean: HumanTime::new(), + p2pool_block_mean: HumanTime::new(), + p2pool_share_mean: HumanTime::new(), + p2pool_percent: HumanNumber::unknown(), + user_p2pool_percent: HumanNumber::unknown(), + user_monero_percent: HumanNumber::unknown(), + } + } + + #[inline] + // The issue with just doing [gui_api = pub_api] is that values get overwritten. + // This doesn't matter for any of the values EXCEPT for the output, so we must + // manually append it instead of overwriting. + // This is used in the "helper" thread. + pub(super) fn combine_gui_pub_api(gui_api: &mut Self, pub_api: &mut Self) { + let mut output = std::mem::take(&mut gui_api.output); + let buf = std::mem::take(&mut pub_api.output); + if !buf.is_empty() { + output.push_str(&buf); + } + *gui_api = Self { + output, + tick: std::mem::take(&mut gui_api.tick), + ..pub_api.clone() + }; + } + + #[inline] + // 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) -> (u128 /* payout count */, f64 /* total xmr */) { + let iter = P2POOL_REGEX.payout.find_iter(output); + let mut sum: f64 = 0.0; + let mut count: u128 = 0; + for i in iter { + if let Some(word) = P2POOL_REGEX.payout_float.find(i.as_str()) { + match word.as_str().parse::() { + Ok(num) => { + sum += num; + count += 1; + } + Err(e) => error!("P2Pool | Total XMR sum calculation error: [{}]", e), + } + } + } + (count, sum) + } + + // Mutate "watchdog"'s [PubP2poolApi] with data the process output. + pub(super) fn update_from_output( + public: &Arc>, + output_parse: &Arc>, + output_pub: &Arc>, + elapsed: std::time::Duration, + process: &Arc>, + ) { + // 1. Take the process's current output buffer and combine it with Pub (if not empty) + let mut output_pub = lock!(output_pub); + if !output_pub.is_empty() { + lock!(public) + .output + .push_str(&std::mem::take(&mut *output_pub)); + } + + // 2. Parse the full STDOUT + let mut output_parse = lock!(output_parse); + let (payouts_new, xmr_new) = Self::calc_payouts_and_xmr(&output_parse); + // Check for "SYNCHRONIZED" only if we aren't already. + if lock!(process).state == ProcessState::Syncing { + // How many times the word was captured. + let synchronized_captures = P2POOL_REGEX.synchronized.find_iter(&output_parse).count(); + + // If P2Pool receives shares before syncing, it will start mining on its own sidechain. + // In this instance, we technically are "synced" on block 1 and P2Pool will print "SYNCHRONIZED" + // although, that doesn't necessarily mean we're synced on main/mini-chain. + // + // So, if we find a `next block = 1`, that means we + // must look for at least 2 instances of "SYNCHRONIZED", + // one for the sidechain, one for main/mini. + if P2POOL_REGEX.next_height_1.is_match(&output_parse) { + if synchronized_captures > 1 { + lock!(process).state = ProcessState::Alive; + } + } else if synchronized_captures > 0 { + // if there is no `next block = 1`, fallback to + // just finding 1 instance of "SYNCHRONIZED". + lock!(process).state = ProcessState::Alive; + } + } + // 3. Throw away [output_parse] + output_parse.clear(); + drop(output_parse); + // 4. Add to current values + let mut public = lock!(public); + let (payouts, xmr) = (public.payouts + payouts_new, public.xmr + xmr_new); + + // 5. Calculate hour/day/month given elapsed time + let elapsed_as_secs_f64 = elapsed.as_secs_f64(); + // Payouts + let per_sec = (payouts as f64) / elapsed_as_secs_f64; + let payouts_hour = (per_sec * 60.0) * 60.0; + let payouts_day = payouts_hour * 24.0; + let payouts_month = payouts_day * 30.0; + // Total XMR + let per_sec = xmr / elapsed_as_secs_f64; + let xmr_hour = (per_sec * 60.0) * 60.0; + let xmr_day = xmr_hour * 24.0; + let xmr_month = xmr_day * 30.0; + + if payouts_new != 0 { + debug!( + "P2Pool Watchdog | New [Payout] found in output ... {}", + payouts_new + ); + debug!("P2Pool Watchdog | Total [Payout] should be ... {}", payouts); + debug!( + "P2Pool Watchdog | Correct [Payout per] should be ... [{}/hour, {}/day, {}/month]", + payouts_hour, payouts_day, payouts_month + ); + } + if xmr_new != 0.0 { + debug!( + "P2Pool Watchdog | New [XMR mined] found in output ... {}", + xmr_new + ); + debug!("P2Pool Watchdog | Total [XMR mined] should be ... {}", xmr); + debug!("P2Pool Watchdog | Correct [XMR mined per] should be ... [{}/hour, {}/day, {}/month]", xmr_hour, xmr_day, xmr_month); + } + + // 6. Mutate the struct with the new info + *public = Self { + uptime: HumanTime::into_human(elapsed), + payouts, + xmr, + payouts_hour, + payouts_day, + payouts_month, + xmr_hour, + xmr_day, + xmr_month, + ..std::mem::take(&mut *public) + }; + } + + // Mutate [PubP2poolApi] with data from a [PrivP2poolLocalApi] and the process output. + pub(super) fn update_from_local(public: &Arc>, local: PrivP2poolLocalApi) { + let mut public = lock!(public); + *public = Self { + hashrate_15m: HumanNumber::from_u64(local.hashrate_15m), + hashrate_1h: HumanNumber::from_u64(local.hashrate_1h), + hashrate_24h: HumanNumber::from_u64(local.hashrate_24h), + shares_found: HumanNumber::from_u64(local.shares_found), + average_effort: HumanNumber::to_percent(local.average_effort), + current_effort: HumanNumber::to_percent(local.current_effort), + connections: HumanNumber::from_u32(local.connections), + user_p2pool_hashrate_u64: local.hashrate_1h, + ..std::mem::take(&mut *public) + }; + } + + // Mutate [PubP2poolApi] with data from a [PrivP2pool(Network|Pool)Api]. + pub(super) fn update_from_network_pool( + public: &Arc>, + net: PrivP2poolNetworkApi, + pool: PrivP2poolPoolApi, + ) { + let user_hashrate = lock!(public).user_p2pool_hashrate_u64; // The user's total P2Pool hashrate + let monero_difficulty = net.difficulty; + let monero_hashrate = monero_difficulty / MONERO_BLOCK_TIME_IN_SECONDS; + let p2pool_hashrate = pool.pool_statistics.hashRate; + let p2pool_difficulty = p2pool_hashrate * P2POOL_BLOCK_TIME_IN_SECONDS; + // These [0] checks prevent dividing by 0 (it [panic!()]s) + let p2pool_block_mean; + let user_p2pool_percent; + if p2pool_hashrate == 0 { + p2pool_block_mean = HumanTime::new(); + user_p2pool_percent = HumanNumber::unknown(); + } else { + p2pool_block_mean = HumanTime::into_human(std::time::Duration::from_secs( + monero_difficulty / p2pool_hashrate, + )); + let f = (user_hashrate as f64 / p2pool_hashrate as f64) * 100.0; + user_p2pool_percent = HumanNumber::from_f64_to_percent_6_point(f); + }; + let p2pool_percent; + let user_monero_percent; + if monero_hashrate == 0 { + p2pool_percent = HumanNumber::unknown(); + user_monero_percent = HumanNumber::unknown(); + } else { + let f = (p2pool_hashrate as f64 / monero_hashrate as f64) * 100.0; + p2pool_percent = HumanNumber::from_f64_to_percent_6_point(f); + let f = (user_hashrate as f64 / monero_hashrate as f64) * 100.0; + user_monero_percent = HumanNumber::from_f64_to_percent_6_point(f); + }; + let solo_block_mean; + let p2pool_share_mean; + if user_hashrate == 0 { + solo_block_mean = HumanTime::new(); + p2pool_share_mean = HumanTime::new(); + } else { + solo_block_mean = HumanTime::into_human(std::time::Duration::from_secs( + monero_difficulty / user_hashrate, + )); + p2pool_share_mean = HumanTime::into_human(std::time::Duration::from_secs( + p2pool_difficulty / user_hashrate, + )); + } + let mut public = lock!(public); + *public = Self { + p2pool_difficulty_u64: p2pool_difficulty, + monero_difficulty_u64: monero_difficulty, + p2pool_hashrate_u64: p2pool_hashrate, + monero_hashrate_u64: monero_hashrate, + monero_difficulty: HumanNumber::from_u64(monero_difficulty), + monero_hashrate: HumanNumber::from_u64_to_gigahash_3_point(monero_hashrate), + hash: net.hash, + height: HumanNumber::from_u32(net.height), + reward: AtomicUnit::from_u64(net.reward), + p2pool_difficulty: HumanNumber::from_u64(p2pool_difficulty), + p2pool_hashrate: HumanNumber::from_u64_to_megahash_3_point(p2pool_hashrate), + miners: HumanNumber::from_u32(pool.pool_statistics.miners), + solo_block_mean, + p2pool_block_mean, + p2pool_share_mean, + p2pool_percent, + user_p2pool_percent, + user_monero_percent, + ..std::mem::take(&mut *public) + }; + } + + #[inline] + pub fn calculate_share_or_block_time(hashrate: u64, difficulty: u64) -> HumanTime { + if hashrate == 0 { + HumanTime::new() + } else { + HumanTime::from_u64(difficulty / hashrate) + } + } + + #[inline] + pub fn calculate_dominance(my_hashrate: u64, global_hashrate: u64) -> HumanNumber { + if global_hashrate == 0 { + HumanNumber::unknown() + } else { + let f = (my_hashrate as f64 / global_hashrate as f64) * 100.0; + HumanNumber::from_f64_to_percent_6_point(f) + } + } + + pub const fn calculate_tick_bar(&self) -> &'static str { + // The stars are reduced by one because it takes a frame to render the stats. + // We want 0 stars at the same time stats are rendered, so it looks a little off here. + // let stars = "*".repeat(self.tick - 1); + // let blanks = " ".repeat(60 - (self.tick - 1)); + // [use crate::PubP2poolApi;use crate::PubP2poolApi;"[", &stars, &blanks, "]"].concat().as_str() + match self.tick { + 1 => "[ ]", + 2 => "[* ]", + 3 => "[** ]", + 4 => "[*** ]", + 5 => "[**** ]", + 6 => "[***** ]", + 7 => "[****** ]", + 8 => "[******* ]", + 9 => "[******** ]", + 10 => "[********* ]", + 11 => "[********** ]", + 12 => "[*********** ]", + 13 => "[************ ]", + 14 => "[************* ]", + 15 => "[************** ]", + 16 => "[*************** ]", + 17 => "[**************** ]", + 18 => "[***************** ]", + 19 => "[****************** ]", + 20 => "[******************* ]", + 21 => "[******************** ]", + 22 => "[********************* ]", + 23 => "[********************** ]", + 24 => "[*********************** ]", + 25 => "[************************ ]", + 26 => "[************************* ]", + 27 => "[************************** ]", + 28 => "[*************************** ]", + 29 => "[**************************** ]", + 30 => "[***************************** ]", + 31 => "[****************************** ]", + 32 => "[******************************* ]", + 33 => "[******************************** ]", + 34 => "[********************************* ]", + 35 => "[********************************** ]", + 36 => "[*********************************** ]", + 37 => "[************************************ ]", + 38 => "[************************************* ]", + 39 => "[************************************** ]", + 40 => "[*************************************** ]", + 41 => "[**************************************** ]", + 42 => "[***************************************** ]", + 43 => "[****************************************** ]", + 44 => "[******************************************* ]", + 45 => "[******************************************** ]", + 46 => "[********************************************* ]", + 47 => "[********************************************** ]", + 48 => "[*********************************************** ]", + 49 => "[************************************************ ]", + 50 => "[************************************************* ]", + 51 => "[************************************************** ]", + 52 => "[*************************************************** ]", + 53 => "[**************************************************** ]", + 54 => "[***************************************************** ]", + 55 => "[****************************************************** ]", + 56 => "[******************************************************* ]", + 57 => "[******************************************************** ]", + 58 => "[********************************************************* ]", + 59 => "[********************************************************** ]", + 60 => "[*********************************************************** ]", + _ => "[************************************************************]", + } + } +} + +//---------------------------------------------------------------------------------------------------- Private P2Pool "Local" Api +// This matches directly to P2Pool's [local/stratum] 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, Clone, Copy)] +pub(super) struct PrivP2poolLocalApi { + pub hashrate_15m: u64, + pub hashrate_1h: u64, + pub hashrate_24h: u64, + pub shares_found: u64, + pub average_effort: f32, + pub current_effort: f32, + pub connections: u32, // This is a `uint32_t` in `p2pool` +} + +impl Default for PrivP2poolLocalApi { + fn default() -> Self { + Self::new() + } +} + +impl PrivP2poolLocalApi { + fn new() -> Self { + Self { + hashrate_15m: 0, + hashrate_1h: 0, + hashrate_24h: 0, + shares_found: 0, + average_effort: 0.0, + current_effort: 0.0, + connections: 0, + } + } + + // Deserialize the above [String] into a [PrivP2poolApi] + pub(super) fn from_str(string: &str) -> std::result::Result { + match serde_json::from_str::(string) { + Ok(a) => Ok(a), + Err(e) => { + warn!("P2Pool Local API | Could not deserialize API data: {}", e); + Err(e) + } + } + } +} + +//---------------------------------------------------------------------------------------------------- Private P2Pool "Network" API +// This matches P2Pool's [network/stats] JSON API file. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub(super) struct PrivP2poolNetworkApi { + pub difficulty: u64, + pub hash: String, + pub height: u32, + pub reward: u64, + pub timestamp: u32, +} + +impl Default for PrivP2poolNetworkApi { + fn default() -> Self { + Self::new() + } +} + +impl PrivP2poolNetworkApi { + fn new() -> Self { + Self { + difficulty: 0, + hash: String::from("???"), + height: 0, + reward: 0, + timestamp: 0, + } + } + + pub(super) fn from_str(string: &str) -> std::result::Result { + match serde_json::from_str::(string) { + Ok(a) => Ok(a), + Err(e) => { + warn!("P2Pool Network API | Could not deserialize API data: {}", e); + Err(e) + } + } + } +} + +//---------------------------------------------------------------------------------------------------- Private P2Pool "Pool" API +// This matches P2Pool's [pool/stats] JSON API file. +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub(super) struct PrivP2poolPoolApi { + pub pool_statistics: PoolStatistics, +} + +impl Default for PrivP2poolPoolApi { + fn default() -> Self { + Self::new() + } +} + +impl PrivP2poolPoolApi { + fn new() -> Self { + Self { + pool_statistics: PoolStatistics::new(), + } + } + + pub(super) fn from_str(string: &str) -> std::result::Result { + match serde_json::from_str::(string) { + Ok(a) => Ok(a), + Err(e) => { + warn!("P2Pool Pool API | Could not deserialize API data: {}", e); + Err(e) + } + } + } +} + +#[allow(non_snake_case)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub(super) struct PoolStatistics { + pub hashRate: u64, + pub miners: u32, +} +impl Default for PoolStatistics { + fn default() -> Self { + Self::new() + } +} +impl PoolStatistics { + fn new() -> Self { + Self { + hashRate: 0, + miners: 0, + } + } +} diff --git a/src/helper/xmrig.rs b/src/helper/xmrig.rs new file mode 100644 index 0000000..8f6896c --- /dev/null +++ b/src/helper/xmrig.rs @@ -0,0 +1,782 @@ +use crate::helper::{ProcessName, ProcessSignal, ProcessState}; +use crate::regex::XMRIG_REGEX; +use crate::utils::sudo::SudoState; +use crate::{constants::*, human::*, macros::*}; +use log::*; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use std::{ + fmt::Write, + path::PathBuf, + process::Stdio, + sync::{Arc, Mutex}, + thread, + time::*, +}; + +use super::{Helper, Process}; +impl Helper { + #[cold] + #[inline(never)] + fn read_pty_xmrig( + output_parse: Arc>, + output_pub: Arc>, + reader: Box, + ) { + use std::io::BufRead; + let mut stdout = std::io::BufReader::new(reader).lines(); + + // Run a ANSI escape sequence filter for the first few lines. + let mut i = 0; + while let Some(Ok(line)) = stdout.next() { + let line = strip_ansi_escapes::strip_str(line); + if let Err(e) = writeln!(lock!(output_parse), "{}", line) { + error!("XMRig PTY Parse | Output error: {}", e); + } + if let Err(e) = writeln!(lock!(output_pub), "{}", line) { + error!("XMRig PTY Pub | Output error: {}", e); + } + if i > 20 { + break; + } else { + i += 1; + } + } + + while let Some(Ok(line)) = stdout.next() { + // println!("{}", line); // For debugging. + if let Err(e) = writeln!(lock!(output_parse), "{}", line) { + error!("XMRig PTY Parse | Output error: {}", e); + } + if let Err(e) = writeln!(lock!(output_pub), "{}", line) { + error!("XMRig PTY Pub | Output error: {}", e); + } + } + } + //---------------------------------------------------------------------------------------------------- XMRig specific, most functions are very similar to P2Pool's + #[cold] + #[inline(never)] + // If processes are started with [sudo] on macOS, they must also + // be killed with [sudo] (even if I have a direct handle to it as the + // parent process...!). This is only needed on macOS, not Linux. + fn sudo_kill(pid: u32, sudo: &Arc>) -> bool { + // Spawn [sudo] to execute [kill] on the given [pid] + let mut child = std::process::Command::new("sudo") + .args(["--stdin", "kill", "-9", &pid.to_string()]) + .stdin(Stdio::piped()) + .spawn() + .unwrap(); + + // Write the [sudo] password to STDIN. + let mut stdin = child.stdin.take().unwrap(); + use std::io::Write; + if let Err(e) = writeln!(stdin, "{}\n", lock!(sudo).pass) { + error!("Sudo Kill | STDIN error: {}", e); + } + + // Return exit code of [sudo/kill]. + child.wait().unwrap().success() + } + + #[cold] + #[inline(never)] + // Just sets some signals for the watchdog thread to pick up on. + pub fn stop_xmrig(helper: &Arc>) { + info!("XMRig | Attempting to stop..."); + lock2!(helper, xmrig).signal = ProcessSignal::Stop; + lock2!(helper, xmrig).state = ProcessState::Middle; + } + + #[cold] + #[inline(never)] + // The "restart frontend" to a "frontend" function. + // Basically calls to kill the current xmrig, waits a little, then starts the below function in a a new thread, then exit. + pub fn restart_xmrig( + helper: &Arc>, + state: &crate::disk::state::Xmrig, + path: &Path, + sudo: Arc>, + ) { + info!("XMRig | Attempting to restart..."); + lock2!(helper, xmrig).signal = ProcessSignal::Restart; + lock2!(helper, xmrig).state = ProcessState::Middle; + + let helper = Arc::clone(helper); + let state = state.clone(); + let path = path.to_path_buf(); + // This thread lives to wait, start xmrig then die. + thread::spawn(move || { + while lock2!(helper, xmrig).state != ProcessState::Waiting { + warn!("XMRig | Want to restart but process is still alive, waiting..."); + sleep!(1000); + } + // Ok, process is not alive, start the new one! + info!("XMRig | Old process seems dead, starting new one!"); + Self::start_xmrig(&helper, &state, &path, sudo); + }); + info!("XMRig | Restart ... OK"); + } + + #[cold] + #[inline(never)] + pub fn start_xmrig( + helper: &Arc>, + state: &crate::disk::state::Xmrig, + path: &Path, + sudo: Arc>, + ) { + lock2!(helper, xmrig).state = ProcessState::Middle; + + let (args, api_ip_port) = Self::build_xmrig_args_and_mutate_img(helper, state, path); + + // Print arguments & user settings to console + crate::disk::print_dash(&format!("XMRig | Launch arguments: {:#?}", args)); + info!("XMRig | Using path: [{}]", path.display()); + + // Spawn watchdog thread + let process = Arc::clone(&lock!(helper).xmrig); + let gui_api = Arc::clone(&lock!(helper).gui_api_xmrig); + let pub_api = Arc::clone(&lock!(helper).pub_api_xmrig); + let path = path.to_path_buf(); + thread::spawn(move || { + Self::spawn_xmrig_watchdog(process, gui_api, pub_api, args, path, sudo, api_ip_port); + }); + } + + #[cold] + #[inline(never)] + // Takes in some [State/Xmrig] and parses it to build the actual command arguments. + // Returns the [Vec] of actual arguments, and mutates the [ImgXmrig] for the main GUI thread + // It returns a value... and mutates a deeply nested passed argument... this is some pretty bad code... + pub fn build_xmrig_args_and_mutate_img( + helper: &Arc>, + state: &crate::disk::state::Xmrig, + path: &std::path::Path, + ) -> (Vec, String) { + let mut args = Vec::with_capacity(500); + let mut api_ip = String::with_capacity(15); + let mut api_port = String::with_capacity(5); + let path = path.to_path_buf(); + // The actual binary we're executing is [sudo], technically + // the XMRig path is just an argument to sudo, so add it. + // Before that though, add the ["--prompt"] flag and set it + // to emptyness so that it doesn't show up in the output. + if cfg!(unix) { + args.push(r#"--prompt="#.to_string()); + args.push("--".to_string()); + args.push(path.display().to_string()); + } + + // [Simple] + if state.simple { + // Build the xmrig argument + let rig = if state.simple_rig.is_empty() { + GUPAX_VERSION_UNDERSCORE.to_string() + } else { + state.simple_rig.clone() + }; // Rig name + args.push("--url".to_string()); + args.push("127.0.0.1:3333".to_string()); // Local P2Pool (the default) + args.push("--threads".to_string()); + args.push(state.current_threads.to_string()); // Threads + args.push("--user".to_string()); + args.push(rig); // Rig name + args.push("--no-color".to_string()); // No color + args.push("--http-host".to_string()); + args.push("127.0.0.1".to_string()); // HTTP API IP + args.push("--http-port".to_string()); + args.push("18088".to_string()); // HTTP API Port + if state.pause != 0 { + args.push("--pause-on-active".to_string()); + args.push(state.pause.to_string()); + } // Pause on active + *lock2!(helper, img_xmrig) = ImgXmrig { + threads: state.current_threads.to_string(), + url: "127.0.0.1:3333 (Local P2Pool)".to_string(), + }; + api_ip = "127.0.0.1".to_string(); + api_port = "18088".to_string(); + + // [Advanced] + } else { + // Overriding command arguments + if !state.arguments.is_empty() { + // This parses the input and attempts to fill out + // the [ImgXmrig]... This is pretty bad code... + let mut last = ""; + let lock = lock!(helper); + let mut xmrig_image = lock!(lock.img_xmrig); + for arg in state.arguments.split_whitespace() { + match last { + "--threads" => xmrig_image.threads = arg.to_string(), + "--url" => xmrig_image.url = arg.to_string(), + "--http-host" => { + api_ip = if arg == "localhost" { + "127.0.0.1".to_string() + } else { + arg.to_string() + } + } + "--http-port" => api_port = arg.to_string(), + _ => (), + } + args.push(if arg == "localhost" { + "127.0.0.1".to_string() + } else { + arg.to_string() + }); + last = arg; + } + // Else, build the argument + } else { + // XMRig doesn't understand [localhost] + let ip = if state.ip == "localhost" || state.ip.is_empty() { + "127.0.0.1" + } else { + &state.ip + }; + api_ip = if state.api_ip == "localhost" || state.api_ip.is_empty() { + "127.0.0.1".to_string() + } else { + state.api_ip.to_string() + }; + api_port = if state.api_port.is_empty() { + "18088".to_string() + } else { + state.api_port.to_string() + }; + let url = format!("{}:{}", ip, state.port); // Combine IP:Port into one string + args.push("--user".to_string()); + args.push(state.address.clone()); // Wallet + args.push("--threads".to_string()); + args.push(state.current_threads.to_string()); // Threads + args.push("--rig-id".to_string()); + args.push(state.rig.to_string()); // Rig ID + args.push("--url".to_string()); + args.push(url.clone()); // IP/Port + args.push("--http-host".to_string()); + args.push(api_ip.to_string()); // HTTP API IP + args.push("--http-port".to_string()); + args.push(api_port.to_string()); // HTTP API Port + args.push("--no-color".to_string()); // No color escape codes + if state.tls { + args.push("--tls".to_string()); + } // TLS + if state.keepalive { + args.push("--keepalive".to_string()); + } // Keepalive + if state.pause != 0 { + args.push("--pause-on-active".to_string()); + args.push(state.pause.to_string()); + } // Pause on active + *lock2!(helper, img_xmrig) = ImgXmrig { + url, + threads: state.current_threads.to_string(), + }; + } + } + (args, format!("{}:{}", api_ip, api_port)) + } + + // We actually spawn [sudo] on Unix, with XMRig being the argument. + #[cfg(target_family = "unix")] + fn create_xmrig_cmd_unix(args: Vec, path: PathBuf) -> portable_pty::CommandBuilder { + let mut cmd = portable_pty::cmdbuilder::CommandBuilder::new("sudo"); + cmd.args(args); + cmd.cwd(path.as_path().parent().unwrap()); + cmd + } + + // Gupax should be admin on Windows, so just spawn XMRig normally. + #[cfg(target_os = "windows")] + fn create_xmrig_cmd_windows(args: Vec, path: PathBuf) -> portable_pty::CommandBuilder { + let mut cmd = portable_pty::cmdbuilder::CommandBuilder::new(path.clone()); + cmd.args(args); + cmd.cwd(path.as_path().parent().unwrap()); + cmd + } + + #[cold] + #[inline(never)] + // The XMRig watchdog. Spawns 1 OS thread for reading a PTY (STDOUT+STDERR), and combines the [Child] with a PTY so STDIN actually works. + // This isn't actually async, a tokio runtime is unfortunately needed because [Hyper] is an async library (HTTP API calls) + #[tokio::main] + #[allow(clippy::await_holding_lock)] + async fn spawn_xmrig_watchdog( + process: Arc>, + gui_api: Arc>, + pub_api: Arc>, + args: Vec, + path: std::path::PathBuf, + sudo: Arc>, + mut api_ip_port: String, + ) { + // 1a. Create PTY + debug!("XMRig | Creating PTY..."); + let pty = portable_pty::native_pty_system(); + let pair = pty + .openpty(portable_pty::PtySize { + rows: 100, + cols: 1000, + pixel_width: 0, + pixel_height: 0, + }) + .unwrap(); + // 1b. Create command + debug!("XMRig | Creating command..."); + #[cfg(target_os = "windows")] + let cmd = Self::create_xmrig_cmd_windows(args, path); + #[cfg(target_family = "unix")] + let cmd = Self::create_xmrig_cmd_unix(args, path); + // 1c. Create child + debug!("XMRig | Creating child..."); + let child_pty = arc_mut!(pair.slave.spawn_command(cmd).unwrap()); + drop(pair.slave); + + let mut stdin = pair.master.take_writer().unwrap(); + + // 2. Input [sudo] pass, wipe, then drop. + if cfg!(unix) { + debug!("XMRig | Inputting [sudo] and wiping..."); + // a) Sleep to wait for [sudo]'s non-echo prompt (on Unix). + // this prevents users pass from showing up in the STDOUT. + sleep!(3000); + if let Err(e) = writeln!(stdin, "{}", lock!(sudo).pass) { + error!("XMRig | Sudo STDIN error: {}", e); + }; + SudoState::wipe(&sudo); + + // b) Reset GUI STDOUT just in case. + debug!("XMRig | Clearing GUI output..."); + lock!(gui_api).output.clear(); + } + + // 3. Set process state + debug!("XMRig | Setting process state..."); + let mut lock = lock!(process); + lock.state = ProcessState::NotMining; + lock.signal = ProcessSignal::None; + lock.start = Instant::now(); + let reader = pair.master.try_clone_reader().unwrap(); // Get STDOUT/STDERR before moving the PTY + drop(lock); + + // 4. Spawn PTY read thread + debug!("XMRig | Spawning PTY read thread..."); + let output_parse = Arc::clone(&lock!(process).output_parse); + let output_pub = Arc::clone(&lock!(process).output_pub); + thread::spawn(move || { + Self::read_pty_xmrig(output_parse, output_pub, reader); + }); + let output_parse = Arc::clone(&lock!(process).output_parse); + let output_pub = Arc::clone(&lock!(process).output_pub); + + let client: hyper::Client = + hyper::Client::builder().build(hyper::client::HttpConnector::new()); + let start = lock!(process).start; + let api_uri = { + if !api_ip_port.ends_with('/') { + api_ip_port.push('/'); + } + "http://".to_owned() + &api_ip_port + XMRIG_API_URI + }; + info!("XMRig | Final API URI: {}", api_uri); + + // Reset stats before loop + *lock!(pub_api) = PubXmrigApi::new(); + *lock!(gui_api) = PubXmrigApi::new(); + + // 5. Loop as watchdog + info!("XMRig | Entering watchdog mode... woof!"); + loop { + // Set timer + let now = Instant::now(); + debug!("XMRig Watchdog | ----------- Start of loop -----------"); + + // Check if the process secretly died without us knowing :) + if let Ok(Some(code)) = lock!(child_pty).try_wait() { + debug!("XMRig Watchdog | Process secretly died on us! Getting exit status..."); + let exit_status = match code.success() { + true => { + lock!(process).state = ProcessState::Dead; + "Successful" + } + false => { + lock!(process).state = ProcessState::Failed; + "Failed" + } + }; + let uptime = HumanTime::into_human(start.elapsed()); + info!( + "XMRig | Stopped ... Uptime was: [{}], Exit status: [{}]", + uptime, exit_status + ); + if let Err(e) = writeln!( + lock!(gui_api).output, + "{}\nXMRig stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n", + HORI_CONSOLE, + uptime, + exit_status, + HORI_CONSOLE + ) { + error!( + "XMRig Watchdog | GUI Uptime/Exit status write failed: {}", + e + ); + } + lock!(process).signal = ProcessSignal::None; + debug!("XMRig Watchdog | Secret dead process reap OK, breaking"); + break; + } + + // Stop on [Stop/Restart] SIGNAL + let signal = lock!(process).signal; + if signal == ProcessSignal::Stop || signal == ProcessSignal::Restart { + debug!("XMRig Watchdog | Stop/Restart SIGNAL caught"); + // macOS requires [sudo] again to kill [XMRig] + if cfg!(target_os = "macos") { + // If we're at this point, that means the user has + // entered their [sudo] pass again, after we wiped it. + // So, we should be able to find it in our [Arc>]. + Self::sudo_kill(lock!(child_pty).process_id().unwrap(), &sudo); + // And... wipe it again (only if we're stopping full). + // If we're restarting, the next start will wipe it for us. + if signal != ProcessSignal::Restart { + SudoState::wipe(&sudo); + } + } else if let Err(e) = lock!(child_pty).kill() { + error!("XMRig Watchdog | Kill error: {}", e); + } + let exit_status = match lock!(child_pty).wait() { + Ok(e) => { + let mut process = lock!(process); + if e.success() { + if process.signal == ProcessSignal::Stop { + process.state = ProcessState::Dead; + } + "Successful" + } else { + if process.signal == ProcessSignal::Stop { + process.state = ProcessState::Failed; + } + "Failed" + } + } + _ => { + let mut process = lock!(process); + if process.signal == ProcessSignal::Stop { + process.state = ProcessState::Failed; + } + "Unknown Error" + } + }; + let uptime = HumanTime::into_human(start.elapsed()); + info!( + "XMRig | Stopped ... Uptime was: [{}], Exit status: [{}]", + uptime, exit_status + ); + if let Err(e) = writeln!( + lock!(gui_api).output, + "{}\nXMRig stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n", + HORI_CONSOLE, + uptime, + exit_status, + HORI_CONSOLE + ) { + error!( + "XMRig Watchdog | GUI Uptime/Exit status write failed: {}", + e + ); + } + let mut process = lock!(process); + match process.signal { + ProcessSignal::Stop => process.signal = ProcessSignal::None, + ProcessSignal::Restart => process.state = ProcessState::Waiting, + _ => (), + } + debug!("XMRig Watchdog | Stop/Restart SIGNAL done, breaking"); + break; + } + + // Check vector of user input + { + let mut lock = lock!(process); + if !lock.input.is_empty() { + let input = std::mem::take(&mut lock.input); + for line in input { + if line.is_empty() { + continue; + } + debug!( + "XMRig Watchdog | User input not empty, writing to STDIN: [{}]", + line + ); + #[cfg(target_os = "windows")] + if let Err(e) = write!(stdin, "{}\r\n", line) { + error!("XMRig Watchdog | STDIN error: {}", e); + } + #[cfg(target_family = "unix")] + if let Err(e) = writeln!(stdin, "{}", line) { + error!("XMRig Watchdog | STDIN error: {}", e); + } + // Flush. + if let Err(e) = stdin.flush() { + error!("XMRig Watchdog | STDIN flush error: {}", e); + } + } + } + } + // Check if logs need resetting + debug!("XMRig Watchdog | Attempting GUI log reset check"); + { + let mut lock = lock!(gui_api); + Self::check_reset_gui_output(&mut lock.output, ProcessName::Xmrig); + } + // Always update from output + debug!("XMRig Watchdog | Starting [update_from_output()]"); + PubXmrigApi::update_from_output( + &pub_api, + &output_pub, + &output_parse, + start.elapsed(), + &process, + ); + + // Send an HTTP API request + debug!("XMRig Watchdog | Attempting HTTP API request..."); + if let Ok(priv_api) = PrivXmrigApi::request_xmrig_api(client.clone(), &api_uri).await { + debug!("XMRig Watchdog | HTTP API request OK, attempting [update_from_priv()]"); + PubXmrigApi::update_from_priv(&pub_api, priv_api); + } else { + warn!( + "XMRig Watchdog | Could not send HTTP API request to: {}", + api_uri + ); + } + + // 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 + if elapsed < 900 { + let sleep = (900 - elapsed) as u64; + debug!( + "XMRig Watchdog | END OF LOOP - Sleeping for [{}]ms...", + sleep + ); + sleep!(sleep); + } else { + debug!("XMRig Watchdog | END OF LOOP - Not sleeping!"); + } + } + + // 5. If loop broke, we must be done here. + info!("XMRig Watchdog | Watchdog thread exiting... Goodbye!"); + } +} +//---------------------------------------------------------------------------------------------------- [ImgXmrig] +#[derive(Debug, Clone)] +pub struct ImgXmrig { + pub threads: String, + pub url: String, +} + +impl Default for ImgXmrig { + fn default() -> Self { + Self::new() + } +} + +impl ImgXmrig { + pub fn new() -> Self { + Self { + threads: "???".to_string(), + url: "???".to_string(), + } + } +} + +//---------------------------------------------------------------------------------------------------- Public XMRig API +#[derive(Debug, Clone)] +pub struct PubXmrigApi { + pub output: String, + pub uptime: HumanTime, + pub worker_id: String, + pub resources: HumanNumber, + pub hashrate: HumanNumber, + pub diff: HumanNumber, + pub accepted: HumanNumber, + pub rejected: HumanNumber, + + pub hashrate_raw: f32, +} + +impl Default for PubXmrigApi { + fn default() -> Self { + Self::new() + } +} + +impl PubXmrigApi { + pub fn new() -> Self { + Self { + output: String::new(), + uptime: HumanTime::new(), + worker_id: "???".to_string(), + resources: HumanNumber::unknown(), + hashrate: HumanNumber::unknown(), + diff: HumanNumber::unknown(), + accepted: HumanNumber::unknown(), + rejected: HumanNumber::unknown(), + hashrate_raw: 0.0, + } + } + + #[inline] + pub(super) fn combine_gui_pub_api(gui_api: &mut Self, pub_api: &mut Self) { + let output = std::mem::take(&mut gui_api.output); + let buf = std::mem::take(&mut pub_api.output); + *gui_api = Self { + output, + ..std::mem::take(pub_api) + }; + if !buf.is_empty() { + gui_api.output.push_str(&buf); + } + } + + // This combines the buffer from the PTY thread [output_pub] + // with the actual [PubApiXmrig] output field. + pub(super) fn update_from_output( + public: &Arc>, + output_parse: &Arc>, + output_pub: &Arc>, + elapsed: std::time::Duration, + process: &Arc>, + ) { + // 1. Take the process's current output buffer and combine it with Pub (if not empty) + let mut output_pub = lock!(output_pub); + + { + let mut public = lock!(public); + if !output_pub.is_empty() { + public.output.push_str(&std::mem::take(&mut *output_pub)); + } + // Update uptime + public.uptime = HumanTime::into_human(elapsed); + } + + // 2. Check for "new job"/"no active...". + let mut output_parse = lock!(output_parse); + if XMRIG_REGEX.new_job.is_match(&output_parse) { + lock!(process).state = ProcessState::Alive; + } else if XMRIG_REGEX.not_mining.is_match(&output_parse) { + lock!(process).state = ProcessState::NotMining; + } + + // 3. Throw away [output_parse] + output_parse.clear(); + drop(output_parse); + } + + // Formats raw private data into ready-to-print human readable version. + fn update_from_priv(public: &Arc>, private: PrivXmrigApi) { + let mut public = lock!(public); + let hashrate_raw = match private.hashrate.total.first() { + Some(Some(h)) => *h, + _ => 0.0, + }; + + *public = Self { + worker_id: private.worker_id, + resources: HumanNumber::from_load(private.resources.load_average), + hashrate: HumanNumber::from_hashrate(private.hashrate.total), + diff: HumanNumber::from_u128(private.connection.diff), + accepted: HumanNumber::from_u128(private.connection.accepted), + rejected: HumanNumber::from_u128(private.connection.rejected), + hashrate_raw, + ..std::mem::take(&mut *public) + } + } +} + +//---------------------------------------------------------------------------------------------------- Private XMRig API +// This matches to some JSON stats in the HTTP call [summary], +// 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, Clone)] +pub(super) struct PrivXmrigApi { + worker_id: String, + resources: Resources, + connection: Connection, + hashrate: Hashrate, +} + +impl PrivXmrigApi { + fn new() -> Self { + Self { + worker_id: String::new(), + resources: Resources::new(), + connection: Connection::new(), + hashrate: Hashrate::new(), + } + } + + #[inline] + // Send an HTTP request to XMRig's API, serialize it into [Self] and return it + async fn request_xmrig_api( + client: hyper::Client, + api_uri: &str, + ) -> std::result::Result { + let request = hyper::Request::builder() + .method("GET") + .uri(api_uri) + .body(hyper::Body::empty())?; + let response = tokio::time::timeout( + std::time::Duration::from_millis(500), + client.request(request), + ) + .await?; + let body = hyper::body::to_bytes(response?.body_mut()).await?; + Ok(serde_json::from_slice::(&body)?) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +struct Resources { + load_average: [Option; 3], +} +impl Resources { + fn new() -> Self { + Self { + load_average: [Some(0.0), Some(0.0), Some(0.0)], + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct Connection { + diff: u128, + accepted: u128, + rejected: u128, +} +impl Connection { + fn new() -> Self { + Self { + diff: 0, + accepted: 0, + rejected: 0, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +struct Hashrate { + total: [Option; 3], +} +impl Hashrate { + fn new() -> Self { + Self { + total: [Some(0.0), Some(0.0), Some(0.0)], + } + } +} diff --git a/src/inits.rs b/src/inits.rs new file mode 100644 index 0000000..0548b6b --- /dev/null +++ b/src/inits.rs @@ -0,0 +1,209 @@ +use std::io::Write; +use crate::components::update::Update; +use crate::helper::{Helper, ProcessSignal}; +use crate::utils::constants::{APP_MIN_WIDTH, APP_MIN_HEIGHT, APP_MAX_WIDTH, APP_MAX_HEIGHT, BYTES_ICON}; +use crate::utils::regex::Regexes; +//---------------------------------------------------------------------------------------------------- Init functions +use crate::{components::node::Ping, miscs::clamp_scale}; +use crate::app::App; +use std::sync::Arc; +use std::time::Instant; +use eframe::NativeOptions; +use env_logger::fmt::style::Style; +use env_logger::{Builder, WriteStyle}; +use log::LevelFilter; +use egui::TextStyle::{Body, Button, Monospace, Heading, Name}; +use crate::{disk::state::*, utils::macros::lock}; +use egui::TextStyle::Small; +use crate::{info, warn}; +use egui::*; + +#[cold] +#[inline(never)] +pub fn init_text_styles(ctx: &egui::Context, width: f32, pixels_per_point: f32) { + let scale = width / 35.5; + let mut style = (*ctx.style()).clone(); + style.text_styles = [ + (Small, FontId::new(scale / 3.0, egui::FontFamily::Monospace)), + (Body, FontId::new(scale / 2.0, egui::FontFamily::Monospace)), + ( + Button, + FontId::new(scale / 2.0, egui::FontFamily::Monospace), + ), + ( + Monospace, + FontId::new(scale / 2.0, egui::FontFamily::Monospace), + ), + ( + Heading, + FontId::new(scale / 1.5, egui::FontFamily::Monospace), + ), + ( + Name("Tab".into()), + FontId::new(scale * 1.2, egui::FontFamily::Monospace), + ), + ( + Name("Bottom".into()), + FontId::new(scale / 2.0, egui::FontFamily::Monospace), + ), + ( + Name("MonospaceSmall".into()), + FontId::new(scale / 2.5, egui::FontFamily::Monospace), + ), + ( + Name("MonospaceLarge".into()), + FontId::new(scale / 1.5, egui::FontFamily::Monospace), + ), + ] + .into(); + style.spacing.icon_width_inner = width / 35.0; + style.spacing.icon_width = width / 25.0; + style.spacing.icon_spacing = 20.0; + style.spacing.scroll = egui::style::ScrollStyle { + bar_width: width / 150.0, + ..egui::style::ScrollStyle::solid() + }; + ctx.set_style(style); + // Make sure scale f32 is a regular number. + let pixels_per_point = clamp_scale(pixels_per_point); + ctx.set_pixels_per_point(pixels_per_point); + ctx.request_repaint(); +} + +#[cold] +#[inline(never)] +pub fn init_logger(now: Instant) { + let filter_env = std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".to_string()); + let filter = match filter_env.as_str() { + "error" | "Error" | "ERROR" => LevelFilter::Error, + "warn" | "Warn" | "WARN" => LevelFilter::Warn, + "debug" | "Debug" | "DEBUG" => LevelFilter::Debug, + "trace" | "Trace" | "TRACE" => LevelFilter::Trace, + _ => LevelFilter::Info, + }; + std::env::set_var("RUST_LOG", format!("off,gupax={}", filter_env)); + + Builder::new() + .format(move |buf, record| { + let level = record.level(); + let level_style = buf.default_level_style(level); + let dimmed = Style::new().dimmed(); + writeln!( + buf, + "{level_style}[{}]{level_style:#} [{dimmed}{:.3}{dimmed:#}] [{dimmed}{}{dimmed:#}:{dimmed}{}{dimmed:#}] {}", + level, + now.elapsed().as_secs_f32(), + record.file().unwrap_or("???"), + record.line().unwrap_or(0), + record.args(), + ) + }) + .filter_level(filter) + .write_style(WriteStyle::Always) + .parse_default_env() + .format_timestamp_millis() + .init(); + info!("init_logger() ... OK"); + info!("Log level ... {}", filter); +} + +#[cold] +#[inline(never)] +pub fn init_options(initial_window_size: Option) -> NativeOptions { + let mut options = eframe::NativeOptions::default(); + options.viewport.min_inner_size = Some(Vec2::new(APP_MIN_WIDTH, APP_MIN_HEIGHT)); + options.viewport.max_inner_size = Some(Vec2::new(APP_MAX_WIDTH, APP_MAX_HEIGHT)); + options.viewport.inner_size = initial_window_size; + options.follow_system_theme = false; + options.default_theme = eframe::Theme::Dark; + let icon = image::load_from_memory(BYTES_ICON) + .expect("Failed to read icon bytes") + .to_rgba8(); + let (icon_width, icon_height) = icon.dimensions(); + options.viewport.icon = Some(Arc::new(egui::viewport::IconData { + rgba: icon.into_raw(), + width: icon_width, + height: icon_height, + })); + info!("init_options() ... OK"); + options +} + +#[cold] +#[inline(never)] +pub fn init_auto(app: &mut App) { + // Return early if [--no-startup] was not passed + if app.no_startup { + info!("[--no-startup] flag passed, skipping init_auto()..."); + return; + } else if app.error_state.error { + info!("App error detected, skipping init_auto()..."); + return; + } else { + info!("Starting init_auto()..."); + } + + // [Auto-Update] + #[cfg(not(feature = "distro"))] + if app.state.gupax.auto_update { + Update::spawn_thread( + &app.og, + &app.state.gupax, + &app.state_path, + &app.update, + &mut app.error_state, + &app.restart, + ); + } else { + info!("Skipping auto-update..."); + } + + // [Auto-Ping] + if app.state.p2pool.auto_ping && app.state.p2pool.simple { + Ping::spawn_thread(&app.ping) + } else { + info!("Skipping auto-ping..."); + } + + // [Auto-P2Pool] + if app.state.gupax.auto_p2pool { + if !Regexes::addr_ok(&app.state.p2pool.address) { + warn!("Gupax | P2Pool address is not valid! Skipping auto-p2pool..."); + } else if !Gupax::path_is_file(&app.state.gupax.p2pool_path) { + warn!("Gupax | P2Pool path is not a file! Skipping auto-p2pool..."); + } else if !crate::components::update::check_p2pool_path(&app.state.gupax.p2pool_path) { + warn!("Gupax | P2Pool path is not valid! Skipping auto-p2pool..."); + } else { + let backup_hosts = app.gather_backup_hosts(); + Helper::start_p2pool( + &app.helper, + &app.state.p2pool, + &app.state.gupax.absolute_p2pool_path, + backup_hosts, + ); + } + } else { + info!("Skipping auto-p2pool..."); + } + + // [Auto-XMRig] + if app.state.gupax.auto_xmrig { + if !Gupax::path_is_file(&app.state.gupax.xmrig_path) { + warn!("Gupax | XMRig path is not an executable! Skipping auto-xmrig..."); + } else if !crate::components::update::check_xmrig_path(&app.state.gupax.xmrig_path) { + warn!("Gupax | XMRig path is not valid! Skipping auto-xmrig..."); + } else if cfg!(windows) { + Helper::start_xmrig( + &app.helper, + &app.state.xmrig, + &app.state.gupax.absolute_xmrig_path, + Arc::clone(&app.sudo), + ); + } else { + lock!(app.sudo).signal = ProcessSignal::Start; + app.error_state.ask_sudo(&app.sudo); + } + } else { + info!("Skipping auto-xmrig..."); + } +} diff --git a/src/main.rs b/src/main.rs index 1ae81f7..768a77f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,1364 +25,31 @@ compile_error!("gupax is only compatible with 64-bit CPUs"); #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux",)))] compile_error!("gupax is only built for windows/macos/linux"); +use crate::app::App; //---------------------------------------------------------------------------------------------------- Imports -// egui/eframe -use eframe::{egui, NativeOptions}; -use egui::{ - Align, Button, CentralPanel, Color32, FontId, Hyperlink, Key, Label, - Layout, Modifiers, RichText, SelectableLabel, Spinner, TextEdit, TextStyle, - TextStyle::*, TopBottomPanel, Vec2, Image, -}; -// Logging -use env_logger::{ - fmt::style::Style, - Builder, WriteStyle, -}; -use log::*; -// Regex -use ::regex::Regex; -// Serde -use serde::{Deserialize, Serialize}; -// std -use std::{ - env, - io::Write, - path::PathBuf, - process::exit, - sync::{Arc, Mutex}, - time::Instant, -}; -// Modules -//mod benchmark; -mod constants; +use crate::constants::*; +use crate::inits::init_auto; +use crate::inits::init_logger; +use crate::inits::init_options; +use crate::miscs::clean_dir; +use crate::utils::*; +use egui::Vec2; +use log::info; +use log::warn; +use std::time::Instant; + +mod app; +mod components; mod disk; -mod ferris; -mod free; -mod gupax; mod helper; -mod human; -mod macros; -mod node; -mod p2pool; -mod panic; -mod regex; -mod status; -mod update; -mod xmr; -mod xmrig; -mod xvb; -use { - crate::regex::*, constants::*, disk::*, ferris::*, gupax::*, helper::*, macros::*, node::*, - update::*, -}; +mod inits; +mod miscs; +mod utils; // Sudo (dummy values for Windows) -mod sudo; -use crate::sudo::*; #[cfg(target_family = "unix")] extern crate sudo as sudo_check; -//---------------------------------------------------------------------------------------------------- Struct + Impl -// The state of the outer main [App]. -// See the [State] struct in [state.rs] for the -// actual inner state of the tab settings. -pub struct App { - // Misc state - tab: Tab, // What tab are we on? - width: f32, // Top-level width - height: f32, // Top-level height - // Alpha (transparency) - // This value is used to incrementally increase/decrease - // the transparency when resizing. Basically, it fades - // in/out of black to hide jitter when resizing with [init_text_styles()] - alpha: u8, - // This is a one time trigger so [init_text_styles()] isn't - // called 60x a second when resizing the window. Instead, - // it only gets called if this bool is true and the user - // is hovering over egui (ctx.is_pointer_over_area()). - must_resize: bool, // Sets the flag so we know to [init_text_styles()] - resizing: bool, // Are we in the process of resizing? (For black fade in/out) - // State - og: Arc>, // og = Old state to compare against - state: State, // state = Working state (current settings) - update: Arc>, // State for update data [update.rs] - file_window: Arc>, // State for the path selector in [Gupax] - ping: Arc>, // Ping data found in [node.rs] - og_node_vec: Vec<(String, Node)>, // Manual Node database - node_vec: Vec<(String, Node)>, // Manual Node database - og_pool_vec: Vec<(String, Pool)>, // Manual Pool database - pool_vec: Vec<(String, Pool)>, // Manual Pool database - diff: bool, // This bool indicates state changes - // Restart state: - // If Gupax updated itself, this represents that the - // user should (but isn't required to) restart Gupax. - restart: Arc>, - // Error State: - // These values are essentially global variables that - // indicate if an error message needs to be displayed - // (it takes up the whole screen with [error_msg] and buttons for ok/quit/etc) - error_state: ErrorState, - // 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>, // [Helper] state, mostly for Gupax uptime - pub_sys: Arc>, // [Sys] state, read by [Status], mutated by [Helper] - 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 - p2pool_img: Arc>, // A one-time snapshot of what data P2Pool started with - xmrig_img: Arc>, // A one-time snapshot of what data XMRig started with - // STDIN Buffer - p2pool_stdin: String, // The buffer between the p2pool console and the [Helper] - xmrig_stdin: String, // The buffer between the xmrig console and the [Helper] - // Sudo State - sudo: Arc>, // This is just a dummy struct on [Windows]. - // State from [--flags] - no_startup: bool, - // Gupax-P2Pool API - // Gupax's P2Pool API (e.g: ~/.local/share/gupax/p2pool/) - // This is a file-based API that contains data for permanent stats. - // The below struct holds everything needed for it, the paths, the - // actual stats, and all the functions needed to mutate them. - gupax_p2pool_api: Arc>, - // Static stuff - benchmarks: Vec, // XMRig CPU benchmarks - pid: sysinfo::Pid, // Gupax's PID - max_threads: usize, // Max amount of detected system threads - now: Instant, // Internal timer - exe: String, // Path for [Gupax] binary - dir: String, // Directory [Gupax] binary is in - resolution: Vec2, // Frame resolution - os: &'static str, // OS - admin: bool, // Are we admin? (for Windows) - os_data_path: PathBuf, // OS data path (e.g: ~/.local/share/gupax/) - gupax_p2pool_api_path: PathBuf, // Gupax-P2Pool API path (e.g: ~/.local/share/gupax/p2pool/) - state_path: PathBuf, // State file path - node_path: PathBuf, // Node file path - pool_path: PathBuf, // Pool file path - version: &'static str, // Gupax version - name_version: String, // [Gupax vX.X.X] -} - -impl App { - #[cold] - #[inline(never)] - fn cc(cc: &eframe::CreationContext<'_>, resolution: Vec2, app: Self) -> Self { - init_text_styles( - &cc.egui_ctx, - resolution[0], - crate::free::clamp_scale(app.state.gupax.selected_scale), - ); - cc.egui_ctx.set_visuals(VISUALS.clone()); - Self { resolution, ..app } - } - - #[cold] - #[inline(never)] - fn save_before_quit(&mut self) { - if let Err(e) = State::save(&mut self.state, &self.state_path) { - error!("State file: {}", e); - } - if let Err(e) = Node::save(&self.node_vec, &self.node_path) { - error!("Node list: {}", e); - } - if let Err(e) = Pool::save(&self.pool_vec, &self.pool_path) { - error!("Pool list: {}", e); - } - } - - #[cold] - #[inline(never)] - fn new(now: Instant) -> Self { - info!("Initializing App Struct..."); - info!("App Init | P2Pool & XMRig processes..."); - let p2pool = arc_mut!(Process::new( - ProcessName::P2pool, - String::new(), - PathBuf::new() - )); - let xmrig = arc_mut!(Process::new( - ProcessName::Xmrig, - String::new(), - PathBuf::new() - )); - let p2pool_api = arc_mut!(PubP2poolApi::new()); - let xmrig_api = arc_mut!(PubXmrigApi::new()); - let p2pool_img = arc_mut!(ImgP2pool::new()); - let xmrig_img = arc_mut!(ImgXmrig::new()); - - info!("App Init | Sysinfo..."); - // We give this to the [Helper] thread. - let mut sysinfo = sysinfo::System::new_with_specifics( - sysinfo::RefreshKind::new() - .with_cpu(sysinfo::CpuRefreshKind::everything()) - .with_processes(sysinfo::ProcessRefreshKind::new().with_cpu()) - .with_memory(sysinfo::MemoryRefreshKind::everything()), - ); - sysinfo.refresh_all(); - let pid = match sysinfo::get_current_pid() { - Ok(pid) => pid, - Err(e) => { - error!("App Init | Failed to get sysinfo PID: {}", e); - exit(1) - } - }; - let pub_sys = arc_mut!(Sys::new()); - - // CPU Benchmark data initialization. - info!("App Init | Initializing CPU benchmarks..."); - let benchmarks: Vec = { - let cpu = sysinfo.cpus()[0].brand(); - let mut json: Vec = - serde_json::from_slice(include_bytes!("cpu.json")).unwrap(); - json.sort_by(|a, b| cmp_f64(strsim::jaro(&b.cpu, cpu), strsim::jaro(&a.cpu, cpu))); - json - }; - info!("App Init | Assuming user's CPU is: {}", benchmarks[0].cpu); - - info!("App Init | The rest of the [App]..."); - let mut app = Self { - tab: Tab::default(), - ping: arc_mut!(Ping::new()), - width: APP_DEFAULT_WIDTH, - height: APP_DEFAULT_HEIGHT, - must_resize: false, - og: arc_mut!(State::new()), - state: State::new(), - update: arc_mut!(Update::new( - String::new(), - PathBuf::new(), - PathBuf::new(), - true - )), - file_window: FileWindow::new(), - og_node_vec: Node::new_vec(), - node_vec: Node::new_vec(), - og_pool_vec: Pool::new_vec(), - pool_vec: Pool::new_vec(), - restart: arc_mut!(Restart::No), - diff: false, - error_state: ErrorState::new(), - helper: arc_mut!(Helper::new( - now, - pub_sys.clone(), - p2pool.clone(), - xmrig.clone(), - p2pool_api.clone(), - xmrig_api.clone(), - p2pool_img.clone(), - xmrig_img.clone(), - arc_mut!(GupaxP2poolApi::new()) - )), - p2pool, - xmrig, - p2pool_api, - xmrig_api, - p2pool_img, - xmrig_img, - p2pool_stdin: String::with_capacity(10), - xmrig_stdin: String::with_capacity(10), - sudo: arc_mut!(SudoState::new()), - resizing: false, - alpha: 0, - no_startup: false, - gupax_p2pool_api: arc_mut!(GupaxP2poolApi::new()), - pub_sys, - benchmarks, - pid, - max_threads: benri::threads!(), - now, - admin: false, - exe: String::new(), - dir: String::new(), - resolution: Vec2::new(APP_DEFAULT_HEIGHT, APP_DEFAULT_WIDTH), - os: OS, - os_data_path: PathBuf::new(), - gupax_p2pool_api_path: PathBuf::new(), - state_path: PathBuf::new(), - node_path: PathBuf::new(), - pool_path: PathBuf::new(), - version: GUPAX_VERSION, - name_version: format!("Gupax {}", GUPAX_VERSION), - }; - //---------------------------------------------------------------------------------------------------- App init data that *could* panic - info!("App Init | Getting EXE path..."); - let mut panic = String::new(); - // Get exe path - app.exe = match get_exe() { - Ok(exe) => exe, - Err(e) => { - panic = format!("get_exe(): {}", e); - app.error_state - .set(panic.clone(), ErrorFerris::Panic, ErrorButtons::Quit); - String::new() - } - }; - // Get exe directory path - app.dir = match get_exe_dir() { - Ok(dir) => dir, - Err(e) => { - panic = format!("get_exe_dir(): {}", e); - app.error_state - .set(panic.clone(), ErrorFerris::Panic, ErrorButtons::Quit); - String::new() - } - }; - // Get OS data path - app.os_data_path = match get_gupax_data_path() { - Ok(dir) => dir, - Err(e) => { - panic = format!("get_os_data_path(): {}", e); - app.error_state - .set(panic.clone(), ErrorFerris::Panic, ErrorButtons::Quit); - PathBuf::new() - } - }; - - info!("App Init | Setting TOML path..."); - // Set [*.toml] path - app.state_path = app.os_data_path.clone(); - app.state_path.push(STATE_TOML); - app.node_path = app.os_data_path.clone(); - app.node_path.push(NODE_TOML); - app.pool_path = app.os_data_path.clone(); - app.pool_path.push(POOL_TOML); - // Set GupaxP2poolApi path - app.gupax_p2pool_api_path = crate::disk::get_gupax_p2pool_path(&app.os_data_path); - lock!(app.gupax_p2pool_api).fill_paths(&app.gupax_p2pool_api_path); - - // Apply arg state - // It's not safe to [--reset] if any of the previous variables - // are unset (null path), so make sure we just abort if the [panic] String contains something. - info!("App Init | Applying argument state..."); - let mut app = parse_args(app, panic); - - // Read disk state - info!("App Init | Reading disk state..."); - use TomlError::*; - app.state = match State::get(&app.state_path) { - Ok(toml) => toml, - Err(err) => { - error!("State ... {}", err); - let set = match err { - Io(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)), - Path(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)), - Serialize(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)), - Deserialize(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)), - Format(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)), - Merge(e) => Some((e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState)), - _ => None, - }; - if let Some((e, ferris, button)) = set { - app.error_state.set(format!("State file: {}\n\nTry deleting: {}\n\n(Warning: this will delete your Gupax settings)\n\n", e, app.state_path.display()), ferris, button); - } - - State::new() - } - }; - // Clamp window resolution scaling values. - app.state.gupax.selected_scale = crate::free::clamp_scale(app.state.gupax.selected_scale); - - app.og = arc_mut!(app.state.clone()); - // Read node list - info!("App Init | Reading node list..."); - app.node_vec = match Node::get(&app.node_path) { - Ok(toml) => toml, - Err(err) => { - error!("Node ... {}", err); - let (e, ferris, button) = match err { - Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState), - Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - }; - app.error_state.set(format!("Node list: {}\n\nTry deleting: {}\n\n(Warning: this will delete your custom node list)\n\n", e, app.node_path.display()), ferris, button); - Node::new_vec() - } - }; - app.og_node_vec = app.node_vec.clone(); - debug!("Node Vec:"); - debug!("{:#?}", app.node_vec); - // Read pool list - info!("App Init | Reading pool list..."); - app.pool_vec = match Pool::get(&app.pool_path) { - Ok(toml) => toml, - Err(err) => { - error!("Pool ... {}", err); - let (e, ferris, button) = match err { - Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState), - Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - }; - app.error_state.set(format!("Pool list: {}\n\nTry deleting: {}\n\n(Warning: this will delete your custom pool list)\n\n", e, app.pool_path.display()), ferris, button); - Pool::new_vec() - } - }; - app.og_pool_vec = app.pool_vec.clone(); - debug!("Pool Vec:"); - debug!("{:#?}", app.pool_vec); - - //---------------------------------------------------------------------------------------------------- - // Read [GupaxP2poolApi] disk files - let mut gupax_p2pool_api = lock!(app.gupax_p2pool_api); - match GupaxP2poolApi::create_all_files(&app.gupax_p2pool_api_path) { - Ok(_) => info!("App Init | Creating Gupax-P2Pool API files ... OK"), - Err(err) => { - error!("GupaxP2poolApi ... {}", err); - let (e, ferris, button) = match err { - Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState), - Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - }; - app.error_state.set(format!("Gupax P2Pool Stats: {}\n\nTry deleting: {}\n\n(Warning: this will delete your P2Pool payout history...!)\n\n", e, app.gupax_p2pool_api_path.display()), ferris, button); - } - } - info!("App Init | Reading Gupax-P2Pool API files..."); - match gupax_p2pool_api.read_all_files_and_update() { - Ok(_) => { - info!( - "GupaxP2poolApi ... Payouts: {} | XMR (atomic-units): {}", - gupax_p2pool_api.payout, gupax_p2pool_api.xmr, - ); - } - Err(err) => { - error!("GupaxP2poolApi ... {}", err); - let (e, ferris, button) = match err { - Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState), - Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit), - }; - app.error_state.set(format!("Gupax P2Pool Stats: {}\n\nTry deleting: {}\n\n(Warning: this will delete your P2Pool payout history...!)\n\n", e, app.gupax_p2pool_api_path.display()), ferris, button); - } - }; - drop(gupax_p2pool_api); - lock!(app.helper).gupax_p2pool_api = Arc::clone(&app.gupax_p2pool_api); - - //---------------------------------------------------------------------------------------------------- - let mut og = lock!(app.og); // Lock [og] - // Handle max threads - info!("App Init | Handling max thread overflow..."); - og.xmrig.max_threads = app.max_threads; - let current = og.xmrig.current_threads; - let max = og.xmrig.max_threads; - if current > max { - og.xmrig.current_threads = max; - } - // Handle [node_vec] overflow - info!("App Init | Handling [node_vec] overflow"); - if og.p2pool.selected_index > app.og_node_vec.len() { - warn!( - "App | Overflowing manual node index [{} > {}]", - og.p2pool.selected_index, - app.og_node_vec.len() - ); - let (name, node) = match app.og_node_vec.first() { - Some(zero) => zero.clone(), - None => Node::new_tuple(), - }; - og.p2pool.selected_index = 0; - og.p2pool.selected_name = name.clone(); - og.p2pool.selected_ip = node.ip.clone(); - og.p2pool.selected_rpc = node.rpc.clone(); - og.p2pool.selected_zmq = node.zmq.clone(); - app.state.p2pool.selected_index = 0; - app.state.p2pool.selected_name = name; - app.state.p2pool.selected_ip = node.ip; - app.state.p2pool.selected_rpc = node.rpc; - app.state.p2pool.selected_zmq = node.zmq; - } - // Handle [pool_vec] overflow - info!("App Init | Handling [pool_vec] overflow..."); - if og.xmrig.selected_index > app.og_pool_vec.len() { - warn!( - "App | Overflowing manual pool index [{} > {}], resetting to 1", - og.xmrig.selected_index, - app.og_pool_vec.len() - ); - let (name, pool) = match app.og_pool_vec.first() { - Some(zero) => zero.clone(), - None => Pool::new_tuple(), - }; - og.xmrig.selected_index = 0; - og.xmrig.selected_name = name.clone(); - og.xmrig.selected_ip = pool.ip.clone(); - og.xmrig.selected_port = pool.port.clone(); - app.state.xmrig.selected_index = 0; - app.state.xmrig.selected_name = name; - app.state.xmrig.selected_ip = pool.ip; - app.state.xmrig.selected_port = pool.port; - } - - // Apply TOML values to [Update] - info!("App Init | Applying TOML values to [Update]..."); - let p2pool_path = og.gupax.absolute_p2pool_path.clone(); - let xmrig_path = og.gupax.absolute_xmrig_path.clone(); - let tor = og.gupax.update_via_tor; - app.update = arc_mut!(Update::new(app.exe.clone(), p2pool_path, xmrig_path, tor)); - - // Set state version as compiled in version - info!("App Init | Setting state Gupax version..."); - lock!(og.version).gupax = GUPAX_VERSION.to_string(); - lock!(app.state.version).gupax = GUPAX_VERSION.to_string(); - - // Set saved [Tab] - info!("App Init | Setting saved [Tab]..."); - app.tab = app.state.gupax.tab; - - // Check if [P2pool.node] exists - info!("App Init | Checking if saved remote node still exists..."); - app.state.p2pool.node = RemoteNode::check_exists(&app.state.p2pool.node); - - drop(og); // Unlock [og] - - // Spawn the "Helper" thread. - info!("Helper | Spawning helper thread..."); - Helper::spawn_helper(&app.helper, sysinfo, app.pid, app.max_threads); - info!("Helper ... OK"); - - // Check for privilege. Should be Admin on [Windows] and NOT root on Unix. - info!("App Init | Checking for privilege level..."); - #[cfg(target_os = "windows")] - if is_elevated::is_elevated() { - app.admin = true; - } else { - error!("Windows | Admin user not detected!"); - app.error_state.set(format!("Gupax was not launched as Administrator!\nBe warned, XMRig might have less hashrate!"), ErrorFerris::Sudo, ErrorButtons::WindowsAdmin); - } - #[cfg(target_family = "unix")] - if sudo_check::check() != sudo_check::RunningAs::User { - let id = sudo_check::check(); - error!("Unix | Regular user not detected: [{:?}]", id); - app.error_state.set(format!("Gupax was launched as: [{:?}]\nPlease launch Gupax with regular user permissions.", id), ErrorFerris::Panic, ErrorButtons::Quit); - } - - // macOS re-locates "dangerous" applications into some read-only "/private" directory. - // It _seems_ to be fixed by moving [Gupax.app] into "/Applications". - // So, detect if we are in in "/private" and warn the user. - #[cfg(target_os = "macos")] - if app.exe.starts_with("/private") { - app.error_state.set(format!("macOS thinks Gupax is a virus!\n(macOS has relocated Gupax for security reasons)\n\nThe directory: [{}]\nSince this is a private read-only directory, it causes issues with updates and correctly locating P2Pool/XMRig. Please move Gupax into the [Applications] directory, this lets macOS relax a little.\n", app.exe), ErrorFerris::Panic, ErrorButtons::Quit); - } - - info!("App ... OK"); - app - } - - #[cold] - #[inline(never)] - pub fn gather_backup_hosts(&self) -> Option> { - if !self.state.p2pool.backup_host { - return None; - } - - // INVARIANT: - // We must ensure all nodes are capable of - // sending/receiving valid JSON-RPC requests. - // - // This is done during the `Ping` phase, meaning - // all the nodes listed in our `self.ping` should - // have ping data. We can use this data to filter - // out "dead" nodes. - // - // The user must have at least pinged once so that - // we actually have this data to work off of, else, - // this "backup host" feature will return here - // with 0 extra nodes as we can't be sure that any - // of them are actually online. - // - // Realistically, most of them are, but we can't be sure, - // and checking here without explicitly asking the user - // to connect to nodes is a no-go (also, non-async environment). - if !lock!(self.ping).pinged { - warn!("Backup hosts ... simple node backup: no ping data available, returning None"); - return None; - } - - if self.state.p2pool.simple { - let mut vec = Vec::with_capacity(REMOTE_NODES.len()); - - // Locking during this entire loop should be fine, - // only a few nodes to iter through. - for pinged_node in lock!(self.ping).nodes.iter() { - // Continue if this node is not green/yellow. - if pinged_node.ms > crate::node::RED_NODE_PING { - continue; - } - - let (ip, rpc, zmq) = RemoteNode::get_ip_rpc_zmq(pinged_node.ip); - - let node = Node { - ip: ip.into(), - rpc: rpc.into(), - zmq: zmq.into(), - }; - - vec.push(node); - } - - if vec.is_empty() { - warn!("Backup hosts ... simple node backup: no viable nodes found"); - None - } else { - info!("Backup hosts ... simple node backup list: {vec:#?}"); - Some(vec) - } - } else { - Some(self.node_vec.iter().map(|(_, node)| node.clone()).collect()) - } - } -} - -//---------------------------------------------------------------------------------------------------- [Tab] Enum + Impl -// The tabs inside [App]. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum Tab { - About, - Status, - Gupax, - P2pool, - Xmrig, - Xvb -} - -impl Default for Tab { - fn default() -> Self { - Self::About - } -} - -//---------------------------------------------------------------------------------------------------- CPU Benchmarks. -#[derive(Debug, Serialize, Deserialize)] -pub struct Benchmark { - pub cpu: String, - pub rank: u16, - pub percent: f32, - pub benchmarks: u16, - pub average: f32, - pub high: f32, - pub low: f32, -} - -//---------------------------------------------------------------------------------------------------- [Restart] Enum -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Restart { - No, // We don't need to restart - Yes, // We updated, user should probably (but isn't required to) restart -} - -//---------------------------------------------------------------------------------------------------- [ErrorState] struct -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ErrorButtons { - YesNo, - StayQuit, - ResetState, - ResetNode, - Okay, - Quit, - Sudo, - WindowsAdmin, - Debug, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ErrorFerris { - Happy, - Cute, - Oops, - Error, - Panic, - Sudo, -} - -pub struct ErrorState { - error: bool, // Is there an error? - msg: String, // What message to display? - ferris: ErrorFerris, // Which ferris to display? - buttons: ErrorButtons, // Which buttons to display? - quit_twice: bool, // This indicates the user tried to quit on the [ask_before_quit] screen -} - -impl Default for ErrorState { - fn default() -> Self { - Self::new() - } -} - -impl ErrorState { - pub fn new() -> Self { - Self { - error: false, - msg: "Unknown Error".to_string(), - ferris: ErrorFerris::Oops, - buttons: ErrorButtons::Okay, - quit_twice: false, - } - } - - // Convenience function to enable the [App] error state - pub fn set(&mut self, msg: impl Into, ferris: ErrorFerris, buttons: ErrorButtons) { - if self.error { - // If a panic error is already set and there isn't an [Okay] confirm or another [Panic], return - if self.ferris == ErrorFerris::Panic - && (buttons != ErrorButtons::Okay || ferris != ErrorFerris::Panic) - { - return; - } - } - *self = Self { - error: true, - msg: msg.into(), - ferris, - buttons, - quit_twice: false, - }; - } - - // Just sets the current state to new, resetting it. - pub fn reset(&mut self) { - *self = Self::new(); - } - - // Instead of creating a whole new screen and system, this (ab)uses ErrorState - // to ask for the [sudo] when starting XMRig. Yes, yes I know, it's called "ErrorState" - // but rewriting the UI code and button stuff might be worse. - // It also resets the current [SudoState] - pub fn ask_sudo(&mut self, state: &Arc>) { - *self = Self { - error: true, - msg: String::new(), - ferris: ErrorFerris::Sudo, - buttons: ErrorButtons::Sudo, - quit_twice: false, - }; - SudoState::reset(state) - } -} - -//---------------------------------------------------------------------------------------------------- [Pressed] enum -// These represent the keys pressed during the frame. -// I could use egui's [Key] but there is no option for -// a [None] and wrapping [key_pressed] like [Option] -// meant that I had to destructure like this: -// if let Some(egui::Key)) = key_pressed { /* do thing */ } -// -// That's ugly, so these are used instead so a simple compare can be used. -#[derive(Debug, Clone, Eq, PartialEq)] -enum KeyPressed { - F11, - Up, - Down, - Esc, - Z, - X, - C, - V, - S, - R, - D, - None, -} - -impl KeyPressed { - #[inline] - fn is_f11(&self) -> bool { - *self == Self::F11 - } - #[inline] - fn is_z(&self) -> bool { - *self == Self::Z - } - #[inline] - fn is_x(&self) -> bool { - *self == Self::X - } - #[inline] - fn is_up(&self) -> bool { - *self == Self::Up - } - #[inline] - fn is_down(&self) -> bool { - *self == Self::Down - } - #[inline] - fn is_esc(&self) -> bool { - *self == Self::Esc - } - #[inline] - fn is_s(&self) -> bool { - *self == Self::S - } - #[inline] - fn is_r(&self) -> bool { - *self == Self::R - } - #[inline] - fn is_d(&self) -> bool { - *self == Self::D - } - #[inline] - fn is_c(&self) -> bool { - *self == Self::C - } - #[inline] - fn is_v(&self) -> bool { - *self == Self::V - } - // #[inline] - // fn is_none(&self) -> bool { - // *self == Self::None - // } -} - -//---------------------------------------------------------------------------------------------------- Init functions -#[cold] -#[inline(never)] -fn init_text_styles(ctx: &egui::Context, width: f32, pixels_per_point: f32) { - let scale = width / 35.5; - let mut style = (*ctx.style()).clone(); - style.text_styles = [ - (Small, FontId::new(scale / 3.0, egui::FontFamily::Monospace)), - (Body, FontId::new(scale / 2.0, egui::FontFamily::Monospace)), - ( - Button, - FontId::new(scale / 2.0, egui::FontFamily::Monospace), - ), - ( - Monospace, - FontId::new(scale / 2.0, egui::FontFamily::Monospace), - ), - ( - Heading, - FontId::new(scale / 1.5, egui::FontFamily::Monospace), - ), - ( - Name("Tab".into()), - FontId::new(scale * 1.2, egui::FontFamily::Monospace), - ), - ( - Name("Bottom".into()), - FontId::new(scale / 2.0, egui::FontFamily::Monospace), - ), - ( - Name("MonospaceSmall".into()), - FontId::new(scale / 2.5, egui::FontFamily::Monospace), - ), - ( - Name("MonospaceLarge".into()), - FontId::new(scale / 1.5, egui::FontFamily::Monospace), - ), - ] - .into(); - style.spacing.icon_width_inner = width / 35.0; - style.spacing.icon_width = width / 25.0; - style.spacing.icon_spacing = 20.0; - style.spacing.scroll = egui::style::ScrollStyle { - bar_width: width / 150.0, - ..egui::style::ScrollStyle::solid() - }; - ctx.set_style(style); - // Make sure scale f32 is a regular number. - let pixels_per_point = crate::free::clamp_scale(pixels_per_point); - ctx.set_pixels_per_point(pixels_per_point); - ctx.request_repaint(); -} - -#[cold] -#[inline(never)] -fn init_logger(now: Instant) { - let filter_env = std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".to_string()); - let filter = match filter_env.as_str() { - "error" | "Error" | "ERROR" => LevelFilter::Error, - "warn" | "Warn" | "WARN" => LevelFilter::Warn, - "debug" | "Debug" | "DEBUG" => LevelFilter::Debug, - "trace" | "Trace" | "TRACE" => LevelFilter::Trace, - _ => LevelFilter::Info, - }; - std::env::set_var("RUST_LOG", format!("off,gupax={}", filter_env)); - - Builder::new() - .format(move |buf, record| { - let level = record.level(); - let level_style = buf.default_level_style(level); - let dimmed = Style::new().dimmed(); - writeln!( - buf, - "{level_style}[{}]{level_style:#} [{dimmed}{:.3}{dimmed:#}] [{dimmed}{}{dimmed:#}:{dimmed}{}{dimmed:#}] {}", - level, - now.elapsed().as_secs_f32(), - record.file().unwrap_or("???"), - record.line().unwrap_or(0), - record.args(), - ) - }) - .filter_level(filter) - .write_style(WriteStyle::Always) - .parse_default_env() - .format_timestamp_millis() - .init(); - info!("init_logger() ... OK"); - info!("Log level ... {}", filter); -} - -#[cold] -#[inline(never)] -fn init_options(initial_window_size: Option) -> NativeOptions { - let mut options = eframe::NativeOptions::default(); - options.viewport.min_inner_size = Some(Vec2::new(APP_MIN_WIDTH, APP_MIN_HEIGHT)); - options.viewport.max_inner_size = Some(Vec2::new(APP_MAX_WIDTH, APP_MAX_HEIGHT)); - options.viewport.inner_size = initial_window_size; - options.follow_system_theme = false; - options.default_theme = eframe::Theme::Dark; - let icon = image::load_from_memory(BYTES_ICON) - .expect("Failed to read icon bytes") - .to_rgba8(); - let (icon_width, icon_height) = icon.dimensions(); - options.viewport.icon = Some(Arc::new(egui::viewport::IconData { - rgba: icon.into_raw(), - width: icon_width, - height: icon_height, - })); - info!("init_options() ... OK"); - options -} - -#[cold] -#[inline(never)] -fn init_auto(app: &mut App) { - // Return early if [--no-startup] was not passed - if app.no_startup { - info!("[--no-startup] flag passed, skipping init_auto()..."); - return; - } else if app.error_state.error { - info!("App error detected, skipping init_auto()..."); - return; - } else { - info!("Starting init_auto()..."); - } - - // [Auto-Update] - #[cfg(not(feature = "distro"))] - if app.state.gupax.auto_update { - Update::spawn_thread( - &app.og, - &app.state.gupax, - &app.state_path, - &app.update, - &mut app.error_state, - &app.restart, - ); - } else { - info!("Skipping auto-update..."); - } - - // [Auto-Ping] - if app.state.p2pool.auto_ping && app.state.p2pool.simple { - Ping::spawn_thread(&app.ping) - } else { - info!("Skipping auto-ping..."); - } - - // [Auto-P2Pool] - if app.state.gupax.auto_p2pool { - if !Regexes::addr_ok(&app.state.p2pool.address) { - warn!("Gupax | P2Pool address is not valid! Skipping auto-p2pool..."); - } else if !Gupax::path_is_file(&app.state.gupax.p2pool_path) { - warn!("Gupax | P2Pool path is not a file! Skipping auto-p2pool..."); - } else if !crate::update::check_p2pool_path(&app.state.gupax.p2pool_path) { - warn!("Gupax | P2Pool path is not valid! Skipping auto-p2pool..."); - } else { - let backup_hosts = app.gather_backup_hosts(); - Helper::start_p2pool( - &app.helper, - &app.state.p2pool, - &app.state.gupax.absolute_p2pool_path, - backup_hosts, - ); - } - } else { - info!("Skipping auto-p2pool..."); - } - - // [Auto-XMRig] - if app.state.gupax.auto_xmrig { - if !Gupax::path_is_file(&app.state.gupax.xmrig_path) { - warn!("Gupax | XMRig path is not an executable! Skipping auto-xmrig..."); - } else if !crate::update::check_xmrig_path(&app.state.gupax.xmrig_path) { - warn!("Gupax | XMRig path is not valid! Skipping auto-xmrig..."); - } else if cfg!(windows) { - Helper::start_xmrig( - &app.helper, - &app.state.xmrig, - &app.state.gupax.absolute_xmrig_path, - Arc::clone(&app.sudo), - ); - } else { - lock!(app.sudo).signal = ProcessSignal::Start; - app.error_state.ask_sudo(&app.sudo); - } - } else { - info!("Skipping auto-xmrig..."); - } -} - -//---------------------------------------------------------------------------------------------------- Reset functions -#[cold] -#[inline(never)] -fn reset_state(path: &PathBuf) -> Result<(), TomlError> { - match State::create_new(path) { - Ok(_) => { - info!("Resetting [state.toml] ... OK"); - Ok(()) - } - Err(e) => { - error!("Resetting [state.toml] ... FAIL ... {}", e); - Err(e) - } - } -} - -#[cold] -#[inline(never)] -fn reset_nodes(path: &PathBuf) -> Result<(), TomlError> { - match Node::create_new(path) { - Ok(_) => { - info!("Resetting [node.toml] ... OK"); - Ok(()) - } - Err(e) => { - error!("Resetting [node.toml] ... FAIL ... {}", e); - Err(e) - } - } -} - -#[cold] -#[inline(never)] -fn reset_pools(path: &PathBuf) -> Result<(), TomlError> { - match Pool::create_new(path) { - Ok(_) => { - info!("Resetting [pool.toml] ... OK"); - Ok(()) - } - Err(e) => { - error!("Resetting [pool.toml] ... FAIL ... {}", e); - Err(e) - } - } -} - -#[cold] -#[inline(never)] -fn reset_gupax_p2pool_api(path: &PathBuf) -> Result<(), TomlError> { - match GupaxP2poolApi::create_new(path) { - Ok(_) => { - info!("Resetting GupaxP2poolApi ... OK"); - Ok(()) - } - Err(e) => { - error!("Resetting GupaxP2poolApi folder ... FAIL ... {}", e); - Err(e) - } - } -} - -#[cold] -#[inline(never)] -fn reset( - path: &PathBuf, - state: &PathBuf, - node: &PathBuf, - pool: &PathBuf, - gupax_p2pool_api: &PathBuf, -) { - let mut code = 0; - // Attempt to remove directory first - match std::fs::remove_dir_all(path) { - Ok(_) => info!("Removing OS data path ... OK"), - Err(e) => { - error!("Removing OS data path ... FAIL ... {}", e); - code = 1; - } - } - // Recreate - match create_gupax_dir(path) { - Ok(_) => (), - Err(_) => code = 1, - } - match reset_state(state) { - Ok(_) => (), - Err(_) => code = 1, - } - match reset_nodes(node) { - Ok(_) => (), - Err(_) => code = 1, - } - match reset_pools(pool) { - Ok(_) => (), - Err(_) => code = 1, - } - match reset_gupax_p2pool_api(gupax_p2pool_api) { - Ok(_) => (), - Err(_) => code = 1, - } - match code { - 0 => println!("\nGupax reset ... OK"), - _ => eprintln!("\nGupax reset ... FAIL"), - } - exit(code); -} - -//---------------------------------------------------------------------------------------------------- Misc functions -#[cold] -#[inline(never)] -fn parse_args>(mut app: App, panic: S) -> App { - info!("Parsing CLI arguments..."); - let mut args: Vec = env::args().collect(); - if args.len() == 1 { - info!("No args ... OK"); - return app; - } else { - args.remove(0); - info!("Args ... {:?}", args); - } - // [help/version], exit early - for arg in &args { - match arg.as_str() { - "--help" => { - println!("{}", ARG_HELP); - exit(0); - } - "--version" => { - println!("Gupax {} [OS: {}, Commit: {}]\nThis Gupax was originally bundled with:\n - P2Pool {}\n - XMRig {}\n\n{}", GUPAX_VERSION, OS_NAME, &COMMIT[..40], P2POOL_VERSION, XMRIG_VERSION, ARG_COPYRIGHT); - exit(0); - } - "--ferris" => { - println!("{}", FERRIS_ANSI); - exit(0); - } - _ => (), - } - } - // Abort on panic - let panic = panic.into(); - if !panic.is_empty() { - info!("[Gupax error] {}", panic); - exit(1); - } - - // Everything else - for arg in args { - match arg.as_str() { - "--state" => { - info!("Printing state..."); - print_disk_file(&app.state_path); - } - "--nodes" => { - info!("Printing node list..."); - print_disk_file(&app.node_path); - } - "--payouts" => { - info!("Printing payouts...\n"); - print_gupax_p2pool_api(&app.gupax_p2pool_api); - } - "--reset-state" => { - if let Ok(()) = reset_state(&app.state_path) { - println!("\nState reset ... OK"); - exit(0); - } else { - eprintln!("\nState reset ... FAIL"); - exit(1) - } - } - "--reset-nodes" => { - if let Ok(()) = reset_nodes(&app.node_path) { - println!("\nNode reset ... OK"); - exit(0) - } else { - eprintln!("\nNode reset ... FAIL"); - exit(1) - } - } - "--reset-pools" => { - if let Ok(()) = reset_pools(&app.pool_path) { - println!("\nPool reset ... OK"); - exit(0) - } else { - eprintln!("\nPool reset ... FAIL"); - exit(1) - } - } - "--reset-payouts" => { - if let Ok(()) = reset_gupax_p2pool_api(&app.gupax_p2pool_api_path) { - println!("\nGupaxP2poolApi reset ... OK"); - exit(0) - } else { - eprintln!("\nGupaxP2poolApi reset ... FAIL"); - exit(1) - } - } - "--reset-all" => reset( - &app.os_data_path, - &app.state_path, - &app.node_path, - &app.pool_path, - &app.gupax_p2pool_api_path, - ), - "--no-startup" => app.no_startup = true, - _ => { - eprintln!( - "\n[Gupax error] Invalid option: [{}]\nFor help, use: [--help]", - arg - ); - exit(1); - } - } - } - app -} - -// Get absolute [Gupax] binary path -#[cold] -#[inline(never)] -pub fn get_exe() -> Result { - match std::env::current_exe() { - Ok(path) => Ok(path.display().to_string()), - Err(err) => { - error!("Couldn't get absolute Gupax PATH"); - Err(err) - } - } -} - -// Get absolute [Gupax] directory path -#[cold] -#[inline(never)] -pub fn get_exe_dir() -> Result { - match std::env::current_exe() { - Ok(mut path) => { - path.pop(); - Ok(path.display().to_string()) - } - Err(err) => { - error!("Couldn't get exe basepath PATH"); - Err(err) - } - } -} - -// Clean any [gupax_update_.*] directories -// The trailing random bits must be exactly 10 alphanumeric characters -#[cold] -#[inline(never)] -pub fn clean_dir() -> Result<(), anyhow::Error> { - let regex = Regex::new("^gupax_update_[A-Za-z0-9]{10}$").unwrap(); - for entry in std::fs::read_dir(get_exe_dir()?)? { - let entry = entry?; - if !entry.path().is_dir() { - continue; - } - if Regex::is_match( - ®ex, - entry - .file_name() - .to_str() - .ok_or_else(|| anyhow::Error::msg("Basename failed"))?, - ) { - let path = entry.path(); - match std::fs::remove_dir_all(&path) { - Ok(_) => info!("Remove [{}] ... OK", path.display()), - Err(e) => warn!("Remove [{}] ... FAIL ... {}", path.display(), e), - } - } - } - Ok(()) -} - -// Print disk files to console -#[cold] -#[inline(never)] -fn print_disk_file(path: &PathBuf) { - match std::fs::read_to_string(path) { - Ok(string) => { - print!("{}", string); - exit(0); - } - Err(e) => { - error!("{}", e); - exit(1); - } - } -} - -// Prints the GupaxP2PoolApi files. -#[cold] -#[inline(never)] -fn print_gupax_p2pool_api(gupax_p2pool_api: &Arc>) { - let api = lock!(gupax_p2pool_api); - let log = match std::fs::read_to_string(&api.path_log) { - Ok(string) => string, - Err(e) => { - error!("{}", e); - exit(1); - } - }; - let payout = match std::fs::read_to_string(&api.path_payout) { - Ok(string) => string, - Err(e) => { - error!("{}", e); - exit(1); - } - }; - let xmr = match std::fs::read_to_string(&api.path_xmr) { - Ok(string) => string, - Err(e) => { - error!("{}", e); - exit(1); - } - }; - let xmr = match xmr.trim().parse::() { - Ok(o) => crate::xmr::AtomicUnit::from_u64(o), - Err(e) => { - warn!("GupaxP2poolApi | [xmr] parse error: {}", e); - exit(1); - } - }; - println!( - "{}\nTotal payouts | {}\nTotal XMR | {} ({} Atomic Units)", - log, - payout.trim(), - xmr, - xmr.to_u64() - ); - exit(0); -} - -#[inline] -fn cmp_f64(a: f64, b: f64) -> std::cmp::Ordering { - match (a <= b, a >= b) { - (false, true) => std::cmp::Ordering::Greater, - (true, false) => std::cmp::Ordering::Less, - (true, true) => std::cmp::Ordering::Equal, - _ => std::cmp::Ordering::Less, - } -} - //---------------------------------------------------------------------------------------------------- Main [App] frame fn main() { let now = Instant::now(); @@ -1420,1150 +87,12 @@ fn main() { // Run Gupax. info!("/*************************************/ Init ... OK /*************************************/"); - let _ = eframe::run_native( + let _ = eframe::run_native( &app.name_version.clone(), options, - Box::new(move |cc| {egui_extras::install_image_loaders(&cc.egui_ctx); Box::new(App::cc(cc, resolution, app))}), + Box::new(move |cc| { + egui_extras::install_image_loaders(&cc.egui_ctx); + Box::new(App::cc(cc, resolution, app)) + }), ); } - -impl eframe::App for App { - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - // *-------* - // | DEBUG | - // *-------* - debug!("App | ----------- Start of [update()] -----------"); - - // If closing. - // Used to be `eframe::App::on_close_event(&mut self) -> bool`. - let close_signal = ctx.input(|input| { - use egui::viewport::ViewportCommand; - - if !input.viewport().close_requested() { - return None; - } - if self.state.gupax.ask_before_quit { - // If we're already on the [ask_before_quit] screen and - // the user tried to exit again, exit. - if self.error_state.quit_twice { - if self.state.gupax.save_before_quit { - self.save_before_quit(); - } - return Some(ViewportCommand::Close); - } - // Else, set the error - self.error_state - .set("", ErrorFerris::Oops, ErrorButtons::StayQuit); - self.error_state.quit_twice = true; - Some(ViewportCommand::CancelClose) - // Else, just quit. - } else { - if self.state.gupax.save_before_quit { - self.save_before_quit(); - } - Some(ViewportCommand::Close) - } - }); - // This will either: - // 1. Cancel a close signal - // 2. Close the program - if let Some(cmd) = close_signal { - ctx.send_viewport_cmd(cmd); - } - - // If [F11] was pressed, reverse [fullscreen] bool - let key: KeyPressed = ctx.input_mut(|input| { - if input.consume_key(Modifiers::NONE, Key::F11) { - KeyPressed::F11 - } else if input.consume_key(Modifiers::NONE, Key::Z) { - KeyPressed::Z - } else if input.consume_key(Modifiers::NONE, Key::X) { - KeyPressed::X - } else if input.consume_key(Modifiers::NONE, Key::C) { - KeyPressed::C - } else if input.consume_key(Modifiers::NONE, Key::V) { - KeyPressed::V - } else if input.consume_key(Modifiers::NONE, Key::ArrowUp) { - KeyPressed::Up - } else if input.consume_key(Modifiers::NONE, Key::ArrowDown) { - KeyPressed::Down - } else if input.consume_key(Modifiers::NONE, Key::Escape) { - KeyPressed::Esc - } else if input.consume_key(Modifiers::NONE, Key::S) { - KeyPressed::S - } else if input.consume_key(Modifiers::NONE, Key::R) { - KeyPressed::R - } else if input.consume_key(Modifiers::NONE, Key::D) { - KeyPressed::D - } else { - KeyPressed::None - } - }); - - // Check if egui wants keyboard input. - // This prevents keyboard shortcuts from clobbering TextEdits. - // (Typing S in text would always [Save] instead) - let wants_input = ctx.wants_keyboard_input(); - - if key.is_f11() { - if ctx.input(|i| i.viewport().maximized == Some(true)) { - ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true)); - } - // Change Tabs LEFT - } else if key.is_z() && !wants_input { - match self.tab { - Tab::About => self.tab = Tab::Xvb, - Tab::Status => self.tab = Tab::About, - Tab::Gupax => self.tab = Tab::Status, - Tab::P2pool => self.tab = Tab::Gupax, - Tab::Xmrig => self.tab = Tab::P2pool, - Tab::Xvb => self.tab = Tab::Xmrig - }; - // Change Tabs RIGHT - } else if key.is_x() && !wants_input { - match self.tab { - Tab::About => self.tab = Tab::Status, - Tab::Status => self.tab = Tab::Gupax, - Tab::Gupax => self.tab = Tab::P2pool, - Tab::P2pool => self.tab = Tab::Xmrig, - Tab::Xmrig => self.tab = Tab::Xvb, - Tab::Xvb => self.tab = Tab::About - }; - // Change Submenu LEFT - } else if key.is_c() && !wants_input { - match self.tab { - Tab::Status => match self.state.status.submenu { - Submenu::Processes => self.state.status.submenu = Submenu::Benchmarks, - Submenu::P2pool => self.state.status.submenu = Submenu::Processes, - Submenu::Benchmarks => self.state.status.submenu = Submenu::P2pool, - }, - Tab::Gupax => flip!(self.state.gupax.simple), - Tab::P2pool => flip!(self.state.p2pool.simple), - Tab::Xmrig => flip!(self.state.xmrig.simple), - _ => (), - }; - // Change Submenu RIGHT - } else if key.is_v() && !wants_input { - match self.tab { - Tab::Status => match self.state.status.submenu { - Submenu::Processes => self.state.status.submenu = Submenu::P2pool, - Submenu::P2pool => self.state.status.submenu = Submenu::Benchmarks, - Submenu::Benchmarks => self.state.status.submenu = Submenu::Processes, - }, - Tab::Gupax => flip!(self.state.gupax.simple), - Tab::P2pool => flip!(self.state.p2pool.simple), - Tab::Xmrig => flip!(self.state.xmrig.simple), - _ => (), - }; - } - - // Refresh AT LEAST once a second - debug!("App | Refreshing frame once per second"); - ctx.request_repaint_after(SECOND); - - // Get P2Pool/XMRig process state. - // These values are checked multiple times so - // might as well check only once here to save - // on a bunch of [.lock().unwrap()]s. - debug!("App | Locking and collecting P2Pool state..."); - let p2pool = lock!(self.p2pool); - let p2pool_is_alive = p2pool.is_alive(); - let p2pool_is_waiting = p2pool.is_waiting(); - let p2pool_state = p2pool.state; - drop(p2pool); - debug!("App | Locking and collecting XMRig state..."); - let xmrig = lock!(self.xmrig); - let xmrig_is_alive = xmrig.is_alive(); - let xmrig_is_waiting = xmrig.is_waiting(); - let xmrig_state = xmrig.state; - drop(xmrig); - - // This sets the top level Ui dimensions. - // Used as a reference for other uis. - debug!("App | Setting width/height"); - CentralPanel::default().show(ctx, |ui| { - let available_width = ui.available_width(); - if self.width != available_width { - self.width = available_width; - if self.now.elapsed().as_secs() > 5 { - self.must_resize = true; - } - }; - self.height = ui.available_height(); - }); - // This resizes fonts/buttons/etc globally depending on the width. - // This is separate from the [self.width != available_width] logic above - // because placing [init_text_styles()] above would mean calling it 60x a second - // while the user was readjusting the frame. It's a pretty heavy operation and looks - // buggy when calling it that many times. Looking for a [must_resize] in addition to - // checking if the user is hovering over the app means that we only have call it once. - debug!("App | Checking if we need to resize"); - if self.must_resize && ctx.is_pointer_over_area() { - self.resizing = true; - self.must_resize = false; - } - // This (ab)uses [Area] and [TextEdit] to overlay a full black layer over whatever UI we had before. - // It incrementally becomes more opaque until [self.alpha] >= 250, when we just switch to pure black (no alpha). - // When black, we're safe to [init_text_styles()], and then incrementally go transparent, until we remove the layer. - if self.resizing { - egui::Area::new("resize_layer") - .order(egui::Order::Foreground) - .anchor(egui::Align2::CENTER_CENTER, (0.0, 0.0)) - .show(ctx, |ui| { - if self.alpha < 250 { - egui::Frame::none() - .fill(Color32::from_rgba_premultiplied(0, 0, 0, self.alpha)) - .show(ui, |ui| { - ui.add_sized( - [ui.available_width() + SPACE, ui.available_height() + SPACE], - egui::TextEdit::multiline(&mut ""), - ); - }); - ctx.request_repaint(); - self.alpha += 10; - } else { - egui::Frame::none() - .fill(Color32::from_rgb(0, 0, 0)) - .show(ui, |ui| { - ui.add_sized( - [ui.available_width() + SPACE, ui.available_height() + SPACE], - egui::TextEdit::multiline(&mut ""), - ); - }); - ctx.request_repaint(); - info!( - "App | Resizing frame to match new internal resolution: [{}x{}]", - self.width, self.height - ); - init_text_styles(ctx, self.width, self.state.gupax.selected_scale); - self.resizing = false; - } - }); - } else if self.alpha != 0 { - egui::Area::new("resize_layer") - .order(egui::Order::Foreground) - .anchor(egui::Align2::CENTER_CENTER, (0.0, 0.0)) - .show(ctx, |ui| { - egui::Frame::none() - .fill(Color32::from_rgba_premultiplied(0, 0, 0, self.alpha)) - .show(ui, |ui| { - ui.add_sized( - [ui.available_width() + SPACE, ui.available_height() + SPACE], - egui::TextEdit::multiline(&mut ""), - ); - }) - }); - self.alpha -= 10; - ctx.request_repaint(); - } - - // If there's an error, display [ErrorState] on the whole screen until user responds - debug!("App | Checking if there is an error in [ErrorState]"); - if self.error_state.error { - CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered(|ui| { - // Set width/height/font - let width = self.width; - let height = self.height/4.0; - ui.style_mut().override_text_style = Some(Name("MonospaceLarge".into())); - - // Display ferris - use ErrorFerris::*; - use ErrorButtons::*; - let ferris = match self.error_state.ferris { - Happy => Image::from_bytes("bytes://happy.png", FERRIS_HAPPY), - Cute => Image::from_bytes("bytes://cute.png", FERRIS_CUTE), - Oops => Image::from_bytes("bytes://oops.png", FERRIS_OOPS), - Error => Image::from_bytes("bytes://error.png", FERRIS_ERROR), - Panic => Image::from_bytes("bytes://panic.png", FERRIS_PANIC), - ErrorFerris::Sudo => Image::from_bytes("bytes://panic.png", FERRIS_SUDO), - }; - - - - match self.error_state.buttons { - Debug => ui.add_sized([width, height/4.0], Label::new("--- Debug Info ---\n\nPress [ESC] to quit")), - _ => ui.add_sized(Vec2::new(width, height), ferris), - }; - - // Error/Quit screen - match self.error_state.buttons { - StayQuit => { - let mut text = "".to_string(); - if *lock2!(self.update,updating) { text = format!("{}\nUpdate is in progress...! Quitting may cause file corruption!", text); } - if p2pool_is_alive { text = format!("{}\nP2Pool is online...!", text); } - if xmrig_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)) - }, - ResetState => { - ui.add_sized([width, height], Label::new(format!("--- Gupax has encountered an error! ---\n{}", &self.error_state.msg))); - ui.add_sized([width, height], Label::new("Reset Gupax state? (Your settings)")) - }, - ResetNode => { - ui.add_sized([width, height], Label::new(format!("--- Gupax has encountered an error! ---\n{}", &self.error_state.msg))); - ui.add_sized([width, height], Label::new("Reset the manual node list?")) - }, - ErrorButtons::Sudo => { - let text = format!("Why does XMRig need admin privilege?\n{}", XMRIG_ADMIN_REASON); - let height = height/4.0; - ui.add_sized([width, height], Label::new(format!("--- Gupax needs sudo/admin privilege for XMRig! ---\n{}", &self.error_state.msg))); - ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into())); - ui.add_sized([width/2.0, height], Label::new(text)); - ui.add_sized([width, height], Hyperlink::from_label_and_url("Click here for more info.", "https://xmrig.com/docs/miner/randomx-optimization-guide")) - }, - Debug => { - egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| { - let width = ui.available_width(); - let height = ui.available_height(); - egui::ScrollArea::vertical().max_width(width).max_height(height).auto_shrink([false; 2]).show_viewport(ui, |ui, _| { - ui.add_sized([width-20.0, height], TextEdit::multiline(&mut self.error_state.msg.as_str())); - }); - }); - ui.label("") - }, - _ => { - match self.error_state.ferris { - Panic => ui.add_sized([width, height], Label::new("--- Gupax has encountered an unrecoverable error! ---")), - Happy => ui.add_sized([width, height], Label::new("--- Success! ---")), - _ => ui.add_sized([width, height], Label::new("--- Gupax has encountered an error! ---")), - }; - let height = height/2.0; - // Show GitHub rant link for Windows admin problems. - if cfg!(windows) && self.error_state.buttons == ErrorButtons::WindowsAdmin { - ui.add_sized([width, height], Hyperlink::from_label_and_url( - "[Why does Gupax need to be Admin? (on Windows)]", - "https://github.com/hinto-janai/gupax/tree/main/src#why-does-gupax-need-to-be-admin-on-windows" - )); - ui.add_sized([width, height], Label::new(&self.error_state.msg)) - } else { - ui.add_sized([width, height], Label::new(&self.error_state.msg)) - } - }, - }; - let height = ui.available_height(); - - match self.error_state.buttons { - YesNo => { - if ui.add_sized([width, height/2.0], Button::new("Yes")).clicked() { self.error_state.reset() } - // If [Esc] was pressed, assume [No] - if key.is_esc() || ui.add_sized([width, height/2.0], Button::new("No")).clicked() { exit(0); } - }, - StayQuit => { - // If [Esc] was pressed, assume [Stay] - if key.is_esc() || ui.add_sized([width, height/2.0], Button::new("Stay")).clicked() { - self.error_state = ErrorState::new(); - } - if ui.add_sized([width, height/2.0], Button::new("Quit")).clicked() { - if self.state.gupax.save_before_quit { self.save_before_quit(); } - exit(0); - } - }, - // This code handles the [state.toml/node.toml] resetting, [panic!]'ing if it errors once more - // Another error after this either means an IO error or permission error, which Gupax can't fix. - // [Yes/No] buttons - ResetState => { - if ui.add_sized([width, height/2.0], Button::new("Yes")).clicked() { - match reset_state(&self.state_path) { - Ok(_) => { - match State::get(&self.state_path) { - Ok(s) => { - self.state = s; - self.og = arc_mut!(self.state.clone()); - self.error_state.set("State read OK", ErrorFerris::Happy, ErrorButtons::Okay); - }, - Err(e) => self.error_state.set(format!("State read fail: {}", e), ErrorFerris::Panic, ErrorButtons::Quit), - } - }, - Err(e) => self.error_state.set(format!("State reset fail: {}", e), ErrorFerris::Panic, ErrorButtons::Quit), - }; - } - if key.is_esc() || ui.add_sized([width, height/2.0], Button::new("No")).clicked() { self.error_state.reset() } - }, - ResetNode => { - if ui.add_sized([width, height/2.0], Button::new("Yes")).clicked() { - match reset_nodes(&self.node_path) { - Ok(_) => { - match Node::get(&self.node_path) { - Ok(s) => { - self.node_vec = s; - self.og_node_vec = self.node_vec.clone(); - self.error_state.set("Node read OK", ErrorFerris::Happy, ErrorButtons::Okay); - }, - Err(e) => self.error_state.set(format!("Node read fail: {}", e), ErrorFerris::Panic, ErrorButtons::Quit), - } - }, - Err(e) => self.error_state.set(format!("Node reset fail: {}", e), ErrorFerris::Panic, ErrorButtons::Quit), - }; - } - if key.is_esc() || ui.add_sized([width, height/2.0], Button::new("No")).clicked() { self.error_state.reset() } - }, - ErrorButtons::Sudo => { - let sudo_width = width/10.0; - let height = ui.available_height()/4.0; - let mut sudo = lock!(self.sudo); - let hide = sudo.hide; - if sudo.testing { - ui.add_sized([width, height], Spinner::new().size(height)); - ui.set_enabled(false); - } else { - ui.add_sized([width, height], Label::new(&sudo.msg)); - } - ui.add_space(height); - let height = ui.available_height()/5.0; - // Password input box with a hider. - ui.horizontal(|ui| { - let response = ui.add_sized([sudo_width*8.0, height], TextEdit::hint_text(TextEdit::singleline(&mut sudo.pass).password(hide), PASSWORD_TEXT)); - let box_width = (ui.available_width()/2.0)-5.0; - if (response.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter))) || - ui.add_sized([box_width, height], Button::new("Enter")).on_hover_text(PASSWORD_ENTER).clicked() { - response.request_focus(); - if !sudo.testing { - SudoState::test_sudo(self.sudo.clone(), &self.helper.clone(), &self.state.xmrig, &self.state.gupax.absolute_xmrig_path); - } - } - let color = if hide { BLACK } else { BRIGHT_YELLOW }; - if ui.add_sized([box_width, height], Button::new(RichText::new("👁").color(color))).on_hover_text(PASSWORD_HIDE).clicked() { flip!(sudo.hide); } - }); - if (key.is_esc() && !sudo.testing) || ui.add_sized([width, height*4.0], Button::new("Leave")).on_hover_text(PASSWORD_LEAVE).clicked() { self.error_state.reset(); }; - // If [test_sudo()] finished, reset error state. - if sudo.success { - self.error_state.reset(); - } - }, - Okay|WindowsAdmin => if key.is_esc() || ui.add_sized([width, height], Button::new("Okay")).clicked() { self.error_state.reset(); }, - Debug => if key.is_esc() { self.error_state.reset(); }, - Quit => if ui.add_sized([width, height], Button::new("Quit")).clicked() { exit(1); }, - } - })}); - return; - } - - // Compare [og == state] & [node_vec/pool_vec] and enable diff if found. - // The struct fields are compared directly because [Version] - // contains Arc's that cannot be compared easily. - // They don't need to be compared anyway. - debug!("App | Checking diff between [og] & [state]"); - let og = lock!(self.og); - self.diff = og.status != self.state.status - || og.gupax != self.state.gupax - || og.p2pool != self.state.p2pool - || og.xmrig != self.state.xmrig - || og.xvb != self.state.xvb - || self.og_node_vec != self.node_vec || self.og_pool_vec != self.pool_vec; - drop(og); - - // Top: Tabs - debug!("App | Rendering TOP tabs"); - TopBottomPanel::top("top").show(ctx, |ui| { - let width = (self.width - (SPACE * 11.0)) / 6.0; - let height = self.height / 15.0; - ui.add_space(4.0); - ui.horizontal(|ui| { - ui.style_mut().override_text_style = Some(Name("Tab".into())); - if ui - .add_sized( - [width, height], - SelectableLabel::new(self.tab == Tab::About, "About"), - ) - .clicked() - { - self.tab = Tab::About; - } - ui.separator(); - if ui - .add_sized( - [width, height], - SelectableLabel::new(self.tab == Tab::Status, "Status"), - ) - .clicked() - { - self.tab = Tab::Status; - } - ui.separator(); - if ui - .add_sized( - [width, height], - SelectableLabel::new(self.tab == Tab::Gupax, "Gupax"), - ) - .clicked() - { - self.tab = Tab::Gupax; - } - ui.separator(); - if ui - .add_sized( - [width, height], - SelectableLabel::new(self.tab == Tab::P2pool, "P2Pool"), - ) - .clicked() - { - self.tab = Tab::P2pool; - } - ui.separator(); - if ui - .add_sized( - [width, height], - SelectableLabel::new(self.tab == Tab::Xmrig, "XMRig"), - ) - .clicked() - { - self.tab = Tab::Xmrig; - } - ui.separator(); - if ui - .add_sized( - [width, height], - SelectableLabel::new(self.tab == Tab::Xvb, "XvB"), - ) - .clicked() - { - self.tab = Tab::Xvb; - } - }); - ui.add_space(4.0); - }); - - // Bottom: app info + state/process buttons - debug!("App | Rendering BOTTOM bar"); - TopBottomPanel::bottom("bottom").show(ctx, |ui| { - let height = self.height / 22.0; - ui.style_mut().override_text_style = Some(Name("Bottom".into())); - ui.horizontal(|ui| { - ui.group(|ui| { - let width = ((self.width / 2.0) / 4.0) - (SPACE * 2.0); - // [Gupax Version] - // Is yellow if the user updated and should (but isn't required to) restart. - match *lock!(self.restart) { - Restart::Yes => ui - .add_sized( - [width, height], - Label::new(RichText::new(&self.name_version).color(YELLOW)), - ) - .on_hover_text(GUPAX_SHOULD_RESTART), - _ => ui.add_sized([width, height], Label::new(&self.name_version)), - }; - ui.separator(); - // [OS] - // Check if admin for windows. - // Unix SHOULDN'T be running as root, and the check is done when - // [App] is initialized, so no reason to check here. - #[cfg(target_os = "windows")] - if self.admin { - ui.add_sized([width, height], Label::new(self.os)); - } else { - ui.add_sized( - [width, height], - Label::new(RichText::new(self.os).color(RED)), - ) - .on_hover_text(WINDOWS_NOT_ADMIN); - } - #[cfg(target_family = "unix")] - ui.add_sized([width, height], Label::new(self.os)); - ui.separator(); - // [P2Pool/XMRig] Status - use ProcessState::*; - match p2pool_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), - Syncing => ui - .add_sized( - [width, height], - Label::new(RichText::new("P2Pool ⏺").color(ORANGE)), - ) - .on_hover_text(P2POOL_SYNCING), - Middle | Waiting | NotMining => ui - .add_sized( - [width, height], - Label::new(RichText::new("P2Pool ⏺").color(YELLOW)), - ) - .on_hover_text(P2POOL_MIDDLE), - }; - ui.separator(); - match xmrig_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), - NotMining => ui - .add_sized( - [width, height], - Label::new(RichText::new("XMRig ⏺").color(ORANGE)), - ) - .on_hover_text(XMRIG_NOT_MINING), - Middle | Waiting | Syncing => ui - .add_sized( - [width, height], - Label::new(RichText::new("XMRig ⏺").color(YELLOW)), - ) - .on_hover_text(XMRIG_MIDDLE), - }; - }); - - // [Save/Reset] - ui.with_layout(Layout::right_to_left(Align::RIGHT), |ui| { - let width = (ui.available_width() / 3.0) - (SPACE * 3.0); - ui.group(|ui| { - ui.set_enabled(self.diff); - let width = width / 2.0; - if key.is_r() && !wants_input && self.diff - || ui - .add_sized([width, height], Button::new("Reset")) - .on_hover_text("Reset changes") - .clicked() - { - let og = lock!(self.og).clone(); - self.state.status = og.status; - self.state.gupax = og.gupax; - self.state.p2pool = og.p2pool; - self.state.xmrig = og.xmrig; - self.node_vec = self.og_node_vec.clone(); - self.pool_vec = self.og_pool_vec.clone(); - } - if key.is_s() && !wants_input && self.diff - || ui - .add_sized([width, height], Button::new("Save")) - .on_hover_text("Save changes") - .clicked() - { - match State::save(&mut self.state, &self.state_path) { - Ok(_) => { - let mut og = lock!(self.og); - og.status = self.state.status.clone(); - og.gupax = self.state.gupax.clone(); - og.p2pool = self.state.p2pool.clone(); - og.xmrig = self.state.xmrig.clone(); - } - Err(e) => { - self.error_state.set( - format!("State file: {}", e), - ErrorFerris::Error, - ErrorButtons::Okay, - ); - } - }; - match Node::save(&self.node_vec, &self.node_path) { - Ok(_) => self.og_node_vec = self.node_vec.clone(), - Err(e) => self.error_state.set( - format!("Node list: {}", e), - ErrorFerris::Error, - ErrorButtons::Okay, - ), - }; - match Pool::save(&self.pool_vec, &self.pool_path) { - Ok(_) => self.og_pool_vec = self.pool_vec.clone(), - Err(e) => self.error_state.set( - format!("Pool list: {}", e), - ErrorFerris::Error, - ErrorButtons::Okay, - ), - }; - } - }); - - // [Simple/Advanced] + [Start/Stop/Restart] - match self.tab { - Tab::Status => { - ui.group(|ui| { - let width = (ui.available_width() / 3.0) - 14.0; - if ui - .add_sized( - [width, height], - SelectableLabel::new( - self.state.status.submenu == Submenu::Benchmarks, - "Benchmarks", - ), - ) - .on_hover_text(STATUS_SUBMENU_HASHRATE) - .clicked() - { - self.state.status.submenu = Submenu::Benchmarks; - } - ui.separator(); - if ui - .add_sized( - [width, height], - SelectableLabel::new( - self.state.status.submenu == Submenu::P2pool, - "P2Pool", - ), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL) - .clicked() - { - self.state.status.submenu = Submenu::P2pool; - } - ui.separator(); - if ui - .add_sized( - [width, height], - SelectableLabel::new( - self.state.status.submenu == Submenu::Processes, - "Processes", - ), - ) - .on_hover_text(STATUS_SUBMENU_PROCESSES) - .clicked() - { - self.state.status.submenu = Submenu::Processes; - } - }); - } - Tab::Gupax => { - ui.group(|ui| { - let width = (ui.available_width() / 2.0) - 10.5; - if ui - .add_sized( - [width, height], - SelectableLabel::new(!self.state.gupax.simple, "Advanced"), - ) - .on_hover_text(GUPAX_ADVANCED) - .clicked() - { - self.state.gupax.simple = false; - } - ui.separator(); - if ui - .add_sized( - [width, height], - SelectableLabel::new(self.state.gupax.simple, "Simple"), - ) - .on_hover_text(GUPAX_SIMPLE) - .clicked() - { - self.state.gupax.simple = true; - } - }); - } - Tab::P2pool => { - ui.group(|ui| { - let width = width / 1.5; - if ui - .add_sized( - [width, height], - SelectableLabel::new(!self.state.p2pool.simple, "Advanced"), - ) - .on_hover_text(P2POOL_ADVANCED) - .clicked() - { - self.state.p2pool.simple = false; - } - ui.separator(); - if ui - .add_sized( - [width, height], - SelectableLabel::new(self.state.p2pool.simple, "Simple"), - ) - .on_hover_text(P2POOL_SIMPLE) - .clicked() - { - self.state.p2pool.simple = true; - } - }); - ui.group(|ui| { - let width = (ui.available_width() / 3.0) - 5.0; - if p2pool_is_waiting { - ui.add_enabled_ui(false, |ui| { - ui.add_sized([width, height], Button::new("⟲")) - .on_disabled_hover_text(P2POOL_MIDDLE); - ui.add_sized([width, height], Button::new("⏹")) - .on_disabled_hover_text(P2POOL_MIDDLE); - ui.add_sized([width, height], Button::new("▶")) - .on_disabled_hover_text(P2POOL_MIDDLE); - }); - } else if p2pool_is_alive { - if key.is_up() && !wants_input - || ui - .add_sized([width, height], Button::new("⟲")) - .on_hover_text("Restart P2Pool") - .clicked() - { - let _ = lock!(self.og).update_absolute_path(); - let _ = self.state.update_absolute_path(); - Helper::restart_p2pool( - &self.helper, - &self.state.p2pool, - &self.state.gupax.absolute_p2pool_path, - self.gather_backup_hosts(), - ); - } - if key.is_down() && !wants_input - || 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_disabled_hover_text("Start P2Pool"); - }); - } else { - ui.add_enabled_ui(false, |ui| { - ui.add_sized([width, height], Button::new("⟲")) - .on_disabled_hover_text("Restart P2Pool"); - ui.add_sized([width, height], Button::new("⏹")) - .on_disabled_hover_text("Stop P2Pool"); - }); - // Check if address is okay before allowing to start. - let mut text = String::new(); - let mut ui_enabled = true; - if !Regexes::addr_ok(&self.state.p2pool.address) { - ui_enabled = false; - text = format!("Error: {}", P2POOL_ADDRESS); - } else if !Gupax::path_is_file(&self.state.gupax.p2pool_path) { - ui_enabled = false; - text = format!("Error: {}", P2POOL_PATH_NOT_FILE); - } else if !crate::update::check_p2pool_path( - &self.state.gupax.p2pool_path, - ) { - ui_enabled = false; - text = format!("Error: {}", P2POOL_PATH_NOT_VALID); - } - ui.set_enabled(ui_enabled); - let color = if ui_enabled { GREEN } else { RED }; - if (ui_enabled && key.is_up() && !wants_input) - || ui - .add_sized( - [width, height], - Button::new(RichText::new("▶").color(color)), - ) - .on_hover_text("Start P2Pool") - .on_disabled_hover_text(text) - .clicked() - { - let _ = lock!(self.og).update_absolute_path(); - let _ = self.state.update_absolute_path(); - Helper::start_p2pool( - &self.helper, - &self.state.p2pool, - &self.state.gupax.absolute_p2pool_path, - self.gather_backup_hosts(), - ); - } - } - }); - } - Tab::Xmrig => { - ui.group(|ui| { - let width = width / 1.5; - if ui - .add_sized( - [width, height], - SelectableLabel::new(!self.state.xmrig.simple, "Advanced"), - ) - .on_hover_text(XMRIG_ADVANCED) - .clicked() - { - self.state.xmrig.simple = false; - } - ui.separator(); - if ui - .add_sized( - [width, height], - SelectableLabel::new(self.state.xmrig.simple, "Simple"), - ) - .on_hover_text(XMRIG_SIMPLE) - .clicked() - { - self.state.xmrig.simple = true; - } - }); - ui.group(|ui| { - let width = (ui.available_width() / 3.0) - 5.0; - if xmrig_is_waiting { - ui.add_enabled_ui(false, |ui| { - ui.add_sized([width, height], Button::new("⟲")) - .on_disabled_hover_text(XMRIG_MIDDLE); - ui.add_sized([width, height], Button::new("⏹")) - .on_disabled_hover_text(XMRIG_MIDDLE); - ui.add_sized([width, height], Button::new("▶")) - .on_disabled_hover_text(XMRIG_MIDDLE); - }); - } else if xmrig_is_alive { - if key.is_up() && !wants_input - || ui - .add_sized([width, height], Button::new("⟲")) - .on_hover_text("Restart XMRig") - .clicked() - { - let _ = lock!(self.og).update_absolute_path(); - let _ = self.state.update_absolute_path(); - if cfg!(windows) { - Helper::restart_xmrig( - &self.helper, - &self.state.xmrig, - &self.state.gupax.absolute_xmrig_path, - Arc::clone(&self.sudo), - ); - } else { - lock!(self.sudo).signal = ProcessSignal::Restart; - self.error_state.ask_sudo(&self.sudo); - } - } - if key.is_down() && !wants_input - || ui - .add_sized([width, height], Button::new("⏹")) - .on_hover_text("Stop XMRig") - .clicked() - { - if cfg!(target_os = "macos") { - lock!(self.sudo).signal = ProcessSignal::Stop; - self.error_state.ask_sudo(&self.sudo); - } else { - Helper::stop_xmrig(&self.helper); - } - } - ui.add_enabled_ui(false, |ui| { - ui.add_sized([width, height], Button::new("▶")) - .on_disabled_hover_text("Start XMRig"); - }); - } else { - ui.add_enabled_ui(false, |ui| { - ui.add_sized([width, height], Button::new("⟲")) - .on_disabled_hover_text("Restart XMRig"); - ui.add_sized([width, height], Button::new("⏹")) - .on_disabled_hover_text("Stop XMRig"); - }); - let mut text = String::new(); - let mut ui_enabled = true; - if !Gupax::path_is_file(&self.state.gupax.xmrig_path) { - ui_enabled = false; - text = format!("Error: {}", XMRIG_PATH_NOT_FILE); - } else if !crate::update::check_xmrig_path( - &self.state.gupax.xmrig_path, - ) { - ui_enabled = false; - text = format!("Error: {}", XMRIG_PATH_NOT_VALID); - } - ui.set_enabled(ui_enabled); - let color = if ui_enabled { GREEN } else { RED }; - if (ui_enabled && key.is_up() && !wants_input) - || ui - .add_sized( - [width, height], - Button::new(RichText::new("▶").color(color)), - ) - .on_hover_text("Start XMRig") - .on_disabled_hover_text(text) - .clicked() - { - let _ = lock!(self.og).update_absolute_path(); - let _ = self.state.update_absolute_path(); - if cfg!(windows) { - Helper::start_xmrig( - &self.helper, - &self.state.xmrig, - &self.state.gupax.absolute_xmrig_path, - Arc::clone(&self.sudo), - ); - } else if cfg!(unix) { - lock!(self.sudo).signal = ProcessSignal::Start; - self.error_state.ask_sudo(&self.sudo); - } - } - } - }); - } - _ => (), - } - }); - }); - }); - - // Middle panel, contents of the [Tab] - debug!("App | Rendering CENTRAL_PANEL (tab contents)"); - CentralPanel::default().show(ctx, |ui| { - // This sets the Ui dimensions after Top/Bottom are filled - self.width = ui.available_width(); - self.height = ui.available_height(); - ui.style_mut().override_text_style = Some(TextStyle::Body); - match self.tab { - Tab::About => { - debug!("App | Entering [About] Tab"); - // If [D], show some debug info with [ErrorState] - if key.is_d() { - debug!("App | Entering [Debug Info]"); - #[cfg(feature = "distro")] - let distro = true; - #[cfg(not(feature = "distro"))] - let distro = false; - let p2pool_gui_len = lock!(self.p2pool_api).output.len(); - let xmrig_gui_len = lock!(self.xmrig_api).output.len(); - let gupax_p2pool_api = lock!(self.gupax_p2pool_api); - let debug_info = format!( -"Gupax version: {}\n -Bundled P2Pool version: {}\n -Bundled XMRig version: {}\n -Gupax uptime: {} seconds\n -Selected resolution: {}x{}\n -Internal resolution: {}x{}\n -Operating system: {}\n -Max detected threads: {}\n -Gupax PID: {}\n -State diff: {}\n -Node list length: {}\n -Pool list length: {}\n -Admin privilege: {}\n -Release build: {}\n -Debug build: {}\n -Distro build: {}\n -Build commit: {}\n -OS Data PATH: {}\n -Gupax PATH: {}\n -P2Pool PATH: {}\n -XMRig PATH: {}\n -P2Pool console byte length: {}\n -XMRig console byte length: {}\n ------------------------------------------- P2POOL IMAGE ------------------------------------------ -{:#?}\n ------------------------------------------- XMRIG IMAGE ------------------------------------------ -{:#?}\n ------------------------------------------- GUPAX-P2POOL API ------------------------------------------ -payout: {:#?} -payout_u64: {:#?} -xmr: {:#?} -path_log: {:#?} -path_payout: {:#?} -path_xmr: {:#?}\n ------------------------------------------- WORKING STATE ------------------------------------------ -{:#?}\n ------------------------------------------- ORIGINAL STATE ------------------------------------------ -{:#?}", - GUPAX_VERSION, - P2POOL_VERSION, - XMRIG_VERSION, - self.now.elapsed().as_secs_f32(), - self.state.gupax.selected_width, - self.state.gupax.selected_height, - self.width, - self.height, - OS_NAME, - self.max_threads, - self.pid, - self.diff, - self.node_vec.len(), - self.pool_vec.len(), - self.admin, - !cfg!(debug_assertions), - cfg!(debug_assertions), - distro, - COMMIT, - self.os_data_path.display(), - self.exe, - self.state.gupax.absolute_p2pool_path.display(), - self.state.gupax.absolute_xmrig_path.display(), - p2pool_gui_len, - xmrig_gui_len, - lock!(self.p2pool_img), - lock!(self.xmrig_img), - gupax_p2pool_api.payout, - gupax_p2pool_api.payout_u64, - gupax_p2pool_api.xmr, - gupax_p2pool_api.path_log, - gupax_p2pool_api.path_payout, - gupax_p2pool_api.path_xmr, - self.state, - lock!(self.og), - ); - self.error_state.set(debug_info, ErrorFerris::Cute, ErrorButtons::Debug); - } - let width = self.width; - let height = self.height/30.0; - let max_height = self.height; - ui.add_space(10.0); - ui.vertical_centered(|ui| { - ui.set_max_height(max_height); - // Display [Gupax] banner - let link_width = width/14.0; - ui.add_sized(Vec2::new(width, height*3.0), Image::from_bytes("bytes://banner.png", BYTES_BANNER)); - ui.add_sized([width, height], Label::new("is a GUI for mining")); - ui.add_sized([link_width, height], Hyperlink::from_label_and_url("[Monero]", "https://www.github.com/monero-project/monero")); - ui.add_sized([width, height], Label::new("on")); - ui.add_sized([link_width, height], Hyperlink::from_label_and_url("[P2Pool]", "https://www.github.com/SChernykh/p2pool")); - ui.add_sized([width, height], Label::new("using")); - ui.add_sized([link_width, height], Hyperlink::from_label_and_url("[XMRig]", "https://www.github.com/xmrig/xmrig")); - - ui.add_space(SPACE*2.0); - ui.add_sized([width, height], Label::new(KEYBOARD_SHORTCUTS)); - ui.add_space(SPACE*2.0); - - if cfg!(debug_assertions) { ui.label(format!("Gupax is running in debug mode - {}", self.now.elapsed().as_secs_f64())); } - ui.label(format!("Gupax has been running for {}", lock!(self.pub_sys).gupax_uptime)); - }); - } - Tab::Status => { - debug!("App | Entering [Status] Tab"); - crate::disk::Status::show(&mut self.state.status, &self.pub_sys, &self.p2pool_api, &self.xmrig_api, &self.p2pool_img, &self.xmrig_img, p2pool_is_alive, xmrig_is_alive, self.max_threads, &self.gupax_p2pool_api, &self.benchmarks, self.width, self.height, ctx, ui); - } - Tab::Gupax => { - debug!("App | Entering [Gupax] Tab"); - crate::disk::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 => { - debug!("App | Entering [P2Pool] Tab"); - crate::disk::P2pool::show(&mut self.state.p2pool, &mut self.node_vec, &self.og, &self.ping, &self.p2pool, &self.p2pool_api, &mut self.p2pool_stdin, self.width, self.height, ctx, ui); - } - Tab::Xmrig => { - debug!("App | Entering [XMRig] Tab"); - crate::disk::Xmrig::show(&mut self.state.xmrig, &mut self.pool_vec, &self.xmrig, &self.xmrig_api, &mut self.xmrig_stdin, self.width, self.height, ctx, ui); - } - Tab::Xvb => { - debug!("App | Entering [XvB] Tab"); - crate::disk::Xvb::show(self.width, self.height, ctx, ui); - } - } - }); - } -} - -//---------------------------------------------------------------------------------------------------- TESTS -#[cfg(test)] -mod test { - #[test] - fn detect_benchmark_cpu() { - use super::{cmp_f64, Benchmark}; - - let cpu = "AMD Ryzen 9 5950X 16-Core Processor"; - - let benchmarks: Vec = { - let mut json: Vec = - serde_json::from_slice(include_bytes!("cpu.json")).unwrap(); - json.sort_by(|a, b| cmp_f64(strsim::jaro(&b.cpu, cpu), strsim::jaro(&a.cpu, cpu))); - json - }; - - assert!(benchmarks[0].cpu == "AMD Ryzen 9 5950X 16-Core Processor"); - } -} diff --git a/src/miscs.rs b/src/miscs.rs new file mode 100644 index 0000000..9837ef1 --- /dev/null +++ b/src/miscs.rs @@ -0,0 +1,271 @@ +//---------------------------------------------------------------------------------------------------- Misc functions +#[cold] +#[inline(never)] +pub fn parse_args>(mut app: App, panic: S) -> App { + info!("Parsing CLI arguments..."); + let mut args: Vec = env::args().collect(); + if args.len() == 1 { + info!("No args ... OK"); + return app; + } else { + args.remove(0); + info!("Args ... {:?}", args); + } + // [help/version], exit early + for arg in &args { + match arg.as_str() { + "--help" => { + println!("{}", ARG_HELP); + exit(0); + } + "--version" => { + println!("Gupax {} [OS: {}, Commit: {}]\nThis Gupax was originally bundled with:\n - P2Pool {}\n - XMRig {}\n\n{}", GUPAX_VERSION, OS_NAME, &COMMIT[..40], P2POOL_VERSION, XMRIG_VERSION, ARG_COPYRIGHT); + exit(0); + } + "--ferris" => { + println!("{}", FERRIS_ANSI); + exit(0); + } + _ => (), + } + } + // Abort on panic + let panic = panic.into(); + if !panic.is_empty() { + info!("[Gupax error] {}", panic); + exit(1); + } + + // Everything else + for arg in args { + match arg.as_str() { + "--state" => { + info!("Printing state..."); + print_disk_file(&app.state_path); + } + "--nodes" => { + info!("Printing node list..."); + print_disk_file(&app.node_path); + } + "--payouts" => { + info!("Printing payouts...\n"); + print_gupax_p2pool_api(&app.gupax_p2pool_api); + } + "--reset-state" => { + if let Ok(()) = reset_state(&app.state_path) { + println!("\nState reset ... OK"); + exit(0); + } else { + eprintln!("\nState reset ... FAIL"); + exit(1) + } + } + "--reset-nodes" => { + if let Ok(()) = reset_nodes(&app.node_path) { + println!("\nNode reset ... OK"); + exit(0) + } else { + eprintln!("\nNode reset ... FAIL"); + exit(1) + } + } + "--reset-pools" => { + if let Ok(()) = reset_pools(&app.pool_path) { + println!("\nPool reset ... OK"); + exit(0) + } else { + eprintln!("\nPool reset ... FAIL"); + exit(1) + } + } + "--reset-payouts" => { + if let Ok(()) = reset_gupax_p2pool_api(&app.gupax_p2pool_api_path) { + println!("\nGupaxP2poolApi reset ... OK"); + exit(0) + } else { + eprintln!("\nGupaxP2poolApi reset ... FAIL"); + exit(1) + } + } + "--reset-all" => reset( + &app.os_data_path, + &app.state_path, + &app.node_path, + &app.pool_path, + &app.gupax_p2pool_api_path, + ), + "--no-startup" => app.no_startup = true, + _ => { + eprintln!( + "\n[Gupax error] Invalid option: [{}]\nFor help, use: [--help]", + arg + ); + exit(1); + } + } + } + app +} + +// Get absolute [Gupax] binary path +#[cold] +#[inline(never)] +pub fn get_exe() -> Result { + match std::env::current_exe() { + Ok(path) => Ok(path.display().to_string()), + Err(err) => { + error!("Couldn't get absolute Gupax PATH"); + Err(err) + } + } +} + +// Get absolute [Gupax] directory path +#[cold] +#[inline(never)] +pub fn get_exe_dir() -> Result { + match std::env::current_exe() { + Ok(mut path) => { + path.pop(); + Ok(path.display().to_string()) + } + Err(err) => { + error!("Couldn't get exe basepath PATH"); + Err(err) + } + } +} + +// Clean any [gupax_update_.*] directories +// The trailing random bits must be exactly 10 alphanumeric characters +#[cold] +#[inline(never)] +pub fn clean_dir() -> Result<(), anyhow::Error> { + let regex = Regex::new("^gupax_update_[A-Za-z0-9]{10}$").unwrap(); + for entry in std::fs::read_dir(get_exe_dir()?)? { + let entry = entry?; + if !entry.path().is_dir() { + continue; + } + if Regex::is_match( + ®ex, + entry + .file_name() + .to_str() + .ok_or_else(|| anyhow::Error::msg("Basename failed"))?, + ) { + let path = entry.path(); + match std::fs::remove_dir_all(&path) { + Ok(_) => info!("Remove [{}] ... OK", path.display()), + Err(e) => warn!("Remove [{}] ... FAIL ... {}", path.display(), e), + } + } + } + Ok(()) +} + +// Print disk files to console +#[cold] +#[inline(never)] +fn print_disk_file(path: &PathBuf) { + match std::fs::read_to_string(path) { + Ok(string) => { + print!("{}", string); + exit(0); + } + Err(e) => { + error!("{}", e); + exit(1); + } + } +} + +// Prints the GupaxP2PoolApi files. +#[cold] +#[inline(never)] +pub fn print_gupax_p2pool_api(gupax_p2pool_api: &Arc>) { + let api = lock!(gupax_p2pool_api); + let log = match std::fs::read_to_string(&api.path_log) { + Ok(string) => string, + Err(e) => { + error!("{}", e); + exit(1); + } + }; + let payout = match std::fs::read_to_string(&api.path_payout) { + Ok(string) => string, + Err(e) => { + error!("{}", e); + exit(1); + } + }; + let xmr = match std::fs::read_to_string(&api.path_xmr) { + Ok(string) => string, + Err(e) => { + error!("{}", e); + exit(1); + } + }; + let xmr = match xmr.trim().parse::() { + Ok(o) => crate::xmr::AtomicUnit::from_u64(o), + Err(e) => { + warn!("GupaxP2poolApi | [xmr] parse error: {}", e); + exit(1); + } + }; + println!( + "{}\nTotal payouts | {}\nTotal XMR | {} ({} Atomic Units)", + log, + payout.trim(), + xmr, + xmr.to_u64() + ); + exit(0); +} + +#[inline] +pub fn cmp_f64(a: f64, b: f64) -> std::cmp::Ordering { + match (a <= b, a >= b) { + (false, true) => std::cmp::Ordering::Greater, + (true, false) => std::cmp::Ordering::Less, + (true, true) => std::cmp::Ordering::Equal, + _ => std::cmp::Ordering::Less, + } +} +// Free functions. + +use crate::disk::gupax_p2pool_api::GupaxP2poolApi; +use crate::utils::macros::lock; +use log::error; +use log::warn; +use regex::Regex; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::Mutex; +use std::{env, process::exit}; + +use log::info; + +//---------------------------------------------------------------------------------------------------- Use +use crate::{ + app::App, + constants::*, + utils::{ + ferris::FERRIS_ANSI, + resets::{reset, reset_gupax_p2pool_api, reset_nodes, reset_pools, reset_state}, + }, +}; + +//---------------------------------------------------------------------------------------------------- +#[cold] +#[inline(never)] +// Clamp the scaling resolution `f32` to a known good `f32`. +pub fn clamp_scale(scale: f32) -> f32 { + // Make sure it is finite. + if !scale.is_finite() { + return APP_DEFAULT_SCALE; + } + + // Clamp between valid range. + scale.clamp(APP_MIN_SCALE, APP_MAX_SCALE) +} diff --git a/src/p2pool.rs b/src/p2pool.rs deleted file mode 100644 index f107840..0000000 --- a/src/p2pool.rs +++ /dev/null @@ -1,620 +0,0 @@ -// Gupax - GUI Uniting P2Pool And XMRig -// -// Copyright (c) 2022-2023 hinto-janai -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use crate::regex::REGEXES; -use crate::{constants::*, disk::*, helper::*, macros::*, node::*, Regexes}; -use egui::{ - Button, Checkbox, Color32, ComboBox, Hyperlink, Label, ProgressBar, RichText, SelectableLabel, - Slider, Spinner, TextEdit, TextStyle::*, -}; -use log::*; - -use std::sync::{Arc, Mutex}; - -impl crate::disk::P2pool { - #[inline(always)] // called once - #[allow(clippy::too_many_arguments)] - pub fn show( - &mut self, - node_vec: &mut Vec<(String, Node)>, - _og: &Arc>, - ping: &Arc>, - process: &Arc>, - api: &Arc>, - buffer: &mut String, - width: f32, - height: f32, - _ctx: &egui::Context, - ui: &mut egui::Ui, - ) { - let text_edit = height / 25.0; - //---------------------------------------------------------------------------------------------------- [Simple] Console - debug!("P2Pool Tab | Rendering [Console]"); - ui.group(|ui| { - if self.simple { - let height = height / 2.8; - let width = width - SPACE; - 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 lock!(api).output.as_str()), - ); - }); - }); - //---------------------------------------------------------------------------------------------------- [Advanced] Console - } else { - let height = height / 2.8; - let width = width - SPACE; - 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 lock!(api).output.as_str()), - ); - }); - }); - ui.separator(); - let response = ui - .add_sized( - [width, text_edit], - TextEdit::hint_text( - TextEdit::singleline(buffer), - r#"Type a command (e.g "help" or "status") and press Enter"#, - ), - ) - .on_hover_text(P2POOL_INPUT); - // If the user pressed enter, dump buffer contents into the process STDIN - if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { - response.request_focus(); // Get focus back - let buffer = std::mem::take(buffer); // Take buffer - let mut process = lock!(process); // Lock - if process.is_alive() { - process.input.push(buffer); - } // Push only if alive - } - } - }); - - //---------------------------------------------------------------------------------------------------- Args - if !self.simple { - debug!("P2Pool Tab | Rendering [Arguments]"); - ui.group(|ui| { - ui.horizontal(|ui| { - let width = (width / 10.0) - SPACE; - ui.add_sized([width, text_edit], Label::new("Command arguments:")); - ui.add_sized( - [ui.available_width(), text_edit], - TextEdit::hint_text( - TextEdit::singleline(&mut self.arguments), - r#"--wallet <...> --host <...>"#, - ), - ) - .on_hover_text(P2POOL_ARGUMENTS); - self.arguments.truncate(1024); - }) - }); - ui.set_enabled(self.arguments.is_empty()); - } - - //---------------------------------------------------------------------------------------------------- Address - debug!("P2Pool Tab | Rendering [Address]"); - ui.group(|ui| { - let width = width - SPACE; - ui.spacing_mut().text_edit_width = (width) - (SPACE * 3.0); - let text; - let color; - let len = format!("{:02}", self.address.len()); - if self.address.is_empty() { - text = format!("Monero Address [{}/95] ➖", len); - color = Color32::LIGHT_GRAY; - } else if Regexes::addr_ok(&self.address) { - text = format!("Monero Address [{}/95] ✔", len); - color = Color32::from_rgb(100, 230, 100); - } else { - text = format!("Monero Address [{}/95] ❌", len); - color = Color32::from_rgb(230, 50, 50); - } - ui.add_sized( - [width, text_edit], - Label::new(RichText::new(text).color(color)), - ); - ui.add_sized( - [width, text_edit], - TextEdit::hint_text(TextEdit::singleline(&mut self.address), "4..."), - ) - .on_hover_text(P2POOL_ADDRESS); - self.address.truncate(95); - }); - - //---------------------------------------------------------------------------------------------------- Simple - let height = ui.available_height(); - if self.simple { - // [Node] - let height = height / 6.5; - ui.spacing_mut().slider_width = width - 8.0; - ui.spacing_mut().icon_width = width / 25.0; - - // [Auto-select] if we haven't already. - // Using [Arc>] as an intermediary here - // saves me the hassle of wrapping [state: State] completely - // and [.lock().unwrap()]ing it everywhere. - // Two atomic bools = enough to represent this data - debug!("P2Pool Tab | Running [auto-select] check"); - if self.auto_select { - let mut ping = lock!(ping); - // If we haven't auto_selected yet, auto-select and turn it off - if ping.pinged && !ping.auto_selected { - self.node = ping.fastest.to_string(); - ping.auto_selected = true; - } - drop(ping); - } - - ui.vertical(|ui| { - ui.horizontal(|ui| { - debug!("P2Pool Tab | Rendering [Ping List]"); - // [Ping List] - let mut ms = 0; - let mut color = Color32::LIGHT_GRAY; - if lock!(ping).pinged { - for data in lock!(ping).nodes.iter() { - if data.ip == self.node { - ms = data.ms; - color = data.color; - break; - } - } - } - debug!("P2Pool Tab | Rendering [ComboBox] of Remote Nodes"); - let ip_location = crate::node::format_ip_location(&self.node, false); - let text = RichText::new(format!(" ⏺ {}ms | {}", ms, ip_location)).color(color); - ComboBox::from_id_source("remote_nodes") - .selected_text(text) - .width(width) - .show_ui(ui, |ui| { - for data in lock!(ping).nodes.iter() { - let ms = crate::node::format_ms(data.ms); - let ip_location = crate::node::format_ip_location(data.ip, true); - let text = RichText::new(format!(" ⏺ {} | {}", ms, ip_location)) - .color(data.color); - ui.selectable_value(&mut self.node, data.ip.to_string(), text); - } - }); - }); - - ui.add_space(5.0); - - debug!("P2Pool Tab | Rendering [Select fastest ... Ping] buttons"); - ui.horizontal(|ui| { - let width = (width / 5.0) - 6.0; - // [Select random node] - if ui - .add_sized([width, height], Button::new("Select random node")) - .on_hover_text(P2POOL_SELECT_RANDOM) - .clicked() - { - self.node = RemoteNode::get_random(&self.node); - } - // [Select fastest node] - if ui - .add_sized([width, height], Button::new("Select fastest node")) - .on_hover_text(P2POOL_SELECT_FASTEST) - .clicked() - && lock!(ping).pinged - { - self.node = lock!(ping).fastest.to_string(); - } - // [Ping Button] - ui.add_enabled_ui(!lock!(ping).pinging, |ui| { - if ui - .add_sized([width, height], Button::new("Ping remote nodes")) - .on_hover_text(P2POOL_PING) - .clicked() - { - Ping::spawn_thread(ping); - } - }); - // [Last <-] - if ui - .add_sized([width, height], Button::new("⬅ Last")) - .on_hover_text(P2POOL_SELECT_LAST) - .clicked() - { - let ping = lock!(ping); - match ping.pinged { - true => { - self.node = RemoteNode::get_last_from_ping(&self.node, &ping.nodes) - } - false => self.node = RemoteNode::get_last(&self.node), - } - drop(ping); - } - // [Next ->] - if ui - .add_sized([width, height], Button::new("Next ➡")) - .on_hover_text(P2POOL_SELECT_NEXT) - .clicked() - { - let ping = lock!(ping); - match ping.pinged { - true => { - self.node = RemoteNode::get_next_from_ping(&self.node, &ping.nodes) - } - false => self.node = RemoteNode::get_next(&self.node), - } - drop(ping); - } - }); - - ui.vertical(|ui| { - let height = height / 2.0; - let pinging = lock!(ping).pinging; - ui.set_enabled(pinging); - let prog = lock!(ping).prog.round(); - let msg = RichText::new(format!("{} ... {}%", lock!(ping).msg, prog)); - let height = height / 1.25; - ui.add_space(5.0); - ui.add_sized([width, height], Label::new(msg)); - ui.add_space(5.0); - if pinging { - ui.add_sized([width, height], Spinner::new().size(height)); - } else { - ui.add_sized([width, height], Label::new("...")); - } - ui.add_sized([width, height], ProgressBar::new(prog.round() / 100.0)); - ui.add_space(5.0); - }); - }); - - debug!("P2Pool Tab | Rendering [Auto-*] buttons"); - ui.group(|ui| { - ui.horizontal(|ui| { - let width = (width / 3.0) - (SPACE * 1.75); - // [Auto-node] - ui.add_sized( - [width, height], - Checkbox::new(&mut self.auto_select, "Auto-select"), - ) - .on_hover_text(P2POOL_AUTO_SELECT); - ui.separator(); - // [Auto-node] - ui.add_sized( - [width, height], - Checkbox::new(&mut self.auto_ping, "Auto-ping"), - ) - .on_hover_text(P2POOL_AUTO_NODE); - ui.separator(); - // [Backup host] - ui.add_sized( - [width, height], - Checkbox::new(&mut self.backup_host, "Backup host"), - ) - .on_hover_text(P2POOL_BACKUP_HOST_SIMPLE); - }) - }); - - debug!("P2Pool Tab | Rendering warning text"); - ui.add_sized([width, height/2.0], Hyperlink::from_label_and_url("WARNING: It is recommended to run/use your own Monero Node (hover for details)", "https://github.com/hinto-janai/gupax#running-a-local-monero-node")).on_hover_text(P2POOL_COMMUNITY_NODE_WARNING); - - //---------------------------------------------------------------------------------------------------- Advanced - } else { - debug!("P2Pool Tab | Rendering [Node List] elements"); - let mut incorrect_input = false; // This will disable [Add/Delete] on bad input - // [Monero node IP/RPC/ZMQ] - ui.horizontal(|ui| { - ui.group(|ui| { - let width = width/10.0; - ui.vertical(|ui| { - ui.spacing_mut().text_edit_width = width*3.32; - ui.horizontal(|ui| { - let text; - let color; - let len = format!("{:02}", self.name.len()); - if self.name.is_empty() { - text = format!("Name [ {}/30 ]➖", len); - color = Color32::LIGHT_GRAY; - incorrect_input = true; - } else if REGEXES.name.is_match(&self.name) { - text = format!("Name [ {}/30 ]✔", len); - color = Color32::from_rgb(100, 230, 100); - } else { - text = format!("Name [ {}/30 ]❌", len); - color = Color32::from_rgb(230, 50, 50); - incorrect_input = true; - } - ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color))); - ui.text_edit_singleline(&mut self.name).on_hover_text(P2POOL_NAME); - self.name.truncate(30); - }); - ui.horizontal(|ui| { - let text; - let color; - let len = format!("{:03}", self.ip.len()); - if self.ip.is_empty() { - text = format!(" IP [{}/255]➖", len); - color = Color32::LIGHT_GRAY; - incorrect_input = true; - } else if self.ip == "localhost" || REGEXES.ipv4.is_match(&self.ip) || REGEXES.domain.is_match(&self.ip) { - text = format!(" IP [{}/255]✔", len); - color = Color32::from_rgb(100, 230, 100); - } else { - text = format!(" IP [{}/255]❌", len); - color = Color32::from_rgb(230, 50, 50); - incorrect_input = true; - } - ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color))); - ui.text_edit_singleline(&mut self.ip).on_hover_text(P2POOL_NODE_IP); - self.ip.truncate(255); - }); - ui.horizontal(|ui| { - let text; - let color; - let len = self.rpc.len(); - if self.rpc.is_empty() { - text = format!(" RPC [ {}/5 ]➖", len); - color = Color32::LIGHT_GRAY; - incorrect_input = true; - } else if REGEXES.port.is_match(&self.rpc) { - text = format!(" RPC [ {}/5 ]✔", len); - color = Color32::from_rgb(100, 230, 100); - } else { - text = format!(" RPC [ {}/5 ]❌", len); - color = Color32::from_rgb(230, 50, 50); - incorrect_input = true; - } - ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color))); - ui.text_edit_singleline(&mut self.rpc).on_hover_text(P2POOL_RPC_PORT); - self.rpc.truncate(5); - }); - ui.horizontal(|ui| { - let text; - let color; - let len = self.zmq.len(); - if self.zmq.is_empty() { - text = format!(" ZMQ [ {}/5 ]➖", len); - color = Color32::LIGHT_GRAY; - incorrect_input = true; - } else if REGEXES.port.is_match(&self.zmq) { - text = format!(" ZMQ [ {}/5 ]✔", len); - color = Color32::from_rgb(100, 230, 100); - } else { - text = format!(" ZMQ [ {}/5 ]❌", len); - color = Color32::from_rgb(230, 50, 50); - incorrect_input = true; - } - ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color))); - ui.text_edit_singleline(&mut self.zmq).on_hover_text(P2POOL_ZMQ_PORT); - self.zmq.truncate(5); - }); - }); - - ui.vertical(|ui| { - let width = ui.available_width(); - ui.add_space(1.0); - // [Manual node selection] - ui.spacing_mut().slider_width = width - 8.0; - ui.spacing_mut().icon_width = width / 25.0; - // [Ping List] - debug!("P2Pool Tab | Rendering [Node List]"); - let text = RichText::new(format!("{}. {}", self.selected_index+1, self.selected_name)); - ComboBox::from_id_source("manual_nodes").selected_text(text).width(width).show_ui(ui, |ui| { - for (n, (name, node)) in node_vec.iter().enumerate() { - let text = RichText::new(format!("{}. {}\n IP: {}\n RPC: {}\n ZMQ: {}", n+1, name, node.ip, node.rpc, node.zmq)); - if ui.add(SelectableLabel::new(self.selected_name == *name, text)).clicked() { - self.selected_index = n; - let node = node.clone(); - self.selected_name = name.clone(); - self.selected_ip = node.ip.clone(); - self.selected_rpc = node.rpc.clone(); - self.selected_zmq = node.zmq.clone(); - self.name = name.clone(); - self.ip = node.ip; - self.rpc = node.rpc; - self.zmq = node.zmq; - } - } - }); - // [Add/Save] - let node_vec_len = node_vec.len(); - let mut exists = false; - let mut save_diff = true; - let mut existing_index = 0; - for (name, node) in node_vec.iter() { - if *name == self.name { - exists = true; - if self.ip == node.ip && self.rpc == node.rpc && self.zmq == node.zmq { - save_diff = false; - } - break - } - existing_index += 1; - } - ui.horizontal(|ui| { - let text = if exists { LIST_SAVE } else { LIST_ADD }; - let text = format!("{}\n Currently selected node: {}. {}\n Current amount of nodes: {}/1000", text, self.selected_index+1, self.selected_name, node_vec_len); - // If the node already exists, show [Save] and mutate the already existing node - if exists { - ui.set_enabled(!incorrect_input && save_diff); - if ui.add_sized([width, text_edit], Button::new("Save")).on_hover_text(text).clicked() { - let node = Node { - ip: self.ip.clone(), - rpc: self.rpc.clone(), - zmq: self.zmq.clone(), - }; - node_vec[existing_index].1 = node; - self.selected_index = existing_index; - self.selected_ip = self.ip.clone(); - self.selected_rpc = self.rpc.clone(); - self.selected_zmq = self.zmq.clone(); - info!("Node | S | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", existing_index+1, self.name, self.ip, self.rpc, self.zmq); - } - // Else, add to the list - } else { - ui.set_enabled(!incorrect_input && node_vec_len < 1000); - if ui.add_sized([width, text_edit], Button::new("Add")).on_hover_text(text).clicked() { - let node = Node { - ip: self.ip.clone(), - rpc: self.rpc.clone(), - zmq: self.zmq.clone(), - }; - node_vec.push((self.name.clone(), node)); - self.selected_index = node_vec_len; - self.selected_name = self.name.clone(); - self.selected_ip = self.ip.clone(); - self.selected_rpc = self.rpc.clone(); - self.selected_zmq = self.zmq.clone(); - info!("Node | A | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", node_vec_len, self.name, self.ip, self.rpc, self.zmq); - } - } - }); - // [Delete] - ui.horizontal(|ui| { - ui.set_enabled(node_vec_len > 1); - let text = format!("{}\n Currently selected node: {}. {}\n Current amount of nodes: {}/1000", LIST_DELETE, self.selected_index+1, self.selected_name, node_vec_len); - if ui.add_sized([width, text_edit], Button::new("Delete")).on_hover_text(text).clicked() { - let new_name; - let new_node; - match self.selected_index { - 0 => { - new_name = node_vec[1].0.clone(); - new_node = node_vec[1].1.clone(); - node_vec.remove(0); - } - _ => { - node_vec.remove(self.selected_index); - self.selected_index -= 1; - new_name = node_vec[self.selected_index].0.clone(); - new_node = node_vec[self.selected_index].1.clone(); - } - }; - self.selected_name = new_name.clone(); - self.selected_ip = new_node.ip.clone(); - self.selected_rpc = new_node.rpc.clone(); - self.selected_zmq = new_node.zmq.clone(); - self.name = new_name; - self.ip = new_node.ip; - self.rpc = new_node.rpc; - self.zmq = new_node.zmq; - info!("Node | D | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", self.selected_index, self.selected_name, self.selected_ip, self.selected_rpc, self.selected_zmq); - } - }); - ui.horizontal(|ui| { - ui.set_enabled(!self.name.is_empty() || !self.ip.is_empty() || !self.rpc.is_empty() || !self.zmq.is_empty()); - if ui.add_sized([width, text_edit], Button::new("Clear")).on_hover_text(LIST_CLEAR).clicked() { - self.name.clear(); - self.ip.clear(); - self.rpc.clear(); - self.zmq.clear(); - } - }); - }); - }); - }); - ui.add_space(5.0); - - debug!("P2Pool Tab | Rendering [Main/Mini/Peers/Log] elements"); - // [Main/Mini] - ui.horizontal(|ui| { - let height = height / 4.0; - ui.group(|ui| { - ui.horizontal(|ui| { - let width = (width / 4.0) - SPACE; - let height = height + 6.0; - if ui - .add_sized( - [width, height], - SelectableLabel::new(!self.mini, "P2Pool Main"), - ) - .on_hover_text(P2POOL_MAIN) - .clicked() - { - self.mini = false; - } - if ui - .add_sized( - [width, height], - SelectableLabel::new(self.mini, "P2Pool Mini"), - ) - .on_hover_text(P2POOL_MINI) - .clicked() - { - self.mini = true; - } - }) - }); - // [Out/In Peers] + [Log Level] - ui.group(|ui| { - ui.vertical(|ui| { - let text = (ui.available_width() / 10.0) - SPACE; - let width = (text * 8.0) - SPACE; - let height = height / 3.0; - ui.style_mut().spacing.slider_width = width / 1.1; - ui.style_mut().spacing.interact_size.y = height; - ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into())); - ui.horizontal(|ui| { - ui.add_sized([text, height], Label::new("Out peers [10-450]:")); - ui.add_sized( - [width, height], - Slider::new(&mut self.out_peers, 10..=450), - ) - .on_hover_text(P2POOL_OUT); - ui.add_space(ui.available_width() - 4.0); - }); - ui.horizontal(|ui| { - ui.add_sized([text, height], Label::new(" In peers [10-450]:")); - ui.add_sized( - [width, height], - Slider::new(&mut self.in_peers, 10..=450), - ) - .on_hover_text(P2POOL_IN); - }); - ui.horizontal(|ui| { - ui.add_sized([text, height], Label::new(" Log level [0-6]:")); - ui.add_sized([width, height], Slider::new(&mut self.log_level, 0..=6)) - .on_hover_text(P2POOL_LOG); - }); - }) - }); - }); - - debug!("P2Pool Tab | Rendering Backup host button"); - ui.group(|ui| { - let width = width - SPACE; - let height = ui.available_height() / 3.0; - // [Backup host] - ui.add_sized( - [width, height], - Checkbox::new(&mut self.backup_host, "Backup host"), - ) - .on_hover_text(P2POOL_BACKUP_HOST_ADVANCED); - }); - } - } -} diff --git a/src/status.rs b/src/status.rs deleted file mode 100644 index 4d44ec2..0000000 --- a/src/status.rs +++ /dev/null @@ -1,956 +0,0 @@ -// Gupax - GUI Uniting P2Pool And XMRig -// -// Copyright (c) 2022-2023 hinto-janai -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use crate::{ - constants::*, human::HumanNumber, macros::*, Benchmark, GupaxP2poolApi, Hash, ImgP2pool, - ImgXmrig, PayoutView, PubP2poolApi, PubXmrigApi, Submenu, Sys, -}; -use egui::{ - Hyperlink, Label, ProgressBar, RichText, SelectableLabel, Slider, Spinner, TextEdit, TextStyle, - TextStyle::Name, -}; -use log::*; -use std::sync::{Arc, Mutex}; - -impl crate::disk::Status { - #[inline(always)] // called once - #[allow(clippy::too_many_arguments)] - pub fn show( - &mut self, - sys: &Arc>, - p2pool_api: &Arc>, - xmrig_api: &Arc>, - p2pool_img: &Arc>, - xmrig_img: &Arc>, - p2pool_alive: bool, - xmrig_alive: bool, - max_threads: usize, - gupax_p2pool_api: &Arc>, - benchmarks: &[Benchmark], - width: f32, - height: f32, - _ctx: &egui::Context, - ui: &mut egui::Ui, - ) { - //---------------------------------------------------------------------------------------------------- [Processes] - if self.submenu == Submenu::Processes { - let width = (width / 3.0) - (SPACE * 1.666); - let min_height = height - SPACE; - let height = height / 25.0; - ui.horizontal(|ui| { - // [Gupax] - ui.group(|ui| { - ui.vertical(|ui| { - debug!("Status Tab | Rendering [Gupax]"); - ui.set_min_height(min_height); - ui.add_sized( - [width, height], - Label::new( - RichText::new("[Gupax]") - .color(LIGHT_GRAY) - .text_style(TextStyle::Name("MonospaceLarge".into())), - ), - ) - .on_hover_text("Gupax is online"); - let sys = lock!(sys); - ui.add_sized( - [width, height], - Label::new(RichText::new("Uptime").underline().color(BONE)), - ) - .on_hover_text(STATUS_GUPAX_UPTIME); - ui.add_sized([width, height], Label::new(sys.gupax_uptime.to_string())); - ui.add_sized( - [width, height], - Label::new(RichText::new("Gupax CPU").underline().color(BONE)), - ) - .on_hover_text(STATUS_GUPAX_CPU_USAGE); - ui.add_sized([width, height], Label::new(sys.gupax_cpu_usage.to_string())); - ui.add_sized( - [width, height], - Label::new(RichText::new("Gupax Memory").underline().color(BONE)), - ) - .on_hover_text(STATUS_GUPAX_MEMORY_USAGE); - ui.add_sized( - [width, height], - Label::new(sys.gupax_memory_used_mb.to_string()), - ); - ui.add_sized( - [width, height], - Label::new(RichText::new("System CPU").underline().color(BONE)), - ) - .on_hover_text(STATUS_GUPAX_SYSTEM_CPU_USAGE); - ui.add_sized( - [width, height], - Label::new(sys.system_cpu_usage.to_string()), - ); - ui.add_sized( - [width, height], - Label::new(RichText::new("System Memory").underline().color(BONE)), - ) - .on_hover_text(STATUS_GUPAX_SYSTEM_MEMORY); - ui.add_sized([width, height], Label::new(sys.system_memory.to_string())); - ui.add_sized( - [width, height], - Label::new(RichText::new("System CPU Model").underline().color(BONE)), - ) - .on_hover_text(STATUS_GUPAX_SYSTEM_CPU_MODEL); - ui.add_sized( - [width, height], - Label::new(sys.system_cpu_model.to_string()), - ); - drop(sys); - }) - }); - // [P2Pool] - ui.group(|ui| { - ui.vertical(|ui| { - debug!("Status Tab | Rendering [P2Pool]"); - ui.set_enabled(p2pool_alive); - ui.set_min_height(min_height); - ui.add_sized( - [width, height], - Label::new( - RichText::new("[P2Pool]") - .color(LIGHT_GRAY) - .text_style(TextStyle::Name("MonospaceLarge".into())), - ), - ) - .on_hover_text("P2Pool is online") - .on_disabled_hover_text("P2Pool is offline"); - ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into())); - let height = height / 1.4; - let api = lock!(p2pool_api); - ui.add_sized( - [width, height], - Label::new(RichText::new("Uptime").underline().color(BONE)), - ) - .on_hover_text(STATUS_P2POOL_UPTIME); - ui.add_sized([width, height], Label::new(format!("{}", api.uptime))); - ui.add_sized( - [width, height], - Label::new(RichText::new("Shares Found").underline().color(BONE)), - ) - .on_hover_text(STATUS_P2POOL_SHARES); - ui.add_sized([width, height], Label::new(format!("{}", api.shares_found))); - ui.add_sized( - [width, height], - Label::new(RichText::new("Payouts").underline().color(BONE)), - ) - .on_hover_text(STATUS_P2POOL_PAYOUTS); - ui.add_sized( - [width, height], - Label::new(format!("Total: {}", api.payouts)), - ); - ui.add_sized( - [width, height], - Label::new(format!( - "[{:.7}/hour]\n[{:.7}/day]\n[{:.7}/month]", - api.payouts_hour, api.payouts_day, api.payouts_month - )), - ); - ui.add_sized( - [width, height], - Label::new(RichText::new("XMR Mined").underline().color(BONE)), - ) - .on_hover_text(STATUS_P2POOL_XMR); - ui.add_sized( - [width, height], - Label::new(format!("Total: {:.13} XMR", api.xmr)), - ); - ui.add_sized( - [width, height], - Label::new(format!( - "[{:.7}/hour]\n[{:.7}/day]\n[{:.7}/month]", - api.xmr_hour, api.xmr_day, api.xmr_month - )), - ); - ui.add_sized( - [width, height], - Label::new( - RichText::new("Hashrate (15m/1h/24h)") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_P2POOL_HASHRATE); - ui.add_sized( - [width, height], - Label::new(format!( - "[{} H/s] [{} H/s] [{} H/s]", - api.hashrate_15m, api.hashrate_1h, api.hashrate_24h - )), - ); - ui.add_sized( - [width, height], - Label::new(RichText::new("Miners Connected").underline().color(BONE)), - ) - .on_hover_text(STATUS_P2POOL_CONNECTIONS); - ui.add_sized([width, height], Label::new(format!("{}", api.connections))); - ui.add_sized( - [width, height], - Label::new(RichText::new("Effort").underline().color(BONE)), - ) - .on_hover_text(STATUS_P2POOL_EFFORT); - ui.add_sized( - [width, height], - Label::new(format!( - "[Average: {}] [Current: {}]", - api.average_effort, api.current_effort - )), - ); - let img = lock!(p2pool_img); - ui.add_sized( - [width, height], - Label::new(RichText::new("Monero Node").underline().color(BONE)), - ) - .on_hover_text(STATUS_P2POOL_MONERO_NODE); - ui.add_sized( - [width, height], - Label::new(format!( - "[IP: {}]\n[RPC: {}] [ZMQ: {}]", - &img.host, &img.rpc, &img.zmq - )), - ); - ui.add_sized( - [width, height], - Label::new(RichText::new("Sidechain").underline().color(BONE)), - ) - .on_hover_text(STATUS_P2POOL_POOL); - ui.add_sized([width, height], Label::new(&img.mini)); - ui.add_sized( - [width, height], - Label::new(RichText::new("Address").underline().color(BONE)), - ) - .on_hover_text(STATUS_P2POOL_ADDRESS); - ui.add_sized([width, height], Label::new(&img.address)); - drop(img); - drop(api); - }) - }); - // [XMRig] - ui.group(|ui| { - ui.vertical(|ui| { - debug!("Status Tab | Rendering [XMRig]"); - ui.set_enabled(xmrig_alive); - ui.set_min_height(min_height); - ui.add_sized( - [width, height], - Label::new( - RichText::new("[XMRig]") - .color(LIGHT_GRAY) - .text_style(TextStyle::Name("MonospaceLarge".into())), - ), - ) - .on_hover_text("XMRig is online") - .on_disabled_hover_text("XMRig is offline"); - let api = lock!(xmrig_api); - ui.add_sized( - [width, height], - Label::new(RichText::new("Uptime").underline().color(BONE)), - ) - .on_hover_text(STATUS_XMRIG_UPTIME); - ui.add_sized([width, height], Label::new(format!("{}", api.uptime))); - ui.add_sized( - [width, height], - Label::new( - RichText::new("CPU Load (10s/60s/15m)") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_XMRIG_CPU); - ui.add_sized([width, height], Label::new(format!("{}", api.resources))); - ui.add_sized( - [width, height], - Label::new( - RichText::new("Hashrate (10s/60s/15m)") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_XMRIG_HASHRATE); - ui.add_sized([width, height], Label::new(format!("{}", api.hashrate))); - ui.add_sized( - [width, height], - Label::new(RichText::new("Difficulty").underline().color(BONE)), - ) - .on_hover_text(STATUS_XMRIG_DIFFICULTY); - ui.add_sized([width, height], Label::new(format!("{}", api.diff))); - ui.add_sized( - [width, height], - Label::new(RichText::new("Shares").underline().color(BONE)), - ) - .on_hover_text(STATUS_XMRIG_SHARES); - ui.add_sized( - [width, height], - Label::new(format!( - "[Accepted: {}] [Rejected: {}]", - api.accepted, api.rejected - )), - ); - ui.add_sized( - [width, height], - Label::new(RichText::new("Pool").underline().color(BONE)), - ) - .on_hover_text(STATUS_XMRIG_POOL); - ui.add_sized([width, height], Label::new(&lock!(xmrig_img).url)); - ui.add_sized( - [width, height], - Label::new(RichText::new("Threads").underline().color(BONE)), - ) - .on_hover_text(STATUS_XMRIG_THREADS); - ui.add_sized( - [width, height], - Label::new(format!("{}/{}", &lock!(xmrig_img).threads, max_threads)), - ); - drop(api); - }) - }); - }); - //---------------------------------------------------------------------------------------------------- [P2Pool] - } else if self.submenu == Submenu::P2pool { - let api = lock!(gupax_p2pool_api); - let text = height / 25.0; - let log = height / 2.8; - // Payout Text + PayoutView buttons - ui.group(|ui| { - ui.horizontal(|ui| { - let width = (width / 3.0) - (SPACE * 4.0); - ui.add_sized( - [width, text], - Label::new( - RichText::new(format!("Total Payouts: {}", api.payout)) - .underline() - .color(LIGHT_GRAY), - ), - ) - .on_hover_text(STATUS_SUBMENU_PAYOUT); - ui.separator(); - ui.add_sized( - [width, text], - Label::new( - RichText::new(format!("Total XMR: {}", api.xmr)) - .underline() - .color(LIGHT_GRAY), - ), - ) - .on_hover_text(STATUS_SUBMENU_XMR); - let width = width / 4.0; - ui.separator(); - if ui - .add_sized( - [width, text], - SelectableLabel::new(self.payout_view == PayoutView::Latest, "Latest"), - ) - .on_hover_text(STATUS_SUBMENU_LATEST) - .clicked() - { - self.payout_view = PayoutView::Latest; - } - ui.separator(); - if ui - .add_sized( - [width, text], - SelectableLabel::new(self.payout_view == PayoutView::Oldest, "Oldest"), - ) - .on_hover_text(STATUS_SUBMENU_OLDEST) - .clicked() - { - self.payout_view = PayoutView::Oldest; - } - ui.separator(); - if ui - .add_sized( - [width, text], - SelectableLabel::new( - self.payout_view == PayoutView::Biggest, - "Biggest", - ), - ) - .on_hover_text(STATUS_SUBMENU_BIGGEST) - .clicked() - { - self.payout_view = PayoutView::Biggest; - } - ui.separator(); - if ui - .add_sized( - [width, text], - SelectableLabel::new( - self.payout_view == PayoutView::Smallest, - "Smallest", - ), - ) - .on_hover_text(STATUS_SUBMENU_SMALLEST) - .clicked() - { - self.payout_view = PayoutView::Smallest; - } - }); - ui.separator(); - // Actual logs - egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| { - egui::ScrollArea::vertical() - .stick_to_bottom(self.payout_view == PayoutView::Oldest) - .max_width(width) - .max_height(log) - .auto_shrink([false; 2]) - .show_viewport(ui, |ui, _| { - ui.style_mut().override_text_style = - Some(Name("MonospaceLarge".into())); - match self.payout_view { - PayoutView::Latest => ui.add_sized( - [width, log], - TextEdit::multiline(&mut api.log_rev.as_str()), - ), - PayoutView::Oldest => ui.add_sized( - [width, log], - TextEdit::multiline(&mut api.log.as_str()), - ), - PayoutView::Biggest => ui.add_sized( - [width, log], - TextEdit::multiline(&mut api.payout_high.as_str()), - ), - PayoutView::Smallest => ui.add_sized( - [width, log], - TextEdit::multiline(&mut api.payout_low.as_str()), - ), - }; - }); - }); - }); - drop(api); - // Payout/Share Calculator - let button = (width / 20.0) - (SPACE * 1.666); - ui.group(|ui| { - ui.horizontal(|ui| { - ui.set_min_width(width - SPACE); - if ui - .add_sized( - [button * 2.0, text], - SelectableLabel::new(!self.manual_hash, "Automatic"), - ) - .on_hover_text(STATUS_SUBMENU_AUTOMATIC) - .clicked() - { - self.manual_hash = false; - } - ui.separator(); - if ui - .add_sized( - [button * 2.0, text], - SelectableLabel::new(self.manual_hash, "Manual"), - ) - .on_hover_text(STATUS_SUBMENU_MANUAL) - .clicked() - { - self.manual_hash = true; - } - ui.separator(); - ui.set_enabled(self.manual_hash); - if ui - .add_sized( - [button, text], - SelectableLabel::new(self.hash_metric == Hash::Hash, "Hash"), - ) - .on_hover_text(STATUS_SUBMENU_HASH) - .clicked() - { - self.hash_metric = Hash::Hash; - } - ui.separator(); - if ui - .add_sized( - [button, text], - SelectableLabel::new(self.hash_metric == Hash::Kilo, "Kilo"), - ) - .on_hover_text(STATUS_SUBMENU_KILO) - .clicked() - { - self.hash_metric = Hash::Kilo; - } - ui.separator(); - if ui - .add_sized( - [button, text], - SelectableLabel::new(self.hash_metric == Hash::Mega, "Mega"), - ) - .on_hover_text(STATUS_SUBMENU_MEGA) - .clicked() - { - self.hash_metric = Hash::Mega; - } - ui.separator(); - if ui - .add_sized( - [button, text], - SelectableLabel::new(self.hash_metric == Hash::Giga, "Giga"), - ) - .on_hover_text(STATUS_SUBMENU_GIGA) - .clicked() - { - self.hash_metric = Hash::Giga; - } - ui.separator(); - ui.spacing_mut().slider_width = button * 11.5; - ui.add_sized( - [button * 14.0, text], - Slider::new(&mut self.hashrate, 1.0..=1_000.0), - ); - }) - }); - // Actual stats - ui.set_enabled(p2pool_alive); - let text = height / 25.0; - let width = (width / 3.0) - (SPACE * 1.666); - let min_height = ui.available_height() / 1.3; - let api = lock!(p2pool_api); - ui.horizontal(|ui| { - ui.group(|ui| { - ui.vertical(|ui| { - ui.set_min_height(min_height); - ui.add_sized( - [width, text], - Label::new(RichText::new("Monero Difficulty").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_MONERO_DIFFICULTY); - ui.add_sized([width, text], Label::new(api.monero_difficulty.as_str())); - ui.add_sized( - [width, text], - Label::new(RichText::new("Monero Hashrate").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_MONERO_HASHRATE); - ui.add_sized([width, text], Label::new(api.monero_hashrate.as_str())); - ui.add_sized( - [width, text], - Label::new(RichText::new("P2Pool Difficulty").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL_DIFFICULTY); - ui.add_sized([width, text], Label::new(api.p2pool_difficulty.as_str())); - ui.add_sized( - [width, text], - Label::new(RichText::new("P2Pool Hashrate").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL_HASHRATE); - ui.add_sized([width, text], Label::new(api.p2pool_hashrate.as_str())); - }) - }); - ui.group(|ui| { - ui.vertical(|ui| { - ui.set_min_height(min_height); - if self.manual_hash { - let hashrate = - Hash::convert_to_hash(self.hashrate, self.hash_metric) as u64; - let p2pool_share_mean = PubP2poolApi::calculate_share_or_block_time( - hashrate, - api.p2pool_difficulty_u64, - ); - let solo_block_mean = PubP2poolApi::calculate_share_or_block_time( - hashrate, - api.monero_difficulty_u64, - ); - ui.add_sized( - [width, text], - Label::new( - RichText::new("Manually Inputted Hashrate") - .underline() - .color(BONE), - ), - ); - ui.add_sized( - [width, text], - Label::new(format!("{} H/s", HumanNumber::from_u64(hashrate))), - ); - ui.add_sized( - [width, text], - Label::new( - RichText::new("P2Pool Block Mean").underline().color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL_BLOCK_MEAN); - ui.add_sized( - [width, text], - Label::new(api.p2pool_block_mean.to_string()), - ); - ui.add_sized( - [width, text], - Label::new( - RichText::new("Your P2Pool Share Mean") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL_SHARE_MEAN); - ui.add_sized([width, text], Label::new(p2pool_share_mean.to_string())); - ui.add_sized( - [width, text], - Label::new( - RichText::new("Your Solo Block Mean") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_SOLO_BLOCK_MEAN); - ui.add_sized([width, text], Label::new(solo_block_mean.to_string())); - } else { - ui.add_sized( - [width, text], - Label::new( - RichText::new("Your P2Pool Hashrate") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_HASHRATE); - ui.add_sized( - [width, text], - Label::new(format!("{} H/s", api.hashrate_1h)), - ); - ui.add_sized( - [width, text], - Label::new( - RichText::new("P2Pool Block Mean").underline().color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL_BLOCK_MEAN); - ui.add_sized( - [width, text], - Label::new(api.p2pool_block_mean.to_string()), - ); - ui.add_sized( - [width, text], - Label::new( - RichText::new("Your P2Pool Share Mean") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL_SHARE_MEAN); - ui.add_sized( - [width, text], - Label::new(api.p2pool_share_mean.to_string()), - ); - ui.add_sized( - [width, text], - Label::new( - RichText::new("Your Solo Block Mean") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_SOLO_BLOCK_MEAN); - ui.add_sized( - [width, text], - Label::new(api.solo_block_mean.to_string()), - ); - } - }) - }); - ui.group(|ui| { - ui.vertical(|ui| { - ui.set_min_height(min_height); - if self.manual_hash { - let hashrate = - Hash::convert_to_hash(self.hashrate, self.hash_metric) as u64; - let user_p2pool_percent = PubP2poolApi::calculate_dominance( - hashrate, - api.p2pool_hashrate_u64, - ); - let user_monero_percent = PubP2poolApi::calculate_dominance( - hashrate, - api.monero_hashrate_u64, - ); - ui.add_sized( - [width, text], - Label::new(RichText::new("P2Pool Miners").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL_MINERS); - ui.add_sized([width, text], Label::new(api.miners.as_str())); - ui.add_sized( - [width, text], - Label::new( - RichText::new("P2Pool Dominance").underline().color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL_DOMINANCE); - ui.add_sized([width, text], Label::new(api.p2pool_percent.as_str())); - ui.add_sized( - [width, text], - Label::new( - RichText::new("Your P2Pool Dominance") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_DOMINANCE); - ui.add_sized([width, text], Label::new(user_p2pool_percent.as_str())); - ui.add_sized( - [width, text], - Label::new( - RichText::new("Your Monero Dominance") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_MONERO_DOMINANCE); - ui.add_sized([width, text], Label::new(user_monero_percent.as_str())); - } else { - ui.add_sized( - [width, text], - Label::new(RichText::new("P2Pool Miners").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL_MINERS); - ui.add_sized([width, text], Label::new(api.miners.as_str())); - ui.add_sized( - [width, text], - Label::new( - RichText::new("P2Pool Dominance").underline().color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_P2POOL_DOMINANCE); - ui.add_sized([width, text], Label::new(api.p2pool_percent.as_str())); - ui.add_sized( - [width, text], - Label::new( - RichText::new("Your P2Pool Dominance") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_DOMINANCE); - ui.add_sized( - [width, text], - Label::new(api.user_p2pool_percent.as_str()), - ); - ui.add_sized( - [width, text], - Label::new( - RichText::new("Your Monero Dominance") - .underline() - .color(BONE), - ), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_MONERO_DOMINANCE); - ui.add_sized( - [width, text], - Label::new(api.user_monero_percent.as_str()), - ); - } - }) - }); - }); - // Tick bar - ui.add_sized( - [ui.available_width(), text], - Label::new(api.calculate_tick_bar()), - ) - .on_hover_text(STATUS_SUBMENU_PROGRESS_BAR); - drop(api); - //---------------------------------------------------------------------------------------------------- [Benchmarks] - } else if self.submenu == Submenu::Benchmarks { - debug!("Status Tab | Rendering [Benchmarks]"); - let text = height / 20.0; - let double = text * 2.0; - let log = height / 3.0; - - // [0], The user's CPU (most likely). - let cpu = &benchmarks[0]; - ui.horizontal(|ui| { - let width = (width / 2.0) - (SPACE * 1.666); - let min_height = log; - ui.group(|ui| { - ui.vertical(|ui| { - ui.set_min_height(min_height); - ui.add_sized( - [width, text], - Label::new(RichText::new("Your CPU").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_CPU); - ui.add_sized([width, text], Label::new(cpu.cpu.as_str())); - ui.add_sized( - [width, text], - Label::new(RichText::new("Total Benchmarks").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_BENCHMARKS); - ui.add_sized([width, text], Label::new(format!("{}", cpu.benchmarks))); - ui.add_sized( - [width, text], - Label::new(RichText::new("Rank").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_RANK); - ui.add_sized( - [width, text], - Label::new(format!("{}/{}", cpu.rank, &benchmarks.len())), - ); - }) - }); - ui.group(|ui| { - ui.vertical(|ui| { - ui.set_min_height(min_height); - ui.add_sized( - [width, text], - Label::new(RichText::new("High Hashrate").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_HIGH); - ui.add_sized( - [width, text], - Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.high))), - ); - ui.add_sized( - [width, text], - Label::new(RichText::new("Average Hashrate").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_AVERAGE); - ui.add_sized( - [width, text], - Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.average))), - ); - ui.add_sized( - [width, text], - Label::new(RichText::new("Low Hashrate").underline().color(BONE)), - ) - .on_hover_text(STATUS_SUBMENU_YOUR_LOW); - ui.add_sized( - [width, text], - Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.low))), - ); - }) - }) - }); - - // User's CPU hashrate comparison (if XMRig is alive). - ui.scope(|ui| { - if xmrig_alive { - let api = lock!(xmrig_api); - let percent = (api.hashrate_raw / cpu.high) * 100.0; - let human = HumanNumber::to_percent(percent); - if percent > 100.0 { - ui.add_sized([width, double], Label::new(format!("Your CPU's is faster than the highest benchmark! It is [{}] faster @ {}!", human, api.hashrate))); - ui.add_sized([width, text], ProgressBar::new(1.0)); - } else if api.hashrate_raw == 0.0 { - ui.add_sized([width, text], Label::new("Measuring hashrate...")); - ui.add_sized([width, text], Spinner::new().size(text)); - ui.add_sized([width, text], ProgressBar::new(0.0)); - } else { - ui.add_sized([width, double], Label::new(format!("Your CPU's hashrate is [{}] of the highest benchmark @ {}", human, api.hashrate))); - ui.add_sized([width, text], ProgressBar::new(percent / 100.0)); - } - } else { - ui.set_enabled(xmrig_alive); - ui.add_sized([width, double], Label::new("XMRig is offline. Hashrate cannot be determined.")); - ui.add_sized([width, text], ProgressBar::new(0.0)); - } - }); - - // Comparison - ui.group(|ui| { - ui.add_sized( - [width, text], - Hyperlink::from_label_and_url("Other CPUs", "https://xmrig.com/benchmark"), - ) - .on_hover_text(STATUS_SUBMENU_OTHER_CPUS); - }); - - egui::ScrollArea::both() - .scroll_bar_visibility( - egui::containers::scroll_area::ScrollBarVisibility::AlwaysVisible, - ) - .max_width(width) - .max_height(height) - .auto_shrink([false; 2]) - .show_viewport(ui, |ui, _| { - let width = width / 20.0; - let (cpu, bar, high, average, low, rank, bench) = ( - width * 10.0, - width * 3.0, - width * 2.0, - width * 2.0, - width * 2.0, - width, - width * 2.0, - ); - ui.group(|ui| { - ui.horizontal(|ui| { - ui.add_sized([cpu, double], Label::new("CPU")) - .on_hover_text(STATUS_SUBMENU_OTHER_CPU); - ui.separator(); - ui.add_sized([bar, double], Label::new("Relative")) - .on_hover_text(STATUS_SUBMENU_OTHER_RELATIVE); - ui.separator(); - ui.add_sized([high, double], Label::new("High")) - .on_hover_text(STATUS_SUBMENU_OTHER_HIGH); - ui.separator(); - ui.add_sized([average, double], Label::new("Average")) - .on_hover_text(STATUS_SUBMENU_OTHER_AVERAGE); - ui.separator(); - ui.add_sized([low, double], Label::new("Low")) - .on_hover_text(STATUS_SUBMENU_OTHER_LOW); - ui.separator(); - ui.add_sized([rank, double], Label::new("Rank")) - .on_hover_text(STATUS_SUBMENU_OTHER_RANK); - ui.separator(); - ui.add_sized([bench, double], Label::new("Benchmarks")) - .on_hover_text(STATUS_SUBMENU_OTHER_BENCHMARKS); - }); - }); - - for benchmark in benchmarks[1..].iter() { - ui.group(|ui| { - ui.horizontal(|ui| { - ui.add_sized([cpu, text], Label::new(benchmark.cpu.as_str())); - ui.separator(); - ui.add_sized( - [bar, text], - ProgressBar::new(benchmark.percent / 100.0), - ) - .on_hover_text(HumanNumber::to_percent(benchmark.percent).as_str()); - ui.separator(); - ui.add_sized( - [high, text], - Label::new(HumanNumber::to_hashrate(benchmark.high).as_str()), - ); - ui.separator(); - ui.add_sized( - [average, text], - Label::new( - HumanNumber::to_hashrate(benchmark.average).as_str(), - ), - ); - ui.separator(); - ui.add_sized( - [low, text], - Label::new(HumanNumber::to_hashrate(benchmark.low).as_str()), - ); - ui.separator(); - ui.add_sized( - [rank, text], - Label::new(HumanNumber::from_u16(benchmark.rank).as_str()), - ); - ui.separator(); - ui.add_sized( - [bench, text], - Label::new( - HumanNumber::from_u16(benchmark.benchmarks).as_str(), - ), - ); - }) - }); - } - }); - } - } -} diff --git a/src/constants.rs b/src/utils/constants.rs similarity index 98% rename from src/constants.rs rename to src/utils/constants.rs index 37d4828..c987bce 100644 --- a/src/constants.rs +++ b/src/utils/constants.rs @@ -49,11 +49,11 @@ pub const DISTRO_NO_UPDATE: &str = r#"This [Gupax] was compiled for use as a Lin // Use macOS shaped icon for macOS #[cfg(target_os = "macos")] -pub const BYTES_ICON: &[u8] = include_bytes!("../images/icons/icon@2x.png"); +pub const BYTES_ICON: &[u8] = include_bytes!("../../assets/images/icons/icon@2x.png"); #[cfg(not(target_os = "macos"))] -pub const BYTES_ICON: &[u8] = include_bytes!("../images/icons/icon.png"); -pub const BYTES_XVB: &[u8] = include_bytes!("../images/xvb.png"); -pub const BYTES_BANNER: &[u8] = include_bytes!("../images/banner.png"); +pub const BYTES_ICON: &[u8] = include_bytes!("../../assets/images/icons/icon.png"); +pub const BYTES_XVB: &[u8] = include_bytes!("../../assets/images/xvb.png"); +pub const BYTES_BANNER: &[u8] = include_bytes!("../../assets/images/banner.png"); pub const HORIZONTAL: &str = "--------------------------------------------"; pub const HORI_CONSOLE: &str = "---------------------------------------------------------------------------------------------------------------------------"; @@ -387,6 +387,10 @@ pub const XMRIG_PATH_NOT_VALID: &str = "XMRig binary at the given PATH in the Gu pub const XMRIG_PATH_OK: &str = "XMRig was found at the given PATH"; pub const XMRIG_PATH_EMPTY: &str = "XMRig PATH is empty! To fix: goto the [Gupax Advanced] tab, select [Open] and specify where XMRig is located."; +// XvB +pub const XVB_HELP: &str = "You need to register an account by clicking on the link above to get your token with the same p2pool XMR address you use for payment."; +pub const XVB_URL: &str = "https://xmrvsbeast.com"; + // CLI argument messages pub const ARG_HELP: &str = r#"USAGE: ./gupax [--flag] @@ -488,8 +492,7 @@ pub static VISUALS: Lazy = Lazy::new(|| { ..Visuals::dark() } }); - -//---------------------------------------------------------------------------------------------------- TESTS +//---------------------------------------------------------------------------------------------------- CONSTANTS #[cfg(test)] mod test { #[test] diff --git a/src/utils/errors.rs b/src/utils/errors.rs new file mode 100644 index 0000000..8d07af1 --- /dev/null +++ b/src/utils/errors.rs @@ -0,0 +1,92 @@ +use std::sync::{Arc, Mutex}; + +use super::sudo::SudoState; + +//---------------------------------------------------------------------------------------------------- [ErrorState] struct +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ErrorButtons { + YesNo, + StayQuit, + ResetState, + ResetNode, + Okay, + Quit, + Sudo, + WindowsAdmin, + Debug, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ErrorFerris { + Happy, + Cute, + Oops, + Error, + Panic, + Sudo, +} + +pub struct ErrorState { + pub error: bool, // Is there an error? + pub msg: String, // What message to display? + pub ferris: ErrorFerris, // Which ferris to display? + pub buttons: ErrorButtons, // Which buttons to display? + pub quit_twice: bool, // This indicates the user tried to quit on the [ask_before_quit] screen +} + +impl Default for ErrorState { + fn default() -> Self { + Self::new() + } +} + +impl ErrorState { + pub fn new() -> Self { + Self { + error: false, + msg: "Unknown Error".to_string(), + ferris: ErrorFerris::Oops, + buttons: ErrorButtons::Okay, + quit_twice: false, + } + } + + // Convenience function to enable the [App] error state + pub fn set(&mut self, msg: impl Into, ferris: ErrorFerris, buttons: ErrorButtons) { + if self.error { + // If a panic error is already set and there isn't an [Okay] confirm or another [Panic], return + if self.ferris == ErrorFerris::Panic + && (buttons != ErrorButtons::Okay || ferris != ErrorFerris::Panic) + { + return; + } + } + *self = Self { + error: true, + msg: msg.into(), + ferris, + buttons, + quit_twice: false, + }; + } + + // Just sets the current state to new, resetting it. + pub fn reset(&mut self) { + *self = Self::new(); + } + + // Instead of creating a whole new screen and system, this (ab)uses ErrorState + // to ask for the [sudo] when starting XMRig. Yes, yes I know, it's called "ErrorState" + // but rewriting the UI code and button stuff might be worse. + // It also resets the current [SudoState] + pub fn ask_sudo(&mut self, state: &Arc>) { + *self = Self { + error: true, + msg: String::new(), + ferris: ErrorFerris::Sudo, + buttons: ErrorButtons::Sudo, + quit_twice: false, + }; + SudoState::reset(state) + } +} diff --git a/src/ferris.rs b/src/utils/ferris.rs similarity index 98% rename from src/ferris.rs rename to src/utils/ferris.rs index f4bb2a5..e1858b0 100644 --- a/src/ferris.rs +++ b/src/utils/ferris.rs @@ -17,12 +17,12 @@ // Some images of ferris in byte form for error messages, etc -pub const FERRIS_HAPPY: &[u8] = include_bytes!("../images/ferris/happy.png"); -pub const FERRIS_CUTE: &[u8] = include_bytes!("../images/ferris/cute.png"); -pub const FERRIS_OOPS: &[u8] = include_bytes!("../images/ferris/oops.png"); -pub const FERRIS_ERROR: &[u8] = include_bytes!("../images/ferris/error.png"); -pub const FERRIS_PANIC: &[u8] = include_bytes!("../images/ferris/panic.png"); // This isnt technically ferris but its ok since its spooky -pub const FERRIS_SUDO: &[u8] = include_bytes!("../images/ferris/sudo.png"); +pub const FERRIS_HAPPY: &[u8] = include_bytes!("../../assets/images/ferris/happy.png"); +pub const FERRIS_CUTE: &[u8] = include_bytes!("../../assets/images/ferris/cute.png"); +pub const FERRIS_OOPS: &[u8] = include_bytes!("../../assets/images/ferris/oops.png"); +pub const FERRIS_ERROR: &[u8] = include_bytes!("../../assets/images/ferris/error.png"); +pub const FERRIS_PANIC: &[u8] = include_bytes!("../../assets/images/ferris/panic.png"); // This isnt technically ferris but its ok since its spooky +pub const FERRIS_SUDO: &[u8] = include_bytes!("../../assets/images/ferris/sudo.png"); // This is the ANSI representation of Ferris in string form. // Calling [println!] on this straight up prints a 256-bit color Ferris to the terminal. diff --git a/src/human.rs b/src/utils/human.rs similarity index 100% rename from src/human.rs rename to src/utils/human.rs diff --git a/src/macros.rs b/src/utils/macros.rs similarity index 100% rename from src/macros.rs rename to src/utils/macros.rs diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..394ad37 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,10 @@ +pub mod constants; +pub mod errors; +pub mod ferris; +pub mod human; +pub mod macros; +pub mod panic; +pub mod regex; +pub mod resets; +pub mod sudo; +pub mod xmr; diff --git a/src/panic.rs b/src/utils/panic.rs similarity index 100% rename from src/panic.rs rename to src/utils/panic.rs diff --git a/src/regex.rs b/src/utils/regex.rs similarity index 99% rename from src/regex.rs rename to src/utils/regex.rs index eca8ddb..a0eeb5b 100644 --- a/src/regex.rs +++ b/src/utils/regex.rs @@ -123,8 +123,7 @@ impl XmrigRegex { } } } - -//---------------------------------------------------------------------------------------------------- TESTS +//---------------------------------------------------------------------------------------------------- TEST #[cfg(test)] mod test { use super::*; diff --git a/src/utils/resets.rs b/src/utils/resets.rs new file mode 100644 index 0000000..88ed073 --- /dev/null +++ b/src/utils/resets.rs @@ -0,0 +1,117 @@ +//---------------------------------------------------------------------------------------------------- Reset functions +use crate::disk::create_gupax_dir; +use crate::disk::errors::TomlError; +use crate::disk::gupax_p2pool_api::GupaxP2poolApi; +use crate::disk::node::Node; +use crate::disk::pool::Pool; +use crate::disk::state::State; +use crate::info; +use log::error; +use std::path::PathBuf; +use std::process::exit; + +#[cold] +#[inline(never)] +pub fn reset_state(path: &PathBuf) -> Result<(), TomlError> { + match State::create_new(path) { + Ok(_) => { + info!("Resetting [state.toml] ... OK"); + Ok(()) + } + Err(e) => { + error!("Resetting [state.toml] ... FAIL ... {}", e); + Err(e) + } + } +} + +#[cold] +#[inline(never)] +pub fn reset_nodes(path: &PathBuf) -> Result<(), TomlError> { + match Node::create_new(path) { + Ok(_) => { + info!("Resetting [node.toml] ... OK"); + Ok(()) + } + Err(e) => { + error!("Resetting [node.toml] ... FAIL ... {}", e); + Err(e) + } + } +} + +#[cold] +#[inline(never)] +pub fn reset_pools(path: &PathBuf) -> Result<(), TomlError> { + match Pool::create_new(path) { + Ok(_) => { + info!("Resetting [pool.toml] ... OK"); + Ok(()) + } + Err(e) => { + error!("Resetting [pool.toml] ... FAIL ... {}", e); + Err(e) + } + } +} + +#[cold] +#[inline(never)] +pub fn reset_gupax_p2pool_api(path: &PathBuf) -> Result<(), TomlError> { + match GupaxP2poolApi::create_new(path) { + Ok(_) => { + info!("Resetting GupaxP2poolApi ... OK"); + Ok(()) + } + Err(e) => { + error!("Resetting GupaxP2poolApi folder ... FAIL ... {}", e); + Err(e) + } + } +} + +#[cold] +#[inline(never)] +pub fn reset( + path: &PathBuf, + state: &PathBuf, + node: &PathBuf, + pool: &PathBuf, + gupax_p2pool_api: &PathBuf, +) { + let mut code = 0; + // Attempt to remove directory first + match std::fs::remove_dir_all(path) { + Ok(_) => info!("Removing OS data path ... OK"), + Err(e) => { + error!("Removing OS data path ... FAIL ... {}", e); + code = 1; + } + } + // Recreate + match create_gupax_dir(path) { + Ok(_) => (), + Err(_) => code = 1, + } + match reset_state(state) { + Ok(_) => (), + Err(_) => code = 1, + } + match reset_nodes(node) { + Ok(_) => (), + Err(_) => code = 1, + } + match reset_pools(pool) { + Ok(_) => (), + Err(_) => code = 1, + } + match reset_gupax_p2pool_api(gupax_p2pool_api) { + Ok(_) => (), + Err(_) => code = 1, + } + match code { + 0 => println!("\nGupax reset ... OK"), + _ => eprintln!("\nGupax reset ... FAIL"), + } + exit(code); +} diff --git a/src/sudo.rs b/src/utils/sudo.rs similarity index 98% rename from src/sudo.rs rename to src/utils/sudo.rs index 8b12101..e9bbebe 100644 --- a/src/sudo.rs +++ b/src/utils/sudo.rs @@ -19,7 +19,12 @@ // [zeroize] is used to wipe the memory after use. // Only gets imported in [main.rs] for Unix. -use crate::{constants::*, disk::Xmrig, macros::*, Helper, ProcessSignal}; +use crate::{ + constants::*, + disk::state::Xmrig, + helper::{Helper, ProcessSignal}, + macros::*, +}; use log::*; use std::{ io::Write, diff --git a/src/xmr.rs b/src/utils/xmr.rs similarity index 100% rename from src/xmr.rs rename to src/utils/xmr.rs