diff --git a/build.rs b/build.rs index c380dd0..d72cc31 100755 --- a/build.rs +++ b/build.rs @@ -5,16 +5,17 @@ // pre-compiled bytes using [include_bytes!()] on the images in [images/]. #[cfg(windows)] fn main() -> std::io::Result<()> { - set_commit_env(); + set_commit_env(); - static_vcruntime::metabuild(); - let mut res = winres::WindowsResource::new(); - // This sets the icon. - res.set_icon("images/icons/icon.ico"); - // This sets the [Run as Administrator] metadata flag for Windows. - // Why do I do this?: [https://github.com/hinto-janai/gupax/tree/main/src#why-does-gupax-need-to-be-admin-on-windows] - // TL;DR: Because Windows. - res.set_manifest(r#" + static_vcruntime::metabuild(); + let mut res = winres::WindowsResource::new(); + // This sets the icon. + res.set_icon("images/icons/icon.ico"); + // This sets the [Run as Administrator] metadata flag for Windows. + // Why do I do this?: [https://github.com/hinto-janai/gupax/tree/main/src#why-does-gupax-need-to-be-admin-on-windows] + // TL;DR: Because Windows. + res.set_manifest( + r#" @@ -24,27 +25,28 @@ fn main() -> std::io::Result<()> { - "#); - res.compile() + "#, + ); + res.compile() } #[cfg(unix)] fn main() { - set_commit_env(); + set_commit_env(); } // Set the current git commit to the env var [COMMIT]. fn set_commit_env() { - println!("cargo:rerun-if-changed=.git/refs/heads/"); + println!("cargo:rerun-if-changed=.git/refs/heads/"); - let output = std::process::Command::new("git") - .args(["rev-parse", "HEAD"]) - .output() - .unwrap(); + let output = std::process::Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .unwrap(); - let commit = String::from_utf8(output.stdout).unwrap(); + let commit = String::from_utf8(output.stdout).unwrap(); - assert!(commit.len() >= 40); + assert!(commit.len() >= 40); - println!("cargo:rustc-env=COMMIT={commit}"); + println!("cargo:rustc-env=COMMIT={commit}"); } diff --git a/src/constants.rs b/src/constants.rs index 828c62f..cb9f70d 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -15,51 +15,49 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -pub const GUPAX_VERSION: &str = concat!("v", env!("CARGO_PKG_VERSION")); // e.g: v1.0.0 +pub const GUPAX_VERSION: &str = concat!("v", env!("CARGO_PKG_VERSION")); // e.g: v1.0.0 pub const P2POOL_VERSION: &str = "v3.10"; -pub const XMRIG_VERSION: &str = "v6.21.1"; -pub const COMMIT: &str = env!("COMMIT"); // set in build.rs -// e.g: Gupax_v1_0_0 -// Would have been [Gupax_v1.0.0] but P2Pool truncates everything after [.] +pub const XMRIG_VERSION: &str = "v6.21.1"; +pub const COMMIT: &str = env!("COMMIT"); // set in build.rs + // e.g: Gupax_v1_0_0 + // Would have been [Gupax_v1.0.0] but P2Pool truncates everything after [.] pub const GUPAX_VERSION_UNDERSCORE: &str = concat!( - "Gupax_v", - env!("CARGO_PKG_VERSION_MAJOR"), - "_", - env!("CARGO_PKG_VERSION_MINOR"), - "_", - env!("CARGO_PKG_VERSION_PATCH"), + "Gupax_v", + env!("CARGO_PKG_VERSION_MAJOR"), + "_", + env!("CARGO_PKG_VERSION_MINOR"), + "_", + env!("CARGO_PKG_VERSION_PATCH"), ); // App frame resolution, [4:3] aspect ratio, [1.33:1] -pub const APP_MIN_WIDTH: f32 = 640.0; +pub const APP_MIN_WIDTH: f32 = 640.0; pub const APP_MIN_HEIGHT: f32 = 480.0; -pub const APP_MAX_WIDTH: f32 = 3840.0; +pub const APP_MAX_WIDTH: f32 = 3840.0; pub const APP_MAX_HEIGHT: f32 = 2160.0; // Default, 1280x960 -pub const APP_DEFAULT_WIDTH: f32 = 1280.0; +pub const APP_DEFAULT_WIDTH: f32 = 1280.0; pub const APP_DEFAULT_HEIGHT: f32 = 960.0; // App resolution scaling -pub const APP_MIN_SCALE: f32 = 0.1; +pub const APP_MIN_SCALE: f32 = 0.1; pub const APP_MAX_SCALE: f32 = 2.0; pub const APP_DEFAULT_SCALE: f32 = 1.0; // Constants specific for Linux distro packaging of Gupax #[cfg(feature = "distro")] -pub const DISTRO_NO_UPDATE: &str = -r#"This [Gupax] was compiled for use as a Linux distro package. Built-in updates are disabled. The below settings [Update-via-Tor] & [Auto-Update] will not do anything. Please use your package manager to update [Gupax/P2Pool/XMRig]."#; +pub const DISTRO_NO_UPDATE: &str = r#"This [Gupax] was compiled for use as a Linux distro package. Built-in updates are disabled. The below settings [Update-via-Tor] & [Auto-Update] will not do anything. Please use your package manager to update [Gupax/P2Pool/XMRig]."#; // 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!("../images/icons/icon@2x.png"); #[cfg(not(target_os = "macos"))] -pub const BYTES_ICON: &[u8] = include_bytes!("../images/icons/icon.png"); +pub const BYTES_ICON: &[u8] = include_bytes!("../images/icons/icon.png"); pub const BYTES_BANNER: &[u8] = include_bytes!("../images/banner.png"); -pub const HORIZONTAL: &str = "--------------------------------------------"; +pub const HORIZONTAL: &str = "--------------------------------------------"; pub const HORI_CONSOLE: &str = "---------------------------------------------------------------------------------------------------------------------------"; // Keyboard shortcuts -pub const KEYBOARD_SHORTCUTS: &str = -r#"*---------------------------------------* +pub const KEYBOARD_SHORTCUTS: &str = r#"*---------------------------------------* | Key shortcuts | |---------------------------------------| | F11 | Fullscreen | @@ -75,28 +73,29 @@ r#"*---------------------------------------* *---------------------------------------*"#; // P2Pool & XMRig default API stuff #[cfg(target_os = "windows")] -pub const P2POOL_API_PATH_LOCAL: &str = r"local\stratum"; +pub const P2POOL_API_PATH_LOCAL: &str = r"local\stratum"; #[cfg(target_os = "windows")] pub const P2POOL_API_PATH_NETWORK: &str = r"network\stats"; #[cfg(target_os = "windows")] -pub const P2POOL_API_PATH_POOL: &str = r"pool\stats"; +pub const P2POOL_API_PATH_POOL: &str = r"pool\stats"; #[cfg(target_family = "unix")] -pub const P2POOL_API_PATH_LOCAL: &str = "local/stratum"; +pub const P2POOL_API_PATH_LOCAL: &str = "local/stratum"; #[cfg(target_family = "unix")] pub const P2POOL_API_PATH_NETWORK: &str = "network/stats"; #[cfg(target_family = "unix")] -pub const P2POOL_API_PATH_POOL: &str = "pool/stats"; -pub const XMRIG_API_URI: &str = "1/summary"; // The default relative URI of XMRig's API +pub const P2POOL_API_PATH_POOL: &str = "pool/stats"; +pub const XMRIG_API_URI: &str = "1/summary"; // The default relative URI of XMRig's API // Process state tooltips (online, offline, etc) -pub const P2POOL_ALIVE: &str = "P2Pool is online and fully synchronized"; -pub const P2POOL_DEAD: &str = "P2Pool is offline"; -pub const P2POOL_FAILED: &str = "P2Pool is offline and failed when exiting"; -pub const P2POOL_MIDDLE: &str = "P2Pool is in the middle of (re)starting/stopping"; -pub const P2POOL_SYNCING: &str = "P2Pool is still syncing. This indicator will turn GREEN when P2Pool is ready"; +pub const P2POOL_ALIVE: &str = "P2Pool is online and fully synchronized"; +pub const P2POOL_DEAD: &str = "P2Pool is offline"; +pub const P2POOL_FAILED: &str = "P2Pool is offline and failed when exiting"; +pub const P2POOL_MIDDLE: &str = "P2Pool is in the middle of (re)starting/stopping"; +pub const P2POOL_SYNCING: &str = + "P2Pool is still syncing. This indicator will turn GREEN when P2Pool is ready"; -pub const XMRIG_ALIVE: &str = "XMRig is online and mining"; -pub const XMRIG_DEAD: &str = "XMRig is offline"; +pub const XMRIG_ALIVE: &str = "XMRig is online and mining"; +pub const XMRIG_DEAD: &str = "XMRig is offline"; pub const XMRIG_FAILED: &str = "XMRig is offline and failed when exiting"; pub const XMRIG_MIDDLE: &str = "XMRig is in the middle of (re)starting/stopping"; pub const XMRIG_NOT_MINING: &str = "XMRig is online, but not mining to any pool"; @@ -108,31 +107,29 @@ pub const XMRIG_NOT_MINING: &str = "XMRig is online, but not mining to any pool" pub const SPACE: f32 = 10.0; // Some colors -pub const RED: egui::Color32 = egui::Color32::from_rgb(230, 50, 50); -pub const GREEN: egui::Color32 = egui::Color32::from_rgb(100, 230, 100); -pub const BLUE: egui::Color32 = egui::Color32::from_rgb(100, 175, 255); -pub const ORANGE: egui::Color32 = egui::Color32::from_rgb(255, 120, 40); -pub const YELLOW: egui::Color32 = egui::Color32::from_rgb(230, 230, 100); +pub const RED: egui::Color32 = egui::Color32::from_rgb(230, 50, 50); +pub const GREEN: egui::Color32 = egui::Color32::from_rgb(100, 230, 100); +pub const BLUE: egui::Color32 = egui::Color32::from_rgb(100, 175, 255); +pub const ORANGE: egui::Color32 = egui::Color32::from_rgb(255, 120, 40); +pub const YELLOW: egui::Color32 = egui::Color32::from_rgb(230, 230, 100); pub const BRIGHT_YELLOW: egui::Color32 = egui::Color32::from_rgb(250, 250, 100); -pub const BONE: egui::Color32 = egui::Color32::from_rgb(190, 190, 190); // In between LIGHT_GRAY <-> GRAY -pub const WHITE: egui::Color32 = egui::Color32::WHITE; -pub const GRAY: egui::Color32 = egui::Color32::GRAY; -pub const LIGHT_GRAY: egui::Color32 = egui::Color32::LIGHT_GRAY; -pub const BLACK: egui::Color32 = egui::Color32::BLACK; -pub const DARK_GRAY: egui::Color32 = egui::Color32::from_gray(13); +pub const BONE: egui::Color32 = egui::Color32::from_rgb(190, 190, 190); // In between LIGHT_GRAY <-> GRAY +pub const WHITE: egui::Color32 = egui::Color32::WHITE; +pub const GRAY: egui::Color32 = egui::Color32::GRAY; +pub const LIGHT_GRAY: egui::Color32 = egui::Color32::LIGHT_GRAY; +pub const BLACK: egui::Color32 = egui::Color32::BLACK; +pub const DARK_GRAY: egui::Color32 = egui::Color32::from_gray(13); // [Duration] constants pub const SECOND: std::time::Duration = std::time::Duration::from_secs(1); // The explanation given to the user on why XMRig needs sudo. -pub const XMRIG_ADMIN_REASON: &str = -r#"The large hashrate difference between XMRig and other miners like Monero and P2Pool's built-in miners is mostly due to XMRig configuring CPU MSRs and setting up hugepages. Other miners like Monero or P2Pool's built-in miner do not do this. It can be done manually but it isn't recommended since XMRig does this for you automatically, but only if it has the proper admin privileges."#; +pub const XMRIG_ADMIN_REASON: &str = r#"The large hashrate difference between XMRig and other miners like Monero and P2Pool's built-in miners is mostly due to XMRig configuring CPU MSRs and setting up hugepages. Other miners like Monero or P2Pool's built-in miner do not do this. It can be done manually but it isn't recommended since XMRig does this for you automatically, but only if it has the proper admin privileges."#; // Password buttons -pub const PASSWORD_TEXT: &str = "Enter sudo/admin password..."; +pub const PASSWORD_TEXT: &str = "Enter sudo/admin password..."; pub const PASSWORD_LEAVE: &str = "Return to the previous screen"; pub const PASSWORD_ENTER: &str = "Attempt with the current password"; -pub const PASSWORD_HIDE: &str = "Toggle hiding/showing the password"; - +pub const PASSWORD_HIDE: &str = "Toggle hiding/showing the password"; // OS specific #[cfg(target_os = "windows")] @@ -154,111 +151,132 @@ pub const OS_NAME: &str = "Linux"; // Tooltips // Status -pub const STATUS_GUPAX_UPTIME: &str = "How long Gupax has been online"; -pub const STATUS_GUPAX_CPU_USAGE: &str = "How much CPU Gupax is currently using. This accounts for all your threads (it is out of 100%)"; -pub const STATUS_GUPAX_MEMORY_USAGE: &str = "How much memory Gupax is currently using in Megabytes"; +pub const STATUS_GUPAX_UPTIME: &str = "How long Gupax has been online"; +pub const STATUS_GUPAX_CPU_USAGE: &str = + "How much CPU Gupax is currently using. This accounts for all your threads (it is out of 100%)"; +pub const STATUS_GUPAX_MEMORY_USAGE: &str = "How much memory Gupax is currently using in Megabytes"; pub const STATUS_GUPAX_SYSTEM_CPU_USAGE: &str = "How much CPU your entire system is currently using. This accounts for all your threads (it is out of 100%)"; -pub const STATUS_GUPAX_SYSTEM_MEMORY: &str = "How much memory your entire system has (including swap) and is currently using in Gigabytes"; -pub const STATUS_GUPAX_SYSTEM_CPU_MODEL: &str = "The detected model of your system's CPU and its current frequency"; +pub const STATUS_GUPAX_SYSTEM_MEMORY: &str = + "How much memory your entire system has (including swap) and is currently using in Gigabytes"; +pub const STATUS_GUPAX_SYSTEM_CPU_MODEL: &str = + "The detected model of your system's CPU and its current frequency"; //-- -pub const STATUS_P2POOL_UPTIME: &str = "How long P2Pool has been online"; +pub const STATUS_P2POOL_UPTIME: &str = "How long P2Pool has been online"; pub const STATUS_P2POOL_PAYOUTS: &str = "The total amount of payouts received in this instance of P2Pool and an extrapolated estimate of how many you will receive. Warning: these stats will be quite inaccurate if your P2Pool hasn't been running for a long time!"; pub const STATUS_P2POOL_XMR: &str = "The total amount of XMR mined in this instance of P2Pool and an extrapolated estimate of how many you will mine in the future. Warning: these stats will be quite inaccurate if your P2Pool hasn't been running for a long time!"; pub const STATUS_P2POOL_HASHRATE: &str = "The total amount of hashrate your P2Pool has pointed at it in 15 minute, 1 hour, and 24 hour averages"; -pub const STATUS_P2POOL_SHARES: &str = "The total amount of shares found on P2Pool"; -pub const STATUS_P2POOL_EFFORT: &str = "The average amount of effort needed to find a share, and the current effort"; +pub const STATUS_P2POOL_SHARES: &str = "The total amount of shares found on P2Pool"; +pub const STATUS_P2POOL_EFFORT: &str = + "The average amount of effort needed to find a share, and the current effort"; pub const STATUS_P2POOL_CONNECTIONS: &str = "The total amount of miner connections on this P2Pool"; pub const STATUS_P2POOL_MONERO_NODE: &str = "The Monero node being used by P2Pool"; -pub const STATUS_P2POOL_POOL: &str = "The P2Pool sidechain you're currently connected to"; -pub const STATUS_P2POOL_ADDRESS: &str = "The Monero address P2Pool will send payouts to"; +pub const STATUS_P2POOL_POOL: &str = "The P2Pool sidechain you're currently connected to"; +pub const STATUS_P2POOL_ADDRESS: &str = "The Monero address P2Pool will send payouts to"; //-- -pub const STATUS_XMRIG_UPTIME: &str = "How long XMRig has been online"; +pub const STATUS_XMRIG_UPTIME: &str = "How long XMRig has been online"; pub const STATUS_XMRIG_CPU: &str = "The average CPU load of XMRig. [1.0] represents 1 thread is maxed out, e.g: If you have 8 threads, [4.0] means half your threads are maxed out."; -pub const STATUS_XMRIG_HASHRATE: &str = "The average hashrate of XMRig"; -pub const STATUS_XMRIG_DIFFICULTY: &str = "The current difficulty of the job XMRig is working on"; -pub const STATUS_XMRIG_SHARES: &str = "The amount of accepted and rejected shares"; -pub const STATUS_XMRIG_POOL: &str = "The pool XMRig is currently mining to"; -pub const STATUS_XMRIG_THREADS: &str = "The amount of threads XMRig is currently using"; +pub const STATUS_XMRIG_HASHRATE: &str = "The average hashrate of XMRig"; +pub const STATUS_XMRIG_DIFFICULTY: &str = "The current difficulty of the job XMRig is working on"; +pub const STATUS_XMRIG_SHARES: &str = "The amount of accepted and rejected shares"; +pub const STATUS_XMRIG_POOL: &str = "The pool XMRig is currently mining to"; +pub const STATUS_XMRIG_THREADS: &str = "The amount of threads XMRig is currently using"; // Status Submenus -pub const STATUS_SUBMENU_PROCESSES: &str = "View the status of process related data for [Gupax|P2Pool|XMRig]"; -pub const STATUS_SUBMENU_P2POOL: &str = "View P2Pool specific data"; -pub const STATUS_SUBMENU_HASHRATE: &str = "Compare your CPU hashrate with others"; +pub const STATUS_SUBMENU_PROCESSES: &str = + "View the status of process related data for [Gupax|P2Pool|XMRig]"; +pub const STATUS_SUBMENU_P2POOL: &str = "View P2Pool specific data"; +pub const STATUS_SUBMENU_HASHRATE: &str = "Compare your CPU hashrate with others"; //-- P2Pool pub const STATUS_SUBMENU_PAYOUT: &str = "The total amount of payouts received via P2Pool across all time. This includes all payouts you have ever received using Gupax and P2Pool."; pub const STATUS_SUBMENU_XMR: &str = "The total of XMR mined via P2Pool across all time. This includes all the XMR you have ever mined using Gupax and P2Pool."; -pub const STATUS_SUBMENU_LATEST: &str = "Sort the payouts from latest to oldest"; -pub const STATUS_SUBMENU_OLDEST: &str = "Sort the payouts from oldest to latest"; -pub const STATUS_SUBMENU_BIGGEST: &str = "Sort the payouts from biggest to smallest"; -pub const STATUS_SUBMENU_SMALLEST: &str = "Sort the payouts from smallest to biggest"; -pub const STATUS_SUBMENU_AUTOMATIC: &str = "Automatically calculate share/block time with your current P2Pool 1 hour average hashrate"; +pub const STATUS_SUBMENU_LATEST: &str = "Sort the payouts from latest to oldest"; +pub const STATUS_SUBMENU_OLDEST: &str = "Sort the payouts from oldest to latest"; +pub const STATUS_SUBMENU_BIGGEST: &str = "Sort the payouts from biggest to smallest"; +pub const STATUS_SUBMENU_SMALLEST: &str = "Sort the payouts from smallest to biggest"; +pub const STATUS_SUBMENU_AUTOMATIC: &str = + "Automatically calculate share/block time with your current P2Pool 1 hour average hashrate"; pub const STATUS_SUBMENU_MANUAL: &str = "Manually input a hashrate to calculate share/block time with current P2Pool/Monero network stats"; -pub const STATUS_SUBMENU_HASH: &str = "Use [Hash] as the hashrate metric"; -pub const STATUS_SUBMENU_KILO: &str = "Use [Kilo] as the hashrate metric (1,000x hash)"; -pub const STATUS_SUBMENU_MEGA: &str = "Use [Mega] as the hashrate metric (1,000,000x hash)"; -pub const STATUS_SUBMENU_GIGA: &str = "Use [Giga] as the hashrate metric (1,000,000,000x hash)"; -pub const STATUS_SUBMENU_P2POOL_BLOCK_MEAN: &str = "The average time it takes for P2Pool to find a block"; -pub const STATUS_SUBMENU_YOUR_P2POOL_HASHRATE: &str = "Your 1 hour average hashrate on P2Pool"; -pub const STATUS_SUBMENU_P2POOL_SHARE_MEAN: &str = "The average time it takes for your hashrate to find a share on P2Pool"; -pub const STATUS_SUBMENU_SOLO_BLOCK_MEAN: &str = "The average time it would take for your hashrate to find a block solo mining Monero"; +pub const STATUS_SUBMENU_HASH: &str = "Use [Hash] as the hashrate metric"; +pub const STATUS_SUBMENU_KILO: &str = "Use [Kilo] as the hashrate metric (1,000x hash)"; +pub const STATUS_SUBMENU_MEGA: &str = "Use [Mega] as the hashrate metric (1,000,000x hash)"; +pub const STATUS_SUBMENU_GIGA: &str = "Use [Giga] as the hashrate metric (1,000,000,000x hash)"; +pub const STATUS_SUBMENU_P2POOL_BLOCK_MEAN: &str = + "The average time it takes for P2Pool to find a block"; +pub const STATUS_SUBMENU_YOUR_P2POOL_HASHRATE: &str = "Your 1 hour average hashrate on P2Pool"; +pub const STATUS_SUBMENU_P2POOL_SHARE_MEAN: &str = + "The average time it takes for your hashrate to find a share on P2Pool"; +pub const STATUS_SUBMENU_SOLO_BLOCK_MEAN: &str = + "The average time it would take for your hashrate to find a block solo mining Monero"; pub const STATUS_SUBMENU_MONERO_DIFFICULTY: &str = "The current Monero network's difficulty (how many hashes it will take on average to find a block)"; -pub const STATUS_SUBMENU_MONERO_HASHRATE: &str = "The current Monero network's hashrate"; +pub const STATUS_SUBMENU_MONERO_HASHRATE: &str = "The current Monero network's hashrate"; pub const STATUS_SUBMENU_P2POOL_DIFFICULTY: &str = "The current P2Pool network's difficulty (how many hashes it will take on average to find a share)"; -pub const STATUS_SUBMENU_P2POOL_HASHRATE: &str = "The current P2Pool network's hashrate"; -pub const STATUS_SUBMENU_P2POOL_MINERS: &str = "The current amount of miners on P2Pool"; -pub const STATUS_SUBMENU_P2POOL_DOMINANCE: &str = "The percent of hashrate P2Pool accounts for in the entire Monero network"; -pub const STATUS_SUBMENU_YOUR_P2POOL_DOMINANCE: &str = "The percent of hashrate you account for in P2Pool"; -pub const STATUS_SUBMENU_YOUR_MONERO_DOMINANCE: &str = "The percent of hashrate you account for in the entire Monero network"; +pub const STATUS_SUBMENU_P2POOL_HASHRATE: &str = "The current P2Pool network's hashrate"; +pub const STATUS_SUBMENU_P2POOL_MINERS: &str = "The current amount of miners on P2Pool"; +pub const STATUS_SUBMENU_P2POOL_DOMINANCE: &str = + "The percent of hashrate P2Pool accounts for in the entire Monero network"; +pub const STATUS_SUBMENU_YOUR_P2POOL_DOMINANCE: &str = + "The percent of hashrate you account for in P2Pool"; +pub const STATUS_SUBMENU_YOUR_MONERO_DOMINANCE: &str = + "The percent of hashrate you account for in the entire Monero network"; pub const STATUS_SUBMENU_PROGRESS_BAR: &str = "The next time Gupax will update P2Pool stats. Each [*] is 900ms (updates roughly every 54 seconds)"; //-- Benchmarks -pub const STATUS_SUBMENU_YOUR_CPU: &str = "The CPU detected by Gupax"; -pub const STATUS_SUBMENU_YOUR_BENCHMARKS: &str = "How many benchmarks your CPU has had uploaded to [https://xmrig.com/benchmark] "; -pub const STATUS_SUBMENU_YOUR_RANK: &str = "Your CPU's rank out of all CPUs listed on [https://xmrig.com/benchmark] (higher is better)"; -pub const STATUS_SUBMENU_YOUR_HIGH: &str = "The highest hashrate recorded for your CPU on [https://xmrig.com/benchmark]"; -pub const STATUS_SUBMENU_YOUR_AVERAGE: &str = "The average hashrate of your CPU based off the data at [https://xmrig.com/benchmark]"; -pub const STATUS_SUBMENU_YOUR_LOW: &str = "The lowest hashrate recorded for your CPU on [https://xmrig.com/benchmark]"; +pub const STATUS_SUBMENU_YOUR_CPU: &str = "The CPU detected by Gupax"; +pub const STATUS_SUBMENU_YOUR_BENCHMARKS: &str = + "How many benchmarks your CPU has had uploaded to [https://xmrig.com/benchmark] "; +pub const STATUS_SUBMENU_YOUR_RANK: &str = + "Your CPU's rank out of all CPUs listed on [https://xmrig.com/benchmark] (higher is better)"; +pub const STATUS_SUBMENU_YOUR_HIGH: &str = + "The highest hashrate recorded for your CPU on [https://xmrig.com/benchmark]"; +pub const STATUS_SUBMENU_YOUR_AVERAGE: &str = + "The average hashrate of your CPU based off the data at [https://xmrig.com/benchmark]"; +pub const STATUS_SUBMENU_YOUR_LOW: &str = + "The lowest hashrate recorded for your CPU on [https://xmrig.com/benchmark]"; pub const STATUS_SUBMENU_OTHER_CPUS: &str = "A list of ALL the recorded CPU benchmarks. The CPUs most similar to yours are listed first. All this data is taken from [https://xmrig.com/benchmark]."; -pub const STATUS_SUBMENU_OTHER_CPU: &str = "The CPU name"; +pub const STATUS_SUBMENU_OTHER_CPU: &str = "The CPU name"; pub const STATUS_SUBMENU_OTHER_RELATIVE: &str = "The relative hashrate power compared to the fastest recorded CPU, which is current: [AMD EPYC 7T83 64-Core Processor]"; -pub const STATUS_SUBMENU_OTHER_HIGH: &str = "Highest hashrate record"; -pub const STATUS_SUBMENU_OTHER_AVERAGE: &str = "Average hashrate"; -pub const STATUS_SUBMENU_OTHER_LOW: &str = "Lowest hashrate record"; -pub const STATUS_SUBMENU_OTHER_RANK: &str = "The rank of this CPU out of [1567] (lower is better)"; -pub const STATUS_SUBMENU_OTHER_BENCHMARKS: &str = "How many benchmarks this CPU has had posted to [https://xmrig.com/benchmark]"; +pub const STATUS_SUBMENU_OTHER_HIGH: &str = "Highest hashrate record"; +pub const STATUS_SUBMENU_OTHER_AVERAGE: &str = "Average hashrate"; +pub const STATUS_SUBMENU_OTHER_LOW: &str = "Lowest hashrate record"; +pub const STATUS_SUBMENU_OTHER_RANK: &str = "The rank of this CPU out of [1567] (lower is better)"; +pub const STATUS_SUBMENU_OTHER_BENCHMARKS: &str = + "How many benchmarks this CPU has had posted to [https://xmrig.com/benchmark]"; // Gupax -pub const GUPAX_UPDATE: &str = "Check for updates on Gupax, P2Pool, and XMRig via GitHub's API and upgrade automatically"; -pub const GUPAX_AUTO_UPDATE: &str = "Automatically check for updates at startup"; -pub const GUPAX_SHOULD_RESTART: &str = "Gupax was updated. A restart is recommended but not required"; -pub const GUPAX_UP_TO_DATE: &str = "Gupax is up-to-date"; +pub const GUPAX_UPDATE: &str = + "Check for updates on Gupax, P2Pool, and XMRig via GitHub's API and upgrade automatically"; +pub const GUPAX_AUTO_UPDATE: &str = "Automatically check for updates at startup"; +pub const GUPAX_SHOULD_RESTART: &str = + "Gupax was updated. A restart is recommended but not required"; +pub const GUPAX_UP_TO_DATE: &str = "Gupax is up-to-date"; #[cfg(not(target_os = "macos"))] pub const GUPAX_UPDATE_VIA_TOR: &str = "Update through the Tor network. Tor is embedded within Gupax; a Tor system proxy is not required"; #[cfg(target_os = "macos")] // Arti library has issues on macOS pub const GUPAX_UPDATE_VIA_TOR: &str = "WARNING: This option is unstable on macOS. Update through the Tor network. Tor is embedded within Gupax; a Tor system proxy is not required"; -pub const GUPAX_ASK_BEFORE_QUIT: &str = "Ask before quitting Gupax"; +pub const GUPAX_ASK_BEFORE_QUIT: &str = "Ask before quitting Gupax"; pub const GUPAX_SAVE_BEFORE_QUIT: &str = "Automatically save any changed settings before quitting"; pub const GUPAX_AUTO_P2POOL: &str = "Automatically start P2Pool on Gupax startup. If you are using [P2Pool Simple], this will NOT wait for your [Auto-Ping] to finish, it will start P2Pool on the pool you already have selected. This option will fail if your P2Pool settings aren't valid!"; pub const GUPAX_AUTO_XMRIG: &str = "Automatically start XMRig on Gupax startup. This option will fail if your XMRig settings aren't valid!"; -pub const GUPAX_ADJUST: &str = "Adjust and set the width/height of the Gupax window"; -pub const GUPAX_WIDTH: &str = "Set the width of the Gupax window"; -pub const GUPAX_HEIGHT: &str = "Set the height of the Gupax window"; -pub const GUPAX_SCALE: &str = "Set the resolution scaling of the Gupax window (resize window to re-apply scaling)"; -pub const GUPAX_LOCK_WIDTH: &str = "Automatically match the HEIGHT against the WIDTH in a 4:3 ratio"; -pub const GUPAX_LOCK_HEIGHT: &str = "Automatically match the WIDTH against the HEIGHT in a 4:3 ratio"; -pub const GUPAX_NO_LOCK: &str = "Allow individual selection of width and height"; -pub const GUPAX_SET: &str = "Set the width/height of the Gupax window to the current values"; -pub const GUPAX_TAB: &str = "Set the default tab Gupax starts on"; -pub const GUPAX_TAB_ABOUT: &str = "Set the tab Gupax starts on to: About"; -pub const GUPAX_TAB_STATUS: &str = "Set the tab Gupax starts on to: Status"; -pub const GUPAX_TAB_GUPAX: &str = "Set the tab Gupax starts on to: Gupax"; -pub const GUPAX_TAB_P2POOL: &str = "Set the tab Gupax starts on to: P2Pool"; -pub const GUPAX_TAB_XMRIG: &str = "Set the tab Gupax starts on to: XMRig"; +pub const GUPAX_ADJUST: &str = "Adjust and set the width/height of the Gupax window"; +pub const GUPAX_WIDTH: &str = "Set the width of the Gupax window"; +pub const GUPAX_HEIGHT: &str = "Set the height of the Gupax window"; +pub const GUPAX_SCALE: &str = + "Set the resolution scaling of the Gupax window (resize window to re-apply scaling)"; +pub const GUPAX_LOCK_WIDTH: &str = + "Automatically match the HEIGHT against the WIDTH in a 4:3 ratio"; +pub const GUPAX_LOCK_HEIGHT: &str = + "Automatically match the WIDTH against the HEIGHT in a 4:3 ratio"; +pub const GUPAX_NO_LOCK: &str = "Allow individual selection of width and height"; +pub const GUPAX_SET: &str = "Set the width/height of the Gupax window to the current values"; +pub const GUPAX_TAB: &str = "Set the default tab Gupax starts on"; +pub const GUPAX_TAB_ABOUT: &str = "Set the tab Gupax starts on to: About"; +pub const GUPAX_TAB_STATUS: &str = "Set the tab Gupax starts on to: Status"; +pub const GUPAX_TAB_GUPAX: &str = "Set the tab Gupax starts on to: Gupax"; +pub const GUPAX_TAB_P2POOL: &str = "Set the tab Gupax starts on to: P2Pool"; +pub const GUPAX_TAB_XMRIG: &str = "Set the tab Gupax starts on to: XMRig"; -pub const GUPAX_SIMPLE: &str = -r#"Use simple Gupax settings: +pub const GUPAX_SIMPLE: &str = r#"Use simple Gupax settings: - Update button - Basic toggles"#; -pub const GUPAX_ADVANCED: &str = -r#"Use advanced Gupax settings: +pub const GUPAX_ADVANCED: &str = r#"Use advanced Gupax settings: - Update button - Basic toggles - P2Pool/XMRig binary path selector @@ -272,24 +290,24 @@ pub const GUPAX_PATH_XMRIG: &str = "The location of the XMRig binary: Both absol // P2Pool pub const P2POOL_MAIN: &str = "Use the P2Pool main-chain. This P2Pool finds blocks faster, but has a higher difficulty. Suitable for miners with more than 50kH/s"; pub const P2POOL_MINI: &str = "Use the P2Pool mini-chain. This P2Pool finds blocks slower, but has a lower difficulty. Suitable for miners with less than 50kH/s"; -pub const P2POOL_OUT: &str = "How many out-bound peers to connect to? (you connecting to others)"; -pub const P2POOL_IN: &str = "How many in-bound peers to allow? (others connecting to you)"; -pub const P2POOL_LOG: &str = "Verbosity of the console log"; -pub const P2POOL_AUTO_NODE: &str = "Automatically ping the remote Monero nodes at Gupax startup"; -pub const P2POOL_AUTO_SELECT: &str = "Automatically select the fastest remote Monero node after pinging"; -pub const P2POOL_BACKUP_HOST_SIMPLE: &str = -r#"Automatically switch to the other nodes listed if the current one is down. +pub const P2POOL_OUT: &str = "How many out-bound peers to connect to? (you connecting to others)"; +pub const P2POOL_IN: &str = "How many in-bound peers to allow? (others connecting to you)"; +pub const P2POOL_LOG: &str = "Verbosity of the console log"; +pub const P2POOL_AUTO_NODE: &str = "Automatically ping the remote Monero nodes at Gupax startup"; +pub const P2POOL_AUTO_SELECT: &str = + "Automatically select the fastest remote Monero node after pinging"; +pub const P2POOL_BACKUP_HOST_SIMPLE: &str = r#"Automatically switch to the other nodes listed if the current one is down. Note: you must ping the remote nodes or this feature will default to only using the currently selected node."#; -pub const P2POOL_BACKUP_HOST_ADVANCED: &str = "Automatically switch to the other nodes in your list if the current one is down."; -pub const P2POOL_SELECT_FASTEST: &str = "Select the fastest remote Monero node"; -pub const P2POOL_SELECT_RANDOM: &str = "Select a random remote Monero node"; -pub const P2POOL_SELECT_LAST: &str = "Select the previous remote Monero node"; -pub const P2POOL_SELECT_NEXT: &str = "Select the next remote Monero node"; -pub const P2POOL_PING: &str = "Ping the built-in remote Monero nodes"; +pub const P2POOL_BACKUP_HOST_ADVANCED: &str = + "Automatically switch to the other nodes in your list if the current one is down."; +pub const P2POOL_SELECT_FASTEST: &str = "Select the fastest remote Monero node"; +pub const P2POOL_SELECT_RANDOM: &str = "Select a random remote Monero node"; +pub const P2POOL_SELECT_LAST: &str = "Select the previous remote Monero node"; +pub const P2POOL_SELECT_NEXT: &str = "Select the next remote Monero node"; +pub const P2POOL_PING: &str = "Ping the built-in remote Monero nodes"; pub const P2POOL_ADDRESS: &str = "You must use a primary Monero address to mine on P2Pool (starts with a 4). It is highly recommended to create a new wallet since addresses are public on P2Pool!"; -pub const P2POOL_COMMUNITY_NODE_WARNING: &str = -r#"TL;DR: Run & use your own Monero Node. +pub const P2POOL_COMMUNITY_NODE_WARNING: &str = r#"TL;DR: Run & use your own Monero Node. Using a Remote Monero Node is convenient but comes at the cost of privacy and reliability. @@ -300,17 +318,14 @@ Running and using your own local Monero node improves privacy and ensures your c For a simple guide, see the [Running a Local Monero Node] section on Gupax's GitHub by clicking this message."#; pub const P2POOL_INPUT: &str = "Send a command to P2Pool"; -pub const P2POOL_ARGUMENTS: &str = -r#"WARNING: Use [--no-color] and make sure to set [--data-api ] & [--local-api] so that the [Status] tab can work! +pub const P2POOL_ARGUMENTS: &str = r#"WARNING: Use [--no-color] and make sure to set [--data-api ] & [--local-api] so that the [Status] tab can work! Start P2Pool with these arguments and override all below settings"#; -pub const P2POOL_SIMPLE: &str = -r#"Use simple P2Pool settings: +pub const P2POOL_SIMPLE: &str = r#"Use simple P2Pool settings: - Remote remote Monero node - Default P2Pool settings + Mini - Backup host setting"#; -pub const P2POOL_ADVANCED: &str = -r#"Use advanced P2Pool settings: +pub const P2POOL_ADVANCED: &str = r#"Use advanced P2Pool settings: - Terminal input - Overriding command arguments - Manual node list @@ -328,19 +343,17 @@ pub const P2POOL_PATH_OK: &str = "P2Pool was found at the given PATH"; pub const P2POOL_PATH_EMPTY: &str = "P2Pool PATH is empty! To fix: goto the [Gupax Advanced] tab, select [Open] and specify where P2Pool is located."; // Node/Pool list -pub const LIST_ADD: &str = "Add the current values to the list"; -pub const LIST_SAVE: &str = "Save the current values to the already existing entry"; +pub const LIST_ADD: &str = "Add the current values to the list"; +pub const LIST_SAVE: &str = "Save the current values to the already existing entry"; pub const LIST_DELETE: &str = "Delete the currently selected entry"; -pub const LIST_CLEAR: &str = "Clear all current values"; +pub const LIST_CLEAR: &str = "Clear all current values"; // XMRig -pub const XMRIG_SIMPLE: &str = -r#"Use simple XMRig settings: +pub const XMRIG_SIMPLE: &str = r#"Use simple XMRig settings: - Mine to local P2Pool (localhost:3333) - CPU thread slider - HTTP API @ localhost:18088"#; -pub const XMRIG_ADVANCED: &str = -r#"Use advanced XMRig settings: +pub const XMRIG_ADVANCED: &str = r#"Use advanced XMRig settings: - Terminal input - Overriding command arguments - Custom payout address @@ -350,30 +363,31 @@ r#"Use advanced XMRig settings: - TLS setting - Keepalive setting"#; pub const XMRIG_INPUT: &str = "Send a command to XMRig"; -pub const XMRIG_ARGUMENTS: &str = -r#"WARNING: Use [--no-color] and make sure to set [--http-host ] & [--http-port ] so that the [Status] tab can work! +pub const XMRIG_ARGUMENTS: &str = r#"WARNING: Use [--no-color] and make sure to set [--http-host ] & [--http-port ] so that the [Status] tab can work! Start XMRig with these arguments and override all below settings"#; pub const XMRIG_ADDRESS: &str = "Specify which Monero address to payout to. This does nothing if mining to P2Pool since the address being paid out to will be the one P2Pool started with. This doubles as a rig identifier for P2Pool and some pools."; pub const XMRIG_NAME: &str = "Add a unique name to identify this pool; Only [A-Za-z0-9-_.] and spaces allowed; Max length = 30 characters"; pub const XMRIG_IP: &str = "Specify the pool IP to connect to with XMRig; It must be a valid IPv4 address or a valid domain name; Max length = 255 characters"; -pub const XMRIG_PORT: &str = "Specify the port of the pool; [1-65535]"; +pub const XMRIG_PORT: &str = "Specify the port of the pool; [1-65535]"; pub const XMRIG_RIG: &str = "Add an optional rig ID. This will be the name shown on the pool; Only [A-Za-z0-9-_] and spaces allowed; Max length = 30 characters"; -#[cfg(not(target_os = "linux"))] -pub const XMRIG_PAUSE: &str = "THIS SETTING IS DISABLED IF SET TO [0]. Pause mining if user is active, resume after"; -pub const XMRIG_API_IP: &str = "Specify which IP to bind to for XMRig's HTTP API; If empty: [localhost/127.0.0.1]"; -pub const XMRIG_API_PORT: &str = "Specify which port to bind to for XMRig's HTTP API; If empty: [18088]"; -pub const XMRIG_TLS: &str = "Enable SSL/TLS connections (needs pool support)"; -pub const XMRIG_KEEPALIVE: &str = "Send keepalive packets to prevent timeout (needs pool support)"; -pub const XMRIG_THREADS: &str = "Number of CPU threads to use for mining"; +#[cfg(not(target_os = "linux"))] +pub const XMRIG_PAUSE: &str = + "THIS SETTING IS DISABLED IF SET TO [0]. Pause mining if user is active, resume after"; +pub const XMRIG_API_IP: &str = + "Specify which IP to bind to for XMRig's HTTP API; If empty: [localhost/127.0.0.1]"; +pub const XMRIG_API_PORT: &str = + "Specify which port to bind to for XMRig's HTTP API; If empty: [18088]"; +pub const XMRIG_TLS: &str = "Enable SSL/TLS connections (needs pool support)"; +pub const XMRIG_KEEPALIVE: &str = "Send keepalive packets to prevent timeout (needs pool support)"; +pub const XMRIG_THREADS: &str = "Number of CPU threads to use for mining"; pub const XMRIG_PATH_NOT_FILE: &str = "XMRig binary not found at the given PATH in the Gupax tab! To fix: goto the [Gupax Advanced] tab, select [Open] and specify where XMRig is located."; pub const XMRIG_PATH_NOT_VALID: &str = "XMRig binary at the given PATH in the Gupax tab doesn't look like XMRig! To fix: goto the [Gupax Advanced] tab, select [Open] and specify where XMRig is located."; -pub const XMRIG_PATH_OK: &str = "XMRig was found at the given PATH"; +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."; // CLI argument messages -pub const ARG_HELP: &str = -r#"USAGE: ./gupax [--flag] +pub const ARG_HELP: &str = r#"USAGE: ./gupax [--flag] --help Print this help message --version Print version and build info @@ -390,29 +404,16 @@ r#"USAGE: ./gupax [--flag] To view more detailed console debug information, start Gupax with the environment variable [RUST_LOG] set to a log level like so: RUST_LOG=(trace|debug|info|warn|error) ./gupax"#; -pub const ARG_COPYRIGHT: &str = -r#"Gupax is licensed under GPLv3. +pub const ARG_COPYRIGHT: &str = r#"Gupax is licensed under GPLv3. For more information, see link below: "#; //---------------------------------------------------------------------------------------------------- Visuals -use egui::epaint::{ - Rounding, - Shadow, - Stroke -}; +use egui::epaint::{Rounding, Shadow, Stroke}; -use egui::{ - Color32, - Visuals, - style::Spacing, -}; +use egui::{style::Spacing, Color32, Visuals}; -use egui::style::{ - Selection, - Widgets, - WidgetVisuals, -}; +use egui::style::{Selection, WidgetVisuals, Widgets}; use once_cell::sync::Lazy; pub const ACCENT_COLOR: Color32 = Color32::from_rgb(200, 100, 100); @@ -420,89 +421,98 @@ pub const BG: Color32 = Color32::from_gray(20); // This is based off [`Visuals::dark()`]. pub static VISUALS: Lazy = Lazy::new(|| { - let selection = Selection { - bg_fill: ACCENT_COLOR, - stroke: Stroke::new(1.0, Color32::from_gray(255)), - }; + let selection = Selection { + bg_fill: ACCENT_COLOR, + stroke: Stroke::new(1.0, Color32::from_gray(255)), + }; - // Based off default dark() mode. - // https://docs.rs/egui/0.24.1/src/egui/style.rs.html#1210 - let widgets = Widgets { - noninteractive: WidgetVisuals { - bg_fill: BG, - bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines - fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color - rounding: Rounding::same(10.0), - expansion: 0.0, - weak_bg_fill: BG, - }, - inactive: WidgetVisuals { - bg_fill: Color32::from_gray(50), - bg_stroke: Default::default(), - fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text - rounding: Rounding::same(10.0), - expansion: 0.0, - weak_bg_fill: Color32::from_gray(50), - }, - hovered: WidgetVisuals { - bg_fill: Color32::from_gray(80), - bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button - fg_stroke: Stroke::new(1.5, Color32::from_gray(240)), - rounding: Rounding::same(10.0), - expansion: 1.0, - weak_bg_fill: Color32::from_gray(80), - }, - active: WidgetVisuals { - bg_fill: Color32::from_gray(55), - bg_stroke: Stroke::new(1.0, Color32::WHITE), - fg_stroke: Stroke::new(2.0, Color32::WHITE), - rounding: Rounding::same(10.0), - expansion: 1.0, - weak_bg_fill: Color32::from_gray(120), - }, - open: WidgetVisuals { - bg_fill: Color32::from_gray(27), - bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), - fg_stroke: Stroke::new(1.0, Color32::from_gray(210)), - rounding: Rounding::same(10.0), - expansion: 0.0, - weak_bg_fill: Color32::from_gray(120), - }, - }; + // Based off default dark() mode. + // https://docs.rs/egui/0.24.1/src/egui/style.rs.html#1210 + let widgets = Widgets { + noninteractive: WidgetVisuals { + bg_fill: BG, + bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines + fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color + rounding: Rounding::same(10.0), + expansion: 0.0, + weak_bg_fill: BG, + }, + inactive: WidgetVisuals { + bg_fill: Color32::from_gray(50), + bg_stroke: Default::default(), + fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text + rounding: Rounding::same(10.0), + expansion: 0.0, + weak_bg_fill: Color32::from_gray(50), + }, + hovered: WidgetVisuals { + bg_fill: Color32::from_gray(80), + bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button + fg_stroke: Stroke::new(1.5, Color32::from_gray(240)), + rounding: Rounding::same(10.0), + expansion: 1.0, + weak_bg_fill: Color32::from_gray(80), + }, + active: WidgetVisuals { + bg_fill: Color32::from_gray(55), + bg_stroke: Stroke::new(1.0, Color32::WHITE), + fg_stroke: Stroke::new(2.0, Color32::WHITE), + rounding: Rounding::same(10.0), + expansion: 1.0, + weak_bg_fill: Color32::from_gray(120), + }, + open: WidgetVisuals { + bg_fill: Color32::from_gray(27), + bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), + fg_stroke: Stroke::new(1.0, Color32::from_gray(210)), + rounding: Rounding::same(10.0), + expansion: 0.0, + weak_bg_fill: Color32::from_gray(120), + }, + }; - // https://docs.rs/egui/0.24.1/src/egui/style.rs.html#1113 + // https://docs.rs/egui/0.24.1/src/egui/style.rs.html#1113 Visuals { - widgets, - selection, - hyperlink_color: Color32::from_rgb(90, 170, 255), - faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so - extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background - code_bg_color: Color32::from_gray(64), - warn_fg_color: Color32::from_rgb(255, 143, 0), // orange - error_fg_color: Color32::from_rgb(255, 0, 0), // red - window_rounding: Rounding::same(6.0), - window_shadow: Shadow::big_dark(), - popup_shadow: Shadow::small_dark(), - ..Visuals::dark() - } + widgets, + selection, + hyperlink_color: Color32::from_rgb(90, 170, 255), + faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so + extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background + code_bg_color: Color32::from_gray(64), + warn_fg_color: Color32::from_rgb(255, 143, 0), // orange + error_fg_color: Color32::from_rgb(255, 0, 0), // red + window_rounding: Rounding::same(6.0), + window_shadow: Shadow::big_dark(), + popup_shadow: Shadow::small_dark(), + ..Visuals::dark() + } }); //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod test { - #[test] - fn gupax_version_is_semver() { - assert_eq!(crate::GUPAX_VERSION.len(), 6); - } + #[test] + fn gupax_version_is_semver() { + assert_eq!(crate::GUPAX_VERSION.len(), 6); + } - #[test] - fn default_app_ratio_is_4_by_3() { - assert_eq!(format!("{:.3}", crate::APP_MIN_WIDTH/crate::APP_MIN_HEIGHT), "1.333"); - assert_eq!(format!("{:.3}", crate::APP_DEFAULT_WIDTH/crate::APP_DEFAULT_HEIGHT), "1.333"); - } + #[test] + fn default_app_ratio_is_4_by_3() { + assert_eq!( + format!("{:.3}", crate::APP_MIN_WIDTH / crate::APP_MIN_HEIGHT), + "1.333" + ); + assert_eq!( + format!( + "{:.3}", + crate::APP_DEFAULT_WIDTH / crate::APP_DEFAULT_HEIGHT + ), + "1.333" + ); + } - #[test] - fn git_commit_eq_or_gt_40_chars() { - assert!(crate::COMMIT.len() >= 40); - } + #[test] + fn git_commit_eq_or_gt_40_chars() { + assert!(crate::COMMIT.len() >= 40); + } } diff --git a/src/disk.rs b/src/disk.rs index bc965c9..f16bbf9 100644 --- a/src/disk.rs +++ b/src/disk.rs @@ -30,28 +30,21 @@ // ├─ Version/ // ├─ ... -use std::{ - fs, - fmt::Display, - path::PathBuf, - result::Result, - sync::{Arc,Mutex}, - fmt::Write, -}; -use serde::{Serialize,Deserialize}; +use crate::{constants::*, gupax::Ratio, human::*, macros::*, xmr::*, Tab}; +use figment::providers::{Format, Toml}; use figment::Figment; -use figment::providers::{Format,Toml}; -use crate::{ - human::*, - constants::*, - gupax::Ratio, - Tab, - xmr::*, - macros::*, -}; use log::*; +use serde::{Deserialize, Serialize}; #[cfg(target_family = "unix")] use std::os::unix::fs::PermissionsExt; +use std::{ + fmt::Display, + fmt::Write, + fs, + path::PathBuf, + result::Result, + sync::{Arc, Mutex}, +}; //---------------------------------------------------------------------------------------------------- Const // State file @@ -77,16 +70,16 @@ pub const POOL_TOML: &str = "pool.toml"; // ├─ 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\"; +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_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_XMR: &str = "xmr"; pub const GUPAX_P2POOL_API_FILE_ARRAY: [&str; 3] = [ - GUPAX_P2POOL_API_LOG, - GUPAX_P2POOL_API_PAYOUT, - GUPAX_P2POOL_API_XMR, + GUPAX_P2POOL_API_LOG, + GUPAX_P2POOL_API_PAYOUT, + GUPAX_P2POOL_API_XMR, ]; #[cfg(target_os = "windows")] @@ -119,1088 +112,1262 @@ pub const DEFAULT_XMRIG_PATH: &str = "/usr/bin/xmrig"; // 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())) }, - } + // 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)) }, - } + #[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)) }, - } + #[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: &PathBuf) -> PathBuf { - let mut gupax_p2pool_dir = os_data_path.clone(); - gupax_p2pool_dir.push(GUPAX_P2POOL_API_DIRECTORY); - gupax_p2pool_dir + let mut gupax_p2pool_dir = os_data_path.clone(); + 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) + // 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)) }, - } + // 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)) - }, - } + 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); + 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) - } + 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() - } + 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), - version: arc_mut!(Version::default()), - } - } + 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), + 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(()) - } + 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 [&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)) }, - } - } + // 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), - } - }, - } - } + // 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) - } + // 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)) }, - } - } + // 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) - } + // 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 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_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()) - } + 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 [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) - } + // 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) - } + // 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) - } + // 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)) }, - } - } + // 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) -// } + // 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 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_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 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 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 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 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 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)) }, - } - } + 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)] +#[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] + 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 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(), - } - } + //---------------------------------------------------------------------------------------------------- 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: &PathBuf) { - let mut path_log = gupax_p2pool_dir.clone(); - let mut path_payout = gupax_p2pool_dir.clone(); - let mut path_xmr = gupax_p2pool_dir.clone(); - 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 fill_paths(&mut self, gupax_p2pool_dir: &PathBuf) { + let mut path_log = gupax_p2pool_dir.clone(); + let mut path_payout = gupax_p2pool_dir.clone(); + let mut path_xmr = gupax_p2pool_dir.clone(); + 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: &PathBuf) -> Result<(), TomlError> { - use std::io::Write; - for file in GUPAX_P2POOL_API_FILE_ARRAY { - let mut path = gupax_p2pool_dir.clone(); - 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 create_all_files(gupax_p2pool_dir: &PathBuf) -> Result<(), TomlError> { + use std::io::Write; + for file in GUPAX_P2POOL_API_FILE_ARRAY { + let mut path = gupax_p2pool_dir.clone(); + 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(()) - } + 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(()) - } + // 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; - } + //---------------------------------------------------------------------------------------------------- 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 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_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 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_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_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(); - } + 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(); - } + // 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 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_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)) }, - } - } + 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), + 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), - } - } + 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) - } + fn from(err: std::io::Error) -> Self { + TomlError::Io(err) + } } impl From for TomlError { - fn from(err: std::fmt::Error) -> Self { - TomlError::Format(err) - } + fn from(err: std::fmt::Error) -> Self { + TomlError::Format(err) + } } //---------------------------------------------------------------------------------------------------- [File] Enum (for matching which file) -#[derive(Clone,Copy,Eq,PartialEq,Debug,Deserialize,Serialize)] +#[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 + // 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 + // 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)] +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] pub enum Submenu { - Processes, - P2pool, - Benchmarks, + Processes, + P2pool, + Benchmarks, } impl Default for Submenu { - fn default() -> Self { - Self::Processes - } + 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), - } - } + 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)] +#[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 + 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 - } + fn new() -> Self { + Self::Latest + } } impl Default for PayoutView { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl Display for PayoutView { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{:?}", self) - } + 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)] +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] pub enum Hash { - Hash, - Kilo, - Mega, - Giga, + Hash, + Kilo, + Mega, + Giga, } impl Default for Hash { - fn default() -> Self { - Self::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_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, - } - }, - } - } + 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), - } - } + 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)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct Node { - pub ip: String, - pub rpc: String, - pub zmq: String, + pub ip: String, + pub rpc: String, + pub zmq: String, } //---------------------------------------------------------------------------------------------------- [Pool] Struct -#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct Pool { - pub rig: String, - pub ip: String, - pub port: String, + pub rig: String, + pub ip: String, + pub port: String, } //---------------------------------------------------------------------------------------------------- [State] Struct -#[derive(Clone,Debug,Deserialize,Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct State { - pub status: Status, - pub gupax: Gupax, - pub p2pool: P2pool, - pub xmrig: Xmrig, - pub version: Arc>, + pub status: Status, + pub gupax: Gupax, + pub p2pool: P2pool, + pub xmrig: Xmrig, + pub version: Arc>, } -#[derive(Clone,PartialEq,Debug,Deserialize,Serialize)] +#[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, + 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)] +#[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, + 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)] +#[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, + 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)] +#[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, + 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,Debug,Deserialize,Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Version { - pub gupax: String, - pub p2pool: String, - pub xmrig: String, + 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(), - } - } + 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, - } - } + 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(), - } - } + 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 - } - } + 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, - } - } + 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(), - } - } + 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_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#" + #[test] + fn serde_custom_state() { + let state = r#" [gupax] simple = true auto_update = true @@ -1276,13 +1443,13 @@ mod test { p2pool = "v2.5" xmrig = "v6.18.0" "#; - let state = crate::State::from_str(state).unwrap(); - crate::State::to_string(&state).unwrap(); - } + let state = crate::State::from_str(state).unwrap(); + crate::State::to_string(&state).unwrap(); + } - #[test] - fn serde_custom_node() { - let node = r#" + #[test] + fn serde_custom_node() { + let node = r#" ['Local Monero Node'] ip = "localhost" rpc = "18081" @@ -1298,13 +1465,13 @@ mod test { rpc = "1" zmq = "65535" "#; - let node = crate::Node::from_str_to_vec(node).unwrap(); - crate::Node::to_string(&node).unwrap(); - } + let node = crate::Node::from_str_to_vec(node).unwrap(); + crate::Node::to_string(&node).unwrap(); + } - #[test] - fn serde_custom_pool() { - let pool = r#" + #[test] + fn serde_custom_pool() { + let pool = r#" ['Local P2Pool'] rig = "Gupax_v1.0.0" ip = "localhost" @@ -1320,15 +1487,15 @@ mod test { ip = "127.0.0.1" port = "65535" "#; - let pool = crate::Pool::from_str_to_vec(pool).unwrap(); - crate::Pool::to_string(&pool).unwrap(); - } + 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#" + // 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 @@ -1397,74 +1564,76 @@ mod test { 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")); - } + 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::PayoutOrd; - use crate::xmr::AtomicUnit; + #[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); + // 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); + // 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); + // 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")); - } + // 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); - } + #[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/ferris.rs b/src/ferris.rs index e5b1a18..f4bb2a5 100644 --- a/src/ferris.rs +++ b/src/ferris.rs @@ -24,65 +24,9 @@ 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"); - - - - - - - - - - - - - - // 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. // The ANSI codes were generated with [https://docs.rs/ansipix], but there is no reason to include the library and // do the computation at runtime since [ansipix] outputs a [String], so why not just put the output here as a const :D // It's hidden below with whitespace because it makes my text editor crash... pub const FERRIS_ANSI: &str = " \n \u{1b}[38;2;244;149;0m▄\u{1b}[0m\u{1b}[38;2;245;150;0m▄\u{1b}[0m \u{1b}[38;2;244;148;0m▄\u{1b}[0m\u{1b}[38;2;244;148;0m▄\u{1b}[0m \u{1b}[38;2;244;146;0m▄\u{1b}[0m \n \u{1b}[38;2;244;146;0m▄\u{1b}[0m\u{1b}[38;2;244;146;0m▄\u{1b}[0m \u{1b}[38;2;244;145;0m▄\u{1b}[0m\u{1b}[38;2;244;144;0;48;2;244;147;0m▄\u{1b}[0m\u{1b}[38;2;244;144;0;48;2;244;147;0m▄\u{1b}[0m\u{1b}[38;2;244;144;0m▄\u{1b}[0m \u{1b}[38;2;244;143;0;48;2;244;146;0m▄\u{1b}[0m\u{1b}[38;2;244;143;0;48;2;244;145;0m▄\u{1b}[0m\u{1b}[38;2;244;143;0;48;2;244;145;0m▄\u{1b}[0m\u{1b}[38;2;244;143;0;48;2;244;145;0m▄\u{1b}[0m \u{1b}[38;2;244;142;0m▄\u{1b}[0m\u{1b}[38;2;244;142;0;48;2;244;144;0m▄\u{1b}[0m\u{1b}[38;2;244;142;0;48;2;244;144;0m▄\u{1b}[0m\u{1b}[38;2;244;141;0;48;2;244;144;0m▄\u{1b}[0m \u{1b}[38;2;244;140;0m▄\u{1b}[0m \n \u{1b}[38;2;244;141;0;48;2;244;143;0m▄\u{1b}[0m\u{1b}[38;2;244;141;0;48;2;244;143;0m▄\u{1b}[0m\u{1b}[38;2;244;141;0;48;2;244;143;0m▄\u{1b}[0m\u{1b}[38;2;244;140;0m▄\u{1b}[0m\u{1b}[38;2;244;140;0m▄\u{1b}[0m\u{1b}[38;2;244;140;0;48;2;244;142;0m▄\u{1b}[0m\u{1b}[38;2;244;140;0;48;2;244;142;0m▄\u{1b}[0m\u{1b}[38;2;244;140;0;48;2;244;142;0m▄\u{1b}[0m\u{1b}[38;2;244;139;0;48;2;244;142;0m▄\u{1b}[0m\u{1b}[38;2;244;139;0;48;2;244;141;0m▄\u{1b}[0m\u{1b}[38;2;244;139;0;48;2;244;141;0m▄\u{1b}[0m\u{1b}[38;2;244;139;0;48;2;244;141;0m▄\u{1b}[0m\u{1b}[38;2;244;139;0;48;2;244;141;0m▄\u{1b}[0m\u{1b}[38;2;244;138;0;48;2;244;141;0m▄\u{1b}[0m\u{1b}[38;2;244;138;0;48;2;244;140;0m▄\u{1b}[0m\u{1b}[38;2;244;138;0;48;2;244;140;0m▄\u{1b}[0m\u{1b}[38;2;244;138;0;48;2;244;140;0m▄\u{1b}[0m\u{1b}[38;2;245;137;0;48;2;244;140;0m▄\u{1b}[0m\u{1b}[38;2;245;137;0;48;2;244;140;0m▄\u{1b}[0m\u{1b}[38;2;245;137;0;48;2;244;139;0m▄\u{1b}[0m\u{1b}[38;2;245;137;0;48;2;244;139;0m▄\u{1b}[0m\u{1b}[38;2;245;137;0m▄\u{1b}[0m\u{1b}[38;2;245;136;0m▄\u{1b}[0m\u{1b}[38;2;245;136;0;48;2;244;138;0m▄\u{1b}[0m\u{1b}[38;2;245;136;0;48;2;244;138;0m▄\u{1b}[0m\u{1b}[38;2;245;136;0;48;2;244;138;0m▄\u{1b}[0m\u{1b}[38;2;245;136;0;48;2;244;138;0m▄\u{1b}[0m \n \u{1b}[38;2;244;138;0;48;2;243;140;0m▄\u{1b}[0m\u{1b}[38;2;245;138;0;48;2;244;140;0m▄\u{1b}[0m\u{1b}[38;2;245;137;0m▄\u{1b}[0m\u{1b}[38;2;245;137;0m▄\u{1b}[0m \u{1b}[38;2;245;137;0;48;2;244;139;0m▄\u{1b}[0m\u{1b}[38;2;245;136;0;48;2;244;139;0m▄\u{1b}[0m\u{1b}[38;2;245;136;0;48;2;244;139;0m▄\u{1b}[0m\u{1b}[38;2;245;136;0;48;2;244;138;0m▄\u{1b}[0m\u{1b}[38;2;245;136;0;48;2;244;138;0m▄\u{1b}[0m\u{1b}[38;2;245;136;0;48;2;244;138;0m▄\u{1b}[0m\u{1b}[38;2;245;135;0;48;2;244;138;0m▄\u{1b}[0m\u{1b}[38;2;245;135;0;48;2;245;137;0m▄\u{1b}[0m\u{1b}[38;2;245;135;0;48;2;245;137;0m▄\u{1b}[0m\u{1b}[38;2;245;135;0;48;2;245;137;0m▄\u{1b}[0m\u{1b}[38;2;245;134;0;48;2;245;137;0m▄\u{1b}[0m\u{1b}[38;2;245;134;0;48;2;245;137;0m▄\u{1b}[0m\u{1b}[38;2;245;134;0;48;2;245;136;0m▄\u{1b}[0m\u{1b}[38;2;245;134;0;48;2;245;136;0m▄\u{1b}[0m\u{1b}[38;2;245;134;0;48;2;245;136;0m▄\u{1b}[0m\u{1b}[38;2;245;133;0;48;2;245;136;0m▄\u{1b}[0m\u{1b}[38;2;245;133;0;48;2;245;136;0m▄\u{1b}[0m\u{1b}[38;2;245;133;0;48;2;245;135;0m▄\u{1b}[0m\u{1b}[38;2;245;133;0;48;2;245;135;0m▄\u{1b}[0m\u{1b}[38;2;245;133;0;48;2;245;135;0m▄\u{1b}[0m\u{1b}[38;2;245;132;0;48;2;245;135;0m▄\u{1b}[0m\u{1b}[38;2;245;132;0;48;2;245;134;0m▄\u{1b}[0m\u{1b}[38;2;245;132;0;48;2;245;134;0m▄\u{1b}[0m\u{1b}[38;2;245;132;0;48;2;245;134;0m▄\u{1b}[0m\u{1b}[38;2;245;131;0;48;2;245;134;0m▄\u{1b}[0m\u{1b}[38;2;245;131;0;48;2;245;134;0m▄\u{1b}[0m\u{1b}[38;2;245;131;0;48;2;245;133;0m▄\u{1b}[0m\u{1b}[38;2;245;131;0;48;2;245;133;0m▄\u{1b}[0m \u{1b}[38;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;130;0;48;2;245;132;0m▄\u{1b}[0m\u{1b}[38;2;245;130;0;48;2;245;132;0m▄\u{1b}[0m \n \u{1b}[38;2;245;133;0;48;2;245;135;0m▄\u{1b}[0m\u{1b}[38;2;245;133;0;48;2;245;135;0m▄\u{1b}[0m\u{1b}[38;2;245;133;0;48;2;245;135;0m▄\u{1b}[0m\u{1b}[38;2;245;132;0;48;2;245;135;0m▄\u{1b}[0m\u{1b}[38;2;245;132;0;48;2;245;135;0m▄\u{1b}[0m\u{1b}[38;2;245;132;0;48;2;245;134;0m▄\u{1b}[0m\u{1b}[38;2;245;132;0;48;2;245;134;0m▄\u{1b}[0m\u{1b}[38;2;245;132;0;48;2;245;134;0m▄\u{1b}[0m\u{1b}[38;2;245;131;0;48;2;245;134;0m▄\u{1b}[0m\u{1b}[38;2;245;131;0;48;2;245;133;0m▄\u{1b}[0m\u{1b}[38;2;245;131;0;48;2;245;133;0m▄\u{1b}[0m\u{1b}[38;2;245;131;0;48;2;245;133;0m▄\u{1b}[0m\u{1b}[38;2;245;130;0;48;2;245;133;0m▄\u{1b}[0m\u{1b}[38;2;245;130;0;48;2;245;133;0m▄\u{1b}[0m\u{1b}[38;2;245;130;0;48;2;245;132;0m▄\u{1b}[0m\u{1b}[38;2;245;130;0;48;2;245;132;0m▄\u{1b}[0m\u{1b}[38;2;245;130;0;48;2;245;132;0m▄\u{1b}[0m\u{1b}[38;2;245;129;0;48;2;245;132;0m▄\u{1b}[0m\u{1b}[38;2;245;129;0;48;2;245;132;0m▄\u{1b}[0m\u{1b}[38;2;245;129;0;48;2;245;131;0m▄\u{1b}[0m\u{1b}[38;2;245;129;0;48;2;245;131;0m▄\u{1b}[0m\u{1b}[38;2;245;129;0;48;2;245;131;0m▄\u{1b}[0m\u{1b}[38;2;245;128;0;48;2;245;131;0m▄\u{1b}[0m\u{1b}[38;2;245;128;0;48;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;128;0;48;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;128;0;48;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;127;0;48;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;127;0;48;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;127;0;48;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;127;0;48;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;127;0;48;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;126;0;48;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;126;0;48;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;126;0;48;2;245;128;0m▄\u{1b}[0m\u{1b}[38;2;245;126;0;48;2;245;128;0m▄\u{1b}[0m\u{1b}[38;2;245;126;0;48;2;245;128;0m▄\u{1b}[0m\u{1b}[38;2;245;125;0;48;2;245;128;0m▄\u{1b}[0m\u{1b}[38;2;245;125;0;48;2;245;127;0m▄\u{1b}[0m \n \u{1b}[38;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;129;0;48;2;245;132;0m▄\u{1b}[0m\u{1b}[38;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;128;0;48;2;245;131;0m▄\u{1b}[0m\u{1b}[38;2;245;128;0;48;2;245;131;0m▄\u{1b}[0m\u{1b}[38;2;245;128;0;48;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;128;0;48;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;128;0;48;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;127;0;48;2;245;130;0m▄\u{1b}[0m\u{1b}[38;2;245;127;0;48;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;127;0;48;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;127;0;48;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;126;0;48;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;126;0;48;2;245;129;0m▄\u{1b}[0m\u{1b}[38;2;245;126;0;48;2;245;128;0m▄\u{1b}[0m\u{1b}[38;2;245;126;0;48;2;245;128;0m▄\u{1b}[0m\u{1b}[38;2;245;126;0;48;2;245;128;0m▄\u{1b}[0m\u{1b}[38;2;245;125;0;48;2;245;128;0m▄\u{1b}[0m\u{1b}[38;2;245;125;0;48;2;245;127;0m▄\u{1b}[0m\u{1b}[38;2;245;125;0;48;2;245;127;0m▄\u{1b}[0m\u{1b}[38;2;245;125;0;48;2;245;127;0m▄\u{1b}[0m\u{1b}[38;2;245;125;0;48;2;245;127;0m▄\u{1b}[0m\u{1b}[38;2;245;124;0;48;2;245;127;0m▄\u{1b}[0m\u{1b}[38;2;245;124;0;48;2;245;126;0m▄\u{1b}[0m\u{1b}[38;2;245;124;0;48;2;245;126;0m▄\u{1b}[0m\u{1b}[38;2;245;124;0;48;2;245;126;0m▄\u{1b}[0m\u{1b}[38;2;245;123;0;48;2;245;126;0m▄\u{1b}[0m\u{1b}[38;2;245;123;0;48;2;245;126;0m▄\u{1b}[0m\u{1b}[38;2;245;123;0;48;2;245;125;0m▄\u{1b}[0m\u{1b}[38;2;245;123;0;48;2;245;125;0m▄\u{1b}[0m\u{1b}[38;2;245;123;0;48;2;245;125;0m▄\u{1b}[0m\u{1b}[38;2;245;122;0;48;2;245;125;0m▄\u{1b}[0m\u{1b}[38;2;245;122;0;48;2;245;124;0m▄\u{1b}[0m\u{1b}[38;2;245;122;0;48;2;245;124;0m▄\u{1b}[0m\u{1b}[38;2;245;122;0;48;2;245;124;0m▄\u{1b}[0m\u{1b}[38;2;245;121;0;48;2;245;124;0m▄\u{1b}[0m\u{1b}[38;2;245;121;0;48;2;245;124;0m▄\u{1b}[0m\u{1b}[38;2;245;121;0;48;2;245;123;0m▄\u{1b}[0m\u{1b}[38;2;245;121;0;48;2;245;123;0m▄\u{1b}[0m\u{1b}[38;2;245;121;0;48;2;245;123;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0;48;2;245;123;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0;48;2;245;123;0m▄\u{1b}[0m\u{1b}[38;2;245;119;0m▄\u{1b}[0m \n \u{1b}[38;2;246;126;0m▀\u{1b}[0m\u{1b}[38;2;245;125;0;48;2;245;127;0m▄\u{1b}[0m\u{1b}[38;2;245;124;0;48;2;245;127;0m▄\u{1b}[0m\u{1b}[38;2;245;124;0;48;2;245;127;0m▄\u{1b}[0m\u{1b}[38;2;245;124;0;48;2;245;126;0m▄\u{1b}[0m\u{1b}[38;2;245;124;0;48;2;245;126;0m▄\u{1b}[0m\u{1b}[38;2;245;124;0;48;2;245;126;0m▄\u{1b}[0m\u{1b}[38;2;245;123;0;48;2;245;126;0m▄\u{1b}[0m\u{1b}[38;2;245;123;0;48;2;245;125;0m▄\u{1b}[0m\u{1b}[38;2;245;123;0;48;2;245;125;0m▄\u{1b}[0m\u{1b}[38;2;245;123;0;48;2;245;125;0m▄\u{1b}[0m\u{1b}[38;2;245;122;0;48;2;245;125;0m▄\u{1b}[0m\u{1b}[38;2;245;122;0;48;2;245;125;0m▄\u{1b}[0m\u{1b}[38;2;245;122;0;48;2;245;124;0m▄\u{1b}[0m\u{1b}[38;2;245;122;0;48;2;245;124;0m▄\u{1b}[0m\u{1b}[38;2;245;122;0;48;2;245;124;0m▄\u{1b}[0m\u{1b}[38;2;245;121;0;48;2;245;124;0m▄\u{1b}[0m\u{1b}[38;2;245;121;0;48;2;245;123;0m▄\u{1b}[0m\u{1b}[38;2;245;121;0;48;2;245;123;0m▄\u{1b}[0m\u{1b}[38;2;245;121;0;48;2;245;123;0m▄\u{1b}[0m\u{1b}[38;2;245;121;0;48;2;245;123;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0;48;2;245;123;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0;48;2;245;122;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0;48;2;245;122;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0;48;2;245;122;0m▄\u{1b}[0m\u{1b}[38;2;245;119;0;48;2;245;122;0m▄\u{1b}[0m\u{1b}[38;2;245;119;0;48;2;245;122;0m▄\u{1b}[0m\u{1b}[38;2;245;119;0;48;2;245;121;0m▄\u{1b}[0m\u{1b}[38;2;245;119;0;48;2;245;121;0m▄\u{1b}[0m\u{1b}[38;2;245;119;0;48;2;245;121;0m▄\u{1b}[0m\u{1b}[38;2;245;118;0;48;2;245;121;0m▄\u{1b}[0m\u{1b}[38;2;245;118;0;48;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;118;0;48;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;118;0;48;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;118;0;48;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;117;0;48;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;117;0;48;2;245;119;0m▄\u{1b}[0m\u{1b}[38;2;245;117;0;48;2;245;119;0m▄\u{1b}[0m\u{1b}[38;2;245;117;0;48;2;245;119;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;119;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;119;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;117;0m▄\u{1b}[0m \n \u{1b}[38;2;245;120;0;48;2;245;122;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0;48;2;245;122;0m▄\u{1b}[0m\u{1b}[38;2;245;120;0;48;2;245;122;0m▄\u{1b}[0m\u{1b}[38;2;245;119;0;48;2;245;122;0m▄\u{1b}[0m\u{1b}[38;2;245;119;0;48;2;245;121;0m▄\u{1b}[0m\u{1b}[38;2;245;119;0;48;2;245;121;0m▄\u{1b}[0m\u{1b}[38;2;245;119;0;48;2;245;121;0m▄\u{1b}[0m\u{1b}[38;2;245;118;0;48;2;245;121;0m▄\u{1b}[0m\u{1b}[38;2;245;118;0;48;2;245;121;0m▄\u{1b}[0m\u{1b}[38;2;245;118;0;48;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;118;0;48;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;118;0;48;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;117;0;48;2;245;120;0m▄\u{1b}[0m\u{1b}[38;2;245;117;0;48;2;245;119;0m▄\u{1b}[0m\u{1b}[38;2;245;117;0;48;2;245;119;0m▄\u{1b}[0m\u{1b}[38;2;245;117;0;48;2;245;119;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;119;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;119;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;114;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;114;0;48;2;245;116;0m▄\u{1b}[0m\u{1b}[38;2;245;114;0;48;2;245;116;0m▄\u{1b}[0m\u{1b}[38;2;245;114;0;48;2;245;116;0m▄\u{1b}[0m\u{1b}[38;2;245;113;0;48;2;245;116;0m▄\u{1b}[0m\u{1b}[38;2;245;113;0;48;2;245;116;0m▄\u{1b}[0m\u{1b}[38;2;245;113;0;48;2;245;115;0m▄\u{1b}[0m\u{1b}[38;2;246;113;0;48;2;245;115;0m▄\u{1b}[0m\u{1b}[38;2;246;113;0;48;2;245;115;0m▄\u{1b}[0m\u{1b}[38;2;246;112;0;48;2;245;115;0m▄\u{1b}[0m\u{1b}[38;2;246;112;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;112;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;112;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;112;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;245;113;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;245;113;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;246;113;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;113;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;113;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0m▄\u{1b}[0m \n \u{1b}[38;2;245;119;0m▀\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;116;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;118;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;115;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;114;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;114;0;48;2;245;117;0m▄\u{1b}[0m\u{1b}[38;2;245;114;0;48;2;245;116;0m▄\u{1b}[0m\u{1b}[38;2;245;114;0;48;2;245;116;0m▄\u{1b}[0m\u{1b}[38;2;245;114;0;48;2;245;116;0m▄\u{1b}[0m\u{1b}[38;2;245;113;0;48;2;245;116;0m▄\u{1b}[0m\u{1b}[38;2;245;113;0;48;2;245;115;0m▄\u{1b}[0m\u{1b}[38;2;246;113;0;48;2;245;115;0m▄\u{1b}[0m\u{1b}[38;2;246;113;0;48;2;245;115;0m▄\u{1b}[0m\u{1b}[38;2;246;112;0;48;2;245;115;0m▄\u{1b}[0m\u{1b}[38;2;246;112;0;48;2;245;115;0m▄\u{1b}[0m\u{1b}[38;2;246;112;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;112;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;112;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;245;113;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;245;113;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;246;113;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;113;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;112;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;112;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;112;0m▄\u{1b}[0m\u{1b}[38;2;246;109;0;48;2;246;112;0m▄\u{1b}[0m\u{1b}[38;2;246;109;0;48;2;246;112;0m▄\u{1b}[0m\u{1b}[38;2;246;109;0;48;2;246;111;0m▄\u{1b}[0m\u{1b}[38;2;246;109;0;48;2;246;111;0m▄\u{1b}[0m\u{1b}[38;2;246;109;0;48;2;246;111;0m▄\u{1b}[0m\u{1b}[38;2;246;108;0;48;2;246;111;0m▄\u{1b}[0m\u{1b}[38;2;246;108;0;48;2;246;110;0m▄\u{1b}[0m\u{1b}[38;2;246;108;0;48;2;246;110;0m▄\u{1b}[0m\u{1b}[38;2;246;108;0;48;2;246;110;0m▄\u{1b}[0m\u{1b}[38;2;246;108;0;48;2;246;110;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;110;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;107;0m▄\u{1b}[0m \n \u{1b}[38;2;246;112;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;245;114;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;245;113;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;245;113;0m▄\u{1b}[0m\u{1b}[38;2;246;111;0;48;2;246;113;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;113;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;112;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;112;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;112;0m▄\u{1b}[0m\u{1b}[38;2;246;110;0;48;2;246;112;0m▄\u{1b}[0m\u{1b}[38;2;246;109;0;48;2;246;112;0m▄\u{1b}[0m\u{1b}[38;2;246;109;0;48;2;246;111;0m▄\u{1b}[0m\u{1b}[38;2;246;109;0;48;2;246;111;0m▄\u{1b}[0m\u{1b}[38;2;246;109;0;48;2;246;111;0m▄\u{1b}[0m\u{1b}[38;2;246;108;0;48;2;246;111;0m▄\u{1b}[0m\u{1b}[38;2;246;108;0;48;2;246;111;0m▄\u{1b}[0m\u{1b}[38;2;246;108;0;48;2;246;110;0m▄\u{1b}[0m\u{1b}[38;2;246;108;0;48;2;246;110;0m▄\u{1b}[0m\u{1b}[38;2;246;108;0;48;2;246;110;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;110;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;104;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;104;0;48;2;246;106;0m▄\u{1b}[0m\u{1b}[38;2;246;104;0;48;2;246;106;0m▄\u{1b}[0m\u{1b}[38;2;246;104;0;48;2;246;106;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;106;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;106;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;102;0m▄\u{1b}[0m \n \u{1b}[38;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;107;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;109;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;106;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;108;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;105;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;104;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;104;0;48;2;246;107;0m▄\u{1b}[0m\u{1b}[38;2;246;104;0;48;2;246;106;0m▄\u{1b}[0m\u{1b}[38;2;246;104;0;48;2;246;106;0m▄\u{1b}[0m\u{1b}[38;2;246;104;0;48;2;246;106;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;106;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;102;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;102;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;102;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;102;0m▄\u{1b}[0m\u{1b}[38;2;246;99;0;48;2;246;102;0m▄\u{1b}[0m\u{1b}[38;2;246;99;0;48;2;246;101;0m▄\u{1b}[0m\u{1b}[38;2;246;99;0;48;2;246;101;0m▄\u{1b}[0m\u{1b}[38;2;246;99;0;48;2;246;101;0m▄\u{1b}[0m\u{1b}[38;2;246;98;0;48;2;246;101;0m▄\u{1b}[0m\u{1b}[38;2;246;98;0;48;2;246;101;0m▄\u{1b}[0m\u{1b}[38;2;246;98;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;246;98;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;246;98;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;246;97;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;246;97;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;97;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;97;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;95;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;94;0m▄\u{1b}[0m \n \u{1b}[38;2;246;105;0m▀\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;103;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;105;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;102;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;104;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;101;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;103;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;102;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;102;0m▄\u{1b}[0m\u{1b}[38;2;246;100;0;48;2;246;102;0m▄\u{1b}[0m\u{1b}[38;2;246;99;0;48;2;246;102;0m▄\u{1b}[0m\u{1b}[38;2;246;99;0;48;2;246;101;0m▄\u{1b}[0m\u{1b}[38;2;246;99;0;48;2;246;101;0m▄\u{1b}[0m\u{1b}[38;2;246;99;0;48;2;246;101;0m▄\u{1b}[0m\u{1b}[38;2;246;98;0;48;2;246;101;0m▄\u{1b}[0m\u{1b}[38;2;246;98;0;48;2;246;101;0m▄\u{1b}[0m\u{1b}[38;2;246;98;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;255;255;255;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;255;255;255;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;97;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;97;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;95;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;95;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;255;255;255;48;2;246;97;0m▄\u{1b}[0m\u{1b}[38;2;255;255;255;48;2;246;97;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;246;97;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;246;97;0m▄\u{1b}[0m\u{1b}[38;2;246;94;0;48;2;246;96;0m▄\u{1b}[0m\u{1b}[38;2;246;94;0;48;2;246;96;0m▄\u{1b}[0m\u{1b}[38;2;246;94;0;48;2;246;96;0m▄\u{1b}[0m\u{1b}[38;2;246;94;0;48;2;246;96;0m▄\u{1b}[0m\u{1b}[38;2;246;93;0;48;2;246;96;0m▄\u{1b}[0m\u{1b}[38;2;246;93;0;48;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;93;0;48;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;93;0;48;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;90;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;90;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;90;0;48;2;246;92;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0m▀\u{1b}[0m \n \u{1b}[38;2;246;98;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;246;98;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;246;98;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;246;97;0;48;2;246;100;0m▄\u{1b}[0m\u{1b}[38;2;246;97;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;97;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;97;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;99;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;96;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;95;0;48;2;246;98;0m▄\u{1b}[0m\u{1b}[38;2;246;95;0;48;2;246;97;0m▄\u{1b}[0m\u{1b}[38;2;246;95;0;48;2;246;97;0m▄\u{1b}[0m\u{1b}[38;2;246;95;0;48;2;246;97;0m▄\u{1b}[0m\u{1b}[38;2;246;94;0;48;2;246;97;0m▄\u{1b}[0m\u{1b}[38;2;246;94;0;48;2;246;97;0m▄\u{1b}[0m\u{1b}[38;2;246;94;0;48;2;246;96;0m▄\u{1b}[0m\u{1b}[38;2;246;94;0;48;2;246;96;0m▄\u{1b}[0m\u{1b}[38;2;246;94;0;48;2;246;96;0m▄\u{1b}[0m\u{1b}[38;2;255;255;255;48;2;248;248;248m▄\u{1b}[0m\u{1b}[38;2;255;255;255;48;2;255;255;255m▄\u{1b}[0m\u{1b}[38;2;255;255;255;48;2;255;255;255m▄\u{1b}[0m\u{1b}[38;2;145;145;145;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;255;255;255;48;2;203;203;203m▄\u{1b}[0m\u{1b}[38;2;255;255;255;48;2;255;255;255m▄\u{1b}[0m\u{1b}[38;2;255;255;255;48;2;255;255;255m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;246;89;0;48;2;246;92;0m▄\u{1b}[0m\u{1b}[38;2;246;89;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;246;89;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;246;89;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;246;88;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;247;88;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;247;88;0;48;2;246;90;0m▄\u{1b}[0m\u{1b}[38;2;247;88;0;48;2;246;90;0m▄\u{1b}[0m\u{1b}[38;2;247;88;0;48;2;246;90;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;90;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;86;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;86;0;48;2;246;88;0m▄\u{1b}[0m\u{1b}[38;2;247;86;0;48;2;247;88;0m▄\u{1b}[0m\u{1b}[38;2;247;86;0;48;2;247;88;0m▄\u{1b}[0m\u{1b}[38;2;247;88;0m▀\u{1b}[0m \n \u{1b}[38;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;93;0;48;2;246;96;0m▄\u{1b}[0m\u{1b}[38;2;246;93;0;48;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;93;0;48;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;93;0;48;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;95;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;92;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;94;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;91;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;90;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;90;0;48;2;246;93;0m▄\u{1b}[0m\u{1b}[38;2;246;90;0;48;2;246;92;0m▄\u{1b}[0m\u{1b}[38;2;246;90;0;48;2;246;92;0m▄\u{1b}[0m\u{1b}[38;2;246;90;0;48;2;246;92;0m▄\u{1b}[0m\u{1b}[38;2;246;89;0;48;2;246;92;0m▄\u{1b}[0m\u{1b}[38;2;246;89;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;246;89;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;255;255;255m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;255;255;255m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;90;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;86;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;255;255;255m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;255;255;255m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;84;0;48;2;247;87;0m▄\u{1b}[0m\u{1b}[38;2;247;84;0;48;2;247;87;0m▄\u{1b}[0m\u{1b}[38;2;247;84;0;48;2;247;86;0m▄\u{1b}[0m\u{1b}[38;2;247;84;0;48;2;247;86;0m▄\u{1b}[0m\u{1b}[38;2;247;84;0;48;2;247;86;0m▄\u{1b}[0m\u{1b}[38;2;247;83;0;48;2;247;86;0m▄\u{1b}[0m\u{1b}[38;2;247;83;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;83;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;83;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;81;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;81;0;48;2;247;83;0m▄\u{1b}[0m\u{1b}[38;2;247;81;0;48;2;247;83;0m▄\u{1b}[0m\u{1b}[38;2;247;81;0m▄\u{1b}[0m\u{1b}[38;2;247;81;0m▄\u{1b}[0m \n \u{1b}[38;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;246;89;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;246;89;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;246;89;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;246;88;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;247;88;0;48;2;246;91;0m▄\u{1b}[0m\u{1b}[38;2;247;88;0;48;2;246;90;0m▄\u{1b}[0m\u{1b}[38;2;247;88;0;48;2;246;90;0m▄\u{1b}[0m\u{1b}[38;2;247;88;0;48;2;246;90;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;90;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;87;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;86;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;86;0;48;2;246;89;0m▄\u{1b}[0m\u{1b}[38;2;247;86;0;48;2;246;88;0m▄\u{1b}[0m\u{1b}[38;2;247;86;0;48;2;247;88;0m▄\u{1b}[0m\u{1b}[38;2;247;86;0;48;2;247;88;0m▄\u{1b}[0m\u{1b}[38;2;247;85;0;48;2;247;88;0m▄\u{1b}[0m\u{1b}[38;2;247;85;0;48;2;247;87;0m▄\u{1b}[0m\u{1b}[38;2;247;85;0;48;2;247;87;0m▄\u{1b}[0m\u{1b}[38;2;247;85;0;48;2;247;87;0m▄\u{1b}[0m\u{1b}[38;2;247;85;0;48;2;247;87;0m▄\u{1b}[0m\u{1b}[38;2;247;84;0;48;2;247;87;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;83;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;82;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;82;0m▄\u{1b}[0m\u{1b}[38;2;247;79;0;48;2;247;82;0m▄\u{1b}[0m\u{1b}[38;2;247;79;0;48;2;247;81;0m▄\u{1b}[0m\u{1b}[38;2;247;79;0;48;2;247;81;0m▄\u{1b}[0m\u{1b}[38;2;247;79;0;48;2;247;81;0m▄\u{1b}[0m\u{1b}[38;2;247;78;0;48;2;247;81;0m▄\u{1b}[0m\u{1b}[38;2;247;78;0;48;2;247;81;0m▄\u{1b}[0m\u{1b}[38;2;247;78;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;78;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;78;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;77;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;77;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0m▄\u{1b}[0m \n \u{1b}[38;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;85;0;48;2;247;87;0m▄\u{1b}[0m\u{1b}[38;2;247;85;0;48;2;247;87;0m▄\u{1b}[0m\u{1b}[38;2;247;84;0;48;2;247;87;0m▄\u{1b}[0m\u{1b}[38;2;247;84;0;48;2;247;86;0m▄\u{1b}[0m\u{1b}[38;2;247;84;0;48;2;247;86;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;86;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;86;0m▄\u{1b}[0m\u{1b}[38;2;247;83;0;48;2;247;86;0m▄\u{1b}[0m\u{1b}[38;2;247;83;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;83;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;83;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;85;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;82;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;81;0;48;2;247;84;0m▄\u{1b}[0m\u{1b}[38;2;247;81;0;48;2;247;83;0m▄\u{1b}[0m\u{1b}[38;2;247;81;0;48;2;247;83;0m▄\u{1b}[0m\u{1b}[38;2;247;81;0;48;2;247;83;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;83;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;83;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;82;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;82;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;82;0m▄\u{1b}[0m\u{1b}[38;2;247;79;0;48;2;247;82;0m▄\u{1b}[0m\u{1b}[38;2;247;79;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;79;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;79;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;79;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;78;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;78;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;78;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;78;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;77;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;77;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;77;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;77;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;77;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m \n \u{1b}[38;2;247;80;0;48;2;247;83;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;82;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;82;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;82;0m▄\u{1b}[0m\u{1b}[38;2;247;80;0;48;2;247;82;0m▄\u{1b}[0m \u{1b}[38;2;165;43;0m▀\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0m▀\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;80;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;77;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;77;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;79;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;232;71;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0m▀\u{1b}[0m\u{1b}[38;2;165;43;0m▀\u{1b}[0m \u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m \u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0m▀\u{1b}[0m \n \u{1b}[38;2;247;78;0m▀\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;78;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;77;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;77;0m▄\u{1b}[0m \u{1b}[38;2;165;43;0m▀\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0m▄\u{1b}[0m \u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;246;76;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;231;69;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;238;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;242;74;0m▀\u{1b}[0m \u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0m▀\u{1b}[0m \u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0m▀\u{1b}[0m \n \u{1b}[38;2;247;76;0m▀\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m \u{1b}[38;2;165;43;0m▀\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0m▄\u{1b}[0m \u{1b}[38;2;242;74;0m▀\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0m▀\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;242;74;0m▄\u{1b}[0m\u{1b}[38;2;237;72;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;0;0;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;20;5;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;0;0;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;229;68;0;48;2;233;70;0m▄\u{1b}[0m\u{1b}[38;2;233;70;0m▀\u{1b}[0m \u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m\u{1b}[38;2;165;43;0m▀\u{1b}[0m \u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m \n \u{1b}[38;2;247;76;0m▀\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0m▄\u{1b}[0m \u{1b}[38;2;165;43;0m▀\u{1b}[0m\u{1b}[38;2;165;43;0m▄\u{1b}[0m \u{1b}[38;2;232;70;0m▀\u{1b}[0m\u{1b}[38;2;227;68;0;48;2;232;70;0m▄\u{1b}[0m\u{1b}[38;2;227;68;0;48;2;232;70;0m▄\u{1b}[0m\u{1b}[38;2;227;68;0;48;2;232;70;0m▄\u{1b}[0m\u{1b}[38;2;227;68;0;48;2;232;70;0m▄\u{1b}[0m\u{1b}[38;2;227;68;0;48;2;232;70;0m▄\u{1b}[0m\u{1b}[38;2;227;68;0;48;2;232;70;0m▄\u{1b}[0m\u{1b}[38;2;227;68;0;48;2;232;70;0m▄\u{1b}[0m\u{1b}[38;2;227;68;0;48;2;232;70;0m▄\u{1b}[0m\u{1b}[38;2;232;70;0m▀\u{1b}[0m\u{1b}[38;2;232;70;0m▀\u{1b}[0m\u{1b}[38;2;232;70;0m▀\u{1b}[0m\u{1b}[38;2;232;70;0m▀\u{1b}[0m\u{1b}[38;2;232;70;0m▀\u{1b}[0m\u{1b}[38;2;232;70;0m▀\u{1b}[0m\u{1b}[38;2;232;70;0m▀\u{1b}[0m\u{1b}[38;2;232;70;0m▀\u{1b}[0m\u{1b}[38;2;232;70;0m▀\u{1b}[0m \u{1b}[38;2;225;67;0m▀\u{1b}[0m \u{1b}[38;2;225;67;0m▀\u{1b}[0m\u{1b}[38;2;225;67;0m▀\u{1b}[0m\u{1b}[38;2;220;65;0;48;2;225;67;0m▄\u{1b}[0m\u{1b}[38;2;220;65;0;48;2;225;67;0m▄\u{1b}[0m\u{1b}[38;2;220;65;0;48;2;225;67;0m▄\u{1b}[0m\u{1b}[38;2;220;65;0;48;2;225;67;0m▄\u{1b}[0m\u{1b}[38;2;220;65;0;48;2;225;67;0m▄\u{1b}[0m\u{1b}[38;2;220;65;0;48;2;225;67;0m▄\u{1b}[0m\u{1b}[38;2;220;65;0;48;2;225;67;0m▄\u{1b}[0m\u{1b}[38;2;220;65;0;48;2;225;67;0m▄\u{1b}[0m\u{1b}[38;2;225;67;0m▀\u{1b}[0m \u{1b}[38;2;165;43;0;48;2;165;43;0m▄\u{1b}[0m \u{1b}[38;2;247;76;0;48;2;248;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0m▀\u{1b}[0m \n \u{1b}[38;2;247;76;0m▀\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0m▄\u{1b}[0m \u{1b}[38;2;222;66;0m▀\u{1b}[0m\u{1b}[38;2;222;66;0m▀\u{1b}[0m\u{1b}[38;2;217;63;0;48;2;222;66;0m▄\u{1b}[0m\u{1b}[38;2;217;63;0;48;2;222;66;0m▄\u{1b}[0m\u{1b}[38;2;217;63;0;48;2;222;66;0m▄\u{1b}[0m\u{1b}[38;2;217;63;0;48;2;222;66;0m▄\u{1b}[0m\u{1b}[38;2;217;63;0;48;2;222;66;0m▄\u{1b}[0m\u{1b}[38;2;217;63;0m▄\u{1b}[0m\u{1b}[38;2;217;63;0m▄\u{1b}[0m \u{1b}[38;2;212;61;0m▄\u{1b}[0m\u{1b}[38;2;212;61;0;48;2;216;63;0m▄\u{1b}[0m\u{1b}[38;2;212;61;0;48;2;216;63;0m▄\u{1b}[0m\u{1b}[38;2;212;61;0;48;2;216;63;0m▄\u{1b}[0m\u{1b}[38;2;212;61;0;48;2;216;63;0m▄\u{1b}[0m\u{1b}[38;2;212;61;0;48;2;216;63;0m▄\u{1b}[0m\u{1b}[38;2;212;61;0;48;2;216;63;0m▄\u{1b}[0m\u{1b}[38;2;216;63;0m▀\u{1b}[0m \u{1b}[38;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;76;0m▀\u{1b}[0m \n \u{1b}[38;2;247;76;0m▀\u{1b}[0m\u{1b}[38;2;247;76;0;48;2;247;76;0m▄\u{1b}[0m \u{1b}[38;2;212;61;0m▀\u{1b}[0m\u{1b}[38;2;207;59;0;48;2;212;61;0m▄\u{1b}[0m\u{1b}[38;2;207;59;0;48;2;212;61;0m▄\u{1b}[0m\u{1b}[38;2;207;59;0;48;2;212;61;0m▄\u{1b}[0m\u{1b}[38;2;207;59;0;48;2;212;61;0m▄\u{1b}[0m\u{1b}[38;2;207;59;0;48;2;212;61;0m▄\u{1b}[0m\u{1b}[38;2;207;59;0;48;2;212;61;0m▄\u{1b}[0m\u{1b}[38;2;207;59;0;48;2;212;61;0m▄\u{1b}[0m\u{1b}[38;2;207;59;0;48;2;212;61;0m▄\u{1b}[0m\u{1b}[38;2;207;59;0m▄\u{1b}[0m\u{1b}[38;2;207;59;0m▄\u{1b}[0m\u{1b}[38;2;207;60;0m▄\u{1b}[0m \u{1b}[38;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;207;59;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;207;59;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;207;59;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;207;59;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;207;59;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;207;59;0m▄\u{1b}[0m\u{1b}[38;2;207;59;0m▀\u{1b}[0m\u{1b}[38;2;207;59;0m▀\u{1b}[0m \u{1b}[38;2;247;76;0m▄\u{1b}[0m\u{1b}[38;2;247;75;0;48;2;247;76;0m▄\u{1b}[0m \n \u{1b}[38;2;247;76;0m▀\u{1b}[0m \u{1b}[38;2;204;58;0m▀\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0m▀\u{1b}[0m \u{1b}[38;2;204;58;0m▀\u{1b}[0m\u{1b}[38;2;204;58;0m▀\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0;48;2;204;58;0m▄\u{1b}[0m\u{1b}[38;2;204;58;0m▀\u{1b}[0m\u{1b}[38;2;204;58;0m▀\u{1b}[0m\u{1b}[38;2;204;58;0m▀\u{1b}[0m \u{1b}[38;2;247;76;0m▀\u{1b}[0m \n Thanks for using Gupax...!\n ( 〃 ω〃)"; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/free.rs b/src/free.rs index 2f20b2b..706fcbf 100644 --- a/src/free.rs +++ b/src/free.rs @@ -8,11 +8,11 @@ use crate::constants::*; #[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; - } + // 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) -} \ No newline at end of file + // Clamp between valid range. + scale.clamp(APP_MIN_SCALE, APP_MAX_SCALE) +} diff --git a/src/gupax.rs b/src/gupax.rs index 5009299..0029b2b 100644 --- a/src/gupax.rs +++ b/src/gupax.rs @@ -16,299 +16,521 @@ // along with this program. If not, see . use crate::State; +use crate::{constants::*, macros::*, update::*, ErrorState, Restart, Tab}; use egui::{ - TextEdit, - TextStyle, - TextStyle::Monospace, - Checkbox,ProgressBar,Spinner,Button,Label,Slider, - SelectableLabel, - RichText, - Vec2, -}; -use crate::{ - constants::*, - update::*, - ErrorState, - Restart, - Tab, - macros::*, -}; -use std::{ - thread, - sync::{Arc,Mutex}, - path::Path, + Button, Checkbox, Label, ProgressBar, RichText, SelectableLabel, Slider, Spinner, TextEdit, + TextStyle, TextStyle::Monospace, Vec2, }; use log::*; -use serde::{Serialize,Deserialize}; +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 + 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(), - }) - } + 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)] +#[derive(Debug, Clone)] pub enum FileType { - P2pool, - Xmrig, + P2pool, + Xmrig, } //---------------------------------------------------------------------------------------------------- Ratio Lock // Enum for the lock ratio in the advanced tab. -#[derive(Clone,Copy,Eq,PartialEq,Debug,Deserialize,Serialize)] +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] pub enum Ratio { - Width, - Height, - None, + Width, + Height, + None, } //---------------------------------------------------------------------------------------------------- Gupax impl crate::disk::Gupax { - #[inline(always)] // called once - pub fn show( - &mut self, - og: &Arc>, - state_path: &Path, - update: &Arc>, - file_window: &Arc>, - error_state: &mut ErrorState, - restart: &Arc>, - width: f32, - height: f32, - frame: &mut eframe::Frame, - _ctx: &egui::Context, - ui: &mut egui::Ui - ) { - // Update button + Progress bar - debug!("Gupax Tab | Rendering [Update] button + progress bar"); - ui.group(|ui| { - let button = if self.simple { height/5.0 } else { height/15.0 }; - let height = if self.simple { height/5.0 } else { height/10.0 }; - let width = width - SPACE; - let updating = *lock2!(update,updating); - ui.vertical(|ui| { - // If [Gupax] is being built for a Linux distro, - // disable built-in updating completely. - #[cfg(feature = "distro")] - ui.set_enabled(false); - #[cfg(feature = "distro")] - ui.add_sized([width, button], Button::new("Updates are disabled")).on_disabled_hover_text(DISTRO_NO_UPDATE); - #[cfg(not(feature = "distro"))] - ui.set_enabled(!updating); - #[cfg(not(feature = "distro"))] - if ui.add_sized([width, button], Button::new("Check for updates")).on_hover_text(GUPAX_UPDATE).clicked() { - Update::spawn_thread(og, self, state_path, update, error_state, restart); - } - }); - ui.vertical(|ui| { - ui.set_enabled(updating); - let prog = *lock2!(update,prog); - let msg = format!("{}\n{}{}", *lock2!(update,msg), prog, "%"); - ui.add_sized([width, height*1.4], Label::new(RichText::new(msg))); - let height = height/2.0; - if updating { - ui.add_sized([width, height], Spinner::new().size(height)); - } else { - ui.add_sized([width, height], Label::new("...")); - } - ui.add_sized([width, height], ProgressBar::new(lock2!(update,prog).round() / 100.0)); - }); - }); + #[inline(always)] // called once + pub fn show( + &mut self, + og: &Arc>, + state_path: &Path, + update: &Arc>, + file_window: &Arc>, + error_state: &mut ErrorState, + restart: &Arc>, + width: f32, + height: f32, + frame: &mut eframe::Frame, + _ctx: &egui::Context, + ui: &mut egui::Ui, + ) { + // Update button + Progress bar + debug!("Gupax Tab | Rendering [Update] button + progress bar"); + ui.group(|ui| { + let button = if self.simple { + height / 5.0 + } else { + height / 15.0 + }; + let height = if self.simple { + height / 5.0 + } else { + height / 10.0 + }; + let width = width - SPACE; + let updating = *lock2!(update, updating); + ui.vertical(|ui| { + // If [Gupax] is being built for a Linux distro, + // disable built-in updating completely. + #[cfg(feature = "distro")] + ui.set_enabled(false); + #[cfg(feature = "distro")] + ui.add_sized([width, button], Button::new("Updates are disabled")) + .on_disabled_hover_text(DISTRO_NO_UPDATE); + #[cfg(not(feature = "distro"))] + ui.set_enabled(!updating); + #[cfg(not(feature = "distro"))] + if ui + .add_sized([width, button], Button::new("Check for updates")) + .on_hover_text(GUPAX_UPDATE) + .clicked() + { + Update::spawn_thread(og, self, state_path, update, error_state, restart); + } + }); + ui.vertical(|ui| { + ui.set_enabled(updating); + let prog = *lock2!(update, prog); + let msg = format!("{}\n{}{}", *lock2!(update, msg), prog, "%"); + ui.add_sized([width, height * 1.4], Label::new(RichText::new(msg))); + let height = height / 2.0; + if updating { + ui.add_sized([width, height], Spinner::new().size(height)); + } else { + ui.add_sized([width, height], Label::new("...")); + } + ui.add_sized( + [width, height], + ProgressBar::new(lock2!(update, prog).round() / 100.0), + ); + }); + }); - debug!("Gupax Tab | Rendering bool buttons"); - ui.horizontal(|ui| { - ui.group(|ui| { - let width = (width - SPACE*12.0)/6.0; - let height = if self.simple { height/10.0 } else { height/15.0 }; - ui.style_mut().override_text_style = Some(egui::TextStyle::Small); - ui.add_sized([width, height], Checkbox::new(&mut self.update_via_tor, "Update via Tor")).on_hover_text(GUPAX_UPDATE_VIA_TOR); - ui.separator(); - ui.add_sized([width, height], Checkbox::new(&mut self.auto_update, "Auto-Update")).on_hover_text(GUPAX_AUTO_UPDATE); - ui.separator(); - ui.add_sized([width, height], Checkbox::new(&mut self.auto_p2pool, "Auto-P2Pool")).on_hover_text(GUPAX_AUTO_P2POOL); - ui.separator(); - ui.add_sized([width, height], Checkbox::new(&mut self.auto_xmrig, "Auto-XMRig")).on_hover_text(GUPAX_AUTO_XMRIG); - ui.separator(); - ui.add_sized([width, height], Checkbox::new(&mut self.ask_before_quit, "Ask before quit")).on_hover_text(GUPAX_ASK_BEFORE_QUIT); - ui.separator(); - ui.add_sized([width, height], Checkbox::new(&mut self.save_before_quit, "Save before quit")).on_hover_text(GUPAX_SAVE_BEFORE_QUIT); - }); - }); + debug!("Gupax Tab | Rendering bool buttons"); + ui.horizontal(|ui| { + ui.group(|ui| { + let width = (width - SPACE * 12.0) / 6.0; + let height = if self.simple { + height / 10.0 + } else { + height / 15.0 + }; + ui.style_mut().override_text_style = Some(egui::TextStyle::Small); + ui.add_sized( + [width, height], + Checkbox::new(&mut self.update_via_tor, "Update via Tor"), + ) + .on_hover_text(GUPAX_UPDATE_VIA_TOR); + ui.separator(); + ui.add_sized( + [width, height], + Checkbox::new(&mut self.auto_update, "Auto-Update"), + ) + .on_hover_text(GUPAX_AUTO_UPDATE); + ui.separator(); + ui.add_sized( + [width, height], + Checkbox::new(&mut self.auto_p2pool, "Auto-P2Pool"), + ) + .on_hover_text(GUPAX_AUTO_P2POOL); + ui.separator(); + ui.add_sized( + [width, height], + Checkbox::new(&mut self.auto_xmrig, "Auto-XMRig"), + ) + .on_hover_text(GUPAX_AUTO_XMRIG); + ui.separator(); + ui.add_sized( + [width, height], + Checkbox::new(&mut self.ask_before_quit, "Ask before quit"), + ) + .on_hover_text(GUPAX_ASK_BEFORE_QUIT); + ui.separator(); + ui.add_sized( + [width, height], + Checkbox::new(&mut self.save_before_quit, "Save before quit"), + ) + .on_hover_text(GUPAX_SAVE_BEFORE_QUIT); + }); + }); - if self.simple { return } + if self.simple { + return; + } - debug!("Gupax Tab | Rendering P2Pool/XMRig path selection"); - // P2Pool/XMRig binary path selection - let height = height/28.0; - let text_edit = (ui.available_width()/10.0)-SPACE; - ui.group(|ui| { - ui.add_sized([ui.available_width(), height/2.0], Label::new(RichText::new("P2Pool/XMRig PATHs").underline().color(LIGHT_GRAY))).on_hover_text("Gupax is online"); - ui.separator(); - ui.horizontal(|ui| { - if self.p2pool_path.is_empty() { - ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ➖").color(LIGHT_GRAY))).on_hover_text(P2POOL_PATH_EMPTY); - } else if !Self::path_is_file(&self.p2pool_path) { - ui.add_sized([text_edit, height], 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) { - ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ❌").color(RED))).on_hover_text(P2POOL_PATH_NOT_VALID); - } else { - ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ✔").color(GREEN))).on_hover_text(P2POOL_PATH_OK); - } - ui.spacing_mut().text_edit_width = ui.available_width() - SPACE; - ui.set_enabled(!lock!(file_window).thread); - if ui.button("Open").on_hover_text(GUPAX_SELECT).clicked() { - Self::spawn_file_window_thread(file_window, FileType::P2pool); - } - ui.add_sized([ui.available_width(), height], TextEdit::singleline(&mut self.p2pool_path)).on_hover_text(GUPAX_PATH_P2POOL); - }); - ui.horizontal(|ui| { - if self.xmrig_path.is_empty() { - ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ➖").color(LIGHT_GRAY))).on_hover_text(XMRIG_PATH_EMPTY); - } else if !Self::path_is_file(&self.xmrig_path) { - ui.add_sized([text_edit, height], 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) { - ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ❌").color(RED))).on_hover_text(XMRIG_PATH_NOT_VALID); - } else { - ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ✔").color(GREEN))).on_hover_text(XMRIG_PATH_OK); - } - ui.spacing_mut().text_edit_width = ui.available_width() - SPACE; - ui.set_enabled(!lock!(file_window).thread); - if ui.button("Open").on_hover_text(GUPAX_SELECT).clicked() { - Self::spawn_file_window_thread(file_window, FileType::Xmrig); - } - ui.add_sized([ui.available_width(), height], TextEdit::singleline(&mut self.xmrig_path)).on_hover_text(GUPAX_PATH_XMRIG); - }); - }); - let mut guard = lock!(file_window); - if guard.picked_p2pool { self.p2pool_path = guard.p2pool_path.clone(); guard.picked_p2pool = false; } - if guard.picked_xmrig { self.xmrig_path = guard.xmrig_path.clone(); guard.picked_xmrig = false; } - drop(guard); + debug!("Gupax Tab | Rendering P2Pool/XMRig path selection"); + // P2Pool/XMRig binary path selection + let height = height / 28.0; + let text_edit = (ui.available_width() / 10.0) - SPACE; + ui.group(|ui| { + ui.add_sized( + [ui.available_width(), height / 2.0], + Label::new( + RichText::new("P2Pool/XMRig PATHs") + .underline() + .color(LIGHT_GRAY), + ), + ) + .on_hover_text("Gupax is online"); + ui.separator(); + ui.horizontal(|ui| { + if self.p2pool_path.is_empty() { + ui.add_sized( + [text_edit, height], + Label::new(RichText::new("P2Pool Binary Path ➖").color(LIGHT_GRAY)), + ) + .on_hover_text(P2POOL_PATH_EMPTY); + } else if !Self::path_is_file(&self.p2pool_path) { + ui.add_sized( + [text_edit, height], + 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) { + ui.add_sized( + [text_edit, height], + Label::new(RichText::new("P2Pool Binary Path ❌").color(RED)), + ) + .on_hover_text(P2POOL_PATH_NOT_VALID); + } else { + ui.add_sized( + [text_edit, height], + Label::new(RichText::new("P2Pool Binary Path ✔").color(GREEN)), + ) + .on_hover_text(P2POOL_PATH_OK); + } + ui.spacing_mut().text_edit_width = ui.available_width() - SPACE; + ui.set_enabled(!lock!(file_window).thread); + if ui.button("Open").on_hover_text(GUPAX_SELECT).clicked() { + Self::spawn_file_window_thread(file_window, FileType::P2pool); + } + ui.add_sized( + [ui.available_width(), height], + TextEdit::singleline(&mut self.p2pool_path), + ) + .on_hover_text(GUPAX_PATH_P2POOL); + }); + ui.horizontal(|ui| { + if self.xmrig_path.is_empty() { + ui.add_sized( + [text_edit, height], + Label::new(RichText::new(" XMRig Binary Path ➖").color(LIGHT_GRAY)), + ) + .on_hover_text(XMRIG_PATH_EMPTY); + } else if !Self::path_is_file(&self.xmrig_path) { + ui.add_sized( + [text_edit, height], + 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) { + ui.add_sized( + [text_edit, height], + Label::new(RichText::new(" XMRig Binary Path ❌").color(RED)), + ) + .on_hover_text(XMRIG_PATH_NOT_VALID); + } else { + ui.add_sized( + [text_edit, height], + Label::new(RichText::new(" XMRig Binary Path ✔").color(GREEN)), + ) + .on_hover_text(XMRIG_PATH_OK); + } + ui.spacing_mut().text_edit_width = ui.available_width() - SPACE; + ui.set_enabled(!lock!(file_window).thread); + if ui.button("Open").on_hover_text(GUPAX_SELECT).clicked() { + Self::spawn_file_window_thread(file_window, FileType::Xmrig); + } + ui.add_sized( + [ui.available_width(), height], + TextEdit::singleline(&mut self.xmrig_path), + ) + .on_hover_text(GUPAX_PATH_XMRIG); + }); + }); + let mut guard = lock!(file_window); + if guard.picked_p2pool { + self.p2pool_path = guard.p2pool_path.clone(); + guard.picked_p2pool = false; + } + if guard.picked_xmrig { + self.xmrig_path = guard.xmrig_path.clone(); + guard.picked_xmrig = false; + } + drop(guard); - let height = ui.available_height()/6.0; + let height = ui.available_height() / 6.0; - // Saved [Tab] - debug!("Gupax Tab | Rendering [Tab] selector"); - ui.group(|ui| { - let width = (width/5.0)-(SPACE*1.93); - ui.add_sized([ui.available_width(), height/2.0], Label::new(RichText::new("Default Tab").underline().color(LIGHT_GRAY))).on_hover_text(GUPAX_TAB); - ui.separator(); - ui.horizontal(|ui| { - if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::About, "About")).on_hover_text(GUPAX_TAB_ABOUT).clicked() { self.tab = Tab::About; } - ui.separator(); - if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::Status, "Status")).on_hover_text(GUPAX_TAB_STATUS).clicked() { self.tab = Tab::Status; } - ui.separator(); - if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::Gupax, "Gupax")).on_hover_text(GUPAX_TAB_GUPAX).clicked() { self.tab = Tab::Gupax; } - ui.separator(); - if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::P2pool, "P2Pool")).on_hover_text(GUPAX_TAB_P2POOL).clicked() { self.tab = Tab::P2pool; } - ui.separator(); - if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::Xmrig, "XMRig")).on_hover_text(GUPAX_TAB_XMRIG).clicked() { self.tab = Tab::Xmrig; } - })}); + // Saved [Tab] + debug!("Gupax Tab | Rendering [Tab] selector"); + ui.group(|ui| { + let width = (width / 5.0) - (SPACE * 1.93); + ui.add_sized( + [ui.available_width(), height / 2.0], + Label::new(RichText::new("Default Tab").underline().color(LIGHT_GRAY)), + ) + .on_hover_text(GUPAX_TAB); + ui.separator(); + ui.horizontal(|ui| { + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::About, "About"), + ) + .on_hover_text(GUPAX_TAB_ABOUT) + .clicked() + { + self.tab = Tab::About; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::Status, "Status"), + ) + .on_hover_text(GUPAX_TAB_STATUS) + .clicked() + { + self.tab = Tab::Status; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::Gupax, "Gupax"), + ) + .on_hover_text(GUPAX_TAB_GUPAX) + .clicked() + { + self.tab = Tab::Gupax; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::P2pool, "P2Pool"), + ) + .on_hover_text(GUPAX_TAB_P2POOL) + .clicked() + { + self.tab = Tab::P2pool; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.tab == Tab::Xmrig, "XMRig"), + ) + .on_hover_text(GUPAX_TAB_XMRIG) + .clicked() + { + self.tab = Tab::Xmrig; + } + }) + }); - // Gupax App resolution sliders - debug!("Gupax Tab | Rendering resolution sliders"); - ui.group(|ui| { - ui.add_sized([ui.available_width(), height/2.0], Label::new(RichText::new("Width/Height Adjust").underline().color(LIGHT_GRAY))).on_hover_text(GUPAX_ADJUST); - ui.separator(); - ui.vertical(|ui| { - let width = width/10.0; - ui.spacing_mut().icon_width = width / 25.0; - ui.spacing_mut().slider_width = width*7.6; - match self.ratio { - Ratio::None => (), - Ratio::Width => { - let width = self.selected_width as f64; - let height = (width / 1.333).round(); - self.selected_height = height as u16; - }, - Ratio::Height => { - let height = self.selected_height as f64; - let width = (height * 1.333).round(); - self.selected_width = width as u16; - }, - } - let height = height/3.5; - ui.horizontal(|ui| { - ui.set_enabled(self.ratio != Ratio::Height); - ui.add_sized([width, height], Label::new(format!(" Width [{}-{}]:", APP_MIN_WIDTH as u16, APP_MAX_WIDTH as u16))); - ui.add_sized([width, height], Slider::new(&mut self.selected_width, APP_MIN_WIDTH as u16..=APP_MAX_WIDTH as u16)).on_hover_text(GUPAX_WIDTH); - }); - ui.horizontal(|ui| { - ui.set_enabled(self.ratio != Ratio::Width); - ui.add_sized([width, height], Label::new(format!("Height [{}-{}]:", APP_MIN_HEIGHT as u16, APP_MAX_HEIGHT as u16))); - ui.add_sized([width, height], Slider::new(&mut self.selected_height, APP_MIN_HEIGHT as u16..=APP_MAX_HEIGHT as u16)).on_hover_text(GUPAX_HEIGHT); - }); - ui.horizontal(|ui| { - ui.add_sized([width, height], Label::new(format!("Scaling [{APP_MIN_SCALE}..{APP_MAX_SCALE}]:"))); - ui.add_sized([width, height], Slider::new(&mut self.selected_scale, APP_MIN_SCALE..=APP_MAX_SCALE).step_by(0.1)).on_hover_text(GUPAX_SCALE); - }); - }); - ui.style_mut().override_text_style = Some(egui::TextStyle::Button); - ui.separator(); - // Width/Height locks - ui.horizontal(|ui| { - use Ratio::*; - let width = (width/4.0)-(SPACE*1.5); - if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Width, "Lock to width")).on_hover_text(GUPAX_LOCK_WIDTH).clicked() { self.ratio = Width; } - ui.separator(); - if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Height, "Lock to height")).on_hover_text(GUPAX_LOCK_HEIGHT).clicked() { self.ratio = Height; } - ui.separator(); - if ui.add_sized([width, height], SelectableLabel::new(self.ratio == None, "No lock")).on_hover_text(GUPAX_NO_LOCK).clicked() { self.ratio = None; } - if ui.add_sized([width, height], Button::new("Set")).on_hover_text(GUPAX_SET).clicked() { - let size = Vec2::new(self.selected_width as f32, self.selected_height as f32); - ui.ctx().send_viewport_cmd(egui::viewport::ViewportCommand::InnerSize(size)); - } - })}); - } + // Gupax App resolution sliders + debug!("Gupax Tab | Rendering resolution sliders"); + ui.group(|ui| { + ui.add_sized( + [ui.available_width(), height / 2.0], + Label::new( + RichText::new("Width/Height Adjust") + .underline() + .color(LIGHT_GRAY), + ), + ) + .on_hover_text(GUPAX_ADJUST); + ui.separator(); + ui.vertical(|ui| { + let width = width / 10.0; + ui.spacing_mut().icon_width = width / 25.0; + ui.spacing_mut().slider_width = width * 7.6; + match self.ratio { + Ratio::None => (), + Ratio::Width => { + let width = self.selected_width as f64; + let height = (width / 1.333).round(); + self.selected_height = height as u16; + } + Ratio::Height => { + let height = self.selected_height as f64; + let width = (height * 1.333).round(); + self.selected_width = width as u16; + } + } + let height = height / 3.5; + ui.horizontal(|ui| { + ui.set_enabled(self.ratio != Ratio::Height); + ui.add_sized( + [width, height], + Label::new(format!( + " Width [{}-{}]:", + APP_MIN_WIDTH as u16, APP_MAX_WIDTH as u16 + )), + ); + ui.add_sized( + [width, height], + Slider::new( + &mut self.selected_width, + APP_MIN_WIDTH as u16..=APP_MAX_WIDTH as u16, + ), + ) + .on_hover_text(GUPAX_WIDTH); + }); + ui.horizontal(|ui| { + ui.set_enabled(self.ratio != Ratio::Width); + ui.add_sized( + [width, height], + Label::new(format!( + "Height [{}-{}]:", + APP_MIN_HEIGHT as u16, APP_MAX_HEIGHT as u16 + )), + ); + ui.add_sized( + [width, height], + Slider::new( + &mut self.selected_height, + APP_MIN_HEIGHT as u16..=APP_MAX_HEIGHT as u16, + ), + ) + .on_hover_text(GUPAX_HEIGHT); + }); + ui.horizontal(|ui| { + ui.add_sized( + [width, height], + Label::new(format!("Scaling [{APP_MIN_SCALE}..{APP_MAX_SCALE}]:")), + ); + ui.add_sized( + [width, height], + Slider::new(&mut self.selected_scale, APP_MIN_SCALE..=APP_MAX_SCALE) + .step_by(0.1), + ) + .on_hover_text(GUPAX_SCALE); + }); + }); + ui.style_mut().override_text_style = Some(egui::TextStyle::Button); + ui.separator(); + // Width/Height locks + ui.horizontal(|ui| { + use Ratio::*; + let width = (width / 4.0) - (SPACE * 1.5); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.ratio == Width, "Lock to width"), + ) + .on_hover_text(GUPAX_LOCK_WIDTH) + .clicked() + { + self.ratio = Width; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.ratio == Height, "Lock to height"), + ) + .on_hover_text(GUPAX_LOCK_HEIGHT) + .clicked() + { + self.ratio = Height; + } + ui.separator(); + if ui + .add_sized( + [width, height], + SelectableLabel::new(self.ratio == None, "No lock"), + ) + .on_hover_text(GUPAX_NO_LOCK) + .clicked() + { + self.ratio = None; + } + if ui + .add_sized([width, height], Button::new("Set")) + .on_hover_text(GUPAX_SET) + .clicked() + { + let size = Vec2::new(self.selected_width as f32, self.selected_height as f32); + ui.ctx() + .send_viewport_cmd(egui::viewport::ViewportCommand::InnerSize(size)); + } + }) + }); + } - // 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, - } - } + // 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; - }); - } + #[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/helper.rs b/src/helper.rs index adc9a61..3ffdb70 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -34,31 +34,20 @@ // 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::{ - sync::{Arc,Mutex}, - path::PathBuf, - process::Stdio, - fmt::Write, - time::*, - thread, -}; -use crate::{ - constants::*, - SudoState, - human::*, - GupaxP2poolApi, - xmr::*, - macros::*, - RemoteNode, + fmt::Write, + path::PathBuf, + process::Stdio, + sync::{Arc, Mutex}, + thread, + time::*, }; use sysinfo::SystemExt; -use serde::{Serialize,Deserialize}; -use sysinfo::{CpuExt,ProcessExt}; -use log::*; -use crate::regex::{ - P2POOL_REGEX, - XMRIG_REGEX, -}; +use sysinfo::{CpuExt, ProcessExt}; //---------------------------------------------------------------------------------------------------- Constants // The max amount of bytes of process output we are willing to @@ -74,18 +63,18 @@ 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>, // + 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: @@ -97,316 +86,400 @@ pub struct Helper { // on a 1-second interval into the [GUI]'s [Pub*Api] struct, atomically. //---------------------------------------------------------------------------------------------------- -#[derive(Debug,Clone)] +#[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, + 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(), - } - } + 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() + } } -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, + 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 + // 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>, + // 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, + // 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()], - } - } + 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() - } + // 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] + // 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_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_syncing(&self) -> bool { + self.state == ProcessState::Syncing + } - #[inline] - pub fn is_not_mining(&self) -> bool { - self.state == ProcessState::NotMining - } + #[inline] + pub fn is_not_mining(&self) -> bool { + self.state == ProcessState::NotMining + } } //---------------------------------------------------------------------------------------------------- [Process*] Enum -#[derive(Copy,Clone,Eq,PartialEq,Debug)] +#[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! + 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 P2Pool, ORANGE. + Syncing, - // Only for XMRig, ORANGE. - NotMining, + // Only for XMRig, ORANGE. + NotMining, } -impl Default for ProcessState { fn default() -> Self { Self::Dead } } +impl Default for ProcessState { + fn default() -> Self { + Self::Dead + } +} -#[derive(Copy,Clone,Eq,PartialEq,Debug)] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum ProcessSignal { - None, - Start, - Stop, - Restart, + None, + Start, + Stop, + Restart, } -impl Default for ProcessSignal { fn default() -> Self { Self::None } } +impl Default for ProcessSignal { + fn default() -> Self { + Self::None + } +} -#[derive(Copy,Clone,Eq,PartialEq,Debug)] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum ProcessName { - P2pool, - Xmrig, + 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"), - } - } +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 - 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, - } - } + //---------------------------------------------------------------------------------------------------- General Functions + 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(); + #[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; - } - } - drop(i); + // 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; + } + } + drop(i); - 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); } - } - } + 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(); + #[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; - } - } - drop(i); + // 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; + } + } + drop(i); - 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); } - } - } + 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); - } - } + // 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: &std::path::PathBuf, 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) }, - } - } + // Read P2Pool/XMRig's API file to a [String]. + fn path_to_string( + path: &std::path::PathBuf, + 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; - } + //---------------------------------------------------------------------------------------------------- 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: &std::path::PathBuf, backup_hosts: Option>) { - info!("P2Pool | Attempting to restart..."); - lock2!(helper,p2pool).signal = ProcessSignal::Restart; - 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: &std::path::PathBuf, + 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.clone(); - // 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"); - } + let helper = Arc::clone(helper); + let state = state.clone(); + let path = path.clone(); + // This thread lives to wait, start p2pool then die. + thread::spawn(move || { + while 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: &std::path::PathBuf, - backup_hosts: Option>, - ) { - lock2!(helper,p2pool).state = ProcessState::Middle; + #[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: &std::path::PathBuf, + 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); + 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!( + // 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, @@ -414,910 +487,1216 @@ impl Helper { 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.clone(); - 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); - }); - } + // 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.clone(); + 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 - } + // 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: &std::path::PathBuf, - backup_hosts: Option>, - ) -> (Vec, PathBuf, PathBuf, PathBuf) { - let mut args = Vec::with_capacity(500); - let path = path.clone(); - let mut api_path = path; - api_path.pop(); + #[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: &std::path::PathBuf, + backup_hosts: Option>, + ) -> (Vec, PathBuf, PathBuf, PathBuf) { + let mut args = Vec::with_capacity(500); + let path = path.clone(); + 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. + // [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()); - } - } - } + // 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(), - }; + *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 + // [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()); - } - } - } + // 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) - } + *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. - 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); + #[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. + 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..."); + 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); + 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); + // 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; + 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(); + // 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; + // 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 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 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 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); + // 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); + // 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; - } - } - } + // 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); - } - } + // 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!"); - } + // 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(); + //---------------------------------------------------------------------------------------------------- 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); } + // 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() - } + // 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)] + // 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: &std::path::PathBuf, sudo: Arc>) { - info!("XMRig | Attempting to restart..."); - lock2!(helper,xmrig).signal = ProcessSignal::Restart; - 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: &std::path::PathBuf, + 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.clone(); - // 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"); - } + let helper = Arc::clone(helper); + let state = state.clone(); + let path = path.clone(); + // 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: &std::path::PathBuf, sudo: Arc>) { - lock2!(helper,xmrig).state = ProcessState::Middle; + #[cold] + #[inline(never)] + pub fn start_xmrig( + helper: &Arc>, + state: &crate::disk::Xmrig, + path: &std::path::PathBuf, + sudo: Arc>, + ) { + lock2!(helper, xmrig).state = ProcessState::Middle; - let (args, api_ip_port) = Self::build_xmrig_args_and_mutate_img(helper, state, path); + 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()); + // 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.clone(); - thread::spawn(move || { - Self::spawn_xmrig_watchdog(process, gui_api, pub_api, args, path, sudo, api_ip_port); - }); - } + // 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.clone(); + 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::PathBuf) -> (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.clone(); - // 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()); - } + #[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::PathBuf, + ) -> (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.clone(); + // 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(); + // [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)) - } + // [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 - } + // 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 - } + // 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] - 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 mut 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); + #[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] + 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 mut 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(); + 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(); - } + // 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..."); + 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); + 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); + // 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); + 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(); + // 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 -----------"); + // 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 - } + // 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 - } + // 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); } - } - } - drop(lock); + // 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); + } + } + } + drop(lock); - // 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); - drop(lock); + // 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); + drop(lock); - // Always update from output - debug!("XMRig Watchdog | Starting [update_from_output()]"); - PubXmrigApi::update_from_output(&pub_api, &output_pub, &output_parse, start.elapsed(), &process); + // 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); - } + // 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!"); - } - } + // 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!"); - } + // 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, - }; - } + //---------------------------------------------------------------------------------------------------- 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. - 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). + #[cold] + #[inline(never)] + // The "helper" thread. Syncs data between threads here and the GUI. + 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 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(); + 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 -----------"); + 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. + // 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. 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); + // 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]"); + // 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!"); - } + // 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 - } - }); - } + // 5. End loop + } + }); + } } //---------------------------------------------------------------------------------------------------- [ImgP2pool] @@ -1327,424 +1706,450 @@ impl Helper { // 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? + 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() - } + 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("???"), - } - } + 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)] +#[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. + // 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() - } + 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(), - } - } + 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] + // 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) - } + #[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)); - } + // 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(); + // 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); + // 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; + // 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); - } + 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) - }; - } + // 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 [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) - }; - } + // 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_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) - } - } + #[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 => "[*********************************************************** ]", - _ => "[************************************************************]", - } - } + 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 @@ -1752,226 +2157,260 @@ impl PubP2poolApi { // 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` + 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 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, - } - } + 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) }, - } - } + // 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, + difficulty: u64, + hash: String, + height: u32, + reward: u64, + timestamp: u32, } -impl Default for PrivP2poolNetworkApi { fn default() -> Self { Self::new() } } +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 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) }, - } - } + 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, + pool_statistics: PoolStatistics, } -impl Default for PrivP2poolPoolApi { fn default() -> Self { Self::new() } } +impl Default for PrivP2poolPoolApi { + fn default() -> Self { + Self::new() + } +} impl PrivP2poolPoolApi { - fn new() -> Self { - Self { - pool_statistics: PoolStatistics::new(), - } - } + 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) }, - } - } + 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, + hashRate: u64, + miners: u32, +} +impl Default for PoolStatistics { + fn default() -> Self { + Self::new() + } +} +impl PoolStatistics { + fn new() -> Self { + Self { + hashRate: 0, + miners: 0, + } + } } -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, + pub threads: String, + pub url: String, } impl Default for ImgXmrig { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl ImgXmrig { - pub fn new() -> Self { - Self { - threads: "???".to_string(), - url: "???".to_string(), - } - } + 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 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, + pub hashrate_raw: f32, } impl Default for PubXmrigApi { - fn default() -> Self { - Self::new() - } + 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, - } - } + 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); } - } + #[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); + // 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); - } + { + 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; - } + // 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); - } + // 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.get(0) { - Some(Some(h)) => *h, - _ => 0.0, - }; + // 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.get(0) { + 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) - } - } + *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 @@ -1981,307 +2420,341 @@ impl PubXmrigApi { // 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, + 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(), - } - } + 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)?) - } + #[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], + load_average: [Option; 3], } impl Resources { - fn new() -> Self { - Self { - load_average: [Some(0.0), Some(0.0), Some(0.0)], - } - } + 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, + diff: u128, + accepted: u128, + rejected: u128, } impl Connection { - fn new() -> Self { - Self { - diff: 0, - accepted: 0, - rejected: 0, - } - } + fn new() -> Self { + Self { + diff: 0, + accepted: 0, + rejected: 0, + } + } } #[derive(Debug, Serialize, Deserialize, Clone, Copy)] struct Hashrate { - total: [Option; 3], + total: [Option; 3], } impl Hashrate { - fn new() -> Self { - Self { - total: [Some(0.0), Some(0.0), Some(0.0)], - } - } + fn new() -> Self { + Self { + total: [Some(0.0), Some(0.0), Some(0.0)], + } + } } //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod test { - use super::*; + 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 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 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 + #[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); - } + 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 + #[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()))); + 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); - } + // 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())); + #[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 + // 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()))); + 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 - } + // 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())); + #[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 + // 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()))); + 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); - } + // 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 std::sync::{Arc,Mutex}; - use crate::helper::PubP2poolApi; - use crate::helper::PrivP2poolLocalApi; - use crate::helper::PrivP2poolNetworkApi; - use crate::helper::PrivP2poolPoolApi; - use crate::helper::PoolStatistics; - 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 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()))); + #[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); + 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); - } + 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#"{ + #[test] + fn serde_priv_p2pool_local_api() { + let data = r#"{ "hashrate_15m": 12, "hashrate_1h": 11111, "hashrate_24h": 468967, @@ -2292,11 +2765,10 @@ mod test { "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#"{ + 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, @@ -2305,37 +2777,34 @@ r#"{ "current_effort": 129.297, "connections": 123 }"#; - assert_eq!(data_after_ser, json) - } + assert_eq!(data_after_ser, json) + } - #[test] - fn serde_priv_p2pool_network_api() { - let data = - r#"{ + #[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#"{ + 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) - } + assert_eq!(data_after_ser, json) + } - #[test] - fn serde_priv_p2pool_pool_api() { - let data = - r#"{ + #[test] + fn serde_priv_p2pool_pool_api() { + let data = r#"{ "pool_list": ["pplns"], "pool_statistics": { "hashRate": 10225772, @@ -2346,23 +2815,21 @@ r#"{ "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#"{ + 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) - } + assert_eq!(data_after_ser, json) + } - #[test] - fn serde_priv_xmrig_api() { - let data = - r#"{ + #[test] + fn serde_priv_xmrig_api() { + let data = r#"{ "id": "6226e3sd0cd1a6es", "worker_id": "hinto", "uptime": 123, @@ -2443,12 +2910,11 @@ r#"{ }, "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#"{ + 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": [ @@ -2470,6 +2936,6 @@ r#"{ ] } }"#; - assert_eq!(data_after_ser, json) - } + assert_eq!(data_after_ser, json) + } } diff --git a/src/human.rs b/src/human.rs index 118b6e2..9179de5 100644 --- a/src/human.rs +++ b/src/human.rs @@ -30,69 +30,74 @@ use std::time::Duration; pub struct HumanTime(Duration); impl Default for HumanTime { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl HumanTime { - #[inline] - pub const fn new() -> HumanTime { - HumanTime(ZERO_SECONDS) - } + #[inline] + pub const fn new() -> HumanTime { + HumanTime(ZERO_SECONDS) + } - #[inline] - pub const fn into_human(d: Duration) -> HumanTime { - HumanTime(d) - } + #[inline] + pub const fn into_human(d: Duration) -> HumanTime { + HumanTime(d) + } - #[inline] - pub const fn from_u64(u: u64) -> HumanTime { - HumanTime(Duration::from_secs(u)) - } + #[inline] + pub const fn from_u64(u: u64) -> HumanTime { + HumanTime(Duration::from_secs(u)) + } - fn plural(f: &mut std::fmt::Formatter, started: &mut bool, name: &str, value: u64) -> std::fmt::Result { - if value > 0 { - if *started { - f.write_str(", ")?; - } - write!(f, "{} {}", value, name)?; - if value > 1 { - f.write_str("s")?; - } - *started = true; - } - Ok(()) - } + fn plural( + f: &mut std::fmt::Formatter, + started: &mut bool, + name: &str, + value: u64, + ) -> std::fmt::Result { + if value > 0 { + if *started { + f.write_str(", ")?; + } + write!(f, "{} {}", value, name)?; + if value > 1 { + f.write_str("s")?; + } + *started = true; + } + Ok(()) + } } impl std::fmt::Display for HumanTime { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let secs = self.0.as_secs(); - if secs == 0 { - f.write_str("0 seconds")?; - return Ok(()); - } + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let secs = self.0.as_secs(); + if secs == 0 { + f.write_str("0 seconds")?; + return Ok(()); + } - let years = secs / 31_557_600; // 365.25d - let ydays = secs % 31_557_600; - let months = ydays / 2_630_016; // 30.44d - let mdays = ydays % 2_630_016; - let days = mdays / 86400; - let day_secs = mdays % 86400; - let hours = day_secs / 3600; - let minutes = day_secs % 3600 / 60; - let seconds = day_secs % 60; + let years = secs / 31_557_600; // 365.25d + let ydays = secs % 31_557_600; + let months = ydays / 2_630_016; // 30.44d + let mdays = ydays % 2_630_016; + let days = mdays / 86400; + let day_secs = mdays % 86400; + let hours = day_secs / 3600; + let minutes = day_secs % 3600 / 60; + let seconds = day_secs % 60; - let started = &mut false; - Self::plural(f, started, "year", years)?; - Self::plural(f, started, "month", months)?; - Self::plural(f, started, "day", days)?; - Self::plural(f, started, "hour", hours)?; - Self::plural(f, started, "minute", minutes)?; - Self::plural(f, started, "second", seconds)?; - Ok(()) - } + let started = &mut false; + Self::plural(f, started, "year", years)?; + Self::plural(f, started, "month", months)?; + Self::plural(f, started, "day", days)?; + Self::plural(f, started, "hour", hours)?; + Self::plural(f, started, "minute", minutes)?; + Self::plural(f, started, "second", seconds)?; + Ok(()) + } } //---------------------------------------------------------------------------------------------------- [HumanNumber] @@ -107,249 +112,304 @@ impl std::fmt::Display for HumanTime { pub struct HumanNumber(String); impl std::fmt::Display for HumanNumber { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", &self.0) - } + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", &self.0) + } } impl HumanNumber { - #[inline] - pub fn unknown() -> Self { - Self("???".to_string()) - } - #[inline] - pub fn from_str(s: &str) -> Self { - Self(s.to_string()) - } - #[inline] - pub fn to_hashrate(f: f32) -> Self { - Self(format!("{} H/s", Self::from_f32(f))) - } - #[inline] - pub fn to_percent(f: f32) -> Self { - if f < 0.01 { - Self("0%".to_string()) - } else { - Self(format!("{:.2}%", f)) - } - } - #[inline] - pub fn to_percent_3_point(f: f32) -> Self { - Self(format!("{:.3}%", f)) - } - #[inline] - pub fn to_percent_no_fmt(f: f32) -> Self { - Self(format!("{}%", f)) - } - #[inline] - pub fn from_f64_to_percent_3_point(f: f64) -> Self { - Self(format!("{:.3}%", f)) - } - #[inline] - pub fn from_f64_to_percent_6_point(f: f64) -> Self { - Self(format!("{:.6}%", f)) - } - #[inline] - pub fn from_f64_to_percent_9_point(f: f64) -> Self { - Self(format!("{:.9}%", f)) - } - #[inline] - pub fn from_f64_to_percent_no_fmt(f: f64) -> Self { - Self(format!("{}%", f)) - } - #[inline] - pub fn from_f32(f: f32) -> Self { - let mut buf = num_format::Buffer::new(); - buf.write_formatted(&(f as u64), &LOCALE); - Self(buf.as_str().to_string()) - } - #[inline] - pub fn from_f64(f: f64) -> Self { - let mut buf = num_format::Buffer::new(); - buf.write_formatted(&(f as u128), &LOCALE); - Self(buf.as_str().to_string()) - } - #[inline] - pub fn from_u16(u: u16) -> Self { - let mut buf = num_format::Buffer::new(); - buf.write_formatted(&u, &LOCALE); - Self(buf.as_str().to_string()) - } - #[inline] - pub fn from_u32(u: u32) -> Self { - let mut buf = num_format::Buffer::new(); - buf.write_formatted(&u, &LOCALE); - Self(buf.as_str().to_string()) - } - #[inline] - pub fn from_u64(u: u64) -> Self { - let mut buf = num_format::Buffer::new(); - buf.write_formatted(&u, &LOCALE); - Self(buf.as_str().to_string()) - } - #[inline] - pub fn from_u128(u: u128) -> Self { - let mut buf = num_format::Buffer::new(); - buf.write_formatted(&u, &LOCALE); - Self(buf.as_str().to_string()) - } - #[inline] - pub fn from_hashrate(array: [Option; 3]) -> Self { - let mut string = "[".to_string(); - let mut buf = num_format::Buffer::new(); + #[inline] + pub fn unknown() -> Self { + Self("???".to_string()) + } + #[inline] + pub fn from_str(s: &str) -> Self { + Self(s.to_string()) + } + #[inline] + pub fn to_hashrate(f: f32) -> Self { + Self(format!("{} H/s", Self::from_f32(f))) + } + #[inline] + pub fn to_percent(f: f32) -> Self { + if f < 0.01 { + Self("0%".to_string()) + } else { + Self(format!("{:.2}%", f)) + } + } + #[inline] + pub fn to_percent_3_point(f: f32) -> Self { + Self(format!("{:.3}%", f)) + } + #[inline] + pub fn to_percent_no_fmt(f: f32) -> Self { + Self(format!("{}%", f)) + } + #[inline] + pub fn from_f64_to_percent_3_point(f: f64) -> Self { + Self(format!("{:.3}%", f)) + } + #[inline] + pub fn from_f64_to_percent_6_point(f: f64) -> Self { + Self(format!("{:.6}%", f)) + } + #[inline] + pub fn from_f64_to_percent_9_point(f: f64) -> Self { + Self(format!("{:.9}%", f)) + } + #[inline] + pub fn from_f64_to_percent_no_fmt(f: f64) -> Self { + Self(format!("{}%", f)) + } + #[inline] + pub fn from_f32(f: f32) -> Self { + let mut buf = num_format::Buffer::new(); + buf.write_formatted(&(f as u64), &LOCALE); + Self(buf.as_str().to_string()) + } + #[inline] + pub fn from_f64(f: f64) -> Self { + let mut buf = num_format::Buffer::new(); + buf.write_formatted(&(f as u128), &LOCALE); + Self(buf.as_str().to_string()) + } + #[inline] + pub fn from_u16(u: u16) -> Self { + let mut buf = num_format::Buffer::new(); + buf.write_formatted(&u, &LOCALE); + Self(buf.as_str().to_string()) + } + #[inline] + pub fn from_u32(u: u32) -> Self { + let mut buf = num_format::Buffer::new(); + buf.write_formatted(&u, &LOCALE); + Self(buf.as_str().to_string()) + } + #[inline] + pub fn from_u64(u: u64) -> Self { + let mut buf = num_format::Buffer::new(); + buf.write_formatted(&u, &LOCALE); + Self(buf.as_str().to_string()) + } + #[inline] + pub fn from_u128(u: u128) -> Self { + let mut buf = num_format::Buffer::new(); + buf.write_formatted(&u, &LOCALE); + Self(buf.as_str().to_string()) + } + #[inline] + pub fn from_hashrate(array: [Option; 3]) -> Self { + let mut string = "[".to_string(); + let mut buf = num_format::Buffer::new(); - let mut n = 0; - for i in array { - match i { - Some(f) => { - let f = f as u128; - buf.write_formatted(&f, &LOCALE); - string.push_str(buf.as_str()); - string.push_str(" H/s"); - }, - None => string.push_str("??? H/s"), - } - if n != 2 { - string.push_str(", "); - n += 1; - } else { - string.push(']'); - break - } - } + let mut n = 0; + for i in array { + match i { + Some(f) => { + let f = f as u128; + buf.write_formatted(&f, &LOCALE); + string.push_str(buf.as_str()); + string.push_str(" H/s"); + } + None => string.push_str("??? H/s"), + } + if n != 2 { + string.push_str(", "); + n += 1; + } else { + string.push(']'); + break; + } + } - Self(string) - } - #[inline] - pub fn from_load(array: [Option; 3]) -> Self { - let mut string = "[".to_string(); - let mut n = 0; - for i in array { - match i { - Some(f) => string.push_str(format!("{:.2}", f).as_str()), - None => string.push_str("???"), - } - if n != 2 { - string.push_str(", "); - n += 1; - } else { - string.push(']'); - break - } - } - Self(string) - } - // [1_000_000] -> [1.000 MH/s] - #[inline] - pub fn from_u64_to_megahash_3_point(hash: u64) -> Self { - let hash = (hash as f64)/1_000_000.0; - let hash = format!("{:.3} MH/s", hash); - Self(hash) - } - // [1_000_000_000] -> [1.000 GH/s] - #[inline] - pub fn from_u64_to_gigahash_3_point(hash: u64) -> Self { - let hash = (hash as f64)/1_000_000_000.0; - let hash = format!("{:.3} GH/s", hash); - Self(hash) - } - #[inline] - pub fn from_f64_12_point(f: f64) -> Self { - let f = format!("{:.12}", f); - Self(f) - } - #[inline] - pub fn from_f64_no_fmt(f: f64) -> Self { - let f = format!("{}", f); - Self(f) - } - #[inline] - pub fn as_str(&self) -> &str { - self.0.as_str() - } + Self(string) + } + #[inline] + pub fn from_load(array: [Option; 3]) -> Self { + let mut string = "[".to_string(); + let mut n = 0; + for i in array { + match i { + Some(f) => string.push_str(format!("{:.2}", f).as_str()), + None => string.push_str("???"), + } + if n != 2 { + string.push_str(", "); + n += 1; + } else { + string.push(']'); + break; + } + } + Self(string) + } + // [1_000_000] -> [1.000 MH/s] + #[inline] + pub fn from_u64_to_megahash_3_point(hash: u64) -> Self { + let hash = (hash as f64) / 1_000_000.0; + let hash = format!("{:.3} MH/s", hash); + Self(hash) + } + // [1_000_000_000] -> [1.000 GH/s] + #[inline] + pub fn from_u64_to_gigahash_3_point(hash: u64) -> Self { + let hash = (hash as f64) / 1_000_000_000.0; + let hash = format!("{:.3} GH/s", hash); + Self(hash) + } + #[inline] + pub fn from_f64_12_point(f: f64) -> Self { + let f = format!("{:.12}", f); + Self(f) + } + #[inline] + pub fn from_f64_no_fmt(f: f64) -> Self { + let f = format!("{}", f); + Self(f) + } + #[inline] + pub fn as_str(&self) -> &str { + self.0.as_str() + } } //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod test { - #[test] - fn human_number() { - use crate::human::HumanNumber; - assert!(HumanNumber::to_percent(0.001).to_string() == "0%"); - assert!(HumanNumber::to_percent(12.123123123123).to_string() == "12.12%"); - assert!(HumanNumber::to_percent_3_point(0.001).to_string() == "0.001%"); - assert!(HumanNumber::from_hashrate([Some(123.1), Some(11111.1), None]).to_string() == "[123 H/s, 11,111 H/s, ??? H/s]"); - assert!(HumanNumber::from_hashrate([None, Some(1.123), Some(123123.312)]).to_string() == "[??? H/s, 1 H/s, 123,123 H/s]"); - assert!(HumanNumber::from_load([Some(123.1234), Some(321.321), None]).to_string() == "[123.12, 321.32, ???]"); - assert!(HumanNumber::from_load([None, Some(4321.43), Some(1234.1)]).to_string() == "[???, 4321.43, 1234.10]"); - assert!(HumanNumber::from_f32(123_123.123123123).to_string() == "123,123"); - assert!(HumanNumber::from_f64(123_123_123.123123123123123).to_string() == "123,123,123"); - assert!(HumanNumber::from_u16(1_000).to_string() == "1,000"); - assert!(HumanNumber::from_u16(65_535).to_string() == "65,535"); - assert!(HumanNumber::from_u32(65_536).to_string() == "65,536"); - assert!(HumanNumber::from_u32(100_000).to_string() == "100,000"); - assert!(HumanNumber::from_u32(1_000_000).to_string() == "1,000,000"); - assert!(HumanNumber::from_u32(10_000_000).to_string() == "10,000,000"); - assert!(HumanNumber::from_u32(100_000_000).to_string() == "100,000,000"); - assert!(HumanNumber::from_u32(1_000_000_000).to_string() == "1,000,000,000"); - assert!(HumanNumber::from_u32(4_294_967_295).to_string() == "4,294,967,295"); - assert!(HumanNumber::from_u64(4_294_967_296).to_string() == "4,294,967,296"); - assert!(HumanNumber::from_u64(10_000_000_000).to_string() == "10,000,000,000"); - assert!(HumanNumber::from_u64(100_000_000_000).to_string() == "100,000,000,000"); - assert!(HumanNumber::from_u64(1_000_000_000_000).to_string() == "1,000,000,000,000"); - assert!(HumanNumber::from_u64(10_000_000_000_000).to_string() == "10,000,000,000,000"); - assert!(HumanNumber::from_u64(100_000_000_000_000).to_string() == "100,000,000,000,000"); - assert!(HumanNumber::from_u64(1_000_000_000_000_000).to_string() == "1,000,000,000,000,000"); - assert!(HumanNumber::from_u64(10_000_000_000_000_000).to_string() == "10,000,000,000,000,000"); - assert!(HumanNumber::from_u64(18_446_744_073_709_551_615).to_string() == "18,446,744,073,709,551,615"); - assert!(HumanNumber::from_u128(18_446_744_073_709_551_616).to_string() == "18,446,744,073,709,551,616"); - assert!(HumanNumber::from_u128(100_000_000_000_000_000_000).to_string() == "100,000,000,000,000,000,000"); - assert_eq!( - HumanNumber::from_u128(340_282_366_920_938_463_463_374_607_431_768_211_455).to_string(), - "340,282,366,920,938,463,463,374,607,431,768,211,455", - ); - assert!(HumanNumber::from_u64_to_gigahash_3_point(1_000_000_000).to_string() == "1.000 GH/s"); - } + #[test] + fn human_number() { + use crate::human::HumanNumber; + assert!(HumanNumber::to_percent(0.001).to_string() == "0%"); + assert!(HumanNumber::to_percent(12.123123123123).to_string() == "12.12%"); + assert!(HumanNumber::to_percent_3_point(0.001).to_string() == "0.001%"); + assert!( + HumanNumber::from_hashrate([Some(123.1), Some(11111.1), None]).to_string() + == "[123 H/s, 11,111 H/s, ??? H/s]" + ); + assert!( + HumanNumber::from_hashrate([None, Some(1.123), Some(123123.312)]).to_string() + == "[??? H/s, 1 H/s, 123,123 H/s]" + ); + assert!( + HumanNumber::from_load([Some(123.1234), Some(321.321), None]).to_string() + == "[123.12, 321.32, ???]" + ); + assert!( + HumanNumber::from_load([None, Some(4321.43), Some(1234.1)]).to_string() + == "[???, 4321.43, 1234.10]" + ); + assert!(HumanNumber::from_f32(123_123.123123123).to_string() == "123,123"); + assert!(HumanNumber::from_f64(123_123_123.123123123123123).to_string() == "123,123,123"); + assert!(HumanNumber::from_u16(1_000).to_string() == "1,000"); + assert!(HumanNumber::from_u16(65_535).to_string() == "65,535"); + assert!(HumanNumber::from_u32(65_536).to_string() == "65,536"); + assert!(HumanNumber::from_u32(100_000).to_string() == "100,000"); + assert!(HumanNumber::from_u32(1_000_000).to_string() == "1,000,000"); + assert!(HumanNumber::from_u32(10_000_000).to_string() == "10,000,000"); + assert!(HumanNumber::from_u32(100_000_000).to_string() == "100,000,000"); + assert!(HumanNumber::from_u32(1_000_000_000).to_string() == "1,000,000,000"); + assert!(HumanNumber::from_u32(4_294_967_295).to_string() == "4,294,967,295"); + assert!(HumanNumber::from_u64(4_294_967_296).to_string() == "4,294,967,296"); + assert!(HumanNumber::from_u64(10_000_000_000).to_string() == "10,000,000,000"); + assert!(HumanNumber::from_u64(100_000_000_000).to_string() == "100,000,000,000"); + assert!(HumanNumber::from_u64(1_000_000_000_000).to_string() == "1,000,000,000,000"); + assert!(HumanNumber::from_u64(10_000_000_000_000).to_string() == "10,000,000,000,000"); + assert!(HumanNumber::from_u64(100_000_000_000_000).to_string() == "100,000,000,000,000"); + assert!( + HumanNumber::from_u64(1_000_000_000_000_000).to_string() == "1,000,000,000,000,000" + ); + assert!( + HumanNumber::from_u64(10_000_000_000_000_000).to_string() == "10,000,000,000,000,000" + ); + assert!( + HumanNumber::from_u64(18_446_744_073_709_551_615).to_string() + == "18,446,744,073,709,551,615" + ); + assert!( + HumanNumber::from_u128(18_446_744_073_709_551_616).to_string() + == "18,446,744,073,709,551,616" + ); + assert!( + HumanNumber::from_u128(100_000_000_000_000_000_000).to_string() + == "100,000,000,000,000,000,000" + ); + assert_eq!( + HumanNumber::from_u128(340_282_366_920_938_463_463_374_607_431_768_211_455).to_string(), + "340,282,366,920,938,463,463,374,607,431,768,211,455", + ); + assert!( + HumanNumber::from_u64_to_gigahash_3_point(1_000_000_000).to_string() == "1.000 GH/s" + ); + } - #[test] - fn human_time() { - use crate::human::HumanTime; - use std::time::Duration; - assert!(HumanTime::into_human(Duration::from_secs(0)).to_string() == "0 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(1)).to_string() == "1 second"); - assert!(HumanTime::into_human(Duration::from_secs(2)).to_string() == "2 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(59)).to_string() == "59 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(60)).to_string() == "1 minute"); - assert!(HumanTime::into_human(Duration::from_secs(61)).to_string() == "1 minute, 1 second"); - assert!(HumanTime::into_human(Duration::from_secs(62)).to_string() == "1 minute, 2 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(120)).to_string() == "2 minutes"); - assert!(HumanTime::into_human(Duration::from_secs(121)).to_string() == "2 minutes, 1 second"); - assert!(HumanTime::into_human(Duration::from_secs(122)).to_string() == "2 minutes, 2 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(179)).to_string() == "2 minutes, 59 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(3599)).to_string() == "59 minutes, 59 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(3600)).to_string() == "1 hour"); - assert!(HumanTime::into_human(Duration::from_secs(3601)).to_string() == "1 hour, 1 second"); - assert!(HumanTime::into_human(Duration::from_secs(3602)).to_string() == "1 hour, 2 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(3660)).to_string() == "1 hour, 1 minute"); - assert!(HumanTime::into_human(Duration::from_secs(3720)).to_string() == "1 hour, 2 minutes"); - assert!(HumanTime::into_human(Duration::from_secs(86399)).to_string() == "23 hours, 59 minutes, 59 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(86400)).to_string() == "1 day"); - assert!(HumanTime::into_human(Duration::from_secs(86401)).to_string() == "1 day, 1 second"); - assert!(HumanTime::into_human(Duration::from_secs(86402)).to_string() == "1 day, 2 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(86460)).to_string() == "1 day, 1 minute"); - assert!(HumanTime::into_human(Duration::from_secs(86520)).to_string() == "1 day, 2 minutes"); - assert!(HumanTime::into_human(Duration::from_secs(90000)).to_string() == "1 day, 1 hour"); - assert!(HumanTime::into_human(Duration::from_secs(93600)).to_string() == "1 day, 2 hours"); - assert!(HumanTime::into_human(Duration::from_secs(604799)).to_string() == "6 days, 23 hours, 59 minutes, 59 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(604800)).to_string() == "7 days"); - assert!(HumanTime::into_human(Duration::from_secs(2630016)).to_string() == "1 month"); - assert!(HumanTime::into_human(Duration::from_secs(3234815)).to_string() == "1 month, 6 days, 23 hours, 59 minutes, 59 seconds"); - assert!(HumanTime::into_human(Duration::from_secs(5260032)).to_string() == "2 months"); - assert!(HumanTime::into_human(Duration::from_secs(31557600)).to_string() == "1 year"); - assert!(HumanTime::into_human(Duration::from_secs(63115200)).to_string() == "2 years"); - assert_eq!( - HumanTime::into_human(Duration::from_secs(18446744073709551615)).to_string(), - "584542046090 years, 7 months, 15 days, 17 hours, 5 minutes, 3 seconds", - ); - } + #[test] + fn human_time() { + use crate::human::HumanTime; + use std::time::Duration; + assert!(HumanTime::into_human(Duration::from_secs(0)).to_string() == "0 seconds"); + assert!(HumanTime::into_human(Duration::from_secs(1)).to_string() == "1 second"); + assert!(HumanTime::into_human(Duration::from_secs(2)).to_string() == "2 seconds"); + assert!(HumanTime::into_human(Duration::from_secs(59)).to_string() == "59 seconds"); + assert!(HumanTime::into_human(Duration::from_secs(60)).to_string() == "1 minute"); + assert!(HumanTime::into_human(Duration::from_secs(61)).to_string() == "1 minute, 1 second"); + assert!( + HumanTime::into_human(Duration::from_secs(62)).to_string() == "1 minute, 2 seconds" + ); + assert!(HumanTime::into_human(Duration::from_secs(120)).to_string() == "2 minutes"); + assert!( + HumanTime::into_human(Duration::from_secs(121)).to_string() == "2 minutes, 1 second" + ); + assert!( + HumanTime::into_human(Duration::from_secs(122)).to_string() == "2 minutes, 2 seconds" + ); + assert!( + HumanTime::into_human(Duration::from_secs(179)).to_string() == "2 minutes, 59 seconds" + ); + assert!( + HumanTime::into_human(Duration::from_secs(3599)).to_string() + == "59 minutes, 59 seconds" + ); + assert!(HumanTime::into_human(Duration::from_secs(3600)).to_string() == "1 hour"); + assert!(HumanTime::into_human(Duration::from_secs(3601)).to_string() == "1 hour, 1 second"); + assert!( + HumanTime::into_human(Duration::from_secs(3602)).to_string() == "1 hour, 2 seconds" + ); + assert!(HumanTime::into_human(Duration::from_secs(3660)).to_string() == "1 hour, 1 minute"); + assert!( + HumanTime::into_human(Duration::from_secs(3720)).to_string() == "1 hour, 2 minutes" + ); + assert!( + HumanTime::into_human(Duration::from_secs(86399)).to_string() + == "23 hours, 59 minutes, 59 seconds" + ); + assert!(HumanTime::into_human(Duration::from_secs(86400)).to_string() == "1 day"); + assert!(HumanTime::into_human(Duration::from_secs(86401)).to_string() == "1 day, 1 second"); + assert!( + HumanTime::into_human(Duration::from_secs(86402)).to_string() == "1 day, 2 seconds" + ); + assert!(HumanTime::into_human(Duration::from_secs(86460)).to_string() == "1 day, 1 minute"); + assert!( + HumanTime::into_human(Duration::from_secs(86520)).to_string() == "1 day, 2 minutes" + ); + assert!(HumanTime::into_human(Duration::from_secs(90000)).to_string() == "1 day, 1 hour"); + assert!(HumanTime::into_human(Duration::from_secs(93600)).to_string() == "1 day, 2 hours"); + assert!( + HumanTime::into_human(Duration::from_secs(604799)).to_string() + == "6 days, 23 hours, 59 minutes, 59 seconds" + ); + assert!(HumanTime::into_human(Duration::from_secs(604800)).to_string() == "7 days"); + assert!(HumanTime::into_human(Duration::from_secs(2630016)).to_string() == "1 month"); + assert!( + HumanTime::into_human(Duration::from_secs(3234815)).to_string() + == "1 month, 6 days, 23 hours, 59 minutes, 59 seconds" + ); + assert!(HumanTime::into_human(Duration::from_secs(5260032)).to_string() == "2 months"); + assert!(HumanTime::into_human(Duration::from_secs(31557600)).to_string() == "1 year"); + assert!(HumanTime::into_human(Duration::from_secs(63115200)).to_string() == "2 years"); + assert_eq!( + HumanTime::into_human(Duration::from_secs(18446744073709551615)).to_string(), + "584542046090 years, 7 months, 15 days, 17 hours, 5 minutes, 3 seconds", + ); + } } diff --git a/src/macros.rs b/src/macros.rs index 76a9246..3a0e8e4 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -39,83 +39,81 @@ // Locks and unwraps an [Arc] macro_rules! lock { - ($arc_mutex:expr) => { - $arc_mutex.lock().unwrap() - }; + ($arc_mutex:expr) => { + $arc_mutex.lock().unwrap() + }; } pub(crate) use lock; // Locks and unwraps a field of a struct, both of them being [Arc] // Yes, I know this is bad code. macro_rules! lock2 { - ($arc_mutex:expr, $arc_mutex_two:ident) => { - $arc_mutex.lock().unwrap().$arc_mutex_two.lock().unwrap() - }; + ($arc_mutex:expr, $arc_mutex_two:ident) => { + $arc_mutex.lock().unwrap().$arc_mutex_two.lock().unwrap() + }; } pub(crate) use lock2; // Creates a new [Arc] macro_rules! arc_mut { - ($arc_mutex:expr) => { - std::sync::Arc::new(std::sync::Mutex::new($arc_mutex)) - }; + ($arc_mutex:expr) => { + std::sync::Arc::new(std::sync::Mutex::new($arc_mutex)) + }; } pub(crate) use arc_mut; // Sleeps a [std::thread] using milliseconds macro_rules! sleep { ($millis:expr) => { - std::thread::sleep(std::time::Duration::from_millis($millis)) + std::thread::sleep(std::time::Duration::from_millis($millis)) }; } pub(crate) use sleep; // Flips a [bool] in place macro_rules! flip { - ($b:expr) => { - match $b { - true|false => $b = !$b, - } - }; + ($b:expr) => { + match $b { + true | false => $b = !$b, + } + }; } pub(crate) use flip; //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod test { - #[test] - fn lock() { - use std::sync::{Arc,Mutex}; - let arc_mutex = Arc::new(Mutex::new(false)); - *lock!(arc_mutex) = true; - assert!(*lock!(arc_mutex) == true); - } + #[test] + fn lock() { + use std::sync::{Arc, Mutex}; + let arc_mutex = Arc::new(Mutex::new(false)); + *lock!(arc_mutex) = true; + assert!(*lock!(arc_mutex) == true); + } - #[test] - fn lock2() { - struct Ab { - a: Arc>, - } - use std::sync::{Arc,Mutex}; - let arc_mutex = Arc::new(Mutex::new( - Ab { - a: Arc::new(Mutex::new(false)), - } - )); - *lock2!(arc_mutex,a) = true; - assert!(*lock2!(arc_mutex,a) == true); - } + #[test] + fn lock2() { + struct Ab { + a: Arc>, + } + use std::sync::{Arc, Mutex}; + let arc_mutex = Arc::new(Mutex::new(Ab { + a: Arc::new(Mutex::new(false)), + })); + *lock2!(arc_mutex, a) = true; + assert!(*lock2!(arc_mutex, a) == true); + } - #[test] - fn arc_mut() { - let a = arc_mut!(false); - assert!(*lock!(a) == false); - } + #[test] + fn arc_mut() { + let a = arc_mut!(false); + assert!(*lock!(a) == false); + } - #[test] - fn flip() { - let mut b = true; - flip!(b); - assert!(b == false); - } + #[test] + fn flip() { + let mut b = true; + flip!(b); + assert!(b == false); + } } diff --git a/src/main.rs b/src/main.rs index 670a517..4665ec3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,66 +22,59 @@ #[cfg(not(target_pointer_width = "64"))] compile_error!("gupax is only compatible with 64-bit CPUs"); -#[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", -)))] +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux",)))] compile_error!("gupax is only built for windows/macos/linux"); //---------------------------------------------------------------------------------------------------- Imports // egui/eframe +use eframe::{egui, NativeOptions}; use egui::{ - TextStyle::*, - Color32, - FontFamily::Proportional, - TextStyle,Spinner, - Layout,Align, - FontId,Label,RichText,Stroke,Vec2,Button,SelectableLabel, - Key,Modifiers,TextEdit, - CentralPanel,TopBottomPanel, - Hyperlink, + Align, Button, CentralPanel, Color32, FontFamily::Proportional, FontId, Hyperlink, Key, Label, + Layout, Modifiers, RichText, SelectableLabel, Spinner, Stroke, TextEdit, TextStyle, + TextStyle::*, TopBottomPanel, Vec2, }; use egui_extras::RetainedImage; -use eframe::{egui,NativeOptions}; // Logging +use env_logger::{Builder, WriteStyle}; use log::*; -use env_logger::{Builder,WriteStyle}; // Regex use ::regex::Regex; // Serde -use serde::{Serialize,Deserialize}; +use serde::{Deserialize, Serialize}; // std use std::{ - env, - io::Write, - process::exit, - sync::{Arc,Mutex}, - time::Instant, - path::PathBuf, + env, + io::Write, + path::PathBuf, + process::exit, + sync::{Arc, Mutex}, + time::Instant, }; // Sysinfo -use sysinfo::SystemExt; use sysinfo::CpuExt; +use sysinfo::SystemExt; // Modules //mod benchmark; -mod ferris; mod constants; -mod node; mod disk; -mod status; +mod ferris; +mod free; mod gupax; -mod p2pool; -mod xmrig; -mod update; mod helper; mod human; -mod regex; -mod xmr; mod macros; -mod free; +mod node; +mod p2pool; mod panic; -use {macros::*,crate::regex::*,ferris::*,constants::*,node::*,disk::*,update::*,gupax::*,helper::*}; +mod regex; +mod status; +mod update; +mod xmr; +mod xmrig; +use { + crate::regex::*, constants::*, disk::*, ferris::*, gupax::*, helper::*, macros::*, node::*, + update::*, +}; // Sudo (dummy values for Windows) mod sudo; @@ -94,528 +87,579 @@ extern crate sudo as sudo_check; // 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] - img: Images, // Custom Struct holding pre-compiled bytes of [Images] + // 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] + img: Images, // Custom Struct holding pre-compiled bytes of [Images] } 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 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 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()); + #[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.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()); + 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.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); + // 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), - img: Images::new(), - }; - //---------------------------------------------------------------------------------------------------- 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 | 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), + img: Images::new(), + }; + //---------------------------------------------------------------------------------------------------- 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); + 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); + // 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); - } + // 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); + 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); + 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); + //---------------------------------------------------------------------------------------------------- + // 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.get(0) { - 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.get(0) { - 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; - } + //---------------------------------------------------------------------------------------------------- + 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.get(0) { + 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.get(0) { + 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)); + // 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 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; + // 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); + // 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] + 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"); + // 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); - } + // 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); - } + // 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 - } + info!("App ... OK"); + app + } - #[cold] - #[inline(never)] - pub fn gather_backup_hosts(&self) -> Option> { - if !self.state.p2pool.backup_host { - return None; - } + #[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; - } + // 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()); + 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; - } + // 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 (ip, rpc, zmq) = RemoteNode::get_ip_rpc_zmq(&pinged_node.ip); - let node = Node { - ip: ip.into(), - rpc: rpc.into(), - zmq: zmq.into(), - }; + let node = Node { + ip: ip.into(), + rpc: rpc.into(), + zmq: zmq.into(), + }; - vec.push(node); - } + 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() - ) - } - } + 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, + About, + Status, + Gupax, + P2pool, + Xmrig, } impl Default for Tab { @@ -625,134 +669,138 @@ impl Default for Tab { } //---------------------------------------------------------------------------------------------------- CPU Benchmarks. -#[derive(Debug,Serialize,Deserialize)] +#[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, + 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 + 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, + YesNo, + StayQuit, + ResetState, + ResetNode, + Okay, + Quit, + Sudo, + WindowsAdmin, + Debug, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ErrorFerris { - Happy, - Cute, - Oops, - Error, - Panic, - Sudo, + 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 + 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() - } + 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, - } - } + 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, - }; - } + // 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(); - } + // 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) - } + // 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) + } } //---------------------------------------------------------------------------------------------------- [Images] struct struct Images { - banner: RetainedImage, - happy: RetainedImage, - cute: RetainedImage, - oops: RetainedImage, - error: RetainedImage, - panic: RetainedImage, - sudo: RetainedImage, + banner: RetainedImage, + happy: RetainedImage, + cute: RetainedImage, + oops: RetainedImage, + error: RetainedImage, + panic: RetainedImage, + sudo: RetainedImage, } impl Images { - #[cold] - #[inline(never)] - fn new() -> Self { - Self { - banner: RetainedImage::from_image_bytes("banner.png", BYTES_BANNER).unwrap(), - happy: RetainedImage::from_image_bytes("happy.png", FERRIS_HAPPY).unwrap(), - cute: RetainedImage::from_image_bytes("cute.png", FERRIS_CUTE).unwrap(), - oops: RetainedImage::from_image_bytes("oops.png", FERRIS_OOPS).unwrap(), - error: RetainedImage::from_image_bytes("error.png", FERRIS_ERROR).unwrap(), - panic: RetainedImage::from_image_bytes("panic.png", FERRIS_PANIC).unwrap(), - sudo: RetainedImage::from_image_bytes("panic.png", FERRIS_SUDO).unwrap(), - } - } + #[cold] + #[inline(never)] + fn new() -> Self { + Self { + banner: RetainedImage::from_image_bytes("banner.png", BYTES_BANNER).unwrap(), + happy: RetainedImage::from_image_bytes("happy.png", FERRIS_HAPPY).unwrap(), + cute: RetainedImage::from_image_bytes("cute.png", FERRIS_CUTE).unwrap(), + oops: RetainedImage::from_image_bytes("oops.png", FERRIS_OOPS).unwrap(), + error: RetainedImage::from_image_bytes("error.png", FERRIS_ERROR).unwrap(), + panic: RetainedImage::from_image_bytes("panic.png", FERRIS_PANIC).unwrap(), + sudo: RetainedImage::from_image_bytes("panic.png", FERRIS_SUDO).unwrap(), + } + } } //---------------------------------------------------------------------------------------------------- [Pressed] enum @@ -763,323 +811,534 @@ impl Images { // 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)] +#[derive(Debug, Clone, Eq, PartialEq)] enum KeyPressed { - F11, - Up, - Down, - Esc, - Z, - X, - C, - V, - S, - R, - D, - None, + 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 } + #[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(); + 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) { - use env_logger::fmt::Color; - 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)); + use env_logger::fmt::Color; + 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 mut style = buf.style(); - let level = match record.level() { - Level::Error => { style.set_color(Color::Red); "ERROR" }, - Level::Warn => { style.set_color(Color::Yellow); "WARN" }, - Level::Info => { style.set_color(Color::White); "INFO" }, - Level::Debug => { style.set_color(Color::Blue); "DEBUG" }, - Level::Trace => { style.set_color(Color::Magenta); "TRACE" }, - }; - writeln!( - buf, - "[{}] [{}] [{}:{}] {}", - style.set_bold(true).value(level), - buf.style().set_dimmed(true).value(format!("{:.3}", now.elapsed().as_secs_f32())), - buf.style().set_dimmed(true).value(record.file().unwrap_or("???")), - buf.style().set_dimmed(true).value(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); + Builder::new() + .format(move |buf, record| { + let mut style = buf.style(); + let level = match record.level() { + Level::Error => { + style.set_color(Color::Red); + "ERROR" + } + Level::Warn => { + style.set_color(Color::Yellow); + "WARN" + } + Level::Info => { + style.set_color(Color::White); + "INFO" + } + Level::Debug => { + style.set_color(Color::Blue); + "DEBUG" + } + Level::Trace => { + style.set_color(Color::Magenta); + "TRACE" + } + }; + writeln!( + buf, + "[{}] [{}] [{}:{}] {}", + style.set_bold(true).value(level), + buf.style() + .set_dimmed(true) + .value(format!("{:.3}", now.elapsed().as_secs_f32())), + buf.style() + .set_dimmed(true) + .value(record.file().unwrap_or("???")), + buf.style() + .set_dimmed(true) + .value(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 + 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()..."); - } + // 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-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-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-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..."); - } + // [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) }, - } + 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) }, - } + 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) }, - } + 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) }, - } + 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); +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); - } + 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 + // 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) }, - } + 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) }, - } + 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 @@ -1087,54 +1346,86 @@ pub fn get_exe_dir() -> Result { #[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(()) + 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); }, - } + 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); + 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] @@ -1149,247 +1440,280 @@ fn cmp_f64(a: f64, b: f64) -> std::cmp::Ordering { //---------------------------------------------------------------------------------------------------- Main [App] frame fn main() { - let now = Instant::now(); + let now = Instant::now(); - // Set custom panic hook. - crate::panic::set_panic_hook(now); + // Set custom panic hook. + crate::panic::set_panic_hook(now); - // Init logger. - init_logger(now); - let mut app = App::new(now); - init_auto(&mut app); + // Init logger. + init_logger(now); + let mut app = App::new(now); + init_auto(&mut app); - // Init GUI stuff. - let selected_width = app.state.gupax.selected_width as f32; - let selected_height = app.state.gupax.selected_height as f32; - let initial_window_size = if selected_width > APP_MAX_WIDTH || selected_height > APP_MAX_HEIGHT { - warn!("App | Set width or height was greater than the maximum! Starting with the default resolution..."); - Some(Vec2::new(APP_DEFAULT_WIDTH, APP_DEFAULT_HEIGHT)) - } else { - Some(Vec2::new(app.state.gupax.selected_width as f32, app.state.gupax.selected_height as f32)) - }; - let options = init_options(initial_window_size); + // Init GUI stuff. + let selected_width = app.state.gupax.selected_width as f32; + let selected_height = app.state.gupax.selected_height as f32; + let initial_window_size = if selected_width > APP_MAX_WIDTH || selected_height > APP_MAX_HEIGHT + { + warn!("App | Set width or height was greater than the maximum! Starting with the default resolution..."); + Some(Vec2::new(APP_DEFAULT_WIDTH, APP_DEFAULT_HEIGHT)) + } else { + Some(Vec2::new( + app.state.gupax.selected_width as f32, + app.state.gupax.selected_height as f32, + )) + }; + let options = init_options(initial_window_size); - // Gupax folder cleanup. - match clean_dir() { - Ok(_) => info!("Temporary folder cleanup ... OK"), - Err(e) => warn!("Could not cleanup [gupax_tmp] folders: {}", e), - } + // Gupax folder cleanup. + match clean_dir() { + Ok(_) => info!("Temporary folder cleanup ... OK"), + Err(e) => warn!("Could not cleanup [gupax_tmp] folders: {}", e), + } - let resolution = Vec2::new(selected_width, selected_height); + let resolution = Vec2::new(selected_width, selected_height); - // Run Gupax. - info!("/*************************************/ Init ... OK /*************************************/"); - eframe::run_native(&app.name_version.clone(), options, Box::new(move |cc| Box::new(App::cc(cc, resolution, app))),); + // Run Gupax. + info!("/*************************************/ Init ... OK /*************************************/"); + eframe::run_native( + &app.name_version.clone(), + options, + Box::new(move |cc| 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()] -----------"); + 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 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::Xmrig, - Tab::Status => self.tab = Tab::About, - Tab::Gupax => self.tab = Tab::Status, - Tab::P2pool => self.tab = Tab::Gupax, - Tab::Xmrig => self.tab = Tab::P2pool, - }; - // 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::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), - _ => (), - }; + 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); } - // Refresh AT LEAST once a second - debug!("App | Refreshing frame once per second"); - ctx.request_repaint_after(SECOND); + // 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 + } + }); - // 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); + // 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(); - // 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 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::Xmrig, + Tab::Status => self.tab = Tab::About, + Tab::Gupax => self.tab = Tab::Status, + Tab::P2pool => self.tab = Tab::Gupax, + Tab::Xmrig => self.tab = Tab::P2pool, + }; + // 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::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), + _ => (), + }; + } - // 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| { + // 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; @@ -1562,302 +1886,570 @@ impl eframe::App for App { Quit => if ui.add_sized([width, height], Button::new("Quit")).clicked() { exit(1); }, } })}); - return - } + 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); - if og.status != self.state.status || - og.gupax != self.state.gupax || - og.p2pool != self.state.p2pool || - og.xmrig != self.state.xmrig || - self.og_node_vec != self.node_vec || - self.og_pool_vec != self.pool_vec { - self.diff = true; - } else { - self.diff = false; - } - drop(og); + // 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); + if og.status != self.state.status + || og.gupax != self.state.gupax + || og.p2pool != self.state.p2pool + || og.xmrig != self.state.xmrig + || self.og_node_vec != self.node_vec + || self.og_pool_vec != self.pool_vec + { + self.diff = true; + } else { + self.diff = false; + } + drop(og); - // Top: Tabs - debug!("App | Rendering TOP tabs"); - TopBottomPanel::top("top").show(ctx, |ui| { - let width = (self.width - (SPACE*10.0))/5.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.add_space(4.0); - }); + // Top: Tabs + debug!("App | Rendering TOP tabs"); + TopBottomPanel::top("top").show(ctx, |ui| { + let width = (self.width - (SPACE * 10.0)) / 5.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.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), - }; - }); + // 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), - }; - } - }); + // [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() { - lock!(self.og).update_absolute_path(); - 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() { - lock!(self.og).update_absolute_path(); - 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() { - lock!(self.og).update_absolute_path(); - 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() { - lock!(self.og).update_absolute_path(); - 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); - } - } - } - }); - }, - _ => (), - } - }); - }); - }); + // [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() + { + lock!(self.og).update_absolute_path(); + 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() + { + lock!(self.og).update_absolute_path(); + 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() + { + lock!(self.og).update_absolute_path(); + 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() + { + lock!(self.og).update_absolute_path(); + 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| { + // 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(); @@ -1994,26 +2586,25 @@ path_xmr: {:#?}\n } } }); - } + } } //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod test { - #[test] - fn detect_benchmark_cpu() { - use super::{Benchmark,cmp_f64}; + #[test] + fn detect_benchmark_cpu() { + use super::{cmp_f64, Benchmark}; - let cpu = "AMD Ryzen 9 5950X 16-Core Processor"; + 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 - }; + 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"); - } + assert!(benchmarks[0].cpu == "AMD Ryzen 9 5950X 16-Core Processor"); + } } diff --git a/src/node.rs b/src/node.rs index dc4bd99..3bc21be 100644 --- a/src/node.rs +++ b/src/node.rs @@ -15,246 +15,286 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{ - constants::*, - macros::*, -}; -use serde::{Serialize,Deserialize}; -use rand::{thread_rng, Rng}; -use std::time::{Instant,Duration}; -use std::sync::{Arc,Mutex}; +use crate::{constants::*, macros::*}; use egui::Color32; +use hyper::{client::HttpConnector, Body, Client, Request}; use log::*; -use hyper::{ - client::HttpConnector, - Client,Body,Request, -}; +use rand::{thread_rng, Rng}; +use serde::{Deserialize, Serialize}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; //---------------------------------------------------------------------------------------------------- Node list // Remote Monero Nodes with ZMQ enabled. // The format is an array of tuples consisting of: (IP, LOCATION, RPC_PORT, ZMQ_PORT) pub const REMOTE_NODES: [(&str, &str, &str, &str); 18] = [ - ("monero.10z.com.ar", "Argentina", "18089", "18084"), - ("monero1.heitechsoft.com","Canada", "18081", "18084"), - ("node.monerodevs.org", "Canada", "18089", "18084"), - ("xmr3.rs.me", "Germany", "18089", "18084"), - ("node.cryptocano.de", "Germany", "18089", "18083"), - ("p2pmd.xmrvsbeast.com", "Germany", "18081", "18083"), - ("fbx.tranbert.com", "France", "18089", "18084"), - ("node2.monerodevs.org", "France", "18089", "18084"), - ("home.allantaylor.kiwi", "New Zealand", "18089", "18083"), - ("xmr1.rs.me", "Singapore", "18089", "18084"), - ("p2pool.uk", "United Kingdom", "18089", "18084"), - ("xmr2.rs.me", "United States", "18089", "18084"), - ("xmr.support", "United States", "18081", "18083"), - ("sf.xmr.support", "United States", "18081", "18083"), - ("xmrbandwagon.hopto.org", "United States", "18081", "18084"), - ("xmr.spotlightsound.com", "United States", "18081", "18084"), - ("xmrnode.facspro.net", "United States", "18089", "18084"), - ("node.richfowler.net", "United States", "18089", "18084"), + ("monero.10z.com.ar", "Argentina", "18089", "18084"), + ("monero1.heitechsoft.com", "Canada", "18081", "18084"), + ("node.monerodevs.org", "Canada", "18089", "18084"), + ("xmr3.rs.me", "Germany", "18089", "18084"), + ("node.cryptocano.de", "Germany", "18089", "18083"), + ("p2pmd.xmrvsbeast.com", "Germany", "18081", "18083"), + ("fbx.tranbert.com", "France", "18089", "18084"), + ("node2.monerodevs.org", "France", "18089", "18084"), + ("home.allantaylor.kiwi", "New Zealand", "18089", "18083"), + ("xmr1.rs.me", "Singapore", "18089", "18084"), + ("p2pool.uk", "United Kingdom", "18089", "18084"), + ("xmr2.rs.me", "United States", "18089", "18084"), + ("xmr.support", "United States", "18081", "18083"), + ("sf.xmr.support", "United States", "18081", "18083"), + ("xmrbandwagon.hopto.org", "United States", "18081", "18084"), + ("xmr.spotlightsound.com", "United States", "18081", "18084"), + ("xmrnode.facspro.net", "United States", "18089", "18084"), + ("node.richfowler.net", "United States", "18089", "18084"), ]; pub const REMOTE_NODE_LENGTH: usize = REMOTE_NODES.len(); // Iterate through all nodes, find the longest domain. pub const REMOTE_NODE_MAX_CHARS: usize = { - let mut len = 0; - let mut index = 0; + let mut len = 0; + let mut index = 0; - while index < REMOTE_NODE_LENGTH { - let (node, _, _, _) = REMOTE_NODES[index]; - if node.len() > len { - len = node.len(); - } - index += 1; - } + while index < REMOTE_NODE_LENGTH { + let (node, _, _, _) = REMOTE_NODES[index]; + if node.len() > len { + len = node.len(); + } + index += 1; + } - assert!(len != 0); - len + assert!(len != 0); + len }; pub struct RemoteNode { - pub ip: &'static str, - pub location: &'static str, - pub rpc: &'static str, - pub zmq: &'static str, + pub ip: &'static str, + pub location: &'static str, + pub rpc: &'static str, + pub zmq: &'static str, } impl Default for RemoteNode { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl RemoteNode { - pub fn new() -> Self { - Self::get_random_same_ok() - } + pub fn new() -> Self { + Self::get_random_same_ok() + } - pub fn check_exists(og_ip: &str) -> String { - for (ip, _, _, _) in REMOTE_NODES { - if og_ip == ip { - info!("Found remote node in array: {}", ip); - return ip.to_string() - } - } - let ip = REMOTE_NODES[0].0.to_string(); - warn!("[{}] remote node does not exist, returning default: {}", og_ip, ip); - ip - } + pub fn check_exists(og_ip: &str) -> String { + for (ip, _, _, _) in REMOTE_NODES { + if og_ip == ip { + info!("Found remote node in array: {}", ip); + return ip.to_string(); + } + } + let ip = REMOTE_NODES[0].0.to_string(); + warn!( + "[{}] remote node does not exist, returning default: {}", + og_ip, ip + ); + ip + } - // Returns a default if IP is not found. - pub fn from_ip(from_ip: &str) -> Self { - for (ip, location, rpc, zmq) in REMOTE_NODES { - if from_ip == ip { - return Self { ip, location, rpc, zmq } - } - } - Self::new() - } + // Returns a default if IP is not found. + pub fn from_ip(from_ip: &str) -> Self { + for (ip, location, rpc, zmq) in REMOTE_NODES { + if from_ip == ip { + return Self { + ip, + location, + rpc, + zmq, + }; + } + } + Self::new() + } - // Returns a default if index is not found in the const array. - pub fn from_index(index: usize) -> Self { - if index > REMOTE_NODE_LENGTH { - Self::new() - } else { - let (ip, location, rpc, zmq) = REMOTE_NODES[index]; - Self { ip, location, rpc, zmq } - } - } + // Returns a default if index is not found in the const array. + pub fn from_index(index: usize) -> Self { + if index > REMOTE_NODE_LENGTH { + Self::new() + } else { + let (ip, location, rpc, zmq) = REMOTE_NODES[index]; + Self { + ip, + location, + rpc, + zmq, + } + } + } - pub fn from_tuple(t: (&'static str, &'static str, &'static str, &'static str)) -> Self { - let (ip, location, rpc, zmq) = (t.0, t.1, t.2, t.3); - Self { ip, location, rpc, zmq } - } + pub fn from_tuple(t: (&'static str, &'static str, &'static str, &'static str)) -> Self { + let (ip, location, rpc, zmq) = (t.0, t.1, t.2, t.3); + Self { + ip, + location, + rpc, + zmq, + } + } - pub fn get_ip_rpc_zmq(og_ip: &str) -> (&str, &str, &str) { - for (ip, _, rpc, zmq) in REMOTE_NODES { - if og_ip == ip { return (ip, rpc, zmq) } - } - let (ip, _, rpc, zmq) = REMOTE_NODES[0]; - (ip, rpc, zmq) - } + pub fn get_ip_rpc_zmq(og_ip: &str) -> (&str, &str, &str) { + for (ip, _, rpc, zmq) in REMOTE_NODES { + if og_ip == ip { + return (ip, rpc, zmq); + } + } + let (ip, _, rpc, zmq) = REMOTE_NODES[0]; + (ip, rpc, zmq) + } - // Return a random node (that isn't the one already selected). - pub fn get_random(current_ip: &str) -> String { - let mut rng = thread_rng().gen_range(0..REMOTE_NODE_LENGTH); - let mut node = REMOTE_NODES[rng].0; - while current_ip == node { - rng = thread_rng().gen_range(0..REMOTE_NODE_LENGTH); - node = REMOTE_NODES[rng].0; - } - node.to_string() - } + // Return a random node (that isn't the one already selected). + pub fn get_random(current_ip: &str) -> String { + let mut rng = thread_rng().gen_range(0..REMOTE_NODE_LENGTH); + let mut node = REMOTE_NODES[rng].0; + while current_ip == node { + rng = thread_rng().gen_range(0..REMOTE_NODE_LENGTH); + node = REMOTE_NODES[rng].0; + } + node.to_string() + } - // Return a random valid node (no input str). - pub fn get_random_same_ok() -> Self { - let rng = thread_rng().gen_range(0..REMOTE_NODE_LENGTH); - Self::from_index(rng) - } + // Return a random valid node (no input str). + pub fn get_random_same_ok() -> Self { + let rng = thread_rng().gen_range(0..REMOTE_NODE_LENGTH); + Self::from_index(rng) + } - // Return the node [-1] of this one - pub fn get_last(current_ip: &str) -> String { - let mut found = false; - let mut last = current_ip; - for (ip, _, _, _) in REMOTE_NODES { - if found { return ip.to_string() } - if current_ip == ip { found = true; } else { last = ip; } - } - last.to_string() - } + // Return the node [-1] of this one + pub fn get_last(current_ip: &str) -> String { + let mut found = false; + let mut last = current_ip; + for (ip, _, _, _) in REMOTE_NODES { + if found { + return ip.to_string(); + } + if current_ip == ip { + found = true; + } else { + last = ip; + } + } + last.to_string() + } - // Return the node [+1] of this one - pub fn get_next(current_ip: &str) -> String { - let mut found = false; - for (ip, _, _, _) in REMOTE_NODES { - if found { return ip.to_string() } - if current_ip == ip { found = true; } - } - current_ip.to_string() - } + // Return the node [+1] of this one + pub fn get_next(current_ip: &str) -> String { + let mut found = false; + for (ip, _, _, _) in REMOTE_NODES { + if found { + return ip.to_string(); + } + if current_ip == ip { + found = true; + } + } + current_ip.to_string() + } - // This returns relative to the ping. - pub fn get_last_from_ping(current_ip: &str, nodes: &Vec) -> String { - let mut found = false; - let mut last = current_ip; - for data in nodes { - if found { return last.to_string() } - if current_ip == data.ip { found = true; } else { last = data.ip; } - } - last.to_string() - } + // This returns relative to the ping. + pub fn get_last_from_ping(current_ip: &str, nodes: &Vec) -> String { + let mut found = false; + let mut last = current_ip; + for data in nodes { + if found { + return last.to_string(); + } + if current_ip == data.ip { + found = true; + } else { + last = data.ip; + } + } + last.to_string() + } - pub fn get_next_from_ping(current_ip: &str, nodes: &Vec) -> String { - let mut found = false; - for data in nodes { - if found { return data.ip.to_string() } - if current_ip == data.ip { found = true; } - } - current_ip.to_string() - } + pub fn get_next_from_ping(current_ip: &str, nodes: &Vec) -> String { + let mut found = false; + for data in nodes { + if found { + return data.ip.to_string(); + } + if current_ip == data.ip { + found = true; + } + } + current_ip.to_string() + } } impl std::fmt::Display for RemoteNode { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{:#?}", self.ip) - } + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:#?}", self.ip) + } } //---------------------------------------------------------------------------------------------------- Formatting // 5000 = 4 max length pub fn format_ms(ms: u128) -> String { - match ms.to_string().len() { - 1 => format!("{ms}ms "), - 2 => format!("{ms}ms "), - 3 => format!("{ms}ms "), - _ => format!("{ms}ms"), - } + match ms.to_string().len() { + 1 => format!("{ms}ms "), + 2 => format!("{ms}ms "), + 3 => format!("{ms}ms "), + _ => format!("{ms}ms"), + } } // format_ip_location(monero1.heitechsoft.com) -> "monero1.heitechsoft.com | XX - LOCATION" // [extra_space] controls whether extra space is appended so the list aligns. pub fn format_ip_location(og_ip: &str, extra_space: bool) -> String { - for (ip, location, _, _) in REMOTE_NODES { - if og_ip == ip { - let ip = if extra_space { format_ip(ip) } else { ip.to_string() }; - return format!("{ip} | {location}"); - } - } - "??? | ???".to_string() + for (ip, location, _, _) in REMOTE_NODES { + if og_ip == ip { + let ip = if extra_space { + format_ip(ip) + } else { + ip.to_string() + }; + return format!("{ip} | {location}"); + } + } + "??? | ???".to_string() } pub fn format_ip(ip: &str) -> String { - const _: () = if 23 != REMOTE_NODE_MAX_CHARS { panic!(); }; - format!("{ip: >23}") + const _: () = if 23 != REMOTE_NODE_MAX_CHARS { + panic!(); + }; + format!("{ip: >23}") } //---------------------------------------------------------------------------------------------------- Node data -pub const GREEN_NODE_PING: u128 = 300; +pub const GREEN_NODE_PING: u128 = 300; // yellow is anything in-between green/red -pub const RED_NODE_PING: u128 = 500; +pub const RED_NODE_PING: u128 = 500; pub const TIMEOUT_NODE_PING: u128 = 5000; #[derive(Debug, Clone)] pub struct NodeData { - pub ip: &'static str, - pub ms: u128, - pub color: Color32, + pub ip: &'static str, + pub ms: u128, + pub color: Color32, } impl NodeData { - pub fn new_vec() -> Vec { - let mut vec = Vec::new(); - for (ip, _, _, _) in REMOTE_NODES { - vec.push(Self { - ip, - ms: 0, - color: Color32::LIGHT_GRAY, - }); - } - vec - } + pub fn new_vec() -> Vec { + let mut vec = Vec::new(); + for (ip, _, _, _) in REMOTE_NODES { + vec.push(Self { + ip, + ms: 0, + color: Color32::LIGHT_GRAY, + }); + } + vec + } } //---------------------------------------------------------------------------------------------------- `/get_info` @@ -265,305 +305,305 @@ impl NodeData { // we only need a few to verify the node is ok. #[derive(Debug, serde::Deserialize)] struct GetInfo<'a> { - id: &'a str, - jsonrpc: &'a str, - result: GetInfoResult, + id: &'a str, + jsonrpc: &'a str, + result: GetInfoResult, } #[derive(Debug, serde::Deserialize)] struct GetInfoResult { - mainnet: bool, - synchronized: bool, + mainnet: bool, + synchronized: bool, } //---------------------------------------------------------------------------------------------------- Ping data #[derive(Debug)] pub struct Ping { - pub nodes: Vec, - pub fastest: &'static str, - pub pinging: bool, - pub msg: String, - pub prog: f32, - pub pinged: bool, - pub auto_selected: bool, + pub nodes: Vec, + pub fastest: &'static str, + pub pinging: bool, + pub msg: String, + pub prog: f32, + pub pinged: bool, + pub auto_selected: bool, } impl Default for Ping { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl Ping { - pub fn new() -> Self { - Self { - nodes: NodeData::new_vec(), - fastest: REMOTE_NODES[0].0, - pinging: false, - msg: "No ping in progress".to_string(), - prog: 0.0, - pinged: false, - auto_selected: true, - } - } + pub fn new() -> Self { + Self { + nodes: NodeData::new_vec(), + fastest: REMOTE_NODES[0].0, + pinging: false, + msg: "No ping in progress".to_string(), + prog: 0.0, + pinged: false, + auto_selected: true, + } + } - //---------------------------------------------------------------------------------------------------- Main Ping function - #[cold] - #[inline(never)] - // Intermediate function for spawning thread - pub fn spawn_thread(ping: &Arc>) { - info!("Spawning ping thread..."); - let ping = Arc::clone(ping); - std::thread::spawn(move|| { - let now = Instant::now(); - match Self::ping(&ping) { - Ok(msg) => { - info!("Ping ... OK"); - lock!(ping).msg = msg; - lock!(ping).pinged = true; - lock!(ping).auto_selected = false; - lock!(ping).prog = 100.0; - }, - Err(err) => { - error!("Ping ... FAIL ... {}", err); - lock!(ping).pinged = false; - lock!(ping).msg = err.to_string(); - }, - } - info!("Ping ... Took [{}] seconds...", now.elapsed().as_secs_f32()); - lock!(ping).pinging = false; - }); - } + //---------------------------------------------------------------------------------------------------- Main Ping function + #[cold] + #[inline(never)] + // Intermediate function for spawning thread + pub fn spawn_thread(ping: &Arc>) { + info!("Spawning ping thread..."); + let ping = Arc::clone(ping); + std::thread::spawn(move || { + let now = Instant::now(); + match Self::ping(&ping) { + Ok(msg) => { + info!("Ping ... OK"); + lock!(ping).msg = msg; + lock!(ping).pinged = true; + lock!(ping).auto_selected = false; + lock!(ping).prog = 100.0; + } + Err(err) => { + error!("Ping ... FAIL ... {}", err); + lock!(ping).pinged = false; + lock!(ping).msg = err.to_string(); + } + } + info!("Ping ... Took [{}] seconds...", now.elapsed().as_secs_f32()); + lock!(ping).pinging = false; + }); + } - // This is for pinging the remote nodes to - // find the fastest/slowest one for the user. - // The process: - // - Send [get_info] JSON-RPC request over HTTP to all IPs - // - Measure each request in milliseconds - // - Timeout on requests over 5 seconds - // - Add data to appropriate struct - // - Sorting fastest to lowest is automatic (fastest nodes return ... the fastest) - // - // This used to be done 3x linearly but after testing, sending a single - // JSON-RPC call to all IPs asynchronously resulted in the same data. - // - // <300ms = GREEN - // >300ms = YELLOW - // >500ms = RED - // timeout = BLACK - // default = GRAY - #[cold] - #[inline(never)] - #[tokio::main] - pub async fn ping(ping: &Arc>) -> Result { - // Start ping - let ping = Arc::clone(ping); - lock!(ping).pinging = true; - lock!(ping).prog = 0.0; - let percent = (100.0 / (REMOTE_NODE_LENGTH as f32)).floor(); + // This is for pinging the remote nodes to + // find the fastest/slowest one for the user. + // The process: + // - Send [get_info] JSON-RPC request over HTTP to all IPs + // - Measure each request in milliseconds + // - Timeout on requests over 5 seconds + // - Add data to appropriate struct + // - Sorting fastest to lowest is automatic (fastest nodes return ... the fastest) + // + // This used to be done 3x linearly but after testing, sending a single + // JSON-RPC call to all IPs asynchronously resulted in the same data. + // + // <300ms = GREEN + // >300ms = YELLOW + // >500ms = RED + // timeout = BLACK + // default = GRAY + #[cold] + #[inline(never)] + #[tokio::main] + pub async fn ping(ping: &Arc>) -> Result { + // Start ping + let ping = Arc::clone(ping); + lock!(ping).pinging = true; + lock!(ping).prog = 0.0; + let percent = (100.0 / (REMOTE_NODE_LENGTH as f32)).floor(); - // Create HTTP client - let info = "Creating HTTP Client".to_string(); - lock!(ping).msg = info; - let client: Client = Client::builder() - .build(HttpConnector::new()); + // Create HTTP client + let info = "Creating HTTP Client".to_string(); + lock!(ping).msg = info; + let client: Client = Client::builder().build(HttpConnector::new()); - // Random User Agent - let rand_user_agent = crate::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)); + // Random User Agent + let rand_user_agent = crate::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)); - for (ip, _, rpc, zmq) in REMOTE_NODES { - let client = client.clone(); - let ping = Arc::clone(&ping); - let node_vec = Arc::clone(&node_vec); - let request = Request::builder() - .method("POST") - .uri("http://".to_string() + ip + ":" + rpc + "/json_rpc") - .header("User-Agent", rand_user_agent) - .body(hyper::Body::from(r#"{"jsonrpc":"2.0","id":"0","method":"get_info"}"#)) - .unwrap(); - let handle = tokio::task::spawn(async move { Self::response(client, request, ip, ping, percent, node_vec).await; }); - handles.push(handle); - } + for (ip, _, rpc, zmq) in REMOTE_NODES { + let client = client.clone(); + let ping = Arc::clone(&ping); + let node_vec = Arc::clone(&node_vec); + let request = Request::builder() + .method("POST") + .uri("http://".to_string() + ip + ":" + rpc + "/json_rpc") + .header("User-Agent", rand_user_agent) + .body(hyper::Body::from( + r#"{"jsonrpc":"2.0","id":"0","method":"get_info"}"#, + )) + .unwrap(); + let handle = tokio::task::spawn(async move { + Self::response(client, request, ip, ping, percent, node_vec).await; + }); + handles.push(handle); + } - for handle in handles { - handle.await?; - } + for handle in handles { + handle.await?; + } - let mut node_vec = std::mem::take(&mut *lock!(node_vec)); - node_vec.sort_by(|a, b| a.ms.cmp(&b.ms)); - let fastest_info = format!("Fastest node: {}ms ... {}", node_vec[0].ms, node_vec[0].ip); + let mut node_vec = std::mem::take(&mut *lock!(node_vec)); + node_vec.sort_by(|a, b| a.ms.cmp(&b.ms)); + let fastest_info = format!("Fastest node: {}ms ... {}", node_vec[0].ms, node_vec[0].ip); - let info = "Cleaning up connections".to_string(); - info!("Ping | {}...", info); - let mut ping = lock!(ping); - ping.fastest = node_vec[0].ip; - ping.nodes = node_vec; - ping.msg = info; - drop(ping); - Ok(fastest_info) - } + let info = "Cleaning up connections".to_string(); + info!("Ping | {}...", info); + let mut ping = lock!(ping); + ping.fastest = node_vec[0].ip; + ping.nodes = node_vec; + ping.msg = info; + drop(ping); + Ok(fastest_info) + } - #[cold] - #[inline(never)] - async fn response( - client: Client, - request: Request, - ip: &'static str, - ping: Arc>, - percent: f32, - node_vec: Arc>> - ) { - let ms; - let now = Instant::now(); + #[cold] + #[inline(never)] + async fn response( + client: Client, + request: Request, + ip: &'static str, + ping: Arc>, + percent: f32, + node_vec: Arc>>, + ) { + let ms; + let now = Instant::now(); - match tokio::time::timeout(Duration::from_secs(5), client.request(request)).await { - Ok(Ok(json_rpc)) => { - // Attempt to convert to JSON-RPC. - match hyper::body::to_bytes(json_rpc.into_body()).await { - Ok(b) => { - match serde_json::from_slice::>(&b) { - Ok(rpc) => { - if rpc.result.mainnet && rpc.result.synchronized { - ms = now.elapsed().as_millis(); - } else { - ms = TIMEOUT_NODE_PING; - warn!("Ping | {ip} responded with valid get_info but is not in sync, remove this node!"); - } - } - _ => { - ms = TIMEOUT_NODE_PING; - warn!("Ping | {ip} responded but with invalid get_info, remove this node!"); - } - } - }, - _ => ms = TIMEOUT_NODE_PING, - }; - }, - _ => ms = TIMEOUT_NODE_PING, - }; + match tokio::time::timeout(Duration::from_secs(5), client.request(request)).await { + Ok(Ok(json_rpc)) => { + // Attempt to convert to JSON-RPC. + match hyper::body::to_bytes(json_rpc.into_body()).await { + Ok(b) => match serde_json::from_slice::>(&b) { + Ok(rpc) => { + if rpc.result.mainnet && rpc.result.synchronized { + ms = now.elapsed().as_millis(); + } else { + ms = TIMEOUT_NODE_PING; + warn!("Ping | {ip} responded with valid get_info but is not in sync, remove this node!"); + } + } + _ => { + ms = TIMEOUT_NODE_PING; + warn!("Ping | {ip} responded but with invalid get_info, remove this node!"); + } + }, + _ => ms = TIMEOUT_NODE_PING, + }; + } + _ => ms = TIMEOUT_NODE_PING, + }; - let info = format!("{ms}ms ... {ip}"); - info!("Ping | {ms}ms ... {ip}"); + let info = format!("{ms}ms ... {ip}"); + info!("Ping | {ms}ms ... {ip}"); - let color = if ms < GREEN_NODE_PING { - GREEN - } else if ms < RED_NODE_PING { - YELLOW - } else if ms < TIMEOUT_NODE_PING { - RED - } else { - BLACK - }; + let color = if ms < GREEN_NODE_PING { + GREEN + } else if ms < RED_NODE_PING { + YELLOW + } else if ms < TIMEOUT_NODE_PING { + RED + } else { + BLACK + }; - let mut ping = lock!(ping); - ping.msg = info; - ping.prog += percent; - drop(ping); - lock!(node_vec).push(NodeData { ip, ms, color }); - } + let mut ping = lock!(ping); + ping.msg = info; + ping.prog += percent; + drop(ping); + lock!(node_vec).push(NodeData { ip, ms, color }); + } } //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod test { - #[test] - fn validate_node_ips() { - for (ip, location, rpc, zmq) in crate::REMOTE_NODES { - assert!(ip.len() < 255); - assert!(ip.is_ascii()); - assert!(!location.is_empty()); - assert!(!ip.is_empty()); - assert!(rpc == "18081" || rpc == "18089"); - assert!(zmq == "18083" || zmq == "18084"); - } - } + #[test] + fn validate_node_ips() { + for (ip, location, rpc, zmq) in crate::REMOTE_NODES { + assert!(ip.len() < 255); + assert!(ip.is_ascii()); + assert!(!location.is_empty()); + assert!(!ip.is_empty()); + assert!(rpc == "18081" || rpc == "18089"); + assert!(zmq == "18083" || zmq == "18084"); + } + } - #[test] - fn spacing() { - for (ip, _, _, _) in crate::REMOTE_NODES { - assert!(crate::format_ip(ip).len() <= crate::REMOTE_NODE_MAX_CHARS); - } - } + #[test] + fn spacing() { + for (ip, _, _, _) in crate::REMOTE_NODES { + assert!(crate::format_ip(ip).len() <= crate::REMOTE_NODE_MAX_CHARS); + } + } - // This one pings the IPs defined in [REMOTE_NODES] and fully serializes the JSON data to make sure they work. - // This will only be ran with be ran with [cargo test -- --ignored]. - #[tokio::test] - #[ignore] - async fn full_ping() { - use hyper::{ - client::HttpConnector, - Client,Body,Request, - }; - use crate::{REMOTE_NODES,REMOTE_NODE_LENGTH}; - use serde::{Serialize,Deserialize}; + // This one pings the IPs defined in [REMOTE_NODES] and fully serializes the JSON data to make sure they work. + // This will only be ran with be ran with [cargo test -- --ignored]. + #[tokio::test] + #[ignore] + async fn full_ping() { + use crate::{REMOTE_NODES, REMOTE_NODE_LENGTH}; + use hyper::{client::HttpConnector, Body, Client, Request}; + use serde::{Deserialize, Serialize}; - #[derive(Deserialize,Serialize)] - struct GetInfo { - id: String, - jsonrpc: String, - } + #[derive(Deserialize, Serialize)] + struct GetInfo { + id: String, + jsonrpc: String, + } - // Create HTTP client - let client: Client = Client::builder().build(HttpConnector::new()); + // Create HTTP client + let client: Client = Client::builder().build(HttpConnector::new()); - // Random User Agent - let rand_user_agent = crate::Pkg::get_user_agent(); + // Random User Agent + let rand_user_agent = crate::Pkg::get_user_agent(); - // Only fail this test if >50% of nodes fail. - const HALF_REMOTE_NODES: usize = REMOTE_NODE_LENGTH / 2; - // A string buffer to append the failed node data. - let mut failures = String::new(); - let mut failure_count = 0; + // Only fail this test if >50% of nodes fail. + const HALF_REMOTE_NODES: usize = REMOTE_NODE_LENGTH / 2; + // A string buffer to append the failed node data. + let mut failures = String::new(); + let mut failure_count = 0; - let mut n = 1; - 'outer: for (ip, _, rpc, zmq) in REMOTE_NODES { - println!("[{n}/{REMOTE_NODE_LENGTH}] {ip} | {rpc} | {zmq}"); - let client = client.clone(); - // Try 3 times before failure - let mut i = 1; - let mut response = loop { - let request = Request::builder() - .method("POST") - .uri("http://".to_string() + ip + ":" + rpc + "/json_rpc") - .header("User-Agent", rand_user_agent) - .body(hyper::Body::from(r#"{"jsonrpc":"2.0","id":"0","method":"get_info"}"#)) - .unwrap(); - match client.request(request).await { - Ok(response) => break response, - Err(e) => { - println!("{:#?}", e); - if i >= 3 { - use std::fmt::Write; - writeln!(failures, "Node failure: {ip}:{rpc}:{zmq}"); - failure_count += 1; - continue 'outer; - } - std::thread::sleep(std::time::Duration::from_secs(2)); - i += 1; - } - } - }; - let body = hyper::body::to_bytes(response.body_mut()).await.unwrap(); - let getinfo: GetInfo = serde_json::from_slice(&body).unwrap(); - assert!(getinfo.id == "0"); - assert!(getinfo.jsonrpc == "2.0"); - n += 1; - } + let mut n = 1; + 'outer: for (ip, _, rpc, zmq) in REMOTE_NODES { + println!("[{n}/{REMOTE_NODE_LENGTH}] {ip} | {rpc} | {zmq}"); + let client = client.clone(); + // Try 3 times before failure + let mut i = 1; + let mut response = loop { + let request = Request::builder() + .method("POST") + .uri("http://".to_string() + ip + ":" + rpc + "/json_rpc") + .header("User-Agent", rand_user_agent) + .body(hyper::Body::from( + r#"{"jsonrpc":"2.0","id":"0","method":"get_info"}"#, + )) + .unwrap(); + match client.request(request).await { + Ok(response) => break response, + Err(e) => { + println!("{:#?}", e); + if i >= 3 { + use std::fmt::Write; + writeln!(failures, "Node failure: {ip}:{rpc}:{zmq}"); + failure_count += 1; + continue 'outer; + } + std::thread::sleep(std::time::Duration::from_secs(2)); + i += 1; + } + } + }; + let body = hyper::body::to_bytes(response.body_mut()).await.unwrap(); + let getinfo: GetInfo = serde_json::from_slice(&body).unwrap(); + assert!(getinfo.id == "0"); + assert!(getinfo.jsonrpc == "2.0"); + n += 1; + } - let failure_percent = failure_count as f32 / HALF_REMOTE_NODES as f32; + let failure_percent = failure_count as f32 / HALF_REMOTE_NODES as f32; - // If more than half the nodes fail, something - // is definitely wrong, fail this test. - if failure_count > HALF_REMOTE_NODES { - panic!("[{failure_percent:.2}% of nodes failed, failure log:\n{failures}"); - // If some failures happened, log. - } else if failure_count != 0 { - eprintln!("[{failure_count}] nodes failed ({failure_percent:.2}%):\n{failures}"); - } else { - println!("No nodes failed - all OK"); - } - } + // If more than half the nodes fail, something + // is definitely wrong, fail this test. + if failure_count > HALF_REMOTE_NODES { + panic!("[{failure_percent:.2}% of nodes failed, failure log:\n{failures}"); + // If some failures happened, log. + } else if failure_count != 0 { + eprintln!("[{failure_count}] nodes failed ({failure_percent:.2}%):\n{failures}"); + } else { + println!("No nodes failed - all OK"); + } + } } diff --git a/src/p2pool.rs b/src/p2pool.rs index b12b024..e855483 100644 --- a/src/p2pool.rs +++ b/src/p2pool.rs @@ -15,247 +15,319 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{ - Regexes, - constants::*, - disk::*, - node::*, - helper::*, - macros::*, -}; +use crate::regex::REGEXES; +use crate::{constants::*, disk::*, helper::*, macros::*, node::*, Regexes}; use egui::{ - TextEdit,SelectableLabel,ComboBox,Label,Button, - Color32,RichText,Slider,Checkbox,ProgressBar,Spinner, - TextStyle::*,Hyperlink + Button, Checkbox, Color32, ComboBox, Hyperlink, Label, ProgressBar, RichText, SelectableLabel, + Slider, Spinner, TextEdit, TextStyle::*, }; -use std::sync::{Arc,Mutex}; -use regex::Regex; use log::*; -use crate::regex::{ - REGEXES, -}; +use regex::Regex; +use std::sync::{Arc, Mutex}; impl crate::disk::P2pool { -#[inline(always)] // called once -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 - } - } - }); + #[inline(always)] // called once + 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()); - } + //---------------------------------------------------------------------------------------------------- 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); - }); + //---------------------------------------------------------------------------------------------------- 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; + //---------------------------------------------------------------------------------------------------- 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); - } + // [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| { + 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); + } + }); + }); - 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); - 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); + } + }); - 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); + }); + }); - 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 [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); - 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| { + //---------------------------------------------------------------------------------------------------- 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| { @@ -466,49 +538,84 @@ pub fn show( }); }); }); - ui.add_space(5.0); + 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 [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); - }); - } - } + 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/panic.rs b/src/panic.rs index 30f4dcd..9567a4f 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -1,26 +1,20 @@ //---------------------------------------------------------------------------------------------------- Use -use crate::constants::{ - GUPAX_VERSION, - P2POOL_VERSION, - XMRIG_VERSION, - OS_NAME, - COMMIT, -}; +use crate::constants::{COMMIT, GUPAX_VERSION, OS_NAME, P2POOL_VERSION, XMRIG_VERSION}; //---------------------------------------------------------------------------------------------------- #[cold] #[inline(never)] /// Set custom panic hook. pub(crate) fn set_panic_hook(now: std::time::Instant) { - std::panic::set_hook(Box::new(move |panic_info| { - // Set stack-trace. - let stack_trace = std::backtrace::Backtrace::force_capture(); - let args = std::env::args_os(); - let uptime = now.elapsed().as_secs_f32(); + std::panic::set_hook(Box::new(move |panic_info| { + // Set stack-trace. + let stack_trace = std::backtrace::Backtrace::force_capture(); + let args = std::env::args_os(); + let uptime = now.elapsed().as_secs_f32(); - // Re-format panic info. - let panic_info = format!( -"{panic_info:#?} + // Re-format panic info. + let panic_info = format!( + "{panic_info:#?} info: OS | {OS_NAME} @@ -32,21 +26,23 @@ info: uptime | {uptime} seconds stack backtrace:\n{stack_trace}", - ); + ); - // Attempt to write panic info to disk. - match crate::disk::get_gupax_data_path() { - Ok(mut path) => { - path.push("crash.txt"); - match std::fs::write(&path, &panic_info) { - Ok(_) => eprintln!("\nmass_panic!() - Saved panic log to: {}\n", path.display()), - Err(e) => eprintln!("\nmass_panic!() - Could not save panic log: {e}\n"), - } - }, - Err(e) => eprintln!("panic_hook PATH error: {e}"), - } + // Attempt to write panic info to disk. + match crate::disk::get_gupax_data_path() { + Ok(mut path) => { + path.push("crash.txt"); + match std::fs::write(&path, &panic_info) { + Ok(_) => { + eprintln!("\nmass_panic!() - Saved panic log to: {}\n", path.display()) + } + Err(e) => eprintln!("\nmass_panic!() - Could not save panic log: {e}\n"), + } + } + Err(e) => eprintln!("panic_hook PATH error: {e}"), + } - // Exit all threads. - benri::mass_panic!(panic_info); - })); + // Exit all threads. + benri::mass_panic!(panic_info); + })); } diff --git a/src/regex.rs b/src/regex.rs index f93f219..9ce9ae4 100644 --- a/src/regex.rs +++ b/src/regex.rs @@ -17,48 +17,51 @@ // Some regexes used throughout Gupax. -use regex::Regex; use once_cell::sync::Lazy; +use regex::Regex; //---------------------------------------------------------------------------------------------------- Lazy -pub static REGEXES: Lazy = Lazy::new(|| Regexes::new()); +pub static REGEXES: Lazy = Lazy::new(|| Regexes::new()); pub static P2POOL_REGEX: Lazy = Lazy::new(|| P2poolRegex::new()); -pub static XMRIG_REGEX: Lazy = Lazy::new(|| XmrigRegex::new()); +pub static XMRIG_REGEX: Lazy = Lazy::new(|| XmrigRegex::new()); //---------------------------------------------------------------------------------------------------- [Regexes] struct // General purpose Regexes, mostly used in the GUI. #[derive(Clone, Debug)] pub struct Regexes { - pub name: Regex, - pub address: Regex, - pub ipv4: Regex, - pub domain: Regex, - pub port: Regex, + pub name: Regex, + pub address: Regex, + pub ipv4: Regex, + pub domain: Regex, + pub port: Regex, } impl Regexes { - #[cold] - #[inline(never)] - fn new() -> Self { - Self { + #[cold] + #[inline(never)] + fn new() -> Self { + Self { name: Regex::new("^[A-Za-z0-9-_.]+( [A-Za-z0-9-_.]+)*$").unwrap(), address: Regex::new("^4[A-Za-z1-9]+$").unwrap(), // This still needs to check for (l, I, o, 0) ipv4: Regex::new(r#"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$"#).unwrap(), domain: Regex::new(r#"^[A-Za-z0-9-.]+[A-Za-z0-9-]+$"#).unwrap(), port: Regex::new(r#"^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"#).unwrap(), } - } + } - #[inline] - // Check if a Monero address is correct. - // This actually only checks for length & Base58, and doesn't do any checksum validation - // (the last few bytes of a Monero address are a Keccak hash checksum) so some invalid addresses can trick this function. - pub fn addr_ok(address: &str) -> bool { - address.len() == 95 && REGEXES.address.is_match(address) && !address.contains('0') && !address.contains('O') && !address.contains('l') - } + #[inline] + // Check if a Monero address is correct. + // This actually only checks for length & Base58, and doesn't do any checksum validation + // (the last few bytes of a Monero address are a Keccak hash checksum) so some invalid addresses can trick this function. + pub fn addr_ok(address: &str) -> bool { + address.len() == 95 + && REGEXES.address.is_match(address) + && !address.contains('0') + && !address.contains('O') + && !address.contains('l') + } } - //---------------------------------------------------------------------------------------------------- [P2poolRegex] // Meant for parsing the output of P2Pool and finding payouts and total XMR found. // Why Regex instead of the standard library? @@ -76,102 +79,114 @@ impl Regexes { // let n = regex.find_iter(P2POOL_OUTPUT).count(); // // Both are nominally fast enough where it doesn't matter too much but meh, why not use regex. -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct P2poolRegex { - pub date: Regex, - pub payout: Regex, - pub payout_float: Regex, - pub block: Regex, - pub block_int: Regex, - pub block_comma: Regex, - pub synchronized: Regex, - pub next_height_1: Regex, + pub date: Regex, + pub payout: Regex, + pub payout_float: Regex, + pub block: Regex, + pub block_int: Regex, + pub block_comma: Regex, + pub synchronized: Regex, + pub next_height_1: Regex, } impl P2poolRegex { - #[cold] - #[inline(never)] - fn new() -> Self { - Self { - date: Regex::new("[0-9]+-[0-9]+-[0-9]+ [0-9]+:[0-9]+:[0-9]+.[0-9]+").unwrap(), - payout: Regex::new("payout of [0-9].[0-9]+ XMR").unwrap(), // Assumes 12 digits after the dot. - payout_float: Regex::new("[0-9].[0-9]{12}").unwrap(), // Assumes 12 digits after the dot. - block: Regex::new("block [0-9]{7}").unwrap(), // Monero blocks will be 7 digits for... the next 10,379 years - block_int: Regex::new("[0-9]{7}").unwrap(), - block_comma: Regex::new("[0-9],[0-9]{3},[0-9]{3}").unwrap(), - synchronized: Regex::new("SYNCHRONIZED").unwrap(), - next_height_1: Regex::new("next height = 1").unwrap(), - } - } + #[cold] + #[inline(never)] + fn new() -> Self { + Self { + date: Regex::new("[0-9]+-[0-9]+-[0-9]+ [0-9]+:[0-9]+:[0-9]+.[0-9]+").unwrap(), + payout: Regex::new("payout of [0-9].[0-9]+ XMR").unwrap(), // Assumes 12 digits after the dot. + payout_float: Regex::new("[0-9].[0-9]{12}").unwrap(), // Assumes 12 digits after the dot. + block: Regex::new("block [0-9]{7}").unwrap(), // Monero blocks will be 7 digits for... the next 10,379 years + block_int: Regex::new("[0-9]{7}").unwrap(), + block_comma: Regex::new("[0-9],[0-9]{3},[0-9]{3}").unwrap(), + synchronized: Regex::new("SYNCHRONIZED").unwrap(), + next_height_1: Regex::new("next height = 1").unwrap(), + } + } } //---------------------------------------------------------------------------------------------------- XMRig regex. #[derive(Debug)] pub struct XmrigRegex { - pub not_mining: Regex, - pub new_job: Regex, + pub not_mining: Regex, + pub new_job: Regex, } impl XmrigRegex { - fn new() -> Self { - Self { - not_mining: Regex::new("no active pools, stop mining").unwrap(), - new_job: Regex::new("new job").unwrap(), - } - } + fn new() -> Self { + Self { + not_mining: Regex::new("no active pools, stop mining").unwrap(), + new_job: Regex::new("new job").unwrap(), + } + } } //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod test { - use regex::Regex; - use super::*; + use super::*; + use regex::Regex; - #[test] - fn build_regexes() { - let r = Regexes::new(); - assert!(Regex::is_match(&r.name, "_this_ is... a n-a-m-e.")); - assert!(Regex::is_match(&r.address, "44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW")); - assert!(Regex::is_match(&r.ipv4, "192.168.1.2")); - assert!(Regex::is_match(&r.ipv4, "127.0.0.1")); - assert!(Regex::is_match(&r.domain, "sub.domain.com")); - assert!(Regex::is_match(&r.domain, "sub.domain.longtld")); - assert!(Regex::is_match(&r.domain, "sub.sub.domain.longtld")); - assert!(Regex::is_match(&r.domain, "my.node.com")); - assert!(Regex::is_match(&r.domain, "my.node.longtld")); - assert!(Regex::is_match(&r.domain, "my.monero-node123.net")); - assert!(Regex::is_match(&r.domain, "www.my-node.org")); - assert!(Regex::is_match(&r.domain, "www.my-monero-node123.io")); - assert!(Regex::is_match(&r.domain, "www.my-monero-node123.longtld")); - assert!(Regex::is_match(&r.domain, "www.my-monero-node123.org")); - for i in 1..=65535 { - assert!(Regex::is_match(&r.port, &i.to_string())); - } - assert!(!Regex::is_match(&r.port, "0")); - assert!(!Regex::is_match(&r.port, "65536")); - } + #[test] + fn build_regexes() { + let r = Regexes::new(); + assert!(Regex::is_match(&r.name, "_this_ is... a n-a-m-e.")); + assert!(Regex::is_match(&r.address, "44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW")); + assert!(Regex::is_match(&r.ipv4, "192.168.1.2")); + assert!(Regex::is_match(&r.ipv4, "127.0.0.1")); + assert!(Regex::is_match(&r.domain, "sub.domain.com")); + assert!(Regex::is_match(&r.domain, "sub.domain.longtld")); + assert!(Regex::is_match(&r.domain, "sub.sub.domain.longtld")); + assert!(Regex::is_match(&r.domain, "my.node.com")); + assert!(Regex::is_match(&r.domain, "my.node.longtld")); + assert!(Regex::is_match(&r.domain, "my.monero-node123.net")); + assert!(Regex::is_match(&r.domain, "www.my-node.org")); + assert!(Regex::is_match(&r.domain, "www.my-monero-node123.io")); + assert!(Regex::is_match(&r.domain, "www.my-monero-node123.longtld")); + assert!(Regex::is_match(&r.domain, "www.my-monero-node123.org")); + for i in 1..=65535 { + assert!(Regex::is_match(&r.port, &i.to_string())); + } + assert!(!Regex::is_match(&r.port, "0")); + assert!(!Regex::is_match(&r.port, "65536")); + } - #[test] - fn build_p2pool_regex() { - let r = P2poolRegex::new(); - let text = "NOTICE 2022-11-11 11:11:11.1111 P2Pool You received a payout of 0.111111111111 XMR in block 1111111"; - let text2 = "2022-11-11 11:11:11.1111 | 0.111111111111 XMR | Block 1,111,111"; - let text3 = "NOTICE 2020-12-11 12:35:41.3150 SideChain SYNCHRONIZED"; - assert_eq!(r.payout.find(text).unwrap().as_str(), "payout of 0.111111111111 XMR"); - assert_eq!(r.payout_float.find(text).unwrap().as_str(), "0.111111111111"); - assert_eq!(r.date.find(text).unwrap().as_str(), "2022-11-11 11:11:11.1111"); - assert_eq!(r.block.find(text).unwrap().as_str(), "block 1111111"); - assert_eq!(r.block_int.find(text).unwrap().as_str(), "1111111"); - assert_eq!(r.block_comma.find(text2).unwrap().as_str(), "1,111,111"); - assert_eq!(r.synchronized.find(text3).unwrap().as_str(), "SYNCHRONIZED"); - } + #[test] + fn build_p2pool_regex() { + let r = P2poolRegex::new(); + let text = "NOTICE 2022-11-11 11:11:11.1111 P2Pool You received a payout of 0.111111111111 XMR in block 1111111"; + let text2 = "2022-11-11 11:11:11.1111 | 0.111111111111 XMR | Block 1,111,111"; + let text3 = "NOTICE 2020-12-11 12:35:41.3150 SideChain SYNCHRONIZED"; + assert_eq!( + r.payout.find(text).unwrap().as_str(), + "payout of 0.111111111111 XMR" + ); + assert_eq!( + r.payout_float.find(text).unwrap().as_str(), + "0.111111111111" + ); + assert_eq!( + r.date.find(text).unwrap().as_str(), + "2022-11-11 11:11:11.1111" + ); + assert_eq!(r.block.find(text).unwrap().as_str(), "block 1111111"); + assert_eq!(r.block_int.find(text).unwrap().as_str(), "1111111"); + assert_eq!(r.block_comma.find(text2).unwrap().as_str(), "1,111,111"); + assert_eq!(r.synchronized.find(text3).unwrap().as_str(), "SYNCHRONIZED"); + } - #[test] - fn build_xmrig_regex() { - let r = XmrigRegex::new(); - let text = "[2022-02-12 12:49:30.311] net no active pools, stop mining"; - let text2 = "[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)"; - assert_eq!(r.not_mining.find(text).unwrap().as_str(), "no active pools, stop mining"); - assert_eq!(r.new_job.find(text2).unwrap().as_str(), "new job"); - } + #[test] + fn build_xmrig_regex() { + let r = XmrigRegex::new(); + let text = "[2022-02-12 12:49:30.311] net no active pools, stop mining"; + let text2 = "[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)"; + assert_eq!( + r.not_mining.find(text).unwrap().as_str(), + "no active pools, stop mining" + ); + assert_eq!(r.new_job.find(text2).unwrap().as_str(), "new job"); + } } diff --git a/src/status.rs b/src/status.rs index b62f238..ddfe206 100644 --- a/src/status.rs +++ b/src/status.rs @@ -16,305 +16,823 @@ // along with this program. If not, see . use crate::{ - PubP2poolApi, - PubXmrigApi, - ImgP2pool, - ImgXmrig, - constants::*, - Sys, - Hash, - Submenu, - macros::*, - GupaxP2poolApi, - PayoutView, - human::HumanNumber, - Benchmark, + constants::*, human::HumanNumber, macros::*, Benchmark, GupaxP2poolApi, Hash, ImgP2pool, + ImgXmrig, PayoutView, PubP2poolApi, PubXmrigApi, Submenu, Sys, }; -use std::sync::{Arc,Mutex}; -use log::*; use egui::{ - Label,RichText,TextStyle, - TextStyle::Monospace, - TextStyle::Name, - TextEdit, - SelectableLabel,Hyperlink, - Slider,ProgressBar,Spinner, + Hyperlink, Label, ProgressBar, RichText, SelectableLabel, Slider, Spinner, TextEdit, TextStyle, + TextStyle::Monospace, TextStyle::Name, }; +use log::*; +use std::sync::{Arc, Mutex}; impl crate::disk::Status { -#[inline(always)] // called once -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; + #[inline(always)] // called once + 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)))); - })}) - }); + // [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| { + // 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; @@ -337,63 +855,101 @@ pub fn show(&mut self, } }); - // 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); - }); + // 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); - }); - }); + 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())); - })}); - } - }); - } -} + 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/sudo.rs b/src/sudo.rs index 42eaef9..a9307f5 100644 --- a/src/sudo.rs +++ b/src/sudo.rs @@ -19,176 +19,189 @@ // [zeroize] is used to wipe the memory after use. // Only gets imported in [main.rs] for Unix. -use zeroize::Zeroize; -use std::{ - thread, - sync::{Arc,Mutex}, - process::*, - io::Write, - path::PathBuf, -}; -use crate::{ - Helper, - disk::Xmrig, - ProcessSignal, - constants::*, - macros::*, -}; +use crate::{constants::*, disk::Xmrig, macros::*, Helper, ProcessSignal}; use log::*; +use std::{ + io::Write, + path::PathBuf, + process::*, + sync::{Arc, Mutex}, + thread, +}; +use zeroize::Zeroize; -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub struct SudoState { - pub windows: bool, // If this bool is set, this struct is just a dummy so I don't have to change my type signatures :) - pub testing: bool, // Are we attempting a sudo test right now? - pub success: bool, // Was the sudo test a success? - pub hide: bool, // Are we hiding the password? - pub msg: String, // The message shown to the user if unsuccessful - pub pass: String, // The actual password wrapped in a [SecretVec] - pub signal: ProcessSignal, // Main GUI will set this depending on if we want [Start] or [Restart] + pub windows: bool, // If this bool is set, this struct is just a dummy so I don't have to change my type signatures :) + pub testing: bool, // Are we attempting a sudo test right now? + pub success: bool, // Was the sudo test a success? + pub hide: bool, // Are we hiding the password? + pub msg: String, // The message shown to the user if unsuccessful + pub pass: String, // The actual password wrapped in a [SecretVec] + pub signal: ProcessSignal, // Main GUI will set this depending on if we want [Start] or [Restart] } impl Default for SudoState { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl SudoState { - #[cold] - #[inline(never)] - #[cfg(target_os = "windows")] - pub fn new() -> Self { - Self { - windows: true, - testing: false, - success: false, - hide: true, - msg: String::new(), - pass: String::new(), - signal: ProcessSignal::None, - } - } - #[cold] - #[inline(never)] - #[cfg(target_family = "unix")] - pub fn new() -> Self { - Self { - windows: false, - testing: false, - success: false, - hide: true, - msg: "".to_string(), - pass: String::with_capacity(256), - signal: ProcessSignal::None, - } - } + #[cold] + #[inline(never)] + #[cfg(target_os = "windows")] + pub fn new() -> Self { + Self { + windows: true, + testing: false, + success: false, + hide: true, + msg: String::new(), + pass: String::new(), + signal: ProcessSignal::None, + } + } + #[cold] + #[inline(never)] + #[cfg(target_family = "unix")] + pub fn new() -> Self { + Self { + windows: false, + testing: false, + success: false, + hide: true, + msg: "".to_string(), + pass: String::with_capacity(256), + signal: ProcessSignal::None, + } + } - #[cold] - #[inline(never)] - // Resets the state. - pub fn reset(state: &Arc>) { - Self::wipe(state); - let mut state = lock!(state); - state.testing = false; - state.success = false; -// state.signal = ProcessSignal::None; - } + #[cold] + #[inline(never)] + // Resets the state. + pub fn reset(state: &Arc>) { + Self::wipe(state); + let mut state = lock!(state); + state.testing = false; + state.success = false; + // state.signal = ProcessSignal::None; + } - #[cold] - #[inline(never)] - // Swaps the pass with another 256-capacity String, - // zeroizes the old and drops it. - pub fn wipe(state: &Arc>) { - let mut new = String::with_capacity(256); - // new is now == old, and vice-versa. - std::mem::swap(&mut new, &mut lock!(state).pass); - // we're wiping & dropping the old pass here. - new.zeroize(); - std::mem::drop(new); - info!("Sudo | Password wipe with 0's ... OK"); - } + #[cold] + #[inline(never)] + // Swaps the pass with another 256-capacity String, + // zeroizes the old and drops it. + pub fn wipe(state: &Arc>) { + let mut new = String::with_capacity(256); + // new is now == old, and vice-versa. + std::mem::swap(&mut new, &mut lock!(state).pass); + // we're wiping & dropping the old pass here. + new.zeroize(); + std::mem::drop(new); + info!("Sudo | Password wipe with 0's ... OK"); + } - #[cold] - #[inline(never)] - // Spawns a thread and tests sudo with the provided password. - // Sudo takes the password through STDIN via [--stdin]. - // Sets the appropriate state fields on success/failure. - pub fn test_sudo(state: Arc>, helper: &Arc>, xmrig: &Xmrig, path: &PathBuf) { - let helper = Arc::clone(helper); - let xmrig = xmrig.clone(); - let path = path.clone(); - thread::spawn(move || { - // Set to testing - lock!(state).testing = true; + #[cold] + #[inline(never)] + // Spawns a thread and tests sudo with the provided password. + // Sudo takes the password through STDIN via [--stdin]. + // Sets the appropriate state fields on success/failure. + pub fn test_sudo( + state: Arc>, + helper: &Arc>, + xmrig: &Xmrig, + path: &PathBuf, + ) { + let helper = Arc::clone(helper); + let xmrig = xmrig.clone(); + let path = path.clone(); + thread::spawn(move || { + // Set to testing + lock!(state).testing = true; - // Make sure sudo timestamp is reset - let reset = Command::new("sudo") - .arg("--reset-timestamp") - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::piped()) - .status(); - match reset { - Ok(_) => info!("Sudo | Resetting timestamp ... OK"), - Err(e) => { - error!("Sudo | Couldn't reset timestamp: {}", e); - Self::wipe(&state); - lock!(state).msg = format!("Sudo error: {}", e); - lock!(state).testing = false; - return - }, - } + // Make sure sudo timestamp is reset + let reset = Command::new("sudo") + .arg("--reset-timestamp") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::piped()) + .status(); + match reset { + Ok(_) => info!("Sudo | Resetting timestamp ... OK"), + Err(e) => { + error!("Sudo | Couldn't reset timestamp: {}", e); + Self::wipe(&state); + lock!(state).msg = format!("Sudo error: {}", e); + lock!(state).testing = false; + return; + } + } - // Spawn testing sudo - let mut sudo = Command::new("sudo") - .args(["--stdin", "--validate"]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::piped()) - .spawn() - .unwrap(); + // Spawn testing sudo + let mut sudo = Command::new("sudo") + .args(["--stdin", "--validate"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::piped()) + .spawn() + .unwrap(); - // Write pass to STDIN - let mut stdin = sudo.stdin.take().unwrap(); - stdin.write_all(lock!(state).pass.as_bytes()).unwrap(); - drop(stdin); + // Write pass to STDIN + let mut stdin = sudo.stdin.take().unwrap(); + stdin.write_all(lock!(state).pass.as_bytes()).unwrap(); + drop(stdin); - // Sudo re-prompts and will hang. - // To workaround this, try checking - // results for 5 seconds in a loop. - for i in 1..=5 { - match sudo.try_wait() { - Ok(Some(code)) => if code.success() { - info!("Sudo | Password ... OK!"); - lock!(state).success = true; - break - }, - Ok(None) => { - info!("Sudo | Waiting [{}/5]...", i); - std::thread::sleep(SECOND); - }, - Err(e) => { - error!("Sudo | Couldn't reset timestamp: {}", e); - Self::wipe(&state); - lock!(state).msg = format!("Sudo error: {}", e); - lock!(state).testing = false; - return - }, - } - } - if let Err(e) = sudo.kill() { warn!("Sudo | Kill error (it probably already exited): {}", e); } - if lock!(state).success { - match lock!(state).signal { - ProcessSignal::Restart => crate::helper::Helper::restart_xmrig(&helper, &xmrig, &path, Arc::clone(&state)), - ProcessSignal::Stop => crate::helper::Helper::stop_xmrig(&helper), - _ => crate::helper::Helper::start_xmrig(&helper, &xmrig, &path, Arc::clone(&state)), - } - } else { - lock!(state).msg = "Incorrect password! (or sudo timeout)".to_string(); - Self::wipe(&state); - } - lock!(state).signal = ProcessSignal::None; - lock!(state).testing = false; - }); - } + // Sudo re-prompts and will hang. + // To workaround this, try checking + // results for 5 seconds in a loop. + for i in 1..=5 { + match sudo.try_wait() { + Ok(Some(code)) => { + if code.success() { + info!("Sudo | Password ... OK!"); + lock!(state).success = true; + break; + } + } + Ok(None) => { + info!("Sudo | Waiting [{}/5]...", i); + std::thread::sleep(SECOND); + } + Err(e) => { + error!("Sudo | Couldn't reset timestamp: {}", e); + Self::wipe(&state); + lock!(state).msg = format!("Sudo error: {}", e); + lock!(state).testing = false; + return; + } + } + } + if let Err(e) = sudo.kill() { + warn!("Sudo | Kill error (it probably already exited): {}", e); + } + if lock!(state).success { + match lock!(state).signal { + ProcessSignal::Restart => crate::helper::Helper::restart_xmrig( + &helper, + &xmrig, + &path, + Arc::clone(&state), + ), + ProcessSignal::Stop => crate::helper::Helper::stop_xmrig(&helper), + _ => crate::helper::Helper::start_xmrig( + &helper, + &xmrig, + &path, + Arc::clone(&state), + ), + } + } else { + lock!(state).msg = "Incorrect password! (or sudo timeout)".to_string(); + Self::wipe(&state); + } + lock!(state).signal = ProcessSignal::None; + lock!(state).testing = false; + }); + } } diff --git a/src/update.rs b/src/update.rs index 147f539..53ad41b 100644 --- a/src/update.rs +++ b/src/update.rs @@ -24,27 +24,23 @@ // b. auto-update at startup //---------------------------------------------------------------------------------------------------- Imports -use anyhow::{anyhow,Error}; +use crate::{ + constants::GUPAX_VERSION, disk::*, macros::*, update::Name::*, ErrorButtons, ErrorFerris, + ErrorState, Restart, +}; +use anyhow::{anyhow, Error}; use arti_client::TorClient; use arti_hyper::*; -use crate::{ - constants::GUPAX_VERSION, - disk::*, - update::Name::*, - ErrorState,ErrorFerris,ErrorButtons, - Restart, - macros::*, -}; use hyper::{ - Client,Body,Request, - header::{HeaderValue,LOCATION}, + header::{HeaderValue, LOCATION}, + Body, Client, Request, }; use log::*; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; -use serde::{Serialize,Deserialize}; -use std::path::{Path,PathBuf}; -use std::sync::{Arc,Mutex}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; use tokio::task::JoinHandle; use walkdir::WalkDir; @@ -52,15 +48,12 @@ use walkdir::WalkDir; // tls implementation so this makes it fall back to the openssl variant. // // https://gitlab.torproject.org/tpo/core/arti/-/issues/715 -#[cfg(target_os = "macos")] -use tls_api_openssl::TlsConnector; #[cfg(not(target_os = "macos"))] use tls_api_native_tls::TlsConnector; +#[cfg(target_os = "macos")] +use tls_api_openssl::TlsConnector; -use tls_api::{ - TlsConnector as TlsConnectorTrait, - TlsConnectorBuilder, -}; +use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; #[cfg(target_os = "windows")] use zip::ZipArchive; @@ -99,84 +92,111 @@ const XMRIG_HASH: &str = "SHA256SUMS"; #[cfg(target_os = "windows")] mod impl_platform { - pub(super) const GUPAX_EXTENSION: &str = "-windows-x64-standalone.zip"; - pub(super) const P2POOL_EXTENSION: &str = "-windows-x64.zip"; - pub(super) const XMRIG_EXTENSION: &str = "-msvc-win64.zip"; - pub(super) const GUPAX_BINARY: &str = "Gupax.exe"; - pub(super) const P2POOL_BINARY: &str = "p2pool.exe"; - pub(super) const XMRIG_BINARY: &str = "xmrig.exe"; - pub(super) const VALID_GUPAX_1: &str = "GUPAX.exe"; - pub(super) const VALID_GUPAX_2: &str = "Gupax.exe"; - pub(super) const VALID_GUPAX_3: &str = "gupax.exe"; - pub(super) const VALID_XMRIG_1: &str = "XMRIG.exe"; - pub(super) const VALID_XMRIG_2: &str = "XMRig.exe"; - pub(super) const VALID_XMRIG_3: &str = "Xmrig.exe"; - pub(super) const VALID_XMRIG_4: &str = "xmrig.exe"; - pub(super) const VALID_P2POOL_1: &str = "P2POOL.exe"; - pub(super) const VALID_P2POOL_2: &str = "P2Pool.exe"; - pub(super) const VALID_P2POOL_3: &str = "P2pool.exe"; - pub(super) const VALID_P2POOL_4: &str = "p2pool.exe"; + pub(super) const GUPAX_EXTENSION: &str = "-windows-x64-standalone.zip"; + pub(super) const P2POOL_EXTENSION: &str = "-windows-x64.zip"; + pub(super) const XMRIG_EXTENSION: &str = "-msvc-win64.zip"; + pub(super) const GUPAX_BINARY: &str = "Gupax.exe"; + pub(super) const P2POOL_BINARY: &str = "p2pool.exe"; + pub(super) const XMRIG_BINARY: &str = "xmrig.exe"; + pub(super) const VALID_GUPAX_1: &str = "GUPAX.exe"; + pub(super) const VALID_GUPAX_2: &str = "Gupax.exe"; + pub(super) const VALID_GUPAX_3: &str = "gupax.exe"; + pub(super) const VALID_XMRIG_1: &str = "XMRIG.exe"; + pub(super) const VALID_XMRIG_2: &str = "XMRig.exe"; + pub(super) const VALID_XMRIG_3: &str = "Xmrig.exe"; + pub(super) const VALID_XMRIG_4: &str = "xmrig.exe"; + pub(super) const VALID_P2POOL_1: &str = "P2POOL.exe"; + pub(super) const VALID_P2POOL_2: &str = "P2Pool.exe"; + pub(super) const VALID_P2POOL_3: &str = "P2pool.exe"; + pub(super) const VALID_P2POOL_4: &str = "p2pool.exe"; } #[cfg(target_family = "unix")] mod impl_unix { - pub(super) const GUPAX_BINARY: &str = "gupax"; - pub(super) const P2POOL_BINARY: &str = "p2pool"; - pub(super) const XMRIG_BINARY: &str = "xmrig"; - pub(super) const VALID_GUPAX_1: &str = "GUPAX"; - pub(super) const VALID_GUPAX_2: &str = "Gupax"; - pub(super) const VALID_GUPAX_3: &str = "gupax"; - pub(super) const VALID_XMRIG_1: &str = "XMRIG"; - pub(super) const VALID_XMRIG_2: &str = "XMRig"; - pub(super) const VALID_XMRIG_3: &str = "Xmrig"; - pub(super) const VALID_XMRIG_4: &str = "xmrig"; - pub(super) const VALID_P2POOL_1: &str = "P2POOL"; - pub(super) const VALID_P2POOL_2: &str = "P2Pool"; - pub(super) const VALID_P2POOL_3: &str = "P2pool"; - pub(super) const VALID_P2POOL_4: &str = "p2pool"; + pub(super) const GUPAX_BINARY: &str = "gupax"; + pub(super) const P2POOL_BINARY: &str = "p2pool"; + pub(super) const XMRIG_BINARY: &str = "xmrig"; + pub(super) const VALID_GUPAX_1: &str = "GUPAX"; + pub(super) const VALID_GUPAX_2: &str = "Gupax"; + pub(super) const VALID_GUPAX_3: &str = "gupax"; + pub(super) const VALID_XMRIG_1: &str = "XMRIG"; + pub(super) const VALID_XMRIG_2: &str = "XMRig"; + pub(super) const VALID_XMRIG_3: &str = "Xmrig"; + pub(super) const VALID_XMRIG_4: &str = "xmrig"; + pub(super) const VALID_P2POOL_1: &str = "P2POOL"; + pub(super) const VALID_P2POOL_2: &str = "P2Pool"; + pub(super) const VALID_P2POOL_3: &str = "P2pool"; + pub(super) const VALID_P2POOL_4: &str = "p2pool"; } #[cfg(target_os = "macos")] #[cfg(target_arch = "x86_64")] mod impl_platform { - pub(super) use super::impl_unix::*; + pub(super) use super::impl_unix::*; - pub(super) const GUPAX_EXTENSION: &str = "-macos-x64-standalone.tar.gz"; - pub(super) const P2POOL_EXTENSION: &str = "-macos-x64.tar.gz"; - pub(super) const XMRIG_EXTENSION: &str = "-macos-x64.tar.gz"; + pub(super) const GUPAX_EXTENSION: &str = "-macos-x64-standalone.tar.gz"; + pub(super) const P2POOL_EXTENSION: &str = "-macos-x64.tar.gz"; + pub(super) const XMRIG_EXTENSION: &str = "-macos-x64.tar.gz"; } #[cfg(target_os = "macos")] #[cfg(target_arch = "aarch64")] mod impl_platform { - pub(super) use super::impl_unix::*; + pub(super) use super::impl_unix::*; - pub(super) const GUPAX_EXTENSION: &str = "-macos-arm64-standalone.tar.gz"; - pub(super) const P2POOL_EXTENSION: &str = "-macos-aarch64.tar.gz"; - pub(super) const XMRIG_EXTENSION: &str = "-macos-arm64.tar.gz"; + pub(super) const GUPAX_EXTENSION: &str = "-macos-arm64-standalone.tar.gz"; + pub(super) const P2POOL_EXTENSION: &str = "-macos-aarch64.tar.gz"; + pub(super) const XMRIG_EXTENSION: &str = "-macos-arm64.tar.gz"; } #[cfg(target_os = "linux")] mod impl_platform { - pub(super) use super::impl_unix::*; + pub(super) use super::impl_unix::*; - pub(super) const GUPAX_EXTENSION: &str = "-linux-x64-standalone.tar.gz"; - pub(super) const P2POOL_EXTENSION: &str = "-linux-x64.tar.gz"; - pub(super) const XMRIG_EXTENSION: &str = "-linux-static-x64.tar.gz"; + pub(super) const GUPAX_EXTENSION: &str = "-linux-x64-standalone.tar.gz"; + pub(super) const P2POOL_EXTENSION: &str = "-linux-x64.tar.gz"; + pub(super) const XMRIG_EXTENSION: &str = "-linux-static-x64.tar.gz"; } use impl_platform::*; const VALID_GUPAX: [&str; 3] = [VALID_GUPAX_1, VALID_GUPAX_2, VALID_GUPAX_3]; const VALID_XMRIG: [&str; 4] = [VALID_XMRIG_1, VALID_XMRIG_2, VALID_XMRIG_3, VALID_XMRIG_4]; -const VALID_P2POOL: [&str; 4] = [VALID_P2POOL_1, VALID_P2POOL_2, VALID_P2POOL_3, VALID_P2POOL_4]; +const VALID_P2POOL: [&str; 4] = [ + VALID_P2POOL_1, + VALID_P2POOL_2, + VALID_P2POOL_3, + VALID_P2POOL_4, +]; // Some fake Curl/Wget user-agents because GitHub API requires one and a Tor browser // user-agent might be fingerprintable without all the associated headers. const FAKE_USER_AGENT: [&str; 25] = [ - "Wget/1.16.3","Wget/1.17","Wget/1.17.1","Wget/1.18","Wget/1.18","Wget/1.19","Wget/1.19.1","Wget/1.19.2","Wget/1.19.3","Wget/1.19.4", - "Wget/1.19.5","Wget/1.20","Wget/1.20.1","Wget/1.20.2","Wget/1.20.3","Wget/1.21","Wget/1.21.1","Wget/1.21.2","Wget/1.21.3","Wget/1.21.4", - "curl/7.65.3","curl/7.66.0","curl/7.67.0","curl/7.68.0","curl/8.4.0", + "Wget/1.16.3", + "Wget/1.17", + "Wget/1.17.1", + "Wget/1.18", + "Wget/1.18", + "Wget/1.19", + "Wget/1.19.1", + "Wget/1.19.2", + "Wget/1.19.3", + "Wget/1.19.4", + "Wget/1.19.5", + "Wget/1.20", + "Wget/1.20.1", + "Wget/1.20.2", + "Wget/1.20.3", + "Wget/1.21", + "Wget/1.21.1", + "Wget/1.21.2", + "Wget/1.21.3", + "Wget/1.21.4", + "curl/7.65.3", + "curl/7.66.0", + "curl/7.67.0", + "curl/7.68.0", + "curl/8.4.0", ]; const MSG_NONE: &str = "No update in progress"; @@ -194,7 +214,8 @@ const MSG_EXTRACT: &str = "Extracting packages"; const MSG_UPGRADE: &str = "Upgrading packages"; pub const MSG_SUCCESS: &str = "Update successful"; pub const MSG_FAILED: &str = "Update failed"; -pub const MSG_FAILED_HELP: &str = "Consider manually replacing your executable from: https://gupax.io/downloads"; +pub const MSG_FAILED_HELP: &str = + "Consider manually replacing your executable from: https://gupax.io/downloads"; const INIT: &str = "------------------- Init -------------------"; const METADATA: &str = "----------------- Metadata -----------------"; @@ -205,27 +226,39 @@ const UPGRADE: &str = "----------------- Upgrade ------------------"; //---------------------------------------------------------------------------------------------------- General functions pub fn check_p2pool_path(path: &str) -> bool { - let path = match crate::disk::into_absolute_path(path.to_string()) { - Ok(p) => p, - Err(_) => return false, - }; - let path = match path.file_name() { - Some(p) => p, - None => { error!("Couldn't get P2Pool file name"); return false; }, - }; - path == VALID_P2POOL[0] || path == VALID_P2POOL[1] || path == VALID_P2POOL[2] || path == VALID_P2POOL[3] + let path = match crate::disk::into_absolute_path(path.to_string()) { + Ok(p) => p, + Err(_) => return false, + }; + let path = match path.file_name() { + Some(p) => p, + None => { + error!("Couldn't get P2Pool file name"); + return false; + } + }; + path == VALID_P2POOL[0] + || path == VALID_P2POOL[1] + || path == VALID_P2POOL[2] + || path == VALID_P2POOL[3] } pub fn check_xmrig_path(path: &str) -> bool { - let path = match crate::disk::into_absolute_path(path.to_string()) { - Ok(p) => p, - Err(_) => return false, - }; - let path = match path.file_name() { - Some(p) => p, - None => { error!("Couldn't get XMRig file name"); return false; }, - }; - path == VALID_XMRIG[0] || path == VALID_XMRIG[1] || path == VALID_XMRIG[2] || path == VALID_XMRIG[3] + let path = match crate::disk::into_absolute_path(path.to_string()) { + Ok(p) => p, + Err(_) => return false, + }; + let path = match path.file_name() { + Some(p) => p, + None => { + error!("Couldn't get XMRig file name"); + return false; + } + }; + path == VALID_XMRIG[0] + || path == VALID_XMRIG[1] + || path == VALID_XMRIG[2] + || path == VALID_XMRIG[3] } //---------------------------------------------------------------------------------------------------- Update struct/impl @@ -242,683 +275,828 @@ pub fn check_xmrig_path(path: &str) -> bool { #[derive(Clone)] pub struct Update { - pub path_gupax: String, // Full path to current gupax - pub path_p2pool: String, // Full path to current p2pool - pub path_xmrig: String, // Full path to current xmrig - pub tmp_dir: String, // Full path to temporary directory - pub updating: Arc>, // Is an update in progress? - pub prog: Arc>, // Holds the 0-100% progress bar number - pub msg: Arc>, // Message to display on [Gupax] tab while updating - pub tor: bool, // Is Tor enabled or not? + pub path_gupax: String, // Full path to current gupax + pub path_p2pool: String, // Full path to current p2pool + pub path_xmrig: String, // Full path to current xmrig + pub tmp_dir: String, // Full path to temporary directory + pub updating: Arc>, // Is an update in progress? + pub prog: Arc>, // Holds the 0-100% progress bar number + pub msg: Arc>, // Message to display on [Gupax] tab while updating + pub tor: bool, // Is Tor enabled or not? } impl Update { - // Takes in current paths from [State] - pub fn new(path_gupax: String, path_p2pool: PathBuf, path_xmrig: PathBuf, tor: bool) -> Self { - Self { - path_gupax, - path_p2pool: path_p2pool.display().to_string(), - path_xmrig: path_xmrig.display().to_string(), - tmp_dir: "".to_string(), - updating: arc_mut!(false), - prog: arc_mut!(0.0), - msg: arc_mut!(MSG_NONE.to_string()), - tor, - } - } + // Takes in current paths from [State] + pub fn new(path_gupax: String, path_p2pool: PathBuf, path_xmrig: PathBuf, tor: bool) -> Self { + Self { + path_gupax, + path_p2pool: path_p2pool.display().to_string(), + path_xmrig: path_xmrig.display().to_string(), + tmp_dir: "".to_string(), + updating: arc_mut!(false), + prog: arc_mut!(0.0), + msg: arc_mut!(MSG_NONE.to_string()), + tor, + } + } - // Get a temporary random folder for package download contents - // This used to use [std::env::temp_dir()] but there were issues - // using [std::fs::rename()] on tmpfs -> disk (Invalid cross-device link (os error 18)). - // So, uses the [Gupax] binary directory as a base, something like [/home/hinto/gupax/gupax_update_SG4xsDdVmr] - pub fn get_tmp_dir() -> Result { - let rand_string: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(10) - .map(char::from) - .collect(); - let base = crate::get_exe_dir()?; - #[cfg(target_os = "windows")] - let tmp_dir = format!("{}{}{}{}", base, r"\gupax_update_", rand_string, r"\"); - #[cfg(target_family = "unix")] - let tmp_dir = format!("{}{}{}{}", base, "/gupax_update_", rand_string, "/"); - info!("Update | Temporary directory ... {}", tmp_dir); - Ok(tmp_dir) - } + // Get a temporary random folder for package download contents + // This used to use [std::env::temp_dir()] but there were issues + // using [std::fs::rename()] on tmpfs -> disk (Invalid cross-device link (os error 18)). + // So, uses the [Gupax] binary directory as a base, something like [/home/hinto/gupax/gupax_update_SG4xsDdVmr] + pub fn get_tmp_dir() -> Result { + let rand_string: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect(); + let base = crate::get_exe_dir()?; + #[cfg(target_os = "windows")] + let tmp_dir = format!("{}{}{}{}", base, r"\gupax_update_", rand_string, r"\"); + #[cfg(target_family = "unix")] + let tmp_dir = format!("{}{}{}{}", base, "/gupax_update_", rand_string, "/"); + info!("Update | Temporary directory ... {}", tmp_dir); + Ok(tmp_dir) + } - #[cold] - #[inline(never)] - // Get an HTTPS client. Uses [Arti] if Tor is enabled. - // The base type looks something like [hyper::Client<...>]. - // This is then wrapped with the custom [ClientEnum] type to implement - // dynamically returning either a [Tor+TLS|TLS-only] client at based on user settings. - // tor == true? => return Tor client - // tor == false? => return normal TLS client - // - // Since functions that take generic INPUT are much easier to implement, - // [get_response()] just takes a [hyper::Client], which is passed to - // it via deconstructing this [ClientEnum] with a match, like so: - // ClientEnum::Tor(T) => get_response(... T ...) - // ClientEnum::Https(H) => get_response(... H ...) - // - pub fn get_client(tor: bool) -> Result { - if tor { - // Below is async, bootstraps immediately but has issues when recreating the circuit - // let tor = TorClient::create_bootstrapped(TorClientConfig::default()).await?; - // This one below is non-async, and doesn't bootstrap immediately. - let tor = TorClient::builder().bootstrap_behavior(arti_client::BootstrapBehavior::OnDemand).create_unbootstrapped()?; - // This makes sure the Tor circuit is different each time - let tor = TorClient::isolated_client(&tor); - let tls = TlsConnector::builder()?.build()?; - let connector = ArtiHttpConnector::new(tor, tls); - let client = ClientEnum::Tor(Client::builder().build(connector)); - Ok(client) - } else { - let mut connector = hyper_tls::HttpsConnector::new(); - connector.https_only(true); - let client = ClientEnum::Https(Client::builder().build(connector)); - Ok(client) - } - } + #[cold] + #[inline(never)] + // Get an HTTPS client. Uses [Arti] if Tor is enabled. + // The base type looks something like [hyper::Client<...>]. + // This is then wrapped with the custom [ClientEnum] type to implement + // dynamically returning either a [Tor+TLS|TLS-only] client at based on user settings. + // tor == true? => return Tor client + // tor == false? => return normal TLS client + // + // Since functions that take generic INPUT are much easier to implement, + // [get_response()] just takes a [hyper::Client], which is passed to + // it via deconstructing this [ClientEnum] with a match, like so: + // ClientEnum::Tor(T) => get_response(... T ...) + // ClientEnum::Https(H) => get_response(... H ...) + // + pub fn get_client(tor: bool) -> Result { + if tor { + // Below is async, bootstraps immediately but has issues when recreating the circuit + // let tor = TorClient::create_bootstrapped(TorClientConfig::default()).await?; + // This one below is non-async, and doesn't bootstrap immediately. + let tor = TorClient::builder() + .bootstrap_behavior(arti_client::BootstrapBehavior::OnDemand) + .create_unbootstrapped()?; + // This makes sure the Tor circuit is different each time + let tor = TorClient::isolated_client(&tor); + let tls = TlsConnector::builder()?.build()?; + let connector = ArtiHttpConnector::new(tor, tls); + let client = ClientEnum::Tor(Client::builder().build(connector)); + Ok(client) + } else { + let mut connector = hyper_tls::HttpsConnector::new(); + connector.https_only(true); + let client = ClientEnum::Https(Client::builder().build(connector)); + Ok(client) + } + } - #[cold] - #[inline(never)] - // Intermediate function that spawns a new thread - // which starts the async [start()] function that - // actually contains the code. This is so that everytime - // an update needs to happen (Gupax tab, auto-update), the - // code only needs to be edited once, here. - pub fn spawn_thread(og: &Arc>, gupax: &crate::disk::Gupax, state_path: &Path, update: &Arc>, error_state: &mut ErrorState, restart: &Arc>) { - // We really shouldn't be in the function for - // the Linux distro Gupax (UI gets disabled) - // but if somehow get in here, just return. - #[cfg(feature = "distro")] - error!("Update | This is the [Linux distro] version of Gupax, updates are disabled"); - #[cfg(feature = "distro")] - return; + #[cold] + #[inline(never)] + // Intermediate function that spawns a new thread + // which starts the async [start()] function that + // actually contains the code. This is so that everytime + // an update needs to happen (Gupax tab, auto-update), the + // code only needs to be edited once, here. + pub fn spawn_thread( + og: &Arc>, + gupax: &crate::disk::Gupax, + state_path: &Path, + update: &Arc>, + error_state: &mut ErrorState, + restart: &Arc>, + ) { + // We really shouldn't be in the function for + // the Linux distro Gupax (UI gets disabled) + // but if somehow get in here, just return. + #[cfg(feature = "distro")] + error!("Update | This is the [Linux distro] version of Gupax, updates are disabled"); + #[cfg(feature = "distro")] + return; - // Check P2Pool path for safety - // Attempt relative to absolute path - let p2pool_path = match into_absolute_path(gupax.p2pool_path.clone()) { - Ok(p) => p, - Err(e) => { error_state.set(format!("Provided P2Pool path could not be turned into an absolute path: {}", e), ErrorFerris::Error, ErrorButtons::Okay); return; }, - }; - // Attempt to get basename - let file = match p2pool_path.file_name() { - Some(p) => { - // Attempt to turn into str - match p.to_str() { - Some(p) => p, - None => { error_state.set("Provided P2Pool path could not be turned into a UTF-8 string (are you using non-English characters?)", ErrorFerris::Error, ErrorButtons::Okay); return; }, - } - }, - None => { error_state.set("Provided P2Pool path could not be found", ErrorFerris::Error, ErrorButtons::Okay); return; }, - }; - // If it doesn't look like [P2Pool], its probably a bad move - // to overwrite it with an update, so set an error. - // Doesnt seem like you can [match] on array indexes - // so that explains the ridiculous if/else. - if check_p2pool_path(file) { - info!("Update | Using P2Pool path: [{}]", p2pool_path.display()); - } else { - warn!("Update | Aborting update, incorrect P2Pool path: [{}]", file); - let text = format!("Provided P2Pool path seems incorrect. Not starting update for safety.\nTry one of these: {:?}", VALID_P2POOL); - error_state.set(text, ErrorFerris::Error, ErrorButtons::Okay); - return; - } + // Check P2Pool path for safety + // Attempt relative to absolute path + let p2pool_path = match into_absolute_path(gupax.p2pool_path.clone()) { + Ok(p) => p, + Err(e) => { + error_state.set( + format!( + "Provided P2Pool path could not be turned into an absolute path: {}", + e + ), + ErrorFerris::Error, + ErrorButtons::Okay, + ); + return; + } + }; + // Attempt to get basename + let file = match p2pool_path.file_name() { + Some(p) => { + // Attempt to turn into str + match p.to_str() { + Some(p) => p, + None => { + error_state.set("Provided P2Pool path could not be turned into a UTF-8 string (are you using non-English characters?)", ErrorFerris::Error, ErrorButtons::Okay); + return; + } + } + } + None => { + error_state.set( + "Provided P2Pool path could not be found", + ErrorFerris::Error, + ErrorButtons::Okay, + ); + return; + } + }; + // If it doesn't look like [P2Pool], its probably a bad move + // to overwrite it with an update, so set an error. + // Doesnt seem like you can [match] on array indexes + // so that explains the ridiculous if/else. + if check_p2pool_path(file) { + info!("Update | Using P2Pool path: [{}]", p2pool_path.display()); + } else { + warn!( + "Update | Aborting update, incorrect P2Pool path: [{}]", + file + ); + let text = format!("Provided P2Pool path seems incorrect. Not starting update for safety.\nTry one of these: {:?}", VALID_P2POOL); + error_state.set(text, ErrorFerris::Error, ErrorButtons::Okay); + return; + } - // Check XMRig path for safety - let xmrig_path = match into_absolute_path(gupax.xmrig_path.clone()) { - Ok(p) => p, - Err(e) => { error_state.set(format!("Provided XMRig path could not be turned into an absolute path: {}", e), ErrorFerris::Error, ErrorButtons::Okay); return; }, - }; - let file = match xmrig_path.file_name() { - Some(p) => { - // Attempt to turn into str - match p.to_str() { - Some(p) => p, - None => { error_state.set("Provided XMRig path could not be turned into a UTF-8 string (are you using non-English characters?)", ErrorFerris::Error, ErrorButtons::Okay); return; }, - } - }, - None => { error_state.set("Provided XMRig path could not be found", ErrorFerris::Error, ErrorButtons::Okay); return; }, - }; - if check_xmrig_path(file) { - info!("Update | Using XMRig path: [{}]", xmrig_path.display()); - } else { - warn!("Update | Aborting update, incorrect XMRig path: [{}]", file); - let text = format!("Provided XMRig path seems incorrect. Not starting update for safety.\nTry one of these: {:?}", VALID_XMRIG); - error_state.set(text, ErrorFerris::Error, ErrorButtons::Okay); - return; - } + // Check XMRig path for safety + let xmrig_path = match into_absolute_path(gupax.xmrig_path.clone()) { + Ok(p) => p, + Err(e) => { + error_state.set( + format!( + "Provided XMRig path could not be turned into an absolute path: {}", + e + ), + ErrorFerris::Error, + ErrorButtons::Okay, + ); + return; + } + }; + let file = match xmrig_path.file_name() { + Some(p) => { + // Attempt to turn into str + match p.to_str() { + Some(p) => p, + None => { + error_state.set("Provided XMRig path could not be turned into a UTF-8 string (are you using non-English characters?)", ErrorFerris::Error, ErrorButtons::Okay); + return; + } + } + } + None => { + error_state.set( + "Provided XMRig path could not be found", + ErrorFerris::Error, + ErrorButtons::Okay, + ); + return; + } + }; + if check_xmrig_path(file) { + info!("Update | Using XMRig path: [{}]", xmrig_path.display()); + } else { + warn!("Update | Aborting update, incorrect XMRig path: [{}]", file); + let text = format!("Provided XMRig path seems incorrect. Not starting update for safety.\nTry one of these: {:?}", VALID_XMRIG); + error_state.set(text, ErrorFerris::Error, ErrorButtons::Okay); + return; + } - lock!(update).path_p2pool = p2pool_path.display().to_string(); - lock!(update).path_xmrig = xmrig_path.display().to_string(); - lock!(update).tor = gupax.update_via_tor; + lock!(update).path_p2pool = p2pool_path.display().to_string(); + lock!(update).path_xmrig = xmrig_path.display().to_string(); + lock!(update).tor = gupax.update_via_tor; - // Clone before thread spawn - let og = Arc::clone(og); - let state_ver = Arc::clone(&lock!(og).version); - let state_path = state_path.to_path_buf(); - let update = Arc::clone(update); - let restart = Arc::clone(restart); - info!("Spawning update thread..."); - std::thread::spawn(move|| { - match Update::start(update.clone(), og.clone(), state_ver.clone(), restart) { - Ok(_) => { - info!("Update | Saving state..."); - let original_version = lock!(og).version.clone(); - lock!(og).version = state_ver; - match State::save(&mut lock!(og), &state_path) { - Ok(_) => info!("Update ... OK"), - Err(e) => { - warn!("Update | Saving state ... FAIL: {}", e); - lock!(og).version = original_version; - *lock2!(update,msg) = "Saving new versions into state failed".to_string(); - }, - }; - } - Err(e) => { - info!("Update ... FAIL: {}", e); - *lock2!(update,msg) = format!("{} | {}\n{}", MSG_FAILED, e, MSG_FAILED_HELP); - }, - }; - *lock2!(update,updating) = false; - }); - } + // Clone before thread spawn + let og = Arc::clone(og); + let state_ver = Arc::clone(&lock!(og).version); + let state_path = state_path.to_path_buf(); + let update = Arc::clone(update); + let restart = Arc::clone(restart); + info!("Spawning update thread..."); + std::thread::spawn(move || { + match Update::start(update.clone(), og.clone(), state_ver.clone(), restart) { + Ok(_) => { + info!("Update | Saving state..."); + let original_version = lock!(og).version.clone(); + lock!(og).version = state_ver; + match State::save(&mut lock!(og), &state_path) { + Ok(_) => info!("Update ... OK"), + Err(e) => { + warn!("Update | Saving state ... FAIL: {}", e); + lock!(og).version = original_version; + *lock2!(update, msg) = + "Saving new versions into state failed".to_string(); + } + }; + } + Err(e) => { + info!("Update ... FAIL: {}", e); + *lock2!(update, msg) = format!("{} | {}\n{}", MSG_FAILED, e, MSG_FAILED_HELP); + } + }; + *lock2!(update, updating) = false; + }); + } - #[cold] - #[inline(never)] - // Download process: - // 0. setup tor, client, http, etc - // 1. fill vector with all enums - // 2. loop over vec, download metadata - // 3. if current == version, remove from vec - // 4. loop over vec, download links - // 5. extract, upgrade - #[tokio::main] - pub async fn start(update: Arc>, _og: Arc>, state_ver: Arc>, restart: Arc>) -> Result<(), anyhow::Error> { - #[cfg(feature = "distro")] - error!("Update | This is the [Linux distro] version of Gupax, updates are disabled"); - #[cfg(feature = "distro")] - return Err(anyhow!("This is the [Linux distro] version of Gupax, updates are disabled")); + #[cold] + #[inline(never)] + // Download process: + // 0. setup tor, client, http, etc + // 1. fill vector with all enums + // 2. loop over vec, download metadata + // 3. if current == version, remove from vec + // 4. loop over vec, download links + // 5. extract, upgrade + #[tokio::main] + pub async fn start( + update: Arc>, + _og: Arc>, + state_ver: Arc>, + restart: Arc>, + ) -> Result<(), anyhow::Error> { + #[cfg(feature = "distro")] + error!("Update | This is the [Linux distro] version of Gupax, updates are disabled"); + #[cfg(feature = "distro")] + return Err(anyhow!( + "This is the [Linux distro] version of Gupax, updates are disabled" + )); - //---------------------------------------------------------------------------------------------------- Init - *lock2!(update,updating) = true; - // Set timer - let now = std::time::Instant::now(); + //---------------------------------------------------------------------------------------------------- Init + *lock2!(update, updating) = true; + // Set timer + let now = std::time::Instant::now(); - // Set progress bar - *lock2!(update,msg) = MSG_START.to_string(); - *lock2!(update,prog) = 0.0; - info!("Update | {}", INIT); + // Set progress bar + *lock2!(update, msg) = MSG_START.to_string(); + *lock2!(update, prog) = 0.0; + info!("Update | {}", INIT); - // Get temporary directory - let msg = MSG_TMP.to_string(); - info!("Update | {}", msg); - *lock2!(update,msg) = msg; - let tmp_dir = Self::get_tmp_dir()?; - std::fs::create_dir(&tmp_dir)?; + // Get temporary directory + let msg = MSG_TMP.to_string(); + info!("Update | {}", msg); + *lock2!(update, msg) = msg; + let tmp_dir = Self::get_tmp_dir()?; + std::fs::create_dir(&tmp_dir)?; - // Make Pkg vector - let mut vec = vec![ - Pkg::new(Gupax), - Pkg::new(P2pool), - Pkg::new(Xmrig), - ]; + // Make Pkg vector + let mut vec = vec![Pkg::new(Gupax), Pkg::new(P2pool), Pkg::new(Xmrig)]; - // Generate fake user-agent - let user_agent = Pkg::get_user_agent(); - *lock2!(update,prog) = 5.0; + // Generate fake user-agent + let user_agent = Pkg::get_user_agent(); + *lock2!(update, prog) = 5.0; - // Create Tor/HTTPS client - let lock = lock!(update); - let tor = lock.tor; - if tor { - let msg = MSG_TOR.to_string(); - info!("Update | {}", msg); - *lock!(lock.msg) = msg; - } else { - let msg = MSG_HTTPS.to_string(); - info!("Update | {}", msg); - *lock!(lock.msg) = msg; - } - drop(lock); - let mut client = Self::get_client(tor)?; - *lock2!(update,prog) += 5.0; - info!("Update | Init ... OK ... {}%", lock2!(update,prog)); + // Create Tor/HTTPS client + let lock = lock!(update); + let tor = lock.tor; + if tor { + let msg = MSG_TOR.to_string(); + info!("Update | {}", msg); + *lock!(lock.msg) = msg; + } else { + let msg = MSG_HTTPS.to_string(); + info!("Update | {}", msg); + *lock!(lock.msg) = msg; + } + drop(lock); + let mut client = Self::get_client(tor)?; + *lock2!(update, prog) += 5.0; + info!("Update | Init ... OK ... {}%", lock2!(update, prog)); - //---------------------------------------------------------------------------------------------------- Metadata - *lock2!(update,msg) = MSG_METADATA.to_string(); - info!("Update | {}", METADATA); - let mut vec2 = vec![]; - // Loop process: - // 1. Start all async metadata fetches - // 2. Wait for all to finish - // 3. Iterate over all [pkg.new_ver], check if empty - // 4. If not empty, move [pkg] to different vec - // 5. At end, if original vec isn't empty, that means something failed - // 6. Redo loop [3] times (rebuild circuit if using Tor), with the original vec (that now only has the failed pkgs) - // - // This logic was originally in the [Pkg::get_metadata()] - // function itself but for some reason, it was getting skipped over, - // so the [new_ver] check is now here, in the outer scope. - for i in 1..=3 { - if i > 1 { *lock2!(update,msg) = format!("{} [{}/3]", MSG_METADATA_RETRY, i); } - let mut handles: Vec>> = vec![]; - for pkg in vec.iter() { - // Clone data before sending to async - let new_ver = Arc::clone(&pkg.new_ver); - let client = client.clone(); - let link = pkg.link_metadata.to_string(); - // Send to async - let handle: JoinHandle> = tokio::spawn(async move { - match client { - ClientEnum::Tor(t) => Pkg::get_metadata(new_ver, t, link, user_agent).await, - ClientEnum::Https(h) => Pkg::get_metadata(new_ver, h, link, user_agent).await, - } - }); - handles.push(handle); - } - // Handle await - for handle in handles { - // Two [??] will send the error. - // We don't actually want to return the error here since we - // prefer looping and retrying over immediately erroring out. - if let Err(e) = handle.await? { warn!("Update | {}", e) } - } - // Check for empty version - let mut indexes = vec![]; - for (index, pkg) in vec.iter().enumerate() { - if lock!(pkg.new_ver).is_empty() { - warn!("Update | {} failed, attempt [{}/3]...", pkg.name, i+1); - } else { - indexes.push(index); - vec2.push(pkg.clone()); - *lock2!(update,prog) += 10.0; - info!("Update | {} {} ... OK", pkg.name, lock!(pkg.new_ver)); - } - } - // Order indexes from biggest to smallest - // This prevents shifting the whole vector and causing panics. - indexes.sort(); - indexes.reverse(); - for index in indexes { - vec.remove(index); - } - if vec.is_empty() { break } - // Some Tor exit nodes seem to be blocked by GitHub's API, - // so recreate the circuit every loop. - if tor { - info!("Update | Recreating Tor client..."); - client = Self::get_client(tor)?; - } - } - if vec.is_empty() { - info!("Update | Metadata ... OK ... {}%", lock2!(update,prog)); - } else { - error!("Update | Metadata ... FAIL"); - return Err(anyhow!("Metadata fetch failed")) - } + //---------------------------------------------------------------------------------------------------- Metadata + *lock2!(update, msg) = MSG_METADATA.to_string(); + info!("Update | {}", METADATA); + let mut vec2 = vec![]; + // Loop process: + // 1. Start all async metadata fetches + // 2. Wait for all to finish + // 3. Iterate over all [pkg.new_ver], check if empty + // 4. If not empty, move [pkg] to different vec + // 5. At end, if original vec isn't empty, that means something failed + // 6. Redo loop [3] times (rebuild circuit if using Tor), with the original vec (that now only has the failed pkgs) + // + // This logic was originally in the [Pkg::get_metadata()] + // function itself but for some reason, it was getting skipped over, + // so the [new_ver] check is now here, in the outer scope. + for i in 1..=3 { + if i > 1 { + *lock2!(update, msg) = format!("{} [{}/3]", MSG_METADATA_RETRY, i); + } + let mut handles: Vec>> = vec![]; + for pkg in vec.iter() { + // Clone data before sending to async + let new_ver = Arc::clone(&pkg.new_ver); + let client = client.clone(); + let link = pkg.link_metadata.to_string(); + // Send to async + let handle: JoinHandle> = tokio::spawn(async move { + match client { + ClientEnum::Tor(t) => Pkg::get_metadata(new_ver, t, link, user_agent).await, + ClientEnum::Https(h) => { + Pkg::get_metadata(new_ver, h, link, user_agent).await + } + } + }); + handles.push(handle); + } + // Handle await + for handle in handles { + // Two [??] will send the error. + // We don't actually want to return the error here since we + // prefer looping and retrying over immediately erroring out. + if let Err(e) = handle.await? { + warn!("Update | {}", e) + } + } + // Check for empty version + let mut indexes = vec![]; + for (index, pkg) in vec.iter().enumerate() { + if lock!(pkg.new_ver).is_empty() { + warn!("Update | {} failed, attempt [{}/3]...", pkg.name, i + 1); + } else { + indexes.push(index); + vec2.push(pkg.clone()); + *lock2!(update, prog) += 10.0; + info!("Update | {} {} ... OK", pkg.name, lock!(pkg.new_ver)); + } + } + // Order indexes from biggest to smallest + // This prevents shifting the whole vector and causing panics. + indexes.sort(); + indexes.reverse(); + for index in indexes { + vec.remove(index); + } + if vec.is_empty() { + break; + } + // Some Tor exit nodes seem to be blocked by GitHub's API, + // so recreate the circuit every loop. + if tor { + info!("Update | Recreating Tor client..."); + client = Self::get_client(tor)?; + } + } + if vec.is_empty() { + info!("Update | Metadata ... OK ... {}%", lock2!(update, prog)); + } else { + error!("Update | Metadata ... FAIL"); + return Err(anyhow!("Metadata fetch failed")); + } - //---------------------------------------------------------------------------------------------------- Compare - *lock2!(update,msg) = MSG_COMPARE.to_string(); - info!("Update | {}", COMPARE); - let mut vec3 = vec![]; - let mut new_pkgs = vec![]; - for pkg in vec2.iter() { - let new_ver = lock!(pkg.new_ver).clone(); - let diff; - let old_ver; - let name; - match pkg.name { - Gupax => { - // Compare against the built-in compiled version as well as an in-memory version - // that gets updated during an update. This prevents the updater always thinking - // there's a new Gupax update since the user didnt restart and is still technically - // using the old version (even though the underlying binary was updated). - old_ver = lock!(state_ver).gupax.clone(); - diff = old_ver != new_ver && GUPAX_VERSION != new_ver; - name = "Gupax"; - } - P2pool => { - old_ver = lock!(state_ver).p2pool.clone(); - diff = old_ver != new_ver; - name = "P2Pool"; - } - Xmrig => { - old_ver = lock!(state_ver).xmrig.clone(); - diff = old_ver != new_ver; - name = "XMRig"; - } - } - if diff { - info!("Update | {} {} != {} ... ADDING", pkg.name, old_ver, new_ver); - new_pkgs.push(format!("\n{} {} -> {}", name, old_ver, new_ver)); - vec3.push(pkg); - } else { - info!("Update | {} {} == {} ... SKIPPING", pkg.name, old_ver, new_ver); - } - } - *lock2!(update,prog) += 5.0; - info!("Update | Compare ... OK ... {}%", lock2!(update,prog)); + //---------------------------------------------------------------------------------------------------- Compare + *lock2!(update, msg) = MSG_COMPARE.to_string(); + info!("Update | {}", COMPARE); + let mut vec3 = vec![]; + let mut new_pkgs = vec![]; + for pkg in vec2.iter() { + let new_ver = lock!(pkg.new_ver).clone(); + let diff; + let old_ver; + let name; + match pkg.name { + Gupax => { + // Compare against the built-in compiled version as well as an in-memory version + // that gets updated during an update. This prevents the updater always thinking + // there's a new Gupax update since the user didnt restart and is still technically + // using the old version (even though the underlying binary was updated). + old_ver = lock!(state_ver).gupax.clone(); + diff = old_ver != new_ver && GUPAX_VERSION != new_ver; + name = "Gupax"; + } + P2pool => { + old_ver = lock!(state_ver).p2pool.clone(); + diff = old_ver != new_ver; + name = "P2Pool"; + } + Xmrig => { + old_ver = lock!(state_ver).xmrig.clone(); + diff = old_ver != new_ver; + name = "XMRig"; + } + } + if diff { + info!( + "Update | {} {} != {} ... ADDING", + pkg.name, old_ver, new_ver + ); + new_pkgs.push(format!("\n{} {} -> {}", name, old_ver, new_ver)); + vec3.push(pkg); + } else { + info!( + "Update | {} {} == {} ... SKIPPING", + pkg.name, old_ver, new_ver + ); + } + } + *lock2!(update, prog) += 5.0; + info!("Update | Compare ... OK ... {}%", lock2!(update, prog)); - // Return if 0 (all packages up-to-date) - // Get amount of packages to divide up the percentage increases - let pkg_amount = vec3.len() as f32; - if pkg_amount == 0.0 { - info!("Update | All packages up-to-date ... RETURNING"); - *lock2!(update,prog) = 100.0; - *lock2!(update,msg) = MSG_UP_TO_DATE.to_string(); - return Ok(()) - } - let new_pkgs: String = new_pkgs.concat(); + // Return if 0 (all packages up-to-date) + // Get amount of packages to divide up the percentage increases + let pkg_amount = vec3.len() as f32; + if pkg_amount == 0.0 { + info!("Update | All packages up-to-date ... RETURNING"); + *lock2!(update, prog) = 100.0; + *lock2!(update, msg) = MSG_UP_TO_DATE.to_string(); + return Ok(()); + } + let new_pkgs: String = new_pkgs.concat(); - //---------------------------------------------------------------------------------------------------- Download - *lock2!(update,msg) = format!("{}{}", MSG_DOWNLOAD, new_pkgs); - info!("Update | {}", DOWNLOAD); - let mut vec4 = vec![]; - for i in 1..=3 { - if i > 1 { *lock2!(update,msg) = format!("{} [{}/3]{}", MSG_DOWNLOAD_RETRY, i, new_pkgs); } - let mut handles: Vec>> = vec![]; - for pkg in vec3.iter() { - // Clone data before async - let bytes = Arc::clone(&pkg.bytes); - let client = client.clone(); - let version = lock!(pkg.new_ver); - // Download link = PREFIX + Version (found at runtime) + SUFFIX + Version + EXT - // Example: https://github.com/hinto-janai/gupax/releases/download/v0.0.1/gupax-v0.0.1-linux-x64-standalone - // XMRig doesn't have a [v], so slice it out - let link = match pkg.name { - Name::Xmrig => pkg.link_prefix.to_string() + &version + pkg.link_suffix + &version[1..] + pkg.link_extension, - _ => pkg.link_prefix.to_string() + &version + pkg.link_suffix + &version + pkg.link_extension, - }; - info!("Update | {} ... {}", pkg.name, link); - let handle: JoinHandle> = tokio::spawn(async move { - match client { - ClientEnum::Tor(t) => Pkg::get_bytes(bytes, t, link, user_agent).await, - ClientEnum::Https(h) => Pkg::get_bytes(bytes, h, link, user_agent).await, - } - }); - handles.push(handle); - } - // Handle await - for handle in handles { - if let Err(e) = handle.await? { warn!("Update | {}", e) } - } - // Check for empty bytes - let mut indexes = vec![]; - for (index, pkg) in vec3.iter().enumerate() { - if lock!(pkg.bytes).is_empty() { - warn!("Update | {} failed, attempt [{}/3]...", pkg.name, i); - } else { - indexes.push(index); - vec4.push(pkg.clone()); - *lock2!(update,prog) += (30.0 / pkg_amount).round(); - info!("Update | {} ... OK", pkg.name); - } - } - // Order indexes from biggest to smallest - // This prevents shifting the whole vector and causing panics. - indexes.sort(); - indexes.reverse(); - for index in indexes { - vec3.remove(index); - } - if vec3.is_empty() { break } - } - if vec3.is_empty() { - info!("Update | Download ... OK ... {}%", *lock2!(update,prog)); - } else { - error!("Update | Download ... FAIL"); - return Err(anyhow!("Download failed")) - } + //---------------------------------------------------------------------------------------------------- Download + *lock2!(update, msg) = format!("{}{}", MSG_DOWNLOAD, new_pkgs); + info!("Update | {}", DOWNLOAD); + let mut vec4 = vec![]; + for i in 1..=3 { + if i > 1 { + *lock2!(update, msg) = format!("{} [{}/3]{}", MSG_DOWNLOAD_RETRY, i, new_pkgs); + } + let mut handles: Vec>> = vec![]; + for pkg in vec3.iter() { + // Clone data before async + let bytes = Arc::clone(&pkg.bytes); + let client = client.clone(); + let version = lock!(pkg.new_ver); + // Download link = PREFIX + Version (found at runtime) + SUFFIX + Version + EXT + // Example: https://github.com/hinto-janai/gupax/releases/download/v0.0.1/gupax-v0.0.1-linux-x64-standalone + // XMRig doesn't have a [v], so slice it out + let link = match pkg.name { + Name::Xmrig => { + pkg.link_prefix.to_string() + + &version + + pkg.link_suffix + + &version[1..] + + pkg.link_extension + } + _ => { + pkg.link_prefix.to_string() + + &version + + pkg.link_suffix + + &version + + pkg.link_extension + } + }; + info!("Update | {} ... {}", pkg.name, link); + let handle: JoinHandle> = tokio::spawn(async move { + match client { + ClientEnum::Tor(t) => Pkg::get_bytes(bytes, t, link, user_agent).await, + ClientEnum::Https(h) => Pkg::get_bytes(bytes, h, link, user_agent).await, + } + }); + handles.push(handle); + } + // Handle await + for handle in handles { + if let Err(e) = handle.await? { + warn!("Update | {}", e) + } + } + // Check for empty bytes + let mut indexes = vec![]; + for (index, pkg) in vec3.iter().enumerate() { + if lock!(pkg.bytes).is_empty() { + warn!("Update | {} failed, attempt [{}/3]...", pkg.name, i); + } else { + indexes.push(index); + vec4.push(pkg.clone()); + *lock2!(update, prog) += (30.0 / pkg_amount).round(); + info!("Update | {} ... OK", pkg.name); + } + } + // Order indexes from biggest to smallest + // This prevents shifting the whole vector and causing panics. + indexes.sort(); + indexes.reverse(); + for index in indexes { + vec3.remove(index); + } + if vec3.is_empty() { + break; + } + } + if vec3.is_empty() { + info!("Update | Download ... OK ... {}%", *lock2!(update, prog)); + } else { + error!("Update | Download ... FAIL"); + return Err(anyhow!("Download failed")); + } - //---------------------------------------------------------------------------------------------------- Extract - *lock2!(update,msg) = format!("{}{}", MSG_EXTRACT, new_pkgs); - info!("Update | {}", EXTRACT); - for pkg in vec4.iter() { - let tmp = match pkg.name { - Name::Gupax => tmp_dir.to_owned() + GUPAX_BINARY, - _ => tmp_dir.to_owned() + &pkg.name.to_string(), - }; - #[cfg(target_os = "windows")] - ZipArchive::extract(&mut ZipArchive::new(std::io::Cursor::new(lock!(pkg.bytes).as_ref()))?, tmp)?; - #[cfg(target_family = "unix")] - tar::Archive::new(flate2::read::GzDecoder::new(lock!(pkg.bytes).as_ref())).unpack(tmp)?; - *lock2!(update,prog) += (5.0 / pkg_amount).round(); - info!("Update | {} ... OK", pkg.name); - } - info!("Update | Extract ... OK ... {}%", *lock2!(update,prog)); + //---------------------------------------------------------------------------------------------------- Extract + *lock2!(update, msg) = format!("{}{}", MSG_EXTRACT, new_pkgs); + info!("Update | {}", EXTRACT); + for pkg in vec4.iter() { + let tmp = match pkg.name { + Name::Gupax => tmp_dir.to_owned() + GUPAX_BINARY, + _ => tmp_dir.to_owned() + &pkg.name.to_string(), + }; + #[cfg(target_os = "windows")] + ZipArchive::extract( + &mut ZipArchive::new(std::io::Cursor::new(lock!(pkg.bytes).as_ref()))?, + tmp, + )?; + #[cfg(target_family = "unix")] + tar::Archive::new(flate2::read::GzDecoder::new(lock!(pkg.bytes).as_ref())) + .unpack(tmp)?; + *lock2!(update, prog) += (5.0 / pkg_amount).round(); + info!("Update | {} ... OK", pkg.name); + } + info!("Update | Extract ... OK ... {}%", *lock2!(update, prog)); - //---------------------------------------------------------------------------------------------------- Upgrade - // 1. Walk directories - // 2. If basename matches known binary name, start - // 3. Rename tmp path into current path - // 4. Update [State/Version] - *lock2!(update,msg) = format!("{}{}", MSG_UPGRADE, new_pkgs); - info!("Update | {}", UPGRADE); - // If this bool doesn't get set, something has gone wrong because - // we _didn't_ find a binary even though we downloaded it. - let mut found = false; - for entry in WalkDir::new(tmp_dir.clone()) { - let entry = entry?.clone(); - // If not a file, continue - if ! entry.file_type().is_file() { continue } - let basename = entry.file_name().to_str().ok_or_else(|| anyhow!("WalkDir basename failed"))?; - match basename { - VALID_GUPAX_1|VALID_GUPAX_2|VALID_GUPAX_3| - VALID_P2POOL_1|VALID_P2POOL_2|VALID_P2POOL_3|VALID_P2POOL_4| - VALID_XMRIG_1|VALID_XMRIG_2|VALID_XMRIG_3|VALID_XMRIG_4 => { - found = true; - let name = match basename { - VALID_GUPAX_1|VALID_GUPAX_2|VALID_GUPAX_3 => Gupax, - VALID_P2POOL_1|VALID_P2POOL_2|VALID_P2POOL_3|VALID_P2POOL_4 => P2pool, - _ => Xmrig, - }; - let path = match name { - Gupax => lock!(update).path_gupax.clone(), - P2pool => lock!(update).path_p2pool.clone(), - Xmrig => lock!(update).path_xmrig.clone(), - }; - let path = Path::new(&path); - // Unix can replace running binaries no problem (they're loaded into memory) - // Windows locks binaries in place, so we must move (rename) current binary - // into the temp folder, then move the new binary into the old ones spot. - // Clearing the temp folder is now moved at startup instead at the end - // of this function due to this behavior, thanks Windows. - #[cfg(target_os = "windows")] - if path.exists() { - let tmp_windows = match name { - Gupax => tmp_dir.clone() + "gupax_old.exe", - P2pool => tmp_dir.clone() + "p2pool_old.exe", - Xmrig => tmp_dir.clone() + "xmrig_old.exe", - }; - info!("Update | WINDOWS ONLY ... Moving old [{}] -> [{}]", path.display(), tmp_windows); - std::fs::rename(&path, tmp_windows)?; - } - info!("Update | Moving new [{}] -> [{}]", entry.path().display(), path.display()); - // Create folder for [P2Pool/XMRig] - if name == P2pool || name == Xmrig { - std::fs::create_dir_all(path.parent().ok_or_else(|| anyhow!(format!("{} path failed", name)))?)?; - } - // Move downloaded path into old path - std::fs::rename(entry.path(), path)?; - // Update [State] version - match name { - Gupax => { - lock!(state_ver).gupax = Pkg::get_new_pkg_version(Gupax, &vec4)?; - // If we're updating Gupax, set the [Restart] state so that the user knows to restart - *lock!(restart) = Restart::Yes; - }, - P2pool => lock!(state_ver).p2pool = Pkg::get_new_pkg_version(P2pool, &vec4)?, - Xmrig => lock!(state_ver).xmrig = Pkg::get_new_pkg_version(Xmrig, &vec4)?, - }; - *lock2!(update,prog) += (5.0 / pkg_amount).round(); - }, - _ => (), - } - } - if !found { return Err(anyhow!("Fatal error: Package binary could not be found")) } + //---------------------------------------------------------------------------------------------------- Upgrade + // 1. Walk directories + // 2. If basename matches known binary name, start + // 3. Rename tmp path into current path + // 4. Update [State/Version] + *lock2!(update, msg) = format!("{}{}", MSG_UPGRADE, new_pkgs); + info!("Update | {}", UPGRADE); + // If this bool doesn't get set, something has gone wrong because + // we _didn't_ find a binary even though we downloaded it. + let mut found = false; + for entry in WalkDir::new(tmp_dir.clone()) { + let entry = entry?.clone(); + // If not a file, continue + if !entry.file_type().is_file() { + continue; + } + let basename = entry + .file_name() + .to_str() + .ok_or_else(|| anyhow!("WalkDir basename failed"))?; + match basename { + VALID_GUPAX_1 | VALID_GUPAX_2 | VALID_GUPAX_3 | VALID_P2POOL_1 | VALID_P2POOL_2 + | VALID_P2POOL_3 | VALID_P2POOL_4 | VALID_XMRIG_1 | VALID_XMRIG_2 + | VALID_XMRIG_3 | VALID_XMRIG_4 => { + found = true; + let name = match basename { + VALID_GUPAX_1 | VALID_GUPAX_2 | VALID_GUPAX_3 => Gupax, + VALID_P2POOL_1 | VALID_P2POOL_2 | VALID_P2POOL_3 | VALID_P2POOL_4 => P2pool, + _ => Xmrig, + }; + let path = match name { + Gupax => lock!(update).path_gupax.clone(), + P2pool => lock!(update).path_p2pool.clone(), + Xmrig => lock!(update).path_xmrig.clone(), + }; + let path = Path::new(&path); + // Unix can replace running binaries no problem (they're loaded into memory) + // Windows locks binaries in place, so we must move (rename) current binary + // into the temp folder, then move the new binary into the old ones spot. + // Clearing the temp folder is now moved at startup instead at the end + // of this function due to this behavior, thanks Windows. + #[cfg(target_os = "windows")] + if path.exists() { + let tmp_windows = match name { + Gupax => tmp_dir.clone() + "gupax_old.exe", + P2pool => tmp_dir.clone() + "p2pool_old.exe", + Xmrig => tmp_dir.clone() + "xmrig_old.exe", + }; + info!( + "Update | WINDOWS ONLY ... Moving old [{}] -> [{}]", + path.display(), + tmp_windows + ); + std::fs::rename(&path, tmp_windows)?; + } + info!( + "Update | Moving new [{}] -> [{}]", + entry.path().display(), + path.display() + ); + // Create folder for [P2Pool/XMRig] + if name == P2pool || name == Xmrig { + std::fs::create_dir_all( + path.parent() + .ok_or_else(|| anyhow!(format!("{} path failed", name)))?, + )?; + } + // Move downloaded path into old path + std::fs::rename(entry.path(), path)?; + // Update [State] version + match name { + Gupax => { + lock!(state_ver).gupax = Pkg::get_new_pkg_version(Gupax, &vec4)?; + // If we're updating Gupax, set the [Restart] state so that the user knows to restart + *lock!(restart) = Restart::Yes; + } + P2pool => { + lock!(state_ver).p2pool = Pkg::get_new_pkg_version(P2pool, &vec4)? + } + Xmrig => lock!(state_ver).xmrig = Pkg::get_new_pkg_version(Xmrig, &vec4)?, + }; + *lock2!(update, prog) += (5.0 / pkg_amount).round(); + } + _ => (), + } + } + if !found { + return Err(anyhow!("Fatal error: Package binary could not be found")); + } - // Remove tmp dir (on Unix) - #[cfg(target_family = "unix")] - info!("Update | Removing temporary directory ... {}", tmp_dir); - #[cfg(target_family = "unix")] - std::fs::remove_dir_all(&tmp_dir)?; + // Remove tmp dir (on Unix) + #[cfg(target_family = "unix")] + info!("Update | Removing temporary directory ... {}", tmp_dir); + #[cfg(target_family = "unix")] + std::fs::remove_dir_all(&tmp_dir)?; - let seconds = now.elapsed().as_secs(); - info!("Update | Seconds elapsed ... [{}s]", seconds); - match seconds { - 0 => *lock2!(update,msg) = format!("{}! Took 0 seconds... What...?!{}", MSG_SUCCESS, new_pkgs), - 1 => *lock2!(update,msg) = format!("{}! Took 1 second... Wow!{}", MSG_SUCCESS, new_pkgs), - _ => *lock2!(update,msg) = format!("{}! Took {} seconds.{}", MSG_SUCCESS, seconds, new_pkgs), - } - *lock2!(update,prog) = 100.0; - Ok(()) - } + let seconds = now.elapsed().as_secs(); + info!("Update | Seconds elapsed ... [{}s]", seconds); + match seconds { + 0 => { + *lock2!(update, msg) = + format!("{}! Took 0 seconds... What...?!{}", MSG_SUCCESS, new_pkgs) + } + 1 => { + *lock2!(update, msg) = format!("{}! Took 1 second... Wow!{}", MSG_SUCCESS, new_pkgs) + } + _ => { + *lock2!(update, msg) = + format!("{}! Took {} seconds.{}", MSG_SUCCESS, seconds, new_pkgs) + } + } + *lock2!(update, prog) = 100.0; + Ok(()) + } } -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub enum ClientEnum { Tor(hyper::Client>), Https(hyper::Client>), } //---------------------------------------------------------------------------------------------------- Pkg struct/impl -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub struct Pkg { - name: Name, - link_metadata: &'static str, - link_prefix: &'static str, - link_suffix: &'static str, - link_extension: &'static str, - bytes: Arc>, - new_ver: Arc>, + name: Name, + link_metadata: &'static str, + link_prefix: &'static str, + link_suffix: &'static str, + link_extension: &'static str, + bytes: Arc>, + new_ver: Arc>, } impl Pkg { - #[cold] - #[inline(never)] - pub fn new(name: Name) -> Self { - let link_metadata = match name { - Gupax => GUPAX_METADATA, - P2pool => P2POOL_METADATA, - Xmrig => XMRIG_METADATA, - }; - let link_prefix = match name { - Gupax => GUPAX_PREFIX, - P2pool => P2POOL_PREFIX, - Xmrig => XMRIG_PREFIX, - }; - let link_suffix = match name { - Gupax => GUPAX_SUFFIX, - P2pool => P2POOL_SUFFIX, - Xmrig => XMRIG_SUFFIX, - }; - let link_extension = match name { - Gupax => GUPAX_EXTENSION, - P2pool => P2POOL_EXTENSION, - Xmrig => XMRIG_EXTENSION, - }; - Self { - name, - link_metadata, - link_prefix, - link_suffix, - link_extension, - bytes: arc_mut!(bytes::Bytes::new()), - new_ver: arc_mut!(String::new()), - } - } + #[cold] + #[inline(never)] + pub fn new(name: Name) -> Self { + let link_metadata = match name { + Gupax => GUPAX_METADATA, + P2pool => P2POOL_METADATA, + Xmrig => XMRIG_METADATA, + }; + let link_prefix = match name { + Gupax => GUPAX_PREFIX, + P2pool => P2POOL_PREFIX, + Xmrig => XMRIG_PREFIX, + }; + let link_suffix = match name { + Gupax => GUPAX_SUFFIX, + P2pool => P2POOL_SUFFIX, + Xmrig => XMRIG_SUFFIX, + }; + let link_extension = match name { + Gupax => GUPAX_EXTENSION, + P2pool => P2POOL_EXTENSION, + Xmrig => XMRIG_EXTENSION, + }; + Self { + name, + link_metadata, + link_prefix, + link_suffix, + link_extension, + bytes: arc_mut!(bytes::Bytes::new()), + new_ver: arc_mut!(String::new()), + } + } - //---------------------------------------------------------------------------------------------------- Pkg functions - #[cold] - #[inline(never)] - // Generate fake [User-Agent] HTTP header - pub fn get_user_agent() -> &'static str { - let index = FAKE_USER_AGENT.len() - 1; + //---------------------------------------------------------------------------------------------------- Pkg functions + #[cold] + #[inline(never)] + // Generate fake [User-Agent] HTTP header + pub fn get_user_agent() -> &'static str { + let index = FAKE_USER_AGENT.len() - 1; - let rand = thread_rng().gen_range(0..index); - let user_agent = FAKE_USER_AGENT[rand]; - info!("Randomly selected User-Agent ({rand}/{index}) ... {user_agent}"); - user_agent - } + let rand = thread_rng().gen_range(0..index); + let user_agent = FAKE_USER_AGENT[rand]; + info!("Randomly selected User-Agent ({rand}/{index}) ... {user_agent}"); + user_agent + } - #[cold] - #[inline(never)] - // Generate GET request based off input URI + fake user agent - fn get_request(link: String, user_agent: &'static str) -> Result, anyhow::Error> { - let request = Request::builder() - .method("GET") - .uri(link) - .header(hyper::header::USER_AGENT, HeaderValue::from_static(user_agent)) - .body(Body::empty())?; - Ok(request) - } + #[cold] + #[inline(never)] + // Generate GET request based off input URI + fake user agent + fn get_request(link: String, user_agent: &'static str) -> Result, anyhow::Error> { + let request = Request::builder() + .method("GET") + .uri(link) + .header( + hyper::header::USER_AGENT, + HeaderValue::from_static(user_agent), + ) + .body(Body::empty())?; + Ok(request) + } - #[cold] - #[inline(never)] - // Get metadata using [Generic hyper::client] & [Request] - // and change [version, prog] under an Arc - async fn get_metadata(new_ver: Arc>, client: Client, link: String, user_agent: &'static str) -> Result<(), Error> - where C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, { - let request = Pkg::get_request(link, user_agent)?; - let mut response = client.request(request).await?; - let body = hyper::body::to_bytes(response.body_mut()).await?; - let body: TagName = serde_json::from_slice(&body)?; - *lock!(new_ver) = body.tag_name; - Ok(()) - } + #[cold] + #[inline(never)] + // Get metadata using [Generic hyper::client] & [Request] + // and change [version, prog] under an Arc + async fn get_metadata( + new_ver: Arc>, + client: Client, + link: String, + user_agent: &'static str, + ) -> Result<(), Error> + where + C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, + { + let request = Pkg::get_request(link, user_agent)?; + let mut response = client.request(request).await?; + let body = hyper::body::to_bytes(response.body_mut()).await?; + let body: TagName = serde_json::from_slice(&body)?; + *lock!(new_ver) = body.tag_name; + Ok(()) + } - #[cold] - #[inline(never)] - // Takes a [Request], fills the appropriate [Pkg] - // [bytes] field with the [Archive/Standalone] - async fn get_bytes(bytes: Arc>, client: Client, link: String, user_agent: &'static str) -> Result<(), anyhow::Error> - where C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, { - let request = Self::get_request(link, user_agent)?; - let mut response = client.request(request).await?; - // GitHub sends a 302 redirect, so we must follow - // the [Location] header... only if Reqwest had custom - // connectors so I didn't have to manually do this... - if response.headers().contains_key(LOCATION) { - let request = Self::get_request(response.headers().get(LOCATION).ok_or_else(|| anyhow!("HTTP Location header GET failed"))?.to_str()?.to_string(), user_agent)?; - response = client.request(request).await?; - } - let body = hyper::body::to_bytes(response.into_body()).await?; - *lock!(bytes) = body; - Ok(()) - } + #[cold] + #[inline(never)] + // Takes a [Request], fills the appropriate [Pkg] + // [bytes] field with the [Archive/Standalone] + async fn get_bytes( + bytes: Arc>, + client: Client, + link: String, + user_agent: &'static str, + ) -> Result<(), anyhow::Error> + where + C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, + { + let request = Self::get_request(link, user_agent)?; + let mut response = client.request(request).await?; + // GitHub sends a 302 redirect, so we must follow + // the [Location] header... only if Reqwest had custom + // connectors so I didn't have to manually do this... + if response.headers().contains_key(LOCATION) { + let request = Self::get_request( + response + .headers() + .get(LOCATION) + .ok_or_else(|| anyhow!("HTTP Location header GET failed"))? + .to_str()? + .to_string(), + user_agent, + )?; + response = client.request(request).await?; + } + let body = hyper::body::to_bytes(response.into_body()).await?; + *lock!(bytes) = body; + Ok(()) + } - #[cold] - #[inline(never)] - // Take in a [Name] and [Vec] of [Pkg]s, find - // that [Name]'s corresponding new version. - fn get_new_pkg_version(name: Name, vec: &[&Pkg]) -> Result { - for pkg in vec.iter() { - if pkg.name == name { - return Ok(lock!(pkg.new_ver).to_string()) - } - } - Err(anyhow!("Couldn't find new_pkg_version")) - } + #[cold] + #[inline(never)] + // Take in a [Name] and [Vec] of [Pkg]s, find + // that [Name]'s corresponding new version. + fn get_new_pkg_version(name: Name, vec: &[&Pkg]) -> Result { + for pkg in vec.iter() { + if pkg.name == name { + return Ok(lock!(pkg.new_ver).to_string()); + } + } + Err(anyhow!("Couldn't find new_pkg_version")) + } } // This inherits the value of [tag_name] from GitHub's JSON API #[derive(Debug, Serialize, Deserialize)] struct TagName { - tag_name: String, + tag_name: String, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum Name { - Gupax, - P2pool, - Xmrig, + Gupax, + P2pool, + Xmrig, } impl std::fmt::Display for Name { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{:?}", self) - } + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } } diff --git a/src/xmr.rs b/src/xmr.rs index dc2ba9e..6d44fee 100644 --- a/src/xmr.rs +++ b/src/xmr.rs @@ -22,12 +22,8 @@ // These represent: // "(DATE, ATOMIC_UNIT, MONERO_BLOCK)" -use crate::{ - human::*, -}; -use crate::regex::{ - P2POOL_REGEX, -}; +use crate::human::*; +use crate::regex::P2POOL_REGEX; use log::*; @@ -41,70 +37,70 @@ use log::*; // [u64] can hold max: 18_446_744_073_709_551_615 which equals to 18,446,744,073 XMR (18 billion). // Given the constant XMR tail emission of (0.3 per minute|18 per hour|432 per day|157,680 per year) // this would take: 116,976~ years to overflow. -#[derive(Debug,Clone,Copy,PartialEq,Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AtomicUnit(u64); impl AtomicUnit { - pub const fn new() -> Self { - Self(0) - } + pub const fn new() -> Self { + Self(0) + } - pub const fn from_u64(u: u64) -> Self { - Self(u) - } + pub const fn from_u64(u: u64) -> Self { + Self(u) + } - pub const fn add_u64(self, u: u64) -> Self { - Self(self.0 + u) - } + pub const fn add_u64(self, u: u64) -> Self { + Self(self.0 + u) + } - pub const fn add_self(self, atomic_unit: Self) -> Self { - Self(self.0 + atomic_unit.0) - } + pub const fn add_self(self, atomic_unit: Self) -> Self { + Self(self.0 + atomic_unit.0) + } - pub const fn to_u64(self) -> u64 { - self.0 - } + pub const fn to_u64(self) -> u64 { + self.0 + } - #[allow(clippy::inherent_to_string_shadow_display)] - // This is terrible but it formats it in a different way - // than `Display`, but for backwards compat, changing it - // requires touching other code, so... - pub fn to_string(self) -> String { - self.0.to_string() - } + #[allow(clippy::inherent_to_string_shadow_display)] + // This is terrible but it formats it in a different way + // than `Display`, but for backwards compat, changing it + // requires touching other code, so... + pub fn to_string(self) -> String { + self.0.to_string() + } - pub fn sum_vec(vec: &Vec) -> Self { - let mut sum = 0; - for int in vec { - sum += int.0; - } - Self(sum) - } + pub fn sum_vec(vec: &Vec) -> Self { + let mut sum = 0; + for int in vec { + sum += int.0; + } + Self(sum) + } - pub fn from_f64(f: f64) -> Self { - Self((f * 1_000_000_000_000.0) as u64) - } + pub fn from_f64(f: f64) -> Self { + Self((f * 1_000_000_000_000.0) as u64) + } - pub fn to_f64(&self) -> f64 { - self.0 as f64 / 1_000_000_000_000.0 - } + pub fn to_f64(&self) -> f64 { + self.0 as f64 / 1_000_000_000_000.0 + } - pub fn to_human_number_12_point(&self) -> HumanNumber { - let f = self.0 as f64 / 1_000_000_000_000.0; - HumanNumber::from_f64_12_point(f) - } + pub fn to_human_number_12_point(&self) -> HumanNumber { + let f = self.0 as f64 / 1_000_000_000_000.0; + HumanNumber::from_f64_12_point(f) + } - pub fn to_human_number_no_fmt(&self) -> HumanNumber { - let f = self.0 as f64 / 1_000_000_000_000.0; - HumanNumber::from_f64_no_fmt(f) - } + pub fn to_human_number_no_fmt(&self) -> HumanNumber { + let f = self.0 as f64 / 1_000_000_000_000.0; + HumanNumber::from_f64_no_fmt(f) + } } // Displays AtomicUnit as a real XMR floating point. impl std::fmt::Display for AtomicUnit { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", Self::to_human_number_12_point(self)) - } + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", Self::to_human_number_12_point(self)) + } } //---------------------------------------------------------------------------------------------------- [PayoutOrd] @@ -117,301 +113,385 @@ impl std::fmt::Display for AtomicUnit { // [0] = DATE // [1] = XMR IN ATOMIC-UNITS // [2] = MONERO BLOCK -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub struct PayoutOrd(Vec<(String, AtomicUnit, HumanNumber)>); impl PayoutOrd { - pub fn new() -> Self { - Self(vec![(String::from("????-??-?? ??:??:??.????"), AtomicUnit::new(), HumanNumber::unknown())]) - } + pub fn new() -> Self { + Self(vec![( + String::from("????-??-?? ??:??:??.????"), + AtomicUnit::new(), + HumanNumber::unknown(), + )]) + } - pub const fn from_vec(vec: Vec<(String, AtomicUnit, HumanNumber)>) -> Self { - Self(vec) - } + pub const fn from_vec(vec: Vec<(String, AtomicUnit, HumanNumber)>) -> Self { + Self(vec) + } - pub fn is_same(a: &Self, b: &Self) -> bool { - if a.0.is_empty() && b.0.is_empty() { return true } - if a.0.len() != b.0.len() { return false } - let mut n = 0; - for (date, atomic_unit, block) in &a.0 { - if *date != b.0[n].0 { return false } - if *atomic_unit != b.0[n].1 { return false } - if *block != b.0[n].2 { return false } - n += 1; - } - true - } + pub fn is_same(a: &Self, b: &Self) -> bool { + if a.0.is_empty() && b.0.is_empty() { + return true; + } + if a.0.len() != b.0.len() { + return false; + } + let mut n = 0; + for (date, atomic_unit, block) in &a.0 { + if *date != b.0[n].0 { + return false; + } + if *atomic_unit != b.0[n].1 { + return false; + } + if *block != b.0[n].2 { + return false; + } + n += 1; + } + true + } - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } - // Expected input: "NOTICE 2022-01-27 01:30:23.1377 P2Pool You received a payout of 0.000000000001 XMR in block 2642816" - pub fn parse_raw_payout_line(line: &str) -> (String, AtomicUnit, HumanNumber) { - // Date - let date = match P2POOL_REGEX.date.find(line) { - Some(date) => date.as_str().to_string(), - None => { error!("P2Pool | Date parse error: [{}]", line); "????-??-?? ??:??:??.????".to_string() }, - }; - // AtomicUnit - let atomic_unit = if let Some(word) = P2POOL_REGEX.payout.find(line) { - if let Some(word) = P2POOL_REGEX.payout_float.find(word.as_str()) { - match word.as_str().parse::() { - Ok(au) => AtomicUnit::from_f64(au), - Err(e) => { error!("P2Pool | AtomicUnit parse error: [{}] on [{}]", e, line); AtomicUnit::new() }, - } - } else { - error!("P2Pool | AtomicUnit parse error: [{}]", line); - AtomicUnit::new() - } - } else { - error!("P2Pool | AtomicUnit parse error: [{}]", line); - AtomicUnit::new() - }; - // Block - let block = if let Some(word) = P2POOL_REGEX.block.find(line) { - if let Some(word) = P2POOL_REGEX.block_int.find(word.as_str()) { - match word.as_str().parse::() { - Ok(b) => HumanNumber::from_u64(b), - Err(e) => { error!("P2Pool | Block parse error: [{}] on [{}]", e, line); HumanNumber::unknown() }, - } - } else { - error!("P2Pool | Block parse error: [{}]", line); - HumanNumber::unknown() - } - } else { - error!("P2Pool | Block parse error: [{}]", line); - HumanNumber::unknown() - }; - (date, atomic_unit, block) - } + // Expected input: "NOTICE 2022-01-27 01:30:23.1377 P2Pool You received a payout of 0.000000000001 XMR in block 2642816" + pub fn parse_raw_payout_line(line: &str) -> (String, AtomicUnit, HumanNumber) { + // Date + let date = match P2POOL_REGEX.date.find(line) { + Some(date) => date.as_str().to_string(), + None => { + error!("P2Pool | Date parse error: [{}]", line); + "????-??-?? ??:??:??.????".to_string() + } + }; + // AtomicUnit + let atomic_unit = if let Some(word) = P2POOL_REGEX.payout.find(line) { + if let Some(word) = P2POOL_REGEX.payout_float.find(word.as_str()) { + match word.as_str().parse::() { + Ok(au) => AtomicUnit::from_f64(au), + Err(e) => { + error!("P2Pool | AtomicUnit parse error: [{}] on [{}]", e, line); + AtomicUnit::new() + } + } + } else { + error!("P2Pool | AtomicUnit parse error: [{}]", line); + AtomicUnit::new() + } + } else { + error!("P2Pool | AtomicUnit parse error: [{}]", line); + AtomicUnit::new() + }; + // Block + let block = if let Some(word) = P2POOL_REGEX.block.find(line) { + if let Some(word) = P2POOL_REGEX.block_int.find(word.as_str()) { + match word.as_str().parse::() { + Ok(b) => HumanNumber::from_u64(b), + Err(e) => { + error!("P2Pool | Block parse error: [{}] on [{}]", e, line); + HumanNumber::unknown() + } + } + } else { + error!("P2Pool | Block parse error: [{}]", line); + HumanNumber::unknown() + } + } else { + error!("P2Pool | Block parse error: [{}]", line); + HumanNumber::unknown() + }; + (date, atomic_unit, block) + } - // Expected input: "2022-01-27 01:30:23.1377 | 0.000000000001 XMR | Block 2,642,816" - pub fn parse_formatted_payout_line(line: &str) -> (String, AtomicUnit, HumanNumber) { - // Date - let date = match P2POOL_REGEX.date.find(line) { - Some(date) => date.as_str().to_string(), - None => { error!("P2Pool | Date parse error: [{}]", line); "????-??-?? ??:??:??.????".to_string() }, - }; - // AtomicUnit - let atomic_unit = if let Some(word) = P2POOL_REGEX.payout_float.find(line) { - match word.as_str().parse::() { - Ok(au) => AtomicUnit::from_f64(au), - Err(e) => { error!("P2Pool | AtomicUnit parse error: [{}] on [{}]", e, line); AtomicUnit::new() }, - } - } else { - error!("P2Pool | AtomicUnit parse error: [{}]", line); - AtomicUnit::new() - }; - // Block - let block = match P2POOL_REGEX.block_comma.find(line) { - Some(b) => HumanNumber::from_str(b.as_str()), - None => { error!("P2Pool | Block parse error: [{}]", line); HumanNumber::unknown() }, - }; - (date, atomic_unit, block) - } + // Expected input: "2022-01-27 01:30:23.1377 | 0.000000000001 XMR | Block 2,642,816" + pub fn parse_formatted_payout_line(line: &str) -> (String, AtomicUnit, HumanNumber) { + // Date + let date = match P2POOL_REGEX.date.find(line) { + Some(date) => date.as_str().to_string(), + None => { + error!("P2Pool | Date parse error: [{}]", line); + "????-??-?? ??:??:??.????".to_string() + } + }; + // AtomicUnit + let atomic_unit = if let Some(word) = P2POOL_REGEX.payout_float.find(line) { + match word.as_str().parse::() { + Ok(au) => AtomicUnit::from_f64(au), + Err(e) => { + error!("P2Pool | AtomicUnit parse error: [{}] on [{}]", e, line); + AtomicUnit::new() + } + } + } else { + error!("P2Pool | AtomicUnit parse error: [{}]", line); + AtomicUnit::new() + }; + // Block + let block = match P2POOL_REGEX.block_comma.find(line) { + Some(b) => HumanNumber::from_str(b.as_str()), + None => { + error!("P2Pool | Block parse error: [{}]", line); + HumanNumber::unknown() + } + }; + (date, atomic_unit, block) + } - // Takes in input of ONLY P2Pool payout logs and converts it into a usable [PayoutOrd] - // It expects formatted log lines like this: "2022-04-11 00:20:17.2571 | 0.001371623621 XMR | Block 2,562,511" - // For efficiency reasons, I'd like to know the byte size - // we should allocate for the vector so we aren't adding every loop. - // Given a log [str], the equation for how many bytes the final vec will be is: - // (BYTES_OF_DATE + BYTES OF XMR + BYTES OF BLOCK) + (SPACES, PIPES, MISC WORDS) * amount_of_lines - // The first three are more or less constants (monero block 10m is in 10,379 years...): [23, 14, 7] (sum: 44) - // Spaces, pipes, commas and words (XMR, Block): [19] - // Add 7 more bytes for wrapper type overhead and it's an even [70] bytes per line. - pub fn update_from_payout_log(&mut self, log: &str) { - let amount_of_lines = log.lines().count(); - let mut vec: Vec<(String, AtomicUnit, HumanNumber)> = Vec::with_capacity(70 * amount_of_lines); - for line in log.lines() { - debug!("PayoutOrd | Parsing line: [{}]", line); - vec.push(Self::parse_formatted_payout_line(line)); - } - *self = Self(vec); - } + // Takes in input of ONLY P2Pool payout logs and converts it into a usable [PayoutOrd] + // It expects formatted log lines like this: "2022-04-11 00:20:17.2571 | 0.001371623621 XMR | Block 2,562,511" + // For efficiency reasons, I'd like to know the byte size + // we should allocate for the vector so we aren't adding every loop. + // Given a log [str], the equation for how many bytes the final vec will be is: + // (BYTES_OF_DATE + BYTES OF XMR + BYTES OF BLOCK) + (SPACES, PIPES, MISC WORDS) * amount_of_lines + // The first three are more or less constants (monero block 10m is in 10,379 years...): [23, 14, 7] (sum: 44) + // Spaces, pipes, commas and words (XMR, Block): [19] + // Add 7 more bytes for wrapper type overhead and it's an even [70] bytes per line. + pub fn update_from_payout_log(&mut self, log: &str) { + let amount_of_lines = log.lines().count(); + let mut vec: Vec<(String, AtomicUnit, HumanNumber)> = + Vec::with_capacity(70 * amount_of_lines); + for line in log.lines() { + debug!("PayoutOrd | Parsing line: [{}]", line); + vec.push(Self::parse_formatted_payout_line(line)); + } + *self = Self(vec); + } - // Takes the wrapper types, and pushes to existing [Self] - pub fn push(&mut self, date: String, atomic_unit: AtomicUnit, block: HumanNumber) { - self.0.push((date, atomic_unit, block)); - } + // Takes the wrapper types, and pushes to existing [Self] + pub fn push(&mut self, date: String, atomic_unit: AtomicUnit, block: HumanNumber) { + self.0.push((date, atomic_unit, block)); + } - // Takes the raw components (no wrapper types), convert them and pushes to existing [Self] - pub fn push_raw(&mut self, date: &str, atomic_unit: u64, block: u64) { - let atomic_unit = AtomicUnit(atomic_unit); - let block = HumanNumber::from_u64(block); - self.0.push((date.to_string(), atomic_unit, block)); - } + // Takes the raw components (no wrapper types), convert them and pushes to existing [Self] + pub fn push_raw(&mut self, date: &str, atomic_unit: u64, block: u64) { + let atomic_unit = AtomicUnit(atomic_unit); + let block = HumanNumber::from_u64(block); + self.0.push((date.to_string(), atomic_unit, block)); + } - pub fn atomic_unit_sum(&self) -> AtomicUnit { - let mut sum: u64 = 0; - for (_, atomic_unit, _) in &self.0 { - sum += atomic_unit.to_u64(); - } - AtomicUnit::from_u64(sum) - } + pub fn atomic_unit_sum(&self) -> AtomicUnit { + let mut sum: u64 = 0; + for (_, atomic_unit, _) in &self.0 { + sum += atomic_unit.to_u64(); + } + AtomicUnit::from_u64(sum) + } - // Sort [Self] from highest payout to lowest - pub fn sort_payout_high_to_low(&mut self) { - // This is a little confusing because wrapper types are basically 1 element tuples so: - // self.0 = The [Vec] within [PayoutOrd] - // b.1.0 = [b] is [(String, AtomicUnit, HumanNumber)], [.1] is the [AtomicUnit] inside it, [.0] is the [u64] inside that - // a.1.0 = Same deal, but we compare it with the previous value (b) - self.0.sort_by(|a, b| b.1.0.cmp(&a.1.0)); - } + // Sort [Self] from highest payout to lowest + pub fn sort_payout_high_to_low(&mut self) { + // This is a little confusing because wrapper types are basically 1 element tuples so: + // self.0 = The [Vec] within [PayoutOrd] + // b.1.0 = [b] is [(String, AtomicUnit, HumanNumber)], [.1] is the [AtomicUnit] inside it, [.0] is the [u64] inside that + // a.1.0 = Same deal, but we compare it with the previous value (b) + self.0.sort_by(|a, b| b.1 .0.cmp(&a.1 .0)); + } - // These sorting functions take around [0.0035~] seconds on a Ryzen 5950x - // given a Vec filled with 1_000_000 elements, not bad. - pub fn sort_payout_low_to_high(&mut self) { - self.0.sort_by(|a, b| a.1.0.cmp(&b.1.0)); - } + // These sorting functions take around [0.0035~] seconds on a Ryzen 5950x + // given a Vec filled with 1_000_000 elements, not bad. + pub fn sort_payout_low_to_high(&mut self) { + self.0.sort_by(|a, b| a.1 .0.cmp(&b.1 .0)); + } - // Returns a reversed [Iter] of the [PayoutOrd] - // This is obviously faster than actually reordering the Vec. - pub fn rev_iter(&self) -> std::iter::Rev> { - self.0.iter().rev() - } + // Returns a reversed [Iter] of the [PayoutOrd] + // This is obviously faster than actually reordering the Vec. + pub fn rev_iter( + &self, + ) -> std::iter::Rev> { + self.0.iter().rev() + } - // Recent <-> Oldest relies on the line order. - // The raw log lines will be shown instead of this struct. + // Recent <-> Oldest relies on the line order. + // The raw log lines will be shown instead of this struct. } -impl Default for PayoutOrd { fn default() -> Self { Self::new() } } +impl Default for PayoutOrd { + fn default() -> Self { + Self::new() + } +} impl std::fmt::Display for PayoutOrd { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - for i in &self.0 { - writeln!(f, "{} | {} XMR | Block {}", i.0, i.1, i.2)?; - } - Ok(()) - } + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + for i in &self.0 { + writeln!(f, "{} | {} XMR | Block {}", i.0, i.1, i.2)?; + } + Ok(()) + } } //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod test { - #[test] - fn update_p2pool_payout_log() { - use crate::xmr::PayoutOrd; - let log = -r#"2021-12-21 01:01:01.1111 | 0.001000000000 XMR | Block 1,234,567 + #[test] + fn update_p2pool_payout_log() { + use crate::xmr::PayoutOrd; + let log = r#"2021-12-21 01:01:01.1111 | 0.001000000000 XMR | Block 1,234,567 2021-12-21 02:01:01.1111 | 0.002000000000 XMR | Block 2,345,678 2021-12-21 03:01:01.1111 | 0.003000000000 XMR | Block 3,456,789 "#; - let mut payout_ord = PayoutOrd::new(); - println!("BEFORE: {}", payout_ord); - PayoutOrd::update_from_payout_log(&mut payout_ord, log); - println!("AFTER: {}", payout_ord); - assert_eq!(payout_ord.to_string(), log); - } + let mut payout_ord = PayoutOrd::new(); + println!("BEFORE: {}", payout_ord); + PayoutOrd::update_from_payout_log(&mut payout_ord, log); + println!("AFTER: {}", payout_ord); + assert_eq!(payout_ord.to_string(), log); + } - #[test] - fn push_to_payout_ord() { - use crate::xmr::PayoutOrd; - use crate::xmr::AtomicUnit; - use crate::human::HumanNumber; - let mut payout_ord = PayoutOrd::from_vec(vec![]); - let should_be = "2022-09-08 18:42:55.4636 | 0.000000000001 XMR | Block 2,654,321\n"; - println!("BEFORE: {:#?}", payout_ord); - payout_ord.push_raw("2022-09-08 18:42:55.4636", 1, 2654321); - println!("AFTER: {}", payout_ord); - println!("SHOULD_BE: {}", should_be); - assert_eq!(payout_ord.to_string(), should_be); - } + #[test] + fn push_to_payout_ord() { + use crate::human::HumanNumber; + use crate::xmr::AtomicUnit; + use crate::xmr::PayoutOrd; + let mut payout_ord = PayoutOrd::from_vec(vec![]); + let should_be = "2022-09-08 18:42:55.4636 | 0.000000000001 XMR | Block 2,654,321\n"; + println!("BEFORE: {:#?}", payout_ord); + payout_ord.push_raw("2022-09-08 18:42:55.4636", 1, 2654321); + println!("AFTER: {}", payout_ord); + println!("SHOULD_BE: {}", should_be); + assert_eq!(payout_ord.to_string(), should_be); + } - #[test] - fn sum_payout_ord_atomic_unit() { - use crate::xmr::PayoutOrd; - use crate::xmr::AtomicUnit; - use crate::human::HumanNumber; - let mut payout_ord = PayoutOrd::from_vec(vec![ - ("2022-09-08 18:42:55.4636".to_string(), AtomicUnit::from_u64(1), HumanNumber::from_u64(2654321)), - ("2022-09-09 16:18:26.7582".to_string(), AtomicUnit::from_u64(1), HumanNumber::from_u64(2654322)), - ("2022-09-10 11:15:21.1272".to_string(), AtomicUnit::from_u64(1), HumanNumber::from_u64(2654323)), - ]); - println!("OG: {:#?}", payout_ord); - let sum = PayoutOrd::atomic_unit_sum(&payout_ord); - println!("SUM: {}", sum.to_u64()); - assert_eq!(sum.to_u64(), 3); - } + #[test] + fn sum_payout_ord_atomic_unit() { + use crate::human::HumanNumber; + use crate::xmr::AtomicUnit; + use crate::xmr::PayoutOrd; + let mut payout_ord = PayoutOrd::from_vec(vec![ + ( + "2022-09-08 18:42:55.4636".to_string(), + AtomicUnit::from_u64(1), + HumanNumber::from_u64(2654321), + ), + ( + "2022-09-09 16:18:26.7582".to_string(), + AtomicUnit::from_u64(1), + HumanNumber::from_u64(2654322), + ), + ( + "2022-09-10 11:15:21.1272".to_string(), + AtomicUnit::from_u64(1), + HumanNumber::from_u64(2654323), + ), + ]); + println!("OG: {:#?}", payout_ord); + let sum = PayoutOrd::atomic_unit_sum(&payout_ord); + println!("SUM: {}", sum.to_u64()); + assert_eq!(sum.to_u64(), 3); + } - #[test] - fn sort_p2pool_payout_ord() { - use crate::xmr::PayoutOrd; - use crate::xmr::AtomicUnit; - use crate::human::HumanNumber; - let mut payout_ord = PayoutOrd::from_vec(vec![ - ("2022-09-08 18:42:55.4636".to_string(), AtomicUnit::from_u64(1000000000), HumanNumber::from_u64(2654321)), - ("2022-09-09 16:18:26.7582".to_string(), AtomicUnit::from_u64(2000000000), HumanNumber::from_u64(2654322)), - ("2022-09-10 11:15:21.1272".to_string(), AtomicUnit::from_u64(3000000000), HumanNumber::from_u64(2654323)), - ]); - println!("OG: {:#?}", payout_ord); + #[test] + fn sort_p2pool_payout_ord() { + use crate::human::HumanNumber; + use crate::xmr::AtomicUnit; + use crate::xmr::PayoutOrd; + let mut payout_ord = PayoutOrd::from_vec(vec![ + ( + "2022-09-08 18:42:55.4636".to_string(), + AtomicUnit::from_u64(1000000000), + HumanNumber::from_u64(2654321), + ), + ( + "2022-09-09 16:18:26.7582".to_string(), + AtomicUnit::from_u64(2000000000), + HumanNumber::from_u64(2654322), + ), + ( + "2022-09-10 11:15:21.1272".to_string(), + AtomicUnit::from_u64(3000000000), + HumanNumber::from_u64(2654323), + ), + ]); + println!("OG: {:#?}", payout_ord); - // High to Low - PayoutOrd::sort_payout_high_to_low(&mut payout_ord); - println!("AFTER PAYOUT HIGH TO LOW: {:#?}", payout_ord); - let should_be = -r#"2022-09-10 11:15:21.1272 | 0.003000000000 XMR | Block 2,654,323 + // High to Low + PayoutOrd::sort_payout_high_to_low(&mut payout_ord); + println!("AFTER PAYOUT HIGH TO LOW: {:#?}", payout_ord); + let should_be = r#"2022-09-10 11:15:21.1272 | 0.003000000000 XMR | Block 2,654,323 2022-09-09 16:18:26.7582 | 0.002000000000 XMR | Block 2,654,322 2022-09-08 18:42:55.4636 | 0.001000000000 XMR | Block 2,654,321 "#; - println!("SHOULD_BE:\n{}", should_be); - println!("IS:\n{}", payout_ord); - assert_eq!(payout_ord.to_string(), should_be); + println!("SHOULD_BE:\n{}", should_be); + println!("IS:\n{}", payout_ord); + assert_eq!(payout_ord.to_string(), should_be); - // Low to High - PayoutOrd::sort_payout_low_to_high(&mut payout_ord); - println!("AFTER PAYOUT LOW TO HIGH: {:#?}", payout_ord); - let should_be = -r#"2022-09-08 18:42:55.4636 | 0.001000000000 XMR | Block 2,654,321 + // Low to High + PayoutOrd::sort_payout_low_to_high(&mut payout_ord); + println!("AFTER PAYOUT LOW TO HIGH: {:#?}", payout_ord); + let should_be = r#"2022-09-08 18:42:55.4636 | 0.001000000000 XMR | Block 2,654,321 2022-09-09 16:18:26.7582 | 0.002000000000 XMR | Block 2,654,322 2022-09-10 11:15:21.1272 | 0.003000000000 XMR | Block 2,654,323 "#; - println!("SHOULD_BE:\n{}", should_be); - println!("IS:\n{}", payout_ord); - assert_eq!(payout_ord.to_string(), should_be); - } + println!("SHOULD_BE:\n{}", should_be); + println!("IS:\n{}", payout_ord); + assert_eq!(payout_ord.to_string(), should_be); + } - #[test] - fn payout_ord_is_same() { - use crate::xmr::PayoutOrd; - use crate::xmr::AtomicUnit; - use crate::human::HumanNumber; - let mut payout_ord = PayoutOrd::from_vec(vec![ - ("2022-09-08 18:42:55.4636".to_string(), AtomicUnit::from_u64(1000000000), HumanNumber::from_u64(2654321)), - ("2022-09-09 16:18:26.7582".to_string(), AtomicUnit::from_u64(2000000000), HumanNumber::from_u64(2654322)), - ("2022-09-10 11:15:21.1272".to_string(), AtomicUnit::from_u64(3000000000), HumanNumber::from_u64(2654323)), - ]); - let payout_ord_2 = payout_ord.clone(); - println!("1: {:#?}", payout_ord); - println!("2: {:#?}", payout_ord); + #[test] + fn payout_ord_is_same() { + use crate::human::HumanNumber; + use crate::xmr::AtomicUnit; + use crate::xmr::PayoutOrd; + let mut payout_ord = PayoutOrd::from_vec(vec![ + ( + "2022-09-08 18:42:55.4636".to_string(), + AtomicUnit::from_u64(1000000000), + HumanNumber::from_u64(2654321), + ), + ( + "2022-09-09 16:18:26.7582".to_string(), + AtomicUnit::from_u64(2000000000), + HumanNumber::from_u64(2654322), + ), + ( + "2022-09-10 11:15:21.1272".to_string(), + AtomicUnit::from_u64(3000000000), + HumanNumber::from_u64(2654323), + ), + ]); + let payout_ord_2 = payout_ord.clone(); + println!("1: {:#?}", payout_ord); + println!("2: {:#?}", payout_ord); - assert!(PayoutOrd::is_same(&payout_ord, &payout_ord_2) == true); - payout_ord.push_raw("2022-09-08 18:42:55.4636", 1000000000, 2654321); - println!("1: {:#?}", payout_ord); - println!("2: {:#?}", payout_ord); - assert!(PayoutOrd::is_same(&payout_ord, &payout_ord_2) == false); - } + assert!(PayoutOrd::is_same(&payout_ord, &payout_ord_2) == true); + payout_ord.push_raw("2022-09-08 18:42:55.4636", 1000000000, 2654321); + println!("1: {:#?}", payout_ord); + println!("2: {:#?}", payout_ord); + assert!(PayoutOrd::is_same(&payout_ord, &payout_ord_2) == false); + } - #[test] - fn view_reverse_payout_ord() { - use crate::xmr::PayoutOrd; - use crate::xmr::AtomicUnit; - use crate::human::HumanNumber; - let mut payout_ord = PayoutOrd::from_vec(vec![ - ("2022-09-08 18:42:55.4636".to_string(), AtomicUnit::from_u64(1000000000), HumanNumber::from_u64(2654321)), - ("2022-09-09 16:18:26.7582".to_string(), AtomicUnit::from_u64(2000000000), HumanNumber::from_u64(2654322)), - ("2022-09-10 11:15:21.1272".to_string(), AtomicUnit::from_u64(3000000000), HumanNumber::from_u64(2654323)), - ]); - println!("OG: {:#?}", payout_ord); + #[test] + fn view_reverse_payout_ord() { + use crate::human::HumanNumber; + use crate::xmr::AtomicUnit; + use crate::xmr::PayoutOrd; + let mut payout_ord = PayoutOrd::from_vec(vec![ + ( + "2022-09-08 18:42:55.4636".to_string(), + AtomicUnit::from_u64(1000000000), + HumanNumber::from_u64(2654321), + ), + ( + "2022-09-09 16:18:26.7582".to_string(), + AtomicUnit::from_u64(2000000000), + HumanNumber::from_u64(2654322), + ), + ( + "2022-09-10 11:15:21.1272".to_string(), + AtomicUnit::from_u64(3000000000), + HumanNumber::from_u64(2654323), + ), + ]); + println!("OG: {:#?}", payout_ord); - #[allow(clippy::never_loop)] - for (_, atomic_unit, _) in payout_ord.rev_iter() { - if atomic_unit.to_u64() == 3000000000 { - break - } else { - println!("expected: 3000000000, found: {}", atomic_unit); - panic!("not reversed"); - } - } - } + #[allow(clippy::never_loop)] + for (_, atomic_unit, _) in payout_ord.rev_iter() { + if atomic_unit.to_u64() == 3000000000 { + break; + } else { + println!("expected: 3000000000, found: {}", atomic_unit); + panic!("not reversed"); + } + } + } } diff --git a/src/xmrig.rs b/src/xmrig.rs index abd7f08..16caf50 100644 --- a/src/xmrig.rs +++ b/src/xmrig.rs @@ -15,135 +15,178 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{ - Regexes, - constants::*, - disk::*, - Process, - PubXmrigApi, - macros::*, -}; +use crate::regex::REGEXES; +use crate::{constants::*, disk::*, macros::*, Process, PubXmrigApi, Regexes}; use egui::{ - TextEdit,SelectableLabel,ComboBox,Label,Button,RichText,Slider,Checkbox, - TextStyle::*, + Button, Checkbox, ComboBox, Label, RichText, SelectableLabel, Slider, TextEdit, TextStyle::*, }; -use std::{ - sync::{Arc,Mutex}, -}; -use regex::Regex; use log::*; -use crate::regex::{ - REGEXES, -}; +use regex::Regex; +use std::sync::{Arc, Mutex}; impl crate::disk::Xmrig { -#[inline(always)] // called once -pub fn show( - &mut self, - pool_vec: &mut Vec<(String, Pool)>, - 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!("XMRig Tab | Rendering [Console]"); - ui.group(|ui| { - if self.simple { - let height = height / 1.5; - 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#"Commands: [h]ashrate, [p]ause, [r]esume, re[s]ults, [c]onnection"#)).on_hover_text(XMRIG_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 - } - } - }); + #[inline(always)] // called once + pub fn show( + &mut self, + pool_vec: &mut Vec<(String, Pool)>, + 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!("XMRig Tab | Rendering [Console]"); + ui.group(|ui| { + if self.simple { + let height = height / 1.5; + 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#"Commands: [h]ashrate, [p]ause, [r]esume, re[s]ults, [c]onnection"#, + ), + ) + .on_hover_text(XMRIG_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 + } + } + }); - //---------------------------------------------------------------------------------------------------- Arguments - if !self.simple { - debug!("XMRig 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#"--url <...> --user <...> --config <...>"#)).on_hover_text(XMRIG_ARGUMENTS); - self.arguments.truncate(1024); - })}); - ui.set_enabled(self.arguments.is_empty()); - //---------------------------------------------------------------------------------------------------- Address - debug!("XMRig 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 = LIGHT_GRAY; - } else if Regexes::addr_ok(&self.address) { - text = format!("Monero Address [{}/95] ✔", len); - color = GREEN; - } else { - text = format!("Monero Address [{}/95] ❌", len); - color = RED; - } - 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(XMRIG_ADDRESS); - self.address.truncate(95); - }); - } + //---------------------------------------------------------------------------------------------------- Arguments + if !self.simple { + debug!("XMRig 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#"--url <...> --user <...> --config <...>"#, + ), + ) + .on_hover_text(XMRIG_ARGUMENTS); + self.arguments.truncate(1024); + }) + }); + ui.set_enabled(self.arguments.is_empty()); + //---------------------------------------------------------------------------------------------------- Address + debug!("XMRig 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 = LIGHT_GRAY; + } else if Regexes::addr_ok(&self.address) { + text = format!("Monero Address [{}/95] ✔", len); + color = GREEN; + } else { + text = format!("Monero Address [{}/95] ❌", len); + color = RED; + } + 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(XMRIG_ADDRESS); + self.address.truncate(95); + }); + } - //---------------------------------------------------------------------------------------------------- Threads - if self.simple { ui.add_space(SPACE); } - debug!("XMRig Tab | Rendering [Threads]"); - ui.vertical(|ui| { - let width = width / 10.0; - let text_width = width * 2.4; - ui.spacing_mut().slider_width = width * 6.5; - ui.spacing_mut().icon_width = width / 25.0; - ui.horizontal(|ui| { - ui.add_sized([text_width, text_edit], Label::new(format!("Threads [1-{}]:", self.max_threads))); - ui.add_sized([width, text_edit], Slider::new(&mut self.current_threads, 1..=self.max_threads)).on_hover_text(XMRIG_THREADS); - }); - #[cfg(not(target_os = "linux"))] // Pause on active isn't supported on Linux - ui.horizontal(|ui| { - ui.add_sized([text_width, text_edit], Label::new(format!("Pause on active [0-255]:"))); - ui.add_sized([width, text_edit], Slider::new(&mut self.pause, 0..=255)).on_hover_text(format!("{} [{}] seconds.", XMRIG_PAUSE, self.pause)); - }); - }); + //---------------------------------------------------------------------------------------------------- Threads + if self.simple { + ui.add_space(SPACE); + } + debug!("XMRig Tab | Rendering [Threads]"); + ui.vertical(|ui| { + let width = width / 10.0; + let text_width = width * 2.4; + ui.spacing_mut().slider_width = width * 6.5; + ui.spacing_mut().icon_width = width / 25.0; + ui.horizontal(|ui| { + ui.add_sized( + [text_width, text_edit], + Label::new(format!("Threads [1-{}]:", self.max_threads)), + ); + ui.add_sized( + [width, text_edit], + Slider::new(&mut self.current_threads, 1..=self.max_threads), + ) + .on_hover_text(XMRIG_THREADS); + }); + #[cfg(not(target_os = "linux"))] // Pause on active isn't supported on Linux + ui.horizontal(|ui| { + ui.add_sized( + [text_width, text_edit], + Label::new(format!("Pause on active [0-255]:")), + ); + ui.add_sized([width, text_edit], Slider::new(&mut self.pause, 0..=255)) + .on_hover_text(format!("{} [{}] seconds.", XMRIG_PAUSE, self.pause)); + }); + }); - //---------------------------------------------------------------------------------------------------- Simple - if !self.simple { - debug!("XMRig Tab | Rendering [Pool List] elements"); - let width = ui.available_width() - 10.0; - let mut incorrect_input = false; // This will disable [Add/Delete] on bad input - // [Pool IP/Port] - ui.horizontal(|ui| { + //---------------------------------------------------------------------------------------------------- Simple + if !self.simple { + debug!("XMRig Tab | Rendering [Pool List] elements"); + let width = ui.available_width() - 10.0; + let mut incorrect_input = false; // This will disable [Add/Delete] on bad input + // [Pool IP/Port] + ui.horizontal(|ui| { ui.group(|ui| { let width = width/10.0; ui.vertical(|ui| { @@ -353,77 +396,97 @@ pub fn show( }); }); }); - ui.add_space(5.0); + ui.add_space(5.0); - debug!("XMRig Tab | Rendering [API] TextEdits"); - // [HTTP API IP/Port] - ui.group(|ui| { ui.horizontal(|ui| { - ui.vertical(|ui| { - let width = width/10.0; - ui.spacing_mut().text_edit_width = width*2.39; - // HTTP API - ui.horizontal(|ui| { - let text; - let color; - let len = format!("{:03}", self.api_ip.len()); - if self.api_ip.is_empty() { - text = format!("HTTP API IP [{}/255]➖", len); - color = LIGHT_GRAY; - incorrect_input = true; - } else if self.api_ip == "localhost" || REGEXES.ipv4.is_match(&self.api_ip) || REGEXES.domain.is_match(&self.api_ip) { - text = format!("HTTP API IP [{}/255]✔", len); - color = GREEN; - } else { - text = format!("HTTP API IP [{}/255]❌", len); - color = RED; - incorrect_input = true; - } - ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color))); - ui.text_edit_singleline(&mut self.api_ip).on_hover_text(XMRIG_API_IP); - self.api_ip.truncate(255); - }); - ui.horizontal(|ui| { - let text; - let color; - let len = self.api_port.len(); - if self.api_port.is_empty() { - text = format!("HTTP API Port [ {}/5 ]➖", len); - color = LIGHT_GRAY; - incorrect_input = true; - } else if REGEXES.port.is_match(&self.api_port) { - text = format!("HTTP API Port [ {}/5 ]✔", len); - color = GREEN; - } else { - text = format!("HTTP API Port [ {}/5 ]❌", len); - color = RED; - incorrect_input = true; - } - ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color))); - ui.text_edit_singleline(&mut self.api_port).on_hover_text(XMRIG_API_PORT); - self.api_port.truncate(5); - }); - }); + debug!("XMRig Tab | Rendering [API] TextEdits"); + // [HTTP API IP/Port] + ui.group(|ui| { + ui.horizontal(|ui| { + ui.vertical(|ui| { + let width = width / 10.0; + ui.spacing_mut().text_edit_width = width * 2.39; + // HTTP API + ui.horizontal(|ui| { + let text; + let color; + let len = format!("{:03}", self.api_ip.len()); + if self.api_ip.is_empty() { + text = format!("HTTP API IP [{}/255]➖", len); + color = LIGHT_GRAY; + incorrect_input = true; + } else if self.api_ip == "localhost" + || REGEXES.ipv4.is_match(&self.api_ip) + || REGEXES.domain.is_match(&self.api_ip) + { + text = format!("HTTP API IP [{}/255]✔", len); + color = GREEN; + } else { + text = format!("HTTP API IP [{}/255]❌", len); + color = RED; + incorrect_input = true; + } + ui.add_sized( + [width, text_edit], + Label::new(RichText::new(text).color(color)), + ); + ui.text_edit_singleline(&mut self.api_ip) + .on_hover_text(XMRIG_API_IP); + self.api_ip.truncate(255); + }); + ui.horizontal(|ui| { + let text; + let color; + let len = self.api_port.len(); + if self.api_port.is_empty() { + text = format!("HTTP API Port [ {}/5 ]➖", len); + color = LIGHT_GRAY; + incorrect_input = true; + } else if REGEXES.port.is_match(&self.api_port) { + text = format!("HTTP API Port [ {}/5 ]✔", len); + color = GREEN; + } else { + text = format!("HTTP API Port [ {}/5 ]❌", len); + color = RED; + incorrect_input = true; + } + ui.add_sized( + [width, text_edit], + Label::new(RichText::new(text).color(color)), + ); + ui.text_edit_singleline(&mut self.api_port) + .on_hover_text(XMRIG_API_PORT); + self.api_port.truncate(5); + }); + }); - ui.separator(); + ui.separator(); - debug!("XMRig Tab | Rendering [TLS/Keepalive] buttons"); - ui.vertical(|ui| { - // TLS/Keepalive - ui.horizontal(|ui| { - let width = (ui.available_width()/2.0)-11.0; - let height = text_edit*2.0; -// let mut style = (*ctx.style()).clone(); -// style.spacing.icon_width_inner = width / 8.0; -// style.spacing.icon_width = width / 6.0; -// style.spacing.icon_spacing = 20.0; -// ctx.set_style(style); - ui.add_sized([width, height], Checkbox::new(&mut self.tls, "TLS Connection")).on_hover_text(XMRIG_TLS); - ui.separator(); - ui.add_sized([width, height], Checkbox::new(&mut self.keepalive, "Keepalive")).on_hover_text(XMRIG_KEEPALIVE); - }); - }); - }); - }); - } - } + debug!("XMRig Tab | Rendering [TLS/Keepalive] buttons"); + ui.vertical(|ui| { + // TLS/Keepalive + ui.horizontal(|ui| { + let width = (ui.available_width() / 2.0) - 11.0; + let height = text_edit * 2.0; + // let mut style = (*ctx.style()).clone(); + // style.spacing.icon_width_inner = width / 8.0; + // style.spacing.icon_width = width / 6.0; + // style.spacing.icon_spacing = 20.0; + // ctx.set_style(style); + ui.add_sized( + [width, height], + Checkbox::new(&mut self.tls, "TLS Connection"), + ) + .on_hover_text(XMRIG_TLS); + ui.separator(); + ui.add_sized( + [width, height], + Checkbox::new(&mut self.keepalive, "Keepalive"), + ) + .on_hover_text(XMRIG_KEEPALIVE); + }); + }); + }); + }); + } + } }