feat: separate logic into smaller modules

The original sources, while well documented, had files with over 2.5K LoC.
Separating the logic in different modules add clarity making the code easier to work with.
This commit is contained in:
Louis-Marie Baer 2024-03-03 08:31:22 +01:00
parent dbacd67a64
commit 045dd7ab03
80 changed files with 9440 additions and 8829 deletions

View file

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View file

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 187 KiB

View file

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View file

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View file

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View file

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View file

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View file

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

View file

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

View file

Before

Width:  |  Height:  |  Size: 761 KiB

After

Width:  |  Height:  |  Size: 761 KiB

View file

Before

Width:  |  Height:  |  Size: 505 KiB

After

Width:  |  Height:  |  Size: 505 KiB

View file

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 281 KiB

View file

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View file

Before

Width:  |  Height:  |  Size: 451 KiB

After

Width:  |  Height:  |  Size: 451 KiB

View file

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

View file

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View file

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View file

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View file

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View file

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 314 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

89
src/app/eframe_impl.rs Normal file
View file

@ -0,0 +1,89 @@
use super::App;
use crate::macros::lock;
use crate::SECOND;
use egui::CentralPanel;
use log::debug;
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
// *-------*
// | DEBUG |
// *-------*
debug!("App | ----------- Start of [update()] -----------");
// If closing
self.quit(ctx);
// Handle Keys
let (key, wants_input) = self.keys_handle(ctx);
// Refresh AT LEAST once a second
debug!("App | Refreshing frame once per second");
ctx.request_repaint_after(SECOND);
// Get P2Pool/XMRig process state.
// These values are checked multiple times so
// might as well check only once here to save
// on a bunch of [.lock().unwrap()]s.
debug!("App | Locking and collecting P2Pool state...");
let p2pool = lock!(self.p2pool);
let p2pool_is_alive = p2pool.is_alive();
let p2pool_is_waiting = p2pool.is_waiting();
let p2pool_state = p2pool.state;
drop(p2pool);
debug!("App | Locking and collecting XMRig state...");
let xmrig = lock!(self.xmrig);
let xmrig_is_alive = xmrig.is_alive();
let xmrig_is_waiting = xmrig.is_waiting();
let xmrig_state = xmrig.state;
drop(xmrig);
// This sets the top level Ui dimensions.
// Used as a reference for other uis.
debug!("App | Setting width/height");
CentralPanel::default().show(ctx, |ui| {
let available_width = ui.available_width();
if self.width != available_width {
self.width = available_width;
if self.now.elapsed().as_secs() > 5 {
self.must_resize = true;
}
};
self.height = ui.available_height();
});
self.resize(ctx);
// If there's an error, display [ErrorState] on the whole screen until user responds
debug!("App | Checking if there is an error in [ErrorState]");
if self.error_state.error {
self.quit_error_panel(ctx, p2pool_is_alive, xmrig_is_alive, &key);
return;
}
// Compare [og == state] & [node_vec/pool_vec] and enable diff if found.
// The struct fields are compared directly because [Version]
// contains Arc<Mutex>'s that cannot be compared easily.
// They don't need to be compared anyway.
debug!("App | Checking diff between [og] & [state]");
let og = lock!(self.og);
self.diff = og.status != self.state.status
|| og.gupax != self.state.gupax
|| og.p2pool != self.state.p2pool
|| og.xmrig != self.state.xmrig
|| og.xvb != self.state.xvb
|| self.og_node_vec != self.node_vec
|| self.og_pool_vec != self.pool_vec;
drop(og);
self.top_panel(ctx);
self.bottom_panel(
ctx,
p2pool_state,
xmrig_state,
&key,
wants_input,
p2pool_is_waiting,
xmrig_is_waiting,
p2pool_is_alive,
xmrig_is_alive,
);
self.middle_panel(ctx, frame, key, p2pool_is_alive, xmrig_is_alive);
}
}

172
src/app/keys.rs Normal file
View file

@ -0,0 +1,172 @@
use egui::{Key, Modifiers};
use log::info;
use crate::{disk::status::Submenu, utils::macros::flip};
use super::{App, Tab};
//---------------------------------------------------------------------------------------------------- [Pressed] enum
// These represent the keys pressed during the frame.
// I could use egui's [Key] but there is no option for
// a [None] and wrapping [key_pressed] like [Option<egui::Key>]
// meant that I had to destructure like this:
// if let Some(egui::Key)) = key_pressed { /* do thing */ }
//
// That's ugly, so these are used instead so a simple compare can be used.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum KeyPressed {
F11,
Up,
Down,
Esc,
Z,
X,
C,
V,
S,
R,
D,
None,
}
impl KeyPressed {
#[inline]
pub(super) fn is_f11(&self) -> bool {
*self == Self::F11
}
#[inline]
pub(super) fn is_z(&self) -> bool {
*self == Self::Z
}
#[inline]
pub(super) fn is_x(&self) -> bool {
*self == Self::X
}
#[inline]
pub(super) fn is_up(&self) -> bool {
*self == Self::Up
}
#[inline]
pub(super) fn is_down(&self) -> bool {
*self == Self::Down
}
#[inline]
pub(super) fn is_esc(&self) -> bool {
*self == Self::Esc
}
#[inline]
pub(super) fn is_s(&self) -> bool {
*self == Self::S
}
#[inline]
pub(super) fn is_r(&self) -> bool {
*self == Self::R
}
#[inline]
pub(super) fn is_d(&self) -> bool {
*self == Self::D
}
#[inline]
pub(super) fn is_c(&self) -> bool {
*self == Self::C
}
#[inline]
pub(super) fn is_v(&self) -> bool {
*self == Self::V
}
// #[inline]
// pub(super) fn is_none(&self) -> bool {
// *self == Self::None
// }
}
impl App {
pub fn keys_handle(&mut self, ctx: &egui::Context) -> (KeyPressed, bool) {
// If [F11] was pressed, reverse [fullscreen] bool
let key: KeyPressed = ctx.input_mut(|input| {
if input.consume_key(Modifiers::NONE, Key::F11) {
KeyPressed::F11
} else if input.consume_key(Modifiers::NONE, Key::Z) {
KeyPressed::Z
} else if input.consume_key(Modifiers::NONE, Key::X) {
KeyPressed::X
} else if input.consume_key(Modifiers::NONE, Key::C) {
KeyPressed::C
} else if input.consume_key(Modifiers::NONE, Key::V) {
KeyPressed::V
} else if input.consume_key(Modifiers::NONE, Key::ArrowUp) {
KeyPressed::Up
} else if input.consume_key(Modifiers::NONE, Key::ArrowDown) {
KeyPressed::Down
} else if input.consume_key(Modifiers::NONE, Key::Escape) {
KeyPressed::Esc
} else if input.consume_key(Modifiers::NONE, Key::S) {
KeyPressed::S
} else if input.consume_key(Modifiers::NONE, Key::R) {
KeyPressed::R
} else if input.consume_key(Modifiers::NONE, Key::D) {
KeyPressed::D
} else {
KeyPressed::None
}
});
// Check if egui wants keyboard input.
// This prevents keyboard shortcuts from clobbering TextEdits.
// (Typing S in text would always [Save] instead)
let wants_input = ctx.wants_keyboard_input();
if key.is_f11() {
if ctx.input(|i| i.viewport().maximized == Some(true)) {
info!("fullscreen bool");
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true));
}
// Change Tabs LEFT
} else if key.is_z() && !wants_input {
match self.tab {
Tab::About => self.tab = Tab::Xvb,
Tab::Status => self.tab = Tab::About,
Tab::Gupax => self.tab = Tab::Status,
Tab::P2pool => self.tab = Tab::Gupax,
Tab::Xmrig => self.tab = Tab::P2pool,
Tab::Xvb => self.tab = Tab::Xmrig,
};
// Change Tabs RIGHT
} else if key.is_x() && !wants_input {
match self.tab {
Tab::About => self.tab = Tab::Status,
Tab::Status => self.tab = Tab::Gupax,
Tab::Gupax => self.tab = Tab::P2pool,
Tab::P2pool => self.tab = Tab::Xmrig,
Tab::Xmrig => self.tab = Tab::Xvb,
Tab::Xvb => self.tab = Tab::About,
};
// Change Submenu LEFT
} else if key.is_c() && !wants_input {
match self.tab {
Tab::Status => match self.state.status.submenu {
Submenu::Processes => self.state.status.submenu = Submenu::Benchmarks,
Submenu::P2pool => self.state.status.submenu = Submenu::Processes,
Submenu::Benchmarks => self.state.status.submenu = Submenu::P2pool,
},
Tab::Gupax => flip!(self.state.gupax.simple),
Tab::P2pool => flip!(self.state.p2pool.simple),
Tab::Xmrig => flip!(self.state.xmrig.simple),
_ => (),
};
// Change Submenu RIGHT
} else if key.is_v() && !wants_input {
match self.tab {
Tab::Status => match self.state.status.submenu {
Submenu::Processes => self.state.status.submenu = Submenu::P2pool,
Submenu::P2pool => self.state.status.submenu = Submenu::Benchmarks,
Submenu::Benchmarks => self.state.status.submenu = Submenu::Processes,
},
Tab::Gupax => flip!(self.state.gupax.simple),
Tab::P2pool => flip!(self.state.p2pool.simple),
Tab::Xmrig => flip!(self.state.xmrig.simple),
_ => (),
};
}
(key, wants_input)
}
}

675
src/app/mod.rs Normal file
View file

@ -0,0 +1,675 @@
use crate::components::gupax::FileWindow;
use crate::components::node::Ping;
use crate::components::node::RemoteNode;
use crate::components::node::REMOTE_NODES;
use crate::components::update::Update;
use crate::disk::consts::NODE_TOML;
use crate::disk::consts::POOL_TOML;
use crate::disk::consts::STATE_TOML;
use crate::disk::get_gupax_data_path;
use crate::disk::gupax_p2pool_api::GupaxP2poolApi;
use crate::disk::node::Node;
use crate::disk::pool::Pool;
use crate::disk::state::State;
use crate::errors::ErrorButtons;
use crate::errors::ErrorFerris;
use crate::errors::ErrorState;
use crate::helper::p2pool::ImgP2pool;
use crate::helper::p2pool::PubP2poolApi;
use crate::helper::xmrig::ImgXmrig;
use crate::helper::xmrig::PubXmrigApi;
use crate::helper::Helper;
use crate::helper::Process;
use crate::helper::ProcessName;
use crate::helper::Sys;
use crate::inits::init_text_styles;
use crate::miscs::cmp_f64;
use crate::miscs::get_exe;
use crate::miscs::get_exe_dir;
use crate::miscs::parse_args;
use crate::utils::constants::VISUALS;
use crate::utils::macros::arc_mut;
use crate::utils::macros::lock;
use crate::utils::sudo::SudoState;
use crate::APP_DEFAULT_HEIGHT;
use crate::APP_DEFAULT_WIDTH;
use crate::GUPAX_VERSION;
use crate::OS;
use eframe::CreationContext;
use egui::Vec2;
use log::debug;
use log::error;
use log::info;
use log::warn;
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf;
use std::process::exit;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Instant;
pub mod eframe_impl;
pub mod keys;
pub mod panels;
pub mod quit;
pub mod resize;
//---------------------------------------------------------------------------------------------------- Struct + Impl
// The state of the outer main [App].
// See the [State] struct in [state.rs] for the
// actual inner state of the tab settings.
pub struct App {
// Misc state
pub tab: Tab, // What tab are we on?
pub width: f32, // Top-level width
pub height: f32, // Top-level height
// Alpha (transparency)
// This value is used to incrementally increase/decrease
// the transparency when resizing. Basically, it fades
// in/out of black to hide jitter when resizing with [init_text_styles()]
pub alpha: u8,
// This is a one time trigger so [init_text_styles()] isn't
// called 60x a second when resizing the window. Instead,
// it only gets called if this bool is true and the user
// is hovering over egui (ctx.is_pointer_over_area()).
pub must_resize: bool, // Sets the flag so we know to [init_text_styles()]
pub resizing: bool, // Are we in the process of resizing? (For black fade in/out)
// State
pub og: Arc<Mutex<State>>, // og = Old state to compare against
pub state: State, // state = Working state (current settings)
pub update: Arc<Mutex<Update>>, // State for update data [update.rs]
pub file_window: Arc<Mutex<FileWindow>>, // State for the path selector in [Gupax]
pub ping: Arc<Mutex<Ping>>, // Ping data found in [node.rs]
pub og_node_vec: Vec<(String, Node)>, // Manual Node database
pub node_vec: Vec<(String, Node)>, // Manual Node database
pub og_pool_vec: Vec<(String, Pool)>, // Manual Pool database
pub pool_vec: Vec<(String, Pool)>, // Manual Pool database
pub diff: bool, // This bool indicates state changes
// Restart state:
// If Gupax updated itself, this represents that the
// user should (but isn't required to) restart Gupax.
pub restart: Arc<Mutex<Restart>>,
// Error State:
// These values are essentially global variables that
// indicate if an error message needs to be displayed
// (it takes up the whole screen with [error_msg] and buttons for ok/quit/etc)
pub error_state: ErrorState,
// Helper/API State:
// This holds everything related to the data processed by the "helper thread".
// This includes the "helper" threads public P2Pool/XMRig's API.
pub helper: Arc<Mutex<Helper>>, // [Helper] state, mostly for Gupax uptime
pub pub_sys: Arc<Mutex<Sys>>, // [Sys] state, read by [Status], mutated by [Helper]
pub p2pool: Arc<Mutex<Process>>, // [P2Pool] process state
pub xmrig: Arc<Mutex<Process>>, // [XMRig] process state
pub p2pool_api: Arc<Mutex<PubP2poolApi>>, // Public ready-to-print P2Pool API made by the "helper" thread
pub xmrig_api: Arc<Mutex<PubXmrigApi>>, // Public ready-to-print XMRig API made by the "helper" thread
pub p2pool_img: Arc<Mutex<ImgP2pool>>, // A one-time snapshot of what data P2Pool started with
pub xmrig_img: Arc<Mutex<ImgXmrig>>, // A one-time snapshot of what data XMRig started with
// STDIN Buffer
pub p2pool_stdin: String, // The buffer between the p2pool console and the [Helper]
pub xmrig_stdin: String, // The buffer between the xmrig console and the [Helper]
// Sudo State
pub sudo: Arc<Mutex<SudoState>>, // This is just a dummy struct on [Windows].
// State from [--flags]
pub no_startup: bool,
// Gupax-P2Pool API
// Gupax's P2Pool API (e.g: ~/.local/share/gupax/p2pool/)
// This is a file-based API that contains data for permanent stats.
// The below struct holds everything needed for it, the paths, the
// actual stats, and all the functions needed to mutate them.
pub gupax_p2pool_api: Arc<Mutex<GupaxP2poolApi>>,
// Static stuff
pub benchmarks: Vec<Benchmark>, // XMRig CPU benchmarks
pub pid: sysinfo::Pid, // Gupax's PID
pub max_threads: usize, // Max amount of detected system threads
pub now: Instant, // Internal timer
pub exe: String, // Path for [Gupax] binary
pub dir: String, // Directory [Gupax] binary is in
pub resolution: Vec2, // Frame resolution
pub os: &'static str, // OS
pub admin: bool, // Are we admin? (for Windows)
pub os_data_path: PathBuf, // OS data path (e.g: ~/.local/share/gupax/)
pub gupax_p2pool_api_path: PathBuf, // Gupax-P2Pool API path (e.g: ~/.local/share/gupax/p2pool/)
pub state_path: PathBuf, // State file path
pub node_path: PathBuf, // Node file path
pub pool_path: PathBuf, // Pool file path
pub version: &'static str, // Gupax version
pub name_version: String, // [Gupax vX.X.X]
}
impl App {
#[cold]
#[inline(never)]
pub fn cc(cc: &CreationContext<'_>, resolution: Vec2, app: Self) -> Self {
init_text_styles(
&cc.egui_ctx,
resolution[0],
crate::miscs::clamp_scale(app.state.gupax.selected_scale),
);
cc.egui_ctx.set_visuals(VISUALS.clone());
Self { resolution, ..app }
}
#[cold]
#[inline(never)]
pub fn save_before_quit(&mut self) {
if let Err(e) = State::save(&mut self.state, &self.state_path) {
error!("State file: {}", e);
}
if let Err(e) = Node::save(&self.node_vec, &self.node_path) {
error!("Node list: {}", e);
}
if let Err(e) = Pool::save(&self.pool_vec, &self.pool_path) {
error!("Pool list: {}", e);
}
}
#[cold]
#[inline(never)]
pub fn new(now: Instant) -> Self {
info!("Initializing App Struct...");
info!("App Init | P2Pool & XMRig processes...");
let p2pool = arc_mut!(Process::new(
ProcessName::P2pool,
String::new(),
PathBuf::new()
));
let xmrig = arc_mut!(Process::new(
ProcessName::Xmrig,
String::new(),
PathBuf::new()
));
let p2pool_api = arc_mut!(PubP2poolApi::new());
let xmrig_api = arc_mut!(PubXmrigApi::new());
let p2pool_img = arc_mut!(ImgP2pool::new());
let xmrig_img = arc_mut!(ImgXmrig::new());
info!("App Init | Sysinfo...");
// We give this to the [Helper] thread.
let mut sysinfo = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::new()
.with_cpu(sysinfo::CpuRefreshKind::everything())
.with_processes(sysinfo::ProcessRefreshKind::new().with_cpu())
.with_memory(sysinfo::MemoryRefreshKind::everything()),
);
sysinfo.refresh_all();
let pid = match sysinfo::get_current_pid() {
Ok(pid) => pid,
Err(e) => {
error!("App Init | Failed to get sysinfo PID: {}", e);
exit(1)
}
};
let pub_sys = arc_mut!(Sys::new());
// CPU Benchmark data initialization.
info!("App Init | Initializing CPU benchmarks...");
let benchmarks: Vec<Benchmark> = {
let cpu = sysinfo.cpus()[0].brand();
let mut json: Vec<Benchmark> =
serde_json::from_slice(include_bytes!("../../assets/cpu.json")).unwrap();
json.sort_by(|a, b| cmp_f64(strsim::jaro(&b.cpu, cpu), strsim::jaro(&a.cpu, cpu)));
json
};
info!("App Init | Assuming user's CPU is: {}", benchmarks[0].cpu);
info!("App Init | The rest of the [App]...");
let mut app = Self {
tab: Tab::default(),
ping: arc_mut!(Ping::new()),
width: APP_DEFAULT_WIDTH,
height: APP_DEFAULT_HEIGHT,
must_resize: false,
og: arc_mut!(State::new()),
state: State::new(),
update: arc_mut!(Update::new(
String::new(),
PathBuf::new(),
PathBuf::new(),
true
)),
file_window: FileWindow::new(),
og_node_vec: Node::new_vec(),
node_vec: Node::new_vec(),
og_pool_vec: Pool::new_vec(),
pool_vec: Pool::new_vec(),
restart: arc_mut!(Restart::No),
diff: false,
error_state: ErrorState::new(),
helper: arc_mut!(Helper::new(
now,
pub_sys.clone(),
p2pool.clone(),
xmrig.clone(),
p2pool_api.clone(),
xmrig_api.clone(),
p2pool_img.clone(),
xmrig_img.clone(),
arc_mut!(GupaxP2poolApi::new())
)),
p2pool,
xmrig,
p2pool_api,
xmrig_api,
p2pool_img,
xmrig_img,
p2pool_stdin: String::with_capacity(10),
xmrig_stdin: String::with_capacity(10),
sudo: arc_mut!(SudoState::new()),
resizing: false,
alpha: 0,
no_startup: false,
gupax_p2pool_api: arc_mut!(GupaxP2poolApi::new()),
pub_sys,
benchmarks,
pid,
max_threads: benri::threads!(),
now,
admin: false,
exe: String::new(),
dir: String::new(),
resolution: Vec2::new(APP_DEFAULT_HEIGHT, APP_DEFAULT_WIDTH),
os: OS,
os_data_path: PathBuf::new(),
gupax_p2pool_api_path: PathBuf::new(),
state_path: PathBuf::new(),
node_path: PathBuf::new(),
pool_path: PathBuf::new(),
version: GUPAX_VERSION,
name_version: format!("Gupax {}", GUPAX_VERSION),
};
//---------------------------------------------------------------------------------------------------- App init data that *could* panic
info!("App Init | Getting EXE path...");
let mut panic = String::new();
// Get exe path
app.exe = match get_exe() {
Ok(exe) => exe,
Err(e) => {
panic = format!("get_exe(): {}", e);
app.error_state
.set(panic.clone(), ErrorFerris::Panic, ErrorButtons::Quit);
String::new()
}
};
// Get exe directory path
app.dir = match get_exe_dir() {
Ok(dir) => dir,
Err(e) => {
panic = format!("get_exe_dir(): {}", e);
app.error_state
.set(panic.clone(), ErrorFerris::Panic, ErrorButtons::Quit);
String::new()
}
};
// Get OS data path
app.os_data_path = match get_gupax_data_path() {
Ok(dir) => dir,
Err(e) => {
panic = format!("get_os_data_path(): {}", e);
app.error_state
.set(panic.clone(), ErrorFerris::Panic, ErrorButtons::Quit);
PathBuf::new()
}
};
info!("App Init | Setting TOML path...");
// Set [*.toml] path
app.state_path = app.os_data_path.clone();
app.state_path.push(STATE_TOML);
app.node_path = app.os_data_path.clone();
app.node_path.push(NODE_TOML);
app.pool_path = app.os_data_path.clone();
app.pool_path.push(POOL_TOML);
// Set GupaxP2poolApi path
app.gupax_p2pool_api_path = crate::disk::get_gupax_p2pool_path(&app.os_data_path);
lock!(app.gupax_p2pool_api).fill_paths(&app.gupax_p2pool_api_path);
// Apply arg state
// It's not safe to [--reset] if any of the previous variables
// are unset (null path), so make sure we just abort if the [panic] String contains something.
info!("App Init | Applying argument state...");
let mut app = parse_args(app, panic);
use crate::disk::errors::TomlError::*;
// Read disk state
info!("App Init | Reading disk state...");
app.state = match State::get(&app.state_path) {
Ok(toml) => toml,
Err(err) => {
error!("State ... {}", err);
let set = match err {
Io(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)),
Path(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)),
Serialize(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)),
Deserialize(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)),
Format(e) => Some((e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit)),
Merge(e) => Some((e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState)),
_ => None,
};
if let Some((e, ferris, button)) = set {
app.error_state.set(format!("State file: {}\n\nTry deleting: {}\n\n(Warning: this will delete your Gupax settings)\n\n", e, app.state_path.display()), ferris, button);
}
State::new()
}
};
// Clamp window resolution scaling values.
app.state.gupax.selected_scale = crate::miscs::clamp_scale(app.state.gupax.selected_scale);
app.og = arc_mut!(app.state.clone());
// Read node list
info!("App Init | Reading node list...");
app.node_vec = match Node::get(&app.node_path) {
Ok(toml) => toml,
Err(err) => {
error!("Node ... {}", err);
let (e, ferris, button) = match err {
Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState),
Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
};
app.error_state.set(format!("Node list: {}\n\nTry deleting: {}\n\n(Warning: this will delete your custom node list)\n\n", e, app.node_path.display()), ferris, button);
Node::new_vec()
}
};
app.og_node_vec = app.node_vec.clone();
debug!("Node Vec:");
debug!("{:#?}", app.node_vec);
// Read pool list
info!("App Init | Reading pool list...");
app.pool_vec = match Pool::get(&app.pool_path) {
Ok(toml) => toml,
Err(err) => {
error!("Pool ... {}", err);
let (e, ferris, button) = match err {
Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState),
Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
};
app.error_state.set(format!("Pool list: {}\n\nTry deleting: {}\n\n(Warning: this will delete your custom pool list)\n\n", e, app.pool_path.display()), ferris, button);
Pool::new_vec()
}
};
app.og_pool_vec = app.pool_vec.clone();
debug!("Pool Vec:");
debug!("{:#?}", app.pool_vec);
//----------------------------------------------------------------------------------------------------
// Read [GupaxP2poolApi] disk files
let mut gupax_p2pool_api = lock!(app.gupax_p2pool_api);
match GupaxP2poolApi::create_all_files(&app.gupax_p2pool_api_path) {
Ok(_) => info!("App Init | Creating Gupax-P2Pool API files ... OK"),
Err(err) => {
error!("GupaxP2poolApi ... {}", err);
let (e, ferris, button) = match err {
Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState),
Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
};
app.error_state.set(format!("Gupax P2Pool Stats: {}\n\nTry deleting: {}\n\n(Warning: this will delete your P2Pool payout history...!)\n\n", e, app.gupax_p2pool_api_path.display()), ferris, button);
}
}
info!("App Init | Reading Gupax-P2Pool API files...");
match gupax_p2pool_api.read_all_files_and_update() {
Ok(_) => {
info!(
"GupaxP2poolApi ... Payouts: {} | XMR (atomic-units): {}",
gupax_p2pool_api.payout, gupax_p2pool_api.xmr,
);
}
Err(err) => {
error!("GupaxP2poolApi ... {}", err);
let (e, ferris, button) = match err {
Io(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Path(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Serialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Deserialize(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Format(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
Merge(e) => (e.to_string(), ErrorFerris::Error, ErrorButtons::ResetState),
Parse(e) => (e.to_string(), ErrorFerris::Panic, ErrorButtons::Quit),
};
app.error_state.set(format!("Gupax P2Pool Stats: {}\n\nTry deleting: {}\n\n(Warning: this will delete your P2Pool payout history...!)\n\n", e, app.gupax_p2pool_api_path.display()), ferris, button);
}
};
drop(gupax_p2pool_api);
lock!(app.helper).gupax_p2pool_api = Arc::clone(&app.gupax_p2pool_api);
//----------------------------------------------------------------------------------------------------
let mut og = lock!(app.og); // Lock [og]
// Handle max threads
info!("App Init | Handling max thread overflow...");
og.xmrig.max_threads = app.max_threads;
let current = og.xmrig.current_threads;
let max = og.xmrig.max_threads;
if current > max {
og.xmrig.current_threads = max;
}
// Handle [node_vec] overflow
info!("App Init | Handling [node_vec] overflow");
if og.p2pool.selected_index > app.og_node_vec.len() {
warn!(
"App | Overflowing manual node index [{} > {}]",
og.p2pool.selected_index,
app.og_node_vec.len()
);
let (name, node) = match app.og_node_vec.first() {
Some(zero) => zero.clone(),
None => Node::new_tuple(),
};
og.p2pool.selected_index = 0;
og.p2pool.selected_name = name.clone();
og.p2pool.selected_ip = node.ip.clone();
og.p2pool.selected_rpc = node.rpc.clone();
og.p2pool.selected_zmq = node.zmq.clone();
app.state.p2pool.selected_index = 0;
app.state.p2pool.selected_name = name;
app.state.p2pool.selected_ip = node.ip;
app.state.p2pool.selected_rpc = node.rpc;
app.state.p2pool.selected_zmq = node.zmq;
}
// Handle [pool_vec] overflow
info!("App Init | Handling [pool_vec] overflow...");
if og.xmrig.selected_index > app.og_pool_vec.len() {
warn!(
"App | Overflowing manual pool index [{} > {}], resetting to 1",
og.xmrig.selected_index,
app.og_pool_vec.len()
);
let (name, pool) = match app.og_pool_vec.first() {
Some(zero) => zero.clone(),
None => Pool::new_tuple(),
};
og.xmrig.selected_index = 0;
og.xmrig.selected_name = name.clone();
og.xmrig.selected_ip = pool.ip.clone();
og.xmrig.selected_port = pool.port.clone();
app.state.xmrig.selected_index = 0;
app.state.xmrig.selected_name = name;
app.state.xmrig.selected_ip = pool.ip;
app.state.xmrig.selected_port = pool.port;
}
// Apply TOML values to [Update]
info!("App Init | Applying TOML values to [Update]...");
let p2pool_path = og.gupax.absolute_p2pool_path.clone();
let xmrig_path = og.gupax.absolute_xmrig_path.clone();
let tor = og.gupax.update_via_tor;
app.update = arc_mut!(Update::new(app.exe.clone(), p2pool_path, xmrig_path, tor));
// Set state version as compiled in version
info!("App Init | Setting state Gupax version...");
lock!(og.version).gupax = GUPAX_VERSION.to_string();
lock!(app.state.version).gupax = GUPAX_VERSION.to_string();
// Set saved [Tab]
info!("App Init | Setting saved [Tab]...");
app.tab = app.state.gupax.tab;
// Check if [P2pool.node] exists
info!("App Init | Checking if saved remote node still exists...");
app.state.p2pool.node = RemoteNode::check_exists(&app.state.p2pool.node);
drop(og); // Unlock [og]
// Spawn the "Helper" thread.
info!("Helper | Spawning helper thread...");
Helper::spawn_helper(&app.helper, sysinfo, app.pid, app.max_threads);
info!("Helper ... OK");
// Check for privilege. Should be Admin on [Windows] and NOT root on Unix.
info!("App Init | Checking for privilege level...");
#[cfg(target_os = "windows")]
if is_elevated::is_elevated() {
app.admin = true;
} else {
error!("Windows | Admin user not detected!");
app.error_state.set(format!("Gupax was not launched as Administrator!\nBe warned, XMRig might have less hashrate!"), ErrorFerris::Sudo, ErrorButtons::WindowsAdmin);
}
#[cfg(target_family = "unix")]
if sudo_check::check() != sudo_check::RunningAs::User {
let id = sudo_check::check();
error!("Unix | Regular user not detected: [{:?}]", id);
app.error_state.set(format!("Gupax was launched as: [{:?}]\nPlease launch Gupax with regular user permissions.", id), ErrorFerris::Panic, ErrorButtons::Quit);
}
// macOS re-locates "dangerous" applications into some read-only "/private" directory.
// It _seems_ to be fixed by moving [Gupax.app] into "/Applications".
// So, detect if we are in in "/private" and warn the user.
#[cfg(target_os = "macos")]
if app.exe.starts_with("/private") {
app.error_state.set(format!("macOS thinks Gupax is a virus!\n(macOS has relocated Gupax for security reasons)\n\nThe directory: [{}]\nSince this is a private read-only directory, it causes issues with updates and correctly locating P2Pool/XMRig. Please move Gupax into the [Applications] directory, this lets macOS relax a little.\n", app.exe), ErrorFerris::Panic, ErrorButtons::Quit);
}
info!("App ... OK");
app
}
#[cold]
#[inline(never)]
pub fn gather_backup_hosts(&self) -> Option<Vec<Node>> {
if !self.state.p2pool.backup_host {
return None;
}
// INVARIANT:
// We must ensure all nodes are capable of
// sending/receiving valid JSON-RPC requests.
//
// This is done during the `Ping` phase, meaning
// all the nodes listed in our `self.ping` should
// have ping data. We can use this data to filter
// out "dead" nodes.
//
// The user must have at least pinged once so that
// we actually have this data to work off of, else,
// this "backup host" feature will return here
// with 0 extra nodes as we can't be sure that any
// of them are actually online.
//
// Realistically, most of them are, but we can't be sure,
// and checking here without explicitly asking the user
// to connect to nodes is a no-go (also, non-async environment).
if !lock!(self.ping).pinged {
warn!("Backup hosts ... simple node backup: no ping data available, returning None");
return None;
}
if self.state.p2pool.simple {
let mut vec = Vec::with_capacity(REMOTE_NODES.len());
// Locking during this entire loop should be fine,
// only a few nodes to iter through.
for pinged_node in lock!(self.ping).nodes.iter() {
// Continue if this node is not green/yellow.
if pinged_node.ms > crate::components::node::RED_NODE_PING {
continue;
}
let (ip, rpc, zmq) = RemoteNode::get_ip_rpc_zmq(pinged_node.ip);
let node = Node {
ip: ip.into(),
rpc: rpc.into(),
zmq: zmq.into(),
};
vec.push(node);
}
if vec.is_empty() {
warn!("Backup hosts ... simple node backup: no viable nodes found");
None
} else {
info!("Backup hosts ... simple node backup list: {vec:#?}");
Some(vec)
}
} else {
Some(self.node_vec.iter().map(|(_, node)| node.clone()).collect())
}
}
}
//---------------------------------------------------------------------------------------------------- [Tab] Enum + Impl
// The tabs inside [App].
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Tab {
About,
Status,
Gupax,
P2pool,
Xmrig,
Xvb,
}
impl Default for Tab {
fn default() -> Self {
Self::About
}
}
//---------------------------------------------------------------------------------------------------- [Restart] Enum
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Restart {
No, // We don't need to restart
Yes, // We updated, user should probably (but isn't required to) restart
}
//---------------------------------------------------------------------------------------------------- CPU Benchmarks.
#[derive(Debug, Serialize, Deserialize)]
pub struct Benchmark {
pub cpu: String,
pub rank: u16,
pub percent: f32,
pub benchmarks: u16,
pub average: f32,
pub high: f32,
pub low: f32,
}
#[cfg(test)]
mod test {
use crate::miscs::cmp_f64;
#[test]
fn detect_benchmark_cpu() {
use crate::app::Benchmark;
let cpu = "AMD Ryzen 9 5950X 16-Core Processor";
let benchmarks: Vec<Benchmark> = {
let mut json: Vec<Benchmark> =
serde_json::from_slice(include_bytes!("../../assets/cpu.json")).unwrap();
json.sort_by(|a, b| cmp_f64(strsim::jaro(&b.cpu, cpu), strsim::jaro(&a.cpu, cpu)));
json
};
assert!(benchmarks[0].cpu == "AMD Ryzen 9 5950X 16-Core Processor");
}
}

509
src/app/panels/bottom.rs Normal file
View file

@ -0,0 +1,509 @@
use std::sync::Arc;
use crate::app::{keys::KeyPressed, Restart};
use crate::disk::node::Node;
use crate::disk::pool::Pool;
use crate::disk::state::{Gupax, State};
use crate::disk::status::Submenu;
use crate::helper::{Helper, ProcessSignal, ProcessState};
use crate::utils::constants::*;
use crate::utils::errors::{ErrorButtons, ErrorFerris};
use crate::utils::macros::lock;
use crate::utils::regex::Regexes;
use egui::TextStyle::Name;
use egui::*;
use log::debug;
use crate::{app::Tab, utils::constants::SPACE};
impl crate::app::App {
pub fn bottom_panel(
&mut self,
ctx: &egui::Context,
p2pool_state: ProcessState,
xmrig_state: ProcessState,
key: &KeyPressed,
wants_input: bool,
p2pool_is_waiting: bool,
xmrig_is_waiting: bool,
p2pool_is_alive: bool,
xmrig_is_alive: bool,
) {
// Bottom: app info + state/process buttons
debug!("App | Rendering BOTTOM bar");
TopBottomPanel::bottom("bottom").show(ctx, |ui| {
let height = self.height / 22.0;
ui.style_mut().override_text_style = Some(Name("Bottom".into()));
ui.horizontal(|ui| {
ui.group(|ui| {
let width = ((self.width / 2.0) / 4.0) - (SPACE * 2.0);
// [Gupax Version]
// Is yellow if the user updated and should (but isn't required to) restart.
match *lock!(self.restart) {
Restart::Yes => ui
.add_sized(
[width, height],
Label::new(RichText::new(&self.name_version).color(YELLOW)),
)
.on_hover_text(GUPAX_SHOULD_RESTART),
_ => ui.add_sized([width, height], Label::new(&self.name_version)),
};
ui.separator();
// [OS]
// Check if admin for windows.
// Unix SHOULDN'T be running as root, and the check is done when
// [App] is initialized, so no reason to check here.
#[cfg(target_os = "windows")]
if self.admin {
ui.add_sized([width, height], Label::new(self.os));
} else {
ui.add_sized(
[width, height],
Label::new(RichText::new(self.os).color(RED)),
)
.on_hover_text(WINDOWS_NOT_ADMIN);
}
#[cfg(target_family = "unix")]
ui.add_sized([width, height], Label::new(self.os));
ui.separator();
// [P2Pool/XMRig] Status
use crate::helper::ProcessState::*;
match p2pool_state {
Alive => ui
.add_sized(
[width, height],
Label::new(RichText::new("P2Pool ⏺").color(GREEN)),
)
.on_hover_text(P2POOL_ALIVE),
Dead => ui
.add_sized(
[width, height],
Label::new(RichText::new("P2Pool ⏺").color(GRAY)),
)
.on_hover_text(P2POOL_DEAD),
Failed => ui
.add_sized(
[width, height],
Label::new(RichText::new("P2Pool ⏺").color(RED)),
)
.on_hover_text(P2POOL_FAILED),
Syncing => ui
.add_sized(
[width, height],
Label::new(RichText::new("P2Pool ⏺").color(ORANGE)),
)
.on_hover_text(P2POOL_SYNCING),
Middle | Waiting | NotMining => ui
.add_sized(
[width, height],
Label::new(RichText::new("P2Pool ⏺").color(YELLOW)),
)
.on_hover_text(P2POOL_MIDDLE),
};
ui.separator();
match xmrig_state {
Alive => ui
.add_sized(
[width, height],
Label::new(RichText::new("XMRig ⏺").color(GREEN)),
)
.on_hover_text(XMRIG_ALIVE),
Dead => ui
.add_sized(
[width, height],
Label::new(RichText::new("XMRig ⏺").color(GRAY)),
)
.on_hover_text(XMRIG_DEAD),
Failed => ui
.add_sized(
[width, height],
Label::new(RichText::new("XMRig ⏺").color(RED)),
)
.on_hover_text(XMRIG_FAILED),
NotMining => ui
.add_sized(
[width, height],
Label::new(RichText::new("XMRig ⏺").color(ORANGE)),
)
.on_hover_text(XMRIG_NOT_MINING),
Middle | Waiting | Syncing => ui
.add_sized(
[width, height],
Label::new(RichText::new("XMRig ⏺").color(YELLOW)),
)
.on_hover_text(XMRIG_MIDDLE),
};
});
// [Save/Reset]
ui.with_layout(Layout::right_to_left(Align::RIGHT), |ui| {
let width = (ui.available_width() / 3.0) - (SPACE * 3.0);
ui.group(|ui| {
ui.set_enabled(self.diff);
let width = width / 2.0;
if key.is_r() && !wants_input && self.diff
|| ui
.add_sized([width, height], Button::new("Reset"))
.on_hover_text("Reset changes")
.clicked()
{
let og = lock!(self.og).clone();
self.state.status = og.status;
self.state.gupax = og.gupax;
self.state.p2pool = og.p2pool;
self.state.xmrig = og.xmrig;
self.node_vec = self.og_node_vec.clone();
self.pool_vec = self.og_pool_vec.clone();
}
if key.is_s() && !wants_input && self.diff
|| ui
.add_sized([width, height], Button::new("Save"))
.on_hover_text("Save changes")
.clicked()
{
match State::save(&mut self.state, &self.state_path) {
Ok(_) => {
let mut og = lock!(self.og);
og.status = self.state.status.clone();
og.gupax = self.state.gupax.clone();
og.p2pool = self.state.p2pool.clone();
og.xmrig = self.state.xmrig.clone();
}
Err(e) => {
self.error_state.set(
format!("State file: {}", e),
ErrorFerris::Error,
ErrorButtons::Okay,
);
}
};
match Node::save(&self.node_vec, &self.node_path) {
Ok(_) => self.og_node_vec = self.node_vec.clone(),
Err(e) => self.error_state.set(
format!("Node list: {}", e),
ErrorFerris::Error,
ErrorButtons::Okay,
),
};
match Pool::save(&self.pool_vec, &self.pool_path) {
Ok(_) => self.og_pool_vec = self.pool_vec.clone(),
Err(e) => self.error_state.set(
format!("Pool list: {}", e),
ErrorFerris::Error,
ErrorButtons::Okay,
),
};
}
});
// [Simple/Advanced] + [Start/Stop/Restart]
match self.tab {
Tab::Status => {
ui.group(|ui| {
let width = (ui.available_width() / 3.0) - 14.0;
if ui
.add_sized(
[width, height],
SelectableLabel::new(
self.state.status.submenu == Submenu::Benchmarks,
"Benchmarks",
),
)
.on_hover_text(STATUS_SUBMENU_HASHRATE)
.clicked()
{
self.state.status.submenu = Submenu::Benchmarks;
}
ui.separator();
if ui
.add_sized(
[width, height],
SelectableLabel::new(
self.state.status.submenu == Submenu::P2pool,
"P2Pool",
),
)
.on_hover_text(STATUS_SUBMENU_P2POOL)
.clicked()
{
self.state.status.submenu = Submenu::P2pool;
}
ui.separator();
if ui
.add_sized(
[width, height],
SelectableLabel::new(
self.state.status.submenu == Submenu::Processes,
"Processes",
),
)
.on_hover_text(STATUS_SUBMENU_PROCESSES)
.clicked()
{
self.state.status.submenu = Submenu::Processes;
}
});
}
Tab::Gupax => {
ui.group(|ui| {
let width = (ui.available_width() / 2.0) - 10.5;
if ui
.add_sized(
[width, height],
SelectableLabel::new(!self.state.gupax.simple, "Advanced"),
)
.on_hover_text(GUPAX_ADVANCED)
.clicked()
{
self.state.gupax.simple = false;
}
ui.separator();
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.state.gupax.simple, "Simple"),
)
.on_hover_text(GUPAX_SIMPLE)
.clicked()
{
self.state.gupax.simple = true;
}
});
}
Tab::P2pool => {
ui.group(|ui| {
let width = width / 1.5;
if ui
.add_sized(
[width, height],
SelectableLabel::new(!self.state.p2pool.simple, "Advanced"),
)
.on_hover_text(P2POOL_ADVANCED)
.clicked()
{
self.state.p2pool.simple = false;
}
ui.separator();
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.state.p2pool.simple, "Simple"),
)
.on_hover_text(P2POOL_SIMPLE)
.clicked()
{
self.state.p2pool.simple = true;
}
});
ui.group(|ui| {
let width = (ui.available_width() / 3.0) - 5.0;
if p2pool_is_waiting {
ui.add_enabled_ui(false, |ui| {
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text(P2POOL_MIDDLE);
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text(P2POOL_MIDDLE);
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text(P2POOL_MIDDLE);
});
} else if p2pool_is_alive {
if key.is_up() && !wants_input
|| ui
.add_sized([width, height], Button::new(""))
.on_hover_text("Restart P2Pool")
.clicked()
{
let _ = lock!(self.og).update_absolute_path();
let _ = self.state.update_absolute_path();
Helper::restart_p2pool(
&self.helper,
&self.state.p2pool,
&self.state.gupax.absolute_p2pool_path,
self.gather_backup_hosts(),
);
}
if key.is_down() && !wants_input
|| ui
.add_sized([width, height], Button::new(""))
.on_hover_text("Stop P2Pool")
.clicked()
{
Helper::stop_p2pool(&self.helper);
}
ui.add_enabled_ui(false, |ui| {
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text("Start P2Pool");
});
} else {
ui.add_enabled_ui(false, |ui| {
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text("Restart P2Pool");
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text("Stop P2Pool");
});
// Check if address is okay before allowing to start.
let mut text = String::new();
let mut ui_enabled = true;
if !Regexes::addr_ok(&self.state.p2pool.address) {
ui_enabled = false;
text = format!("Error: {}", P2POOL_ADDRESS);
} else if !Gupax::path_is_file(&self.state.gupax.p2pool_path) {
ui_enabled = false;
text = format!("Error: {}", P2POOL_PATH_NOT_FILE);
} else if !crate::components::update::check_p2pool_path(
&self.state.gupax.p2pool_path,
) {
ui_enabled = false;
text = format!("Error: {}", P2POOL_PATH_NOT_VALID);
}
ui.set_enabled(ui_enabled);
let color = if ui_enabled { GREEN } else { RED };
if (ui_enabled && key.is_up() && !wants_input)
|| ui
.add_sized(
[width, height],
Button::new(RichText::new("").color(color)),
)
.on_hover_text("Start P2Pool")
.on_disabled_hover_text(text)
.clicked()
{
let _ = lock!(self.og).update_absolute_path();
let _ = self.state.update_absolute_path();
Helper::start_p2pool(
&self.helper,
&self.state.p2pool,
&self.state.gupax.absolute_p2pool_path,
self.gather_backup_hosts(),
);
}
}
});
}
Tab::Xmrig => {
ui.group(|ui| {
let width = width / 1.5;
if ui
.add_sized(
[width, height],
SelectableLabel::new(!self.state.xmrig.simple, "Advanced"),
)
.on_hover_text(XMRIG_ADVANCED)
.clicked()
{
self.state.xmrig.simple = false;
}
ui.separator();
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.state.xmrig.simple, "Simple"),
)
.on_hover_text(XMRIG_SIMPLE)
.clicked()
{
self.state.xmrig.simple = true;
}
});
ui.group(|ui| {
let width = (ui.available_width() / 3.0) - 5.0;
if xmrig_is_waiting {
ui.add_enabled_ui(false, |ui| {
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text(XMRIG_MIDDLE);
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text(XMRIG_MIDDLE);
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text(XMRIG_MIDDLE);
});
} else if xmrig_is_alive {
if key.is_up() && !wants_input
|| ui
.add_sized([width, height], Button::new(""))
.on_hover_text("Restart XMRig")
.clicked()
{
let _ = lock!(self.og).update_absolute_path();
let _ = self.state.update_absolute_path();
if cfg!(windows) {
Helper::restart_xmrig(
&self.helper,
&self.state.xmrig,
&self.state.gupax.absolute_xmrig_path,
Arc::clone(&self.sudo),
);
} else {
lock!(self.sudo).signal = ProcessSignal::Restart;
self.error_state.ask_sudo(&self.sudo);
}
}
if key.is_down() && !wants_input
|| ui
.add_sized([width, height], Button::new(""))
.on_hover_text("Stop XMRig")
.clicked()
{
if cfg!(target_os = "macos") {
lock!(self.sudo).signal = ProcessSignal::Stop;
self.error_state.ask_sudo(&self.sudo);
} else {
Helper::stop_xmrig(&self.helper);
}
}
ui.add_enabled_ui(false, |ui| {
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text("Start XMRig");
});
} else {
ui.add_enabled_ui(false, |ui| {
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text("Restart XMRig");
ui.add_sized([width, height], Button::new(""))
.on_disabled_hover_text("Stop XMRig");
});
let mut text = String::new();
let mut ui_enabled = true;
if !Gupax::path_is_file(&self.state.gupax.xmrig_path) {
ui_enabled = false;
text = format!("Error: {}", XMRIG_PATH_NOT_FILE);
} else if !crate::components::update::check_xmrig_path(
&self.state.gupax.xmrig_path,
) {
ui_enabled = false;
text = format!("Error: {}", XMRIG_PATH_NOT_VALID);
}
ui.set_enabled(ui_enabled);
let color = if ui_enabled { GREEN } else { RED };
if (ui_enabled && key.is_up() && !wants_input)
|| ui
.add_sized(
[width, height],
Button::new(RichText::new("").color(color)),
)
.on_hover_text("Start XMRig")
.on_disabled_hover_text(text)
.clicked()
{
let _ = lock!(self.og).update_absolute_path();
let _ = self.state.update_absolute_path();
if cfg!(windows) {
Helper::start_xmrig(
&self.helper,
&self.state.xmrig,
&self.state.gupax.absolute_xmrig_path,
Arc::clone(&self.sudo),
);
} else if cfg!(unix) {
lock!(self.sudo).signal = ProcessSignal::Start;
self.error_state.ask_sudo(&self.sudo);
}
}
}
});
}
_ => (),
}
});
});
});
}
}

View file

@ -1,75 +1,17 @@
// Gupax - GUI Uniting P2Pool And XMRig
//
// Copyright (c) 2022-2023 hinto-janai
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::State;
use crate::{constants::*, macros::*, update::*, ErrorState, Restart, Tab};
use egui::{
Button, Checkbox, Label, ProgressBar, RichText, SelectableLabel, Slider, Spinner, TextEdit,
Vec2,
};
use log::*;
use serde::{Deserialize, Serialize};
use std::{
path::Path,
sync::{Arc, Mutex},
thread,
};
//---------------------------------------------------------------------------------------------------- FileWindow
// Struct for writing/reading the path state.
// The opened file picker is started in a new
// thread so main() needs to be in sync.
pub struct FileWindow {
thread: bool, // Is there already a FileWindow thread?
picked_p2pool: bool, // Did the user pick a path for p2pool?
picked_xmrig: bool, // Did the user pick a path for xmrig?
p2pool_path: String, // The picked p2pool path
xmrig_path: String, // The picked p2pool path
}
impl FileWindow {
pub fn new() -> Arc<Mutex<Self>> {
arc_mut!(Self {
thread: false,
picked_p2pool: false,
picked_xmrig: false,
p2pool_path: String::new(),
xmrig_path: String::new(),
})
}
}
#[derive(Debug, Clone)]
pub enum FileType {
P2pool,
Xmrig,
}
//---------------------------------------------------------------------------------------------------- Ratio Lock
// Enum for the lock ratio in the advanced tab.
#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)]
pub enum Ratio {
Width,
Height,
None,
}
//---------------------------------------------------------------------------------------------------- Gupax
impl crate::disk::Gupax {
use crate::app::panels::middle::*;
use crate::app::ErrorState;
use crate::app::Restart;
use crate::components::gupax::FileWindow;
use crate::components::gupax::*;
use crate::components::update::Update;
use crate::disk::state::*;
use crate::macros::lock2;
use egui::Button;
use log::debug;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;
impl Gupax {
#[inline(always)] // called once
#[allow(clippy::too_many_arguments)]
pub fn show(
@ -218,7 +160,7 @@ impl crate::disk::Gupax {
Label::new(RichText::new("P2Pool Binary Path ❌").color(RED)),
)
.on_hover_text(P2POOL_PATH_NOT_FILE);
} else if !crate::update::check_p2pool_path(&self.p2pool_path) {
} else if !crate::components::update::check_p2pool_path(&self.p2pool_path) {
ui.add_sized(
[text_edit, height],
Label::new(RichText::new("P2Pool Binary Path ❌").color(RED)),
@ -255,7 +197,7 @@ impl crate::disk::Gupax {
Label::new(RichText::new(" XMRig Binary Path ❌").color(RED)),
)
.on_hover_text(XMRIG_PATH_NOT_FILE);
} else if !crate::update::check_xmrig_path(&self.xmrig_path) {
} else if !crate::components::update::check_xmrig_path(&self.xmrig_path) {
ui.add_sized(
[text_edit, height],
Label::new(RichText::new(" XMRig Binary Path ❌").color(RED)),
@ -491,47 +433,4 @@ impl crate::disk::Gupax {
})
});
}
// Checks if a path is a valid path to a file.
pub fn path_is_file(path: &str) -> bool {
let path = path.to_string();
match crate::disk::into_absolute_path(path) {
Ok(path) => path.is_file(),
_ => false,
}
}
#[cold]
#[inline(never)]
fn spawn_file_window_thread(file_window: &Arc<Mutex<FileWindow>>, 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;
});
}
}

View file

@ -0,0 +1,167 @@
use crate::app::keys::KeyPressed;
use crate::app::Tab;
use crate::utils::constants::*;
use crate::utils::errors::{ErrorButtons, ErrorFerris};
use crate::utils::macros::lock;
use egui::*;
use log::debug;
mod gupax;
mod p2pool;
mod status;
mod xmrig;
mod xvb;
impl crate::app::App {
pub fn middle_panel(
&mut self,
ctx: &egui::Context,
frame: &mut eframe::Frame,
key: KeyPressed,
p2pool_is_alive: bool,
xmrig_is_alive: bool,
) {
// Middle panel, contents of the [Tab]
debug!("App | Rendering CENTRAL_PANEL (tab contents)");
CentralPanel::default().show(ctx, |ui| {
// This sets the Ui dimensions after Top/Bottom are filled
self.width = ui.available_width();
self.height = ui.available_height();
ui.style_mut().override_text_style = Some(TextStyle::Body);
match self.tab {
Tab::About => {
debug!("App | Entering [About] Tab");
// If [D], show some debug info with [ErrorState]
if key.is_d() {
debug!("App | Entering [Debug Info]");
#[cfg(feature = "distro")]
let distro = true;
#[cfg(not(feature = "distro"))]
let distro = false;
let p2pool_gui_len = lock!(self.p2pool_api).output.len();
let xmrig_gui_len = lock!(self.xmrig_api).output.len();
let gupax_p2pool_api = lock!(self.gupax_p2pool_api);
let debug_info = format!(
"Gupax version: {}\n
Bundled P2Pool version: {}\n
Bundled XMRig version: {}\n
Gupax uptime: {} seconds\n
Selected resolution: {}x{}\n
Internal resolution: {}x{}\n
Operating system: {}\n
Max detected threads: {}\n
Gupax PID: {}\n
State diff: {}\n
Node list length: {}\n
Pool list length: {}\n
Admin privilege: {}\n
Release build: {}\n
Debug build: {}\n
Distro build: {}\n
Build commit: {}\n
OS Data PATH: {}\n
Gupax PATH: {}\n
P2Pool PATH: {}\n
XMRig PATH: {}\n
P2Pool console byte length: {}\n
XMRig console byte length: {}\n
------------------------------------------ P2POOL IMAGE ------------------------------------------
{:#?}\n
------------------------------------------ XMRIG IMAGE ------------------------------------------
{:#?}\n
------------------------------------------ GUPAX-P2POOL API ------------------------------------------
payout: {:#?}
payout_u64: {:#?}
xmr: {:#?}
path_log: {:#?}
path_payout: {:#?}
path_xmr: {:#?}\n
------------------------------------------ WORKING STATE ------------------------------------------
{:#?}\n
------------------------------------------ ORIGINAL STATE ------------------------------------------
{:#?}",
GUPAX_VERSION,
P2POOL_VERSION,
XMRIG_VERSION,
self.now.elapsed().as_secs_f32(),
self.state.gupax.selected_width,
self.state.gupax.selected_height,
self.width,
self.height,
OS_NAME,
self.max_threads,
self.pid,
self.diff,
self.node_vec.len(),
self.pool_vec.len(),
self.admin,
!cfg!(debug_assertions),
cfg!(debug_assertions),
distro,
COMMIT,
self.os_data_path.display(),
self.exe,
self.state.gupax.absolute_p2pool_path.display(),
self.state.gupax.absolute_xmrig_path.display(),
p2pool_gui_len,
xmrig_gui_len,
lock!(self.p2pool_img),
lock!(self.xmrig_img),
gupax_p2pool_api.payout,
gupax_p2pool_api.payout_u64,
gupax_p2pool_api.xmr,
gupax_p2pool_api.path_log,
gupax_p2pool_api.path_payout,
gupax_p2pool_api.path_xmr,
self.state,
lock!(self.og),
);
self.error_state.set(debug_info, ErrorFerris::Cute, ErrorButtons::Debug);
}
let width = self.width;
let height = self.height/30.0;
let max_height = self.height;
ui.add_space(10.0);
ui.vertical_centered(|ui| {
ui.set_max_height(max_height);
// Display [Gupax] banner
let link_width = width/14.0;
ui.add_sized(Vec2::new(width, height*3.0), Image::from_bytes("bytes://banner.png", BYTES_BANNER));
ui.add_sized([width, height], Label::new("is a GUI for mining"));
ui.add_sized([link_width, height], Hyperlink::from_label_and_url("[Monero]", "https://www.github.com/monero-project/monero"));
ui.add_sized([width, height], Label::new("on"));
ui.add_sized([link_width, height], Hyperlink::from_label_and_url("[P2Pool]", "https://www.github.com/SChernykh/p2pool"));
ui.add_sized([width, height], Label::new("using"));
ui.add_sized([link_width, height], Hyperlink::from_label_and_url("[XMRig]", "https://www.github.com/xmrig/xmrig"));
ui.add_space(SPACE*2.0);
ui.add_sized([width, height], Label::new(KEYBOARD_SHORTCUTS));
ui.add_space(SPACE*2.0);
if cfg!(debug_assertions) { ui.label(format!("Gupax is running in debug mode - {}", self.now.elapsed().as_secs_f64())); }
ui.label(format!("Gupax has been running for {}", lock!(self.pub_sys).gupax_uptime));
});
}
Tab::Status => {
debug!("App | Entering [Status] Tab");
crate::disk::state::Status::show(&mut self.state.status, &self.pub_sys, &self.p2pool_api, &self.xmrig_api, &self.p2pool_img, &self.xmrig_img, p2pool_is_alive, xmrig_is_alive, self.max_threads, &self.gupax_p2pool_api, &self.benchmarks, self.width, self.height, ctx, ui);
}
Tab::Gupax => {
debug!("App | Entering [Gupax] Tab");
crate::disk::state::Gupax::show(&mut self.state.gupax, &self.og, &self.state_path, &self.update, &self.file_window, &mut self.error_state, &self.restart, self.width, self.height, frame, ctx, ui);
}
Tab::P2pool => {
debug!("App | Entering [P2Pool] Tab");
crate::disk::state::P2pool::show(&mut self.state.p2pool, &mut self.node_vec, &self.og, &self.ping, &self.p2pool, &self.p2pool_api, &mut self.p2pool_stdin, self.width, self.height, ctx, ui);
}
Tab::Xmrig => {
debug!("App | Entering [XMRig] Tab");
crate::disk::state::Xmrig::show(&mut self.state.xmrig, &mut self.pool_vec, &self.xmrig, &self.xmrig_api, &mut self.xmrig_stdin, self.width, self.height, ctx, ui);
}
Tab::Xvb => {
debug!("App | Entering [XvB] Tab");
crate::disk::state::Xvb::show(self.width, self.height, ctx, ui);
}
}
});
}
}

View file

@ -0,0 +1,305 @@
use crate::disk::node::Node;
use crate::{disk::state::P2pool, utils::regex::REGEXES};
use egui::Button;
use egui::Checkbox;
use egui::Slider;
use crate::constants::*;
use egui::{Color32, ComboBox, Label, RichText, SelectableLabel, TextStyle::*, Ui};
use log::*;
impl P2pool {
pub(super) fn advanced(
&mut self,
ui: &mut Ui,
width: f32,
height: f32,
text_edit: f32,
node_vec: &mut Vec<(String, Node)>,
) {
debug!("P2Pool Tab | Rendering [Node List] elements");
let mut incorrect_input = false; // This will disable [Add/Delete] on bad input
// [Monero node IP/RPC/ZMQ]
ui.horizontal(|ui| {
ui.group(|ui| {
let width = width/10.0;
ui.vertical(|ui| {
ui.spacing_mut().text_edit_width = width*3.32;
ui.horizontal(|ui| {
let text;
let color;
let len = format!("{:02}", self.name.len());
if self.name.is_empty() {
text = format!("Name [ {}/30 ]", len);
color = Color32::LIGHT_GRAY;
incorrect_input = true;
} else if REGEXES.name.is_match(&self.name) {
text = format!("Name [ {}/30 ]✔", len);
color = Color32::from_rgb(100, 230, 100);
} else {
text = format!("Name [ {}/30 ]❌", len);
color = Color32::from_rgb(230, 50, 50);
incorrect_input = true;
}
ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
ui.text_edit_singleline(&mut self.name).on_hover_text(P2POOL_NAME);
self.name.truncate(30);
});
ui.horizontal(|ui| {
let text;
let color;
let len = format!("{:03}", self.ip.len());
if self.ip.is_empty() {
text = format!(" IP [{}/255]", len);
color = Color32::LIGHT_GRAY;
incorrect_input = true;
} else if self.ip == "localhost" || REGEXES.ipv4.is_match(&self.ip) || REGEXES.domain.is_match(&self.ip) {
text = format!(" IP [{}/255]✔", len);
color = Color32::from_rgb(100, 230, 100);
} else {
text = format!(" IP [{}/255]❌", len);
color = Color32::from_rgb(230, 50, 50);
incorrect_input = true;
}
ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
ui.text_edit_singleline(&mut self.ip).on_hover_text(P2POOL_NODE_IP);
self.ip.truncate(255);
});
ui.horizontal(|ui| {
let text;
let color;
let len = self.rpc.len();
if self.rpc.is_empty() {
text = format!(" RPC [ {}/5 ]", len);
color = Color32::LIGHT_GRAY;
incorrect_input = true;
} else if REGEXES.port.is_match(&self.rpc) {
text = format!(" RPC [ {}/5 ]✔", len);
color = Color32::from_rgb(100, 230, 100);
} else {
text = format!(" RPC [ {}/5 ]❌", len);
color = Color32::from_rgb(230, 50, 50);
incorrect_input = true;
}
ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
ui.text_edit_singleline(&mut self.rpc).on_hover_text(P2POOL_RPC_PORT);
self.rpc.truncate(5);
});
ui.horizontal(|ui| {
let text;
let color;
let len = self.zmq.len();
if self.zmq.is_empty() {
text = format!(" ZMQ [ {}/5 ]", len);
color = Color32::LIGHT_GRAY;
incorrect_input = true;
} else if REGEXES.port.is_match(&self.zmq) {
text = format!(" ZMQ [ {}/5 ]✔", len);
color = Color32::from_rgb(100, 230, 100);
} else {
text = format!(" ZMQ [ {}/5 ]❌", len);
color = Color32::from_rgb(230, 50, 50);
incorrect_input = true;
}
ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
ui.text_edit_singleline(&mut self.zmq).on_hover_text(P2POOL_ZMQ_PORT);
self.zmq.truncate(5);
});
});
ui.vertical(|ui| {
let width = ui.available_width();
ui.add_space(1.0);
// [Manual node selection]
ui.spacing_mut().slider_width = width - 8.0;
ui.spacing_mut().icon_width = width / 25.0;
// [Ping List]
debug!("P2Pool Tab | Rendering [Node List]");
let text = RichText::new(format!("{}. {}", self.selected_index+1, self.selected_name));
ComboBox::from_id_source("manual_nodes").selected_text(text).width(width).show_ui(ui, |ui| {
for (n, (name, node)) in node_vec.iter().enumerate() {
let text = RichText::new(format!("{}. {}\n IP: {}\n RPC: {}\n ZMQ: {}", n+1, name, node.ip, node.rpc, node.zmq));
if ui.add(SelectableLabel::new(self.selected_name == *name, text)).clicked() {
self.selected_index = n;
let node = node.clone();
self.selected_name = name.clone();
self.selected_ip = node.ip.clone();
self.selected_rpc = node.rpc.clone();
self.selected_zmq = node.zmq.clone();
self.name = name.clone();
self.ip = node.ip;
self.rpc = node.rpc;
self.zmq = node.zmq;
}
}
});
// [Add/Save]
let node_vec_len = node_vec.len();
let mut exists = false;
let mut save_diff = true;
let mut existing_index = 0;
for (name, node) in node_vec.iter() {
if *name == self.name {
exists = true;
if self.ip == node.ip && self.rpc == node.rpc && self.zmq == node.zmq {
save_diff = false;
}
break
}
existing_index += 1;
}
ui.horizontal(|ui| {
let text = if exists { LIST_SAVE } else { LIST_ADD };
let text = format!("{}\n Currently selected node: {}. {}\n Current amount of nodes: {}/1000", text, self.selected_index+1, self.selected_name, node_vec_len);
// If the node already exists, show [Save] and mutate the already existing node
if exists {
ui.set_enabled(!incorrect_input && save_diff);
if ui.add_sized([width, text_edit], Button::new("Save")).on_hover_text(text).clicked() {
let node = Node {
ip: self.ip.clone(),
rpc: self.rpc.clone(),
zmq: self.zmq.clone(),
};
node_vec[existing_index].1 = node;
self.selected_index = existing_index;
self.selected_ip = self.ip.clone();
self.selected_rpc = self.rpc.clone();
self.selected_zmq = self.zmq.clone();
info!("Node | S | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", existing_index+1, self.name, self.ip, self.rpc, self.zmq);
}
// Else, add to the list
} else {
ui.set_enabled(!incorrect_input && node_vec_len < 1000);
if ui.add_sized([width, text_edit], Button::new("Add")).on_hover_text(text).clicked() {
let node = Node {
ip: self.ip.clone(),
rpc: self.rpc.clone(),
zmq: self.zmq.clone(),
};
node_vec.push((self.name.clone(), node));
self.selected_index = node_vec_len;
self.selected_name = self.name.clone();
self.selected_ip = self.ip.clone();
self.selected_rpc = self.rpc.clone();
self.selected_zmq = self.zmq.clone();
info!("Node | A | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", node_vec_len, self.name, self.ip, self.rpc, self.zmq);
}
}
});
// [Delete]
ui.horizontal(|ui| {
ui.set_enabled(node_vec_len > 1);
let text = format!("{}\n Currently selected node: {}. {}\n Current amount of nodes: {}/1000", LIST_DELETE, self.selected_index+1, self.selected_name, node_vec_len);
if ui.add_sized([width, text_edit], Button::new("Delete")).on_hover_text(text).clicked() {
let new_name;
let new_node;
match self.selected_index {
0 => {
new_name = node_vec[1].0.clone();
new_node = node_vec[1].1.clone();
node_vec.remove(0);
}
_ => {
node_vec.remove(self.selected_index);
self.selected_index -= 1;
new_name = node_vec[self.selected_index].0.clone();
new_node = node_vec[self.selected_index].1.clone();
}
};
self.selected_name = new_name.clone();
self.selected_ip = new_node.ip.clone();
self.selected_rpc = new_node.rpc.clone();
self.selected_zmq = new_node.zmq.clone();
self.name = new_name;
self.ip = new_node.ip;
self.rpc = new_node.rpc;
self.zmq = new_node.zmq;
info!("Node | D | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", self.selected_index, self.selected_name, self.selected_ip, self.selected_rpc, self.selected_zmq);
}
});
ui.horizontal(|ui| {
ui.set_enabled(!self.name.is_empty() || !self.ip.is_empty() || !self.rpc.is_empty() || !self.zmq.is_empty());
if ui.add_sized([width, text_edit], Button::new("Clear")).on_hover_text(LIST_CLEAR).clicked() {
self.name.clear();
self.ip.clear();
self.rpc.clear();
self.zmq.clear();
}
});
});
});
});
ui.add_space(5.0);
debug!("P2Pool Tab | Rendering [Main/Mini/Peers/Log] elements");
// [Main/Mini]
ui.horizontal(|ui| {
let height = height / 4.0;
ui.group(|ui| {
ui.horizontal(|ui| {
let width = (width / 4.0) - SPACE;
let height = height + 6.0;
if ui
.add_sized(
[width, height],
SelectableLabel::new(!self.mini, "P2Pool Main"),
)
.on_hover_text(P2POOL_MAIN)
.clicked()
{
self.mini = false;
}
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.mini, "P2Pool Mini"),
)
.on_hover_text(P2POOL_MINI)
.clicked()
{
self.mini = true;
}
})
});
// [Out/In Peers] + [Log Level]
ui.group(|ui| {
ui.vertical(|ui| {
let text = (ui.available_width() / 10.0) - SPACE;
let width = (text * 8.0) - SPACE;
let height = height / 3.0;
ui.style_mut().spacing.slider_width = width / 1.1;
ui.style_mut().spacing.interact_size.y = height;
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
ui.horizontal(|ui| {
ui.add_sized([text, height], Label::new("Out peers [10-450]:"));
ui.add_sized([width, height], Slider::new(&mut self.out_peers, 10..=450))
.on_hover_text(P2POOL_OUT);
ui.add_space(ui.available_width() - 4.0);
});
ui.horizontal(|ui| {
ui.add_sized([text, height], Label::new(" In peers [10-450]:"));
ui.add_sized([width, height], Slider::new(&mut self.in_peers, 10..=450))
.on_hover_text(P2POOL_IN);
});
ui.horizontal(|ui| {
ui.add_sized([text, height], Label::new(" Log level [0-6]:"));
ui.add_sized([width, height], Slider::new(&mut self.log_level, 0..=6))
.on_hover_text(P2POOL_LOG);
});
})
});
});
debug!("P2Pool Tab | Rendering Backup host button");
ui.group(|ui| {
let width = width - SPACE;
let height = ui.available_height() / 3.0;
// [Backup host]
ui.add_sized(
[width, height],
Checkbox::new(&mut self.backup_host, "Backup host"),
)
.on_hover_text(P2POOL_BACKUP_HOST_ADVANCED);
});
}
}

View file

@ -0,0 +1,166 @@
use crate::disk::node::Node;
use crate::disk::state::{P2pool, State};
use crate::helper::p2pool::PubP2poolApi;
// Gupax - GUI Uniting P2Pool And XMRig
//
// Copyright (c) 2022-2023 hinto-janai
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::{components::node::*, constants::*, helper::*, macros::*, utils::regex::Regexes};
use egui::{Color32, Label, RichText, TextEdit, TextStyle::*};
use log::*;
use std::sync::{Arc, Mutex};
mod advanced;
mod simple;
impl P2pool {
#[inline(always)] // called once
#[allow(clippy::too_many_arguments)]
pub fn show(
&mut self,
node_vec: &mut Vec<(String, Node)>,
_og: &Arc<Mutex<State>>,
ping: &Arc<Mutex<Ping>>,
process: &Arc<Mutex<Process>>,
api: &Arc<Mutex<PubP2poolApi>>,
buffer: &mut String,
width: f32,
height: f32,
_ctx: &egui::Context,
ui: &mut egui::Ui,
) {
let text_edit = height / 25.0;
//---------------------------------------------------------------------------------------------------- [Simple] Console
debug!("P2Pool Tab | Rendering [Console]");
ui.group(|ui| {
if self.simple {
let height = height / 2.8;
let width = width - SPACE;
egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| {
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.max_width(width)
.max_height(height)
.auto_shrink([false; 2])
.show_viewport(ui, |ui, _| {
ui.add_sized(
[width, height],
TextEdit::multiline(&mut lock!(api).output.as_str()),
);
});
});
//---------------------------------------------------------------------------------------------------- [Advanced] Console
} else {
let height = height / 2.8;
let width = width - SPACE;
egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| {
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.max_width(width)
.max_height(height)
.auto_shrink([false; 2])
.show_viewport(ui, |ui, _| {
ui.add_sized(
[width, height],
TextEdit::multiline(&mut lock!(api).output.as_str()),
);
});
});
ui.separator();
let response = ui
.add_sized(
[width, text_edit],
TextEdit::hint_text(
TextEdit::singleline(buffer),
r#"Type a command (e.g "help" or "status") and press Enter"#,
),
)
.on_hover_text(P2POOL_INPUT);
// If the user pressed enter, dump buffer contents into the process STDIN
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
response.request_focus(); // Get focus back
let buffer = std::mem::take(buffer); // Take buffer
let mut process = lock!(process); // Lock
if process.is_alive() {
process.input.push(buffer);
} // Push only if alive
}
}
});
//---------------------------------------------------------------------------------------------------- Args
if !self.simple {
debug!("P2Pool Tab | Rendering [Arguments]");
ui.group(|ui| {
ui.horizontal(|ui| {
let width = (width / 10.0) - SPACE;
ui.add_sized([width, text_edit], Label::new("Command arguments:"));
ui.add_sized(
[ui.available_width(), text_edit],
TextEdit::hint_text(
TextEdit::singleline(&mut self.arguments),
r#"--wallet <...> --host <...>"#,
),
)
.on_hover_text(P2POOL_ARGUMENTS);
self.arguments.truncate(1024);
})
});
ui.set_enabled(self.arguments.is_empty());
}
//---------------------------------------------------------------------------------------------------- Address
debug!("P2Pool Tab | Rendering [Address]");
ui.group(|ui| {
let width = width - SPACE;
ui.spacing_mut().text_edit_width = (width) - (SPACE * 3.0);
let text;
let color;
let len = format!("{:02}", self.address.len());
if self.address.is_empty() {
text = format!("Monero Address [{}/95] ", len);
color = Color32::LIGHT_GRAY;
} else if Regexes::addr_ok(&self.address) {
text = format!("Monero Address [{}/95] ✔", len);
color = Color32::from_rgb(100, 230, 100);
} else {
text = format!("Monero Address [{}/95] ❌", len);
color = Color32::from_rgb(230, 50, 50);
}
ui.add_sized(
[width, text_edit],
Label::new(RichText::new(text).color(color)),
);
ui.add_sized(
[width, text_edit],
TextEdit::hint_text(TextEdit::singleline(&mut self.address), "4..."),
)
.on_hover_text(P2POOL_ADDRESS);
self.address.truncate(95);
});
let height = ui.available_height();
if self.simple {
//---------------------------------------------------------------------------------------------------- Simple
self.simple(ui, width, height, ping);
//---------------------------------------------------------------------------------------------------- Advanced
} else {
self.advanced(ui, width, height, text_edit, node_vec);
}
}
}

View file

@ -0,0 +1,191 @@
use std::sync::Arc;
use std::sync::Mutex;
use crate::app::panels::middle::Hyperlink;
use crate::app::panels::middle::ProgressBar;
use crate::app::panels::middle::Spinner;
use crate::components::node::format_ip_location;
use crate::components::node::format_ms;
use crate::components::node::Ping;
use crate::components::node::RemoteNode;
use crate::disk::state::P2pool;
use crate::utils::macros::lock;
use egui::Button;
use egui::Checkbox;
use crate::constants::*;
use egui::{Color32, ComboBox, Label, RichText, Ui};
use log::*;
impl P2pool {
pub(super) fn simple(&mut self, ui: &mut Ui, width: f32, height: f32, ping: &Arc<Mutex<Ping>>) {
// [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<Mutex<Ping>>] as an intermediary here
// saves me the hassle of wrapping [state: State] completely
// and [.lock().unwrap()]ing it everywhere.
// Two atomic bools = enough to represent this data
debug!("P2Pool Tab | Running [auto-select] check");
if self.auto_select {
let mut ping = lock!(ping);
// If we haven't auto_selected yet, auto-select and turn it off
if ping.pinged && !ping.auto_selected {
self.node = ping.fastest.to_string();
ping.auto_selected = true;
}
drop(ping);
}
ui.vertical(|ui| {
ui.horizontal(|ui| {
debug!("P2Pool Tab | Rendering [Ping List]");
// [Ping List]
let mut ms = 0;
let mut color = Color32::LIGHT_GRAY;
if lock!(ping).pinged {
for data in lock!(ping).nodes.iter() {
if data.ip == self.node {
ms = data.ms;
color = data.color;
break;
}
}
}
debug!("P2Pool Tab | Rendering [ComboBox] of Remote Nodes");
let ip_location = format_ip_location(&self.node, false);
let text = RichText::new(format!("{}ms | {}", ms, ip_location)).color(color);
ComboBox::from_id_source("remote_nodes")
.selected_text(text)
.width(width)
.show_ui(ui, |ui| {
for data in lock!(ping).nodes.iter() {
let ms = format_ms(data.ms);
let ip_location = format_ip_location(data.ip, true);
let text = RichText::new(format!("{} | {}", ms, ip_location))
.color(data.color);
ui.selectable_value(&mut self.node, data.ip.to_string(), text);
}
});
});
ui.add_space(5.0);
debug!("P2Pool Tab | Rendering [Select fastest ... Ping] buttons");
ui.horizontal(|ui| {
let width = (width / 5.0) - 6.0;
// [Select random node]
if ui
.add_sized([width, height], Button::new("Select random node"))
.on_hover_text(P2POOL_SELECT_RANDOM)
.clicked()
{
self.node = RemoteNode::get_random(&self.node);
}
// [Select fastest node]
if ui
.add_sized([width, height], Button::new("Select fastest node"))
.on_hover_text(P2POOL_SELECT_FASTEST)
.clicked()
&& lock!(ping).pinged
{
self.node = lock!(ping).fastest.to_string();
}
// [Ping Button]
ui.add_enabled_ui(!lock!(ping).pinging, |ui| {
if ui
.add_sized([width, height], Button::new("Ping remote nodes"))
.on_hover_text(P2POOL_PING)
.clicked()
{
Ping::spawn_thread(ping);
}
});
// [Last <-]
if ui
.add_sized([width, height], Button::new("⬅ Last"))
.on_hover_text(P2POOL_SELECT_LAST)
.clicked()
{
let ping = lock!(ping);
match ping.pinged {
true => self.node = RemoteNode::get_last_from_ping(&self.node, &ping.nodes),
false => self.node = RemoteNode::get_last(&self.node),
}
drop(ping);
}
// [Next ->]
if ui
.add_sized([width, height], Button::new("Next ➡"))
.on_hover_text(P2POOL_SELECT_NEXT)
.clicked()
{
let ping = lock!(ping);
match ping.pinged {
true => self.node = RemoteNode::get_next_from_ping(&self.node, &ping.nodes),
false => self.node = RemoteNode::get_next(&self.node),
}
drop(ping);
}
});
ui.vertical(|ui| {
let height = height / 2.0;
let pinging = lock!(ping).pinging;
ui.set_enabled(pinging);
let prog = lock!(ping).prog.round();
let msg = RichText::new(format!("{} ... {}%", lock!(ping).msg, prog));
let height = height / 1.25;
ui.add_space(5.0);
ui.add_sized([width, height], Label::new(msg));
ui.add_space(5.0);
if pinging {
ui.add_sized([width, height], Spinner::new().size(height));
} else {
ui.add_sized([width, height], Label::new("..."));
}
ui.add_sized([width, height], ProgressBar::new(prog.round() / 100.0));
ui.add_space(5.0);
});
});
debug!("P2Pool Tab | Rendering [Auto-*] buttons");
ui.group(|ui| {
ui.horizontal(|ui| {
let width = (width / 3.0) - (SPACE * 1.75);
// [Auto-node]
ui.add_sized(
[width, height],
Checkbox::new(&mut self.auto_select, "Auto-select"),
)
.on_hover_text(P2POOL_AUTO_SELECT);
ui.separator();
// [Auto-node]
ui.add_sized(
[width, height],
Checkbox::new(&mut self.auto_ping, "Auto-ping"),
)
.on_hover_text(P2POOL_AUTO_NODE);
ui.separator();
// [Backup host]
ui.add_sized(
[width, height],
Checkbox::new(&mut self.backup_host, "Backup host"),
)
.on_hover_text(P2POOL_BACKUP_HOST_SIMPLE);
})
});
debug!("P2Pool Tab | Rendering warning text");
ui.add_sized(
[width, height / 2.0],
Hyperlink::from_label_and_url(
"WARNING: It is recommended to run/use your own Monero Node (hover for details)",
"https://github.com/hinto-janai/gupax#running-a-local-monero-node",
),
)
.on_hover_text(P2POOL_COMMUNITY_NODE_WARNING);
}
}

View file

@ -0,0 +1,221 @@
use std::sync::{Arc, Mutex};
use crate::{
app::Benchmark, disk::state::Status, helper::xmrig::PubXmrigApi, utils::human::HumanNumber,
};
use egui::{Hyperlink, ProgressBar, Spinner};
use crate::utils::macros::lock;
use crate::constants::*;
use egui::{Label, RichText};
use log::*;
impl Status {
pub(super) fn benchmarks(
&mut self,
width: f32,
height: f32,
ui: &mut egui::Ui,
benchmarks: &[Benchmark],
xmrig_alive: bool,
xmrig_api: &Arc<Mutex<PubXmrigApi>>,
) {
debug!("Status Tab | Rendering [Benchmarks]");
let text = height / 20.0;
let double = text * 2.0;
let log = height / 3.0;
// [0], The user's CPU (most likely).
let cpu = &benchmarks[0];
ui.horizontal(|ui| {
let width = (width / 2.0) - (SPACE * 1.666);
let min_height = log;
ui.group(|ui| {
ui.vertical(|ui| {
ui.set_min_height(min_height);
ui.add_sized(
[width, text],
Label::new(RichText::new("Your CPU").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_CPU);
ui.add_sized([width, text], Label::new(cpu.cpu.as_str()));
ui.add_sized(
[width, text],
Label::new(RichText::new("Total Benchmarks").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_BENCHMARKS);
ui.add_sized([width, text], Label::new(format!("{}", cpu.benchmarks)));
ui.add_sized(
[width, text],
Label::new(RichText::new("Rank").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_RANK);
ui.add_sized(
[width, text],
Label::new(format!("{}/{}", cpu.rank, &benchmarks.len())),
);
})
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.set_min_height(min_height);
ui.add_sized(
[width, text],
Label::new(RichText::new("High Hashrate").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_HIGH);
ui.add_sized(
[width, text],
Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.high))),
);
ui.add_sized(
[width, text],
Label::new(RichText::new("Average Hashrate").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_AVERAGE);
ui.add_sized(
[width, text],
Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.average))),
);
ui.add_sized(
[width, text],
Label::new(RichText::new("Low Hashrate").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_LOW);
ui.add_sized(
[width, text],
Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.low))),
);
})
})
});
// User's CPU hashrate comparison (if XMRig is alive).
ui.scope(|ui| {
if xmrig_alive {
let api = lock!(xmrig_api);
let percent = (api.hashrate_raw / cpu.high) * 100.0;
let human = HumanNumber::to_percent(percent);
if percent > 100.0 {
ui.add_sized(
[width, double],
Label::new(format!(
"Your CPU's is faster than the highest benchmark! It is [{}] faster @ {}!",
human, api.hashrate
)),
);
ui.add_sized([width, text], ProgressBar::new(1.0));
} else if api.hashrate_raw == 0.0 {
ui.add_sized([width, text], Label::new("Measuring hashrate..."));
ui.add_sized([width, text], Spinner::new().size(text));
ui.add_sized([width, text], ProgressBar::new(0.0));
} else {
ui.add_sized(
[width, double],
Label::new(format!(
"Your CPU's hashrate is [{}] of the highest benchmark @ {}",
human, api.hashrate
)),
);
ui.add_sized([width, text], ProgressBar::new(percent / 100.0));
}
} else {
ui.set_enabled(xmrig_alive);
ui.add_sized(
[width, double],
Label::new("XMRig is offline. Hashrate cannot be determined."),
);
ui.add_sized([width, text], ProgressBar::new(0.0));
}
});
// Comparison
ui.group(|ui| {
ui.add_sized(
[width, text],
Hyperlink::from_label_and_url("Other CPUs", "https://xmrig.com/benchmark"),
)
.on_hover_text(STATUS_SUBMENU_OTHER_CPUS);
});
egui::ScrollArea::both()
.scroll_bar_visibility(
egui::containers::scroll_area::ScrollBarVisibility::AlwaysVisible,
)
.max_width(width)
.max_height(height)
.auto_shrink([false; 2])
.show_viewport(ui, |ui, _| {
let width = width / 20.0;
let (cpu, bar, high, average, low, rank, bench) = (
width * 10.0,
width * 3.0,
width * 2.0,
width * 2.0,
width * 2.0,
width,
width * 2.0,
);
ui.group(|ui| {
ui.horizontal(|ui| {
ui.add_sized([cpu, double], Label::new("CPU"))
.on_hover_text(STATUS_SUBMENU_OTHER_CPU);
ui.separator();
ui.add_sized([bar, double], Label::new("Relative"))
.on_hover_text(STATUS_SUBMENU_OTHER_RELATIVE);
ui.separator();
ui.add_sized([high, double], Label::new("High"))
.on_hover_text(STATUS_SUBMENU_OTHER_HIGH);
ui.separator();
ui.add_sized([average, double], Label::new("Average"))
.on_hover_text(STATUS_SUBMENU_OTHER_AVERAGE);
ui.separator();
ui.add_sized([low, double], Label::new("Low"))
.on_hover_text(STATUS_SUBMENU_OTHER_LOW);
ui.separator();
ui.add_sized([rank, double], Label::new("Rank"))
.on_hover_text(STATUS_SUBMENU_OTHER_RANK);
ui.separator();
ui.add_sized([bench, double], Label::new("Benchmarks"))
.on_hover_text(STATUS_SUBMENU_OTHER_BENCHMARKS);
});
});
for benchmark in benchmarks[1..].iter() {
ui.group(|ui| {
ui.horizontal(|ui| {
ui.add_sized([cpu, text], Label::new(benchmark.cpu.as_str()));
ui.separator();
ui.add_sized([bar, text], ProgressBar::new(benchmark.percent / 100.0))
.on_hover_text(HumanNumber::to_percent(benchmark.percent).as_str());
ui.separator();
ui.add_sized(
[high, text],
Label::new(HumanNumber::to_hashrate(benchmark.high).as_str()),
);
ui.separator();
ui.add_sized(
[average, text],
Label::new(HumanNumber::to_hashrate(benchmark.average).as_str()),
);
ui.separator();
ui.add_sized(
[low, text],
Label::new(HumanNumber::to_hashrate(benchmark.low).as_str()),
);
ui.separator();
ui.add_sized(
[rank, text],
Label::new(HumanNumber::from_u16(benchmark.rank).as_str()),
);
ui.separator();
ui.add_sized(
[bench, text],
Label::new(HumanNumber::from_u16(benchmark.benchmarks).as_str()),
);
})
});
}
});
}
}

View file

@ -0,0 +1,83 @@
// Gupax - GUI Uniting P2Pool And XMRig
//
// Copyright (c) 2022-2023 hinto-janai
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::{
app::Benchmark,
disk::{gupax_p2pool_api::GupaxP2poolApi, state::Status, status::*},
helper::{
p2pool::{ImgP2pool, PubP2poolApi},
xmrig::{ImgXmrig, PubXmrigApi},
Sys,
},
};
use std::sync::{Arc, Mutex};
mod benchmarks;
mod p2pool;
mod processes;
impl Status {
#[inline(always)] // called once
#[allow(clippy::too_many_arguments)]
pub fn show(
&mut self,
sys: &Arc<Mutex<Sys>>,
p2pool_api: &Arc<Mutex<PubP2poolApi>>,
xmrig_api: &Arc<Mutex<PubXmrigApi>>,
p2pool_img: &Arc<Mutex<ImgP2pool>>,
xmrig_img: &Arc<Mutex<ImgXmrig>>,
p2pool_alive: bool,
xmrig_alive: bool,
max_threads: usize,
gupax_p2pool_api: &Arc<Mutex<GupaxP2poolApi>>,
benchmarks: &[Benchmark],
width: f32,
height: f32,
_ctx: &egui::Context,
ui: &mut egui::Ui,
) {
//---------------------------------------------------------------------------------------------------- [Processes]
if self.submenu == Submenu::Processes {
self.processes(
sys,
width,
height,
ui,
p2pool_alive,
p2pool_api,
xmrig_alive,
xmrig_api,
p2pool_img,
xmrig_img,
max_threads,
);
//---------------------------------------------------------------------------------------------------- [P2Pool]
} else if self.submenu == Submenu::P2pool {
self.p2pool(
width,
height,
ui,
gupax_p2pool_api,
p2pool_alive,
p2pool_api,
);
//---------------------------------------------------------------------------------------------------- [Benchmarks]
} else if self.submenu == Submenu::Benchmarks {
self.benchmarks(width, height, ui, benchmarks, xmrig_alive, xmrig_api)
}
}
}

View file

@ -0,0 +1,427 @@
use std::sync::{Arc, Mutex};
use egui::{Label, RichText, SelectableLabel, Slider, TextEdit};
use crate::{
disk::{
gupax_p2pool_api::GupaxP2poolApi,
state::Status,
status::{Hash, PayoutView},
},
helper::p2pool::PubP2poolApi,
utils::{constants::*, human::HumanNumber, macros::lock},
};
impl Status {
pub fn p2pool(
&mut self,
width: f32,
height: f32,
ui: &mut egui::Ui,
gupax_p2pool_api: &Arc<Mutex<GupaxP2poolApi>>,
p2pool_alive: bool,
p2pool_api: &Arc<Mutex<PubP2poolApi>>,
) {
let api = lock!(gupax_p2pool_api);
let text = height / 25.0;
let log = height / 2.8;
// Payout Text + PayoutView buttons
ui.group(|ui| {
ui.horizontal(|ui| {
let width = (width / 3.0) - (SPACE * 4.0);
ui.add_sized(
[width, text],
Label::new(
RichText::new(format!("Total Payouts: {}", api.payout))
.underline()
.color(LIGHT_GRAY),
),
)
.on_hover_text(STATUS_SUBMENU_PAYOUT);
ui.separator();
ui.add_sized(
[width, text],
Label::new(
RichText::new(format!("Total XMR: {}", api.xmr))
.underline()
.color(LIGHT_GRAY),
),
)
.on_hover_text(STATUS_SUBMENU_XMR);
let width = width / 4.0;
ui.separator();
if ui
.add_sized(
[width, text],
SelectableLabel::new(self.payout_view == PayoutView::Latest, "Latest"),
)
.on_hover_text(STATUS_SUBMENU_LATEST)
.clicked()
{
self.payout_view = PayoutView::Latest;
}
ui.separator();
if ui
.add_sized(
[width, text],
SelectableLabel::new(self.payout_view == PayoutView::Oldest, "Oldest"),
)
.on_hover_text(STATUS_SUBMENU_OLDEST)
.clicked()
{
self.payout_view = PayoutView::Oldest;
}
ui.separator();
if ui
.add_sized(
[width, text],
SelectableLabel::new(self.payout_view == PayoutView::Biggest, "Biggest"),
)
.on_hover_text(STATUS_SUBMENU_BIGGEST)
.clicked()
{
self.payout_view = PayoutView::Biggest;
}
ui.separator();
if ui
.add_sized(
[width, text],
SelectableLabel::new(self.payout_view == PayoutView::Smallest, "Smallest"),
)
.on_hover_text(STATUS_SUBMENU_SMALLEST)
.clicked()
{
self.payout_view = PayoutView::Smallest;
}
});
ui.separator();
// Actual logs
egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| {
egui::ScrollArea::vertical()
.stick_to_bottom(self.payout_view == PayoutView::Oldest)
.max_width(width)
.max_height(log)
.auto_shrink([false; 2])
.show_viewport(ui, |ui, _| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Name("MonospaceLarge".into()));
match self.payout_view {
PayoutView::Latest => ui.add_sized(
[width, log],
TextEdit::multiline(&mut api.log_rev.as_str()),
),
PayoutView::Oldest => ui.add_sized(
[width, log],
TextEdit::multiline(&mut api.log.as_str()),
),
PayoutView::Biggest => ui.add_sized(
[width, log],
TextEdit::multiline(&mut api.payout_high.as_str()),
),
PayoutView::Smallest => ui.add_sized(
[width, log],
TextEdit::multiline(&mut api.payout_low.as_str()),
),
};
});
});
});
drop(api);
// Payout/Share Calculator
let button = (width / 20.0) - (SPACE * 1.666);
ui.group(|ui| {
ui.horizontal(|ui| {
ui.set_min_width(width - SPACE);
if ui
.add_sized(
[button * 2.0, text],
SelectableLabel::new(!self.manual_hash, "Automatic"),
)
.on_hover_text(STATUS_SUBMENU_AUTOMATIC)
.clicked()
{
self.manual_hash = false;
}
ui.separator();
if ui
.add_sized(
[button * 2.0, text],
SelectableLabel::new(self.manual_hash, "Manual"),
)
.on_hover_text(STATUS_SUBMENU_MANUAL)
.clicked()
{
self.manual_hash = true;
}
ui.separator();
ui.set_enabled(self.manual_hash);
if ui
.add_sized(
[button, text],
SelectableLabel::new(self.hash_metric == Hash::Hash, "Hash"),
)
.on_hover_text(STATUS_SUBMENU_HASH)
.clicked()
{
self.hash_metric = Hash::Hash;
}
ui.separator();
if ui
.add_sized(
[button, text],
SelectableLabel::new(self.hash_metric == Hash::Kilo, "Kilo"),
)
.on_hover_text(STATUS_SUBMENU_KILO)
.clicked()
{
self.hash_metric = Hash::Kilo;
}
ui.separator();
if ui
.add_sized(
[button, text],
SelectableLabel::new(self.hash_metric == Hash::Mega, "Mega"),
)
.on_hover_text(STATUS_SUBMENU_MEGA)
.clicked()
{
self.hash_metric = Hash::Mega;
}
ui.separator();
if ui
.add_sized(
[button, text],
SelectableLabel::new(self.hash_metric == Hash::Giga, "Giga"),
)
.on_hover_text(STATUS_SUBMENU_GIGA)
.clicked()
{
self.hash_metric = Hash::Giga;
}
ui.separator();
ui.spacing_mut().slider_width = button * 11.5;
ui.add_sized(
[button * 14.0, text],
Slider::new(&mut self.hashrate, 1.0..=1_000.0),
);
})
});
// Actual stats
ui.set_enabled(p2pool_alive);
let text = height / 25.0;
let width = (width / 3.0) - (SPACE * 1.666);
let min_height = ui.available_height() / 1.3;
let api = lock!(p2pool_api);
ui.horizontal(|ui| {
ui.group(|ui| {
ui.vertical(|ui| {
ui.set_min_height(min_height);
ui.add_sized(
[width, text],
Label::new(RichText::new("Monero Difficulty").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_MONERO_DIFFICULTY);
ui.add_sized([width, text], Label::new(api.monero_difficulty.as_str()));
ui.add_sized(
[width, text],
Label::new(RichText::new("Monero Hashrate").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_MONERO_HASHRATE);
ui.add_sized([width, text], Label::new(api.monero_hashrate.as_str()));
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Difficulty").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_DIFFICULTY);
ui.add_sized([width, text], Label::new(api.p2pool_difficulty.as_str()));
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Hashrate").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_HASHRATE);
ui.add_sized([width, text], Label::new(api.p2pool_hashrate.as_str()));
})
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.set_min_height(min_height);
if self.manual_hash {
let hashrate =
Hash::convert_to_hash(self.hashrate, self.hash_metric) as u64;
let p2pool_share_mean = PubP2poolApi::calculate_share_or_block_time(
hashrate,
api.p2pool_difficulty_u64,
);
let solo_block_mean = PubP2poolApi::calculate_share_or_block_time(
hashrate,
api.monero_difficulty_u64,
);
ui.add_sized(
[width, text],
Label::new(
RichText::new("Manually Inputted Hashrate")
.underline()
.color(BONE),
),
);
ui.add_sized(
[width, text],
Label::new(format!("{} H/s", HumanNumber::from_u64(hashrate))),
);
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Block Mean").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_BLOCK_MEAN);
ui.add_sized([width, text], Label::new(api.p2pool_block_mean.to_string()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your P2Pool Share Mean")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_SHARE_MEAN);
ui.add_sized([width, text], Label::new(p2pool_share_mean.to_string()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your Solo Block Mean")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_SOLO_BLOCK_MEAN);
ui.add_sized([width, text], Label::new(solo_block_mean.to_string()));
} else {
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your P2Pool Hashrate")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_HASHRATE);
ui.add_sized(
[width, text],
Label::new(format!("{} H/s", api.hashrate_1h)),
);
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Block Mean").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_BLOCK_MEAN);
ui.add_sized([width, text], Label::new(api.p2pool_block_mean.to_string()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your P2Pool Share Mean")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_SHARE_MEAN);
ui.add_sized([width, text], Label::new(api.p2pool_share_mean.to_string()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your Solo Block Mean")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_SOLO_BLOCK_MEAN);
ui.add_sized([width, text], Label::new(api.solo_block_mean.to_string()));
}
})
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.set_min_height(min_height);
if self.manual_hash {
let hashrate =
Hash::convert_to_hash(self.hashrate, self.hash_metric) as u64;
let user_p2pool_percent =
PubP2poolApi::calculate_dominance(hashrate, api.p2pool_hashrate_u64);
let user_monero_percent =
PubP2poolApi::calculate_dominance(hashrate, api.monero_hashrate_u64);
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Miners").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_MINERS);
ui.add_sized([width, text], Label::new(api.miners.as_str()));
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Dominance").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_DOMINANCE);
ui.add_sized([width, text], Label::new(api.p2pool_percent.as_str()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your P2Pool Dominance")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_DOMINANCE);
ui.add_sized([width, text], Label::new(user_p2pool_percent.as_str()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your Monero Dominance")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_YOUR_MONERO_DOMINANCE);
ui.add_sized([width, text], Label::new(user_monero_percent.as_str()));
} else {
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Miners").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_MINERS);
ui.add_sized([width, text], Label::new(api.miners.as_str()));
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Dominance").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_DOMINANCE);
ui.add_sized([width, text], Label::new(api.p2pool_percent.as_str()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your P2Pool Dominance")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_DOMINANCE);
ui.add_sized([width, text], Label::new(api.user_p2pool_percent.as_str()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your Monero Dominance")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_YOUR_MONERO_DOMINANCE);
ui.add_sized([width, text], Label::new(api.user_monero_percent.as_str()));
}
})
});
});
// Tick bar
ui.add_sized(
[ui.available_width(), text],
Label::new(api.calculate_tick_bar()),
)
.on_hover_text(STATUS_SUBMENU_PROGRESS_BAR);
drop(api);
}
}

View file

@ -0,0 +1,303 @@
use std::sync::{Arc, Mutex};
use crate::disk::state::Status;
use crate::helper::p2pool::{ImgP2pool, PubP2poolApi};
use crate::helper::xmrig::{ImgXmrig, PubXmrigApi};
use crate::helper::Sys;
use crate::utils::macros::lock;
use egui::TextStyle;
use crate::constants::*;
use egui::{Label, RichText, TextStyle::*};
use log::*;
impl Status {
#[allow(clippy::too_many_arguments)]
pub(super) fn processes(
&mut self,
sys: &Arc<Mutex<Sys>>,
width: f32,
height: f32,
ui: &mut egui::Ui,
p2pool_alive: bool,
p2pool_api: &Arc<Mutex<PubP2poolApi>>,
xmrig_alive: bool,
xmrig_api: &Arc<Mutex<PubXmrigApi>>,
p2pool_img: &Arc<Mutex<ImgP2pool>>,
xmrig_img: &Arc<Mutex<ImgXmrig>>,
max_threads: usize,
) {
let width = (width / 3.0) - (SPACE * 1.666);
let min_height = height - SPACE;
let height = height / 25.0;
ui.horizontal(|ui| {
// [Gupax]
ui.group(|ui| {
ui.vertical(|ui| {
debug!("Status Tab | Rendering [Gupax]");
ui.set_min_height(min_height);
ui.add_sized(
[width, height],
Label::new(
RichText::new("[Gupax]")
.color(LIGHT_GRAY)
.text_style(TextStyle::Name("MonospaceLarge".into())),
),
)
.on_hover_text("Gupax is online");
let sys = lock!(sys);
ui.add_sized(
[width, height],
Label::new(RichText::new("Uptime").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_UPTIME);
ui.add_sized([width, height], Label::new(sys.gupax_uptime.to_string()));
ui.add_sized(
[width, height],
Label::new(RichText::new("Gupax CPU").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_CPU_USAGE);
ui.add_sized([width, height], Label::new(sys.gupax_cpu_usage.to_string()));
ui.add_sized(
[width, height],
Label::new(RichText::new("Gupax Memory").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_MEMORY_USAGE);
ui.add_sized(
[width, height],
Label::new(sys.gupax_memory_used_mb.to_string()),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("System CPU").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_SYSTEM_CPU_USAGE);
ui.add_sized(
[width, height],
Label::new(sys.system_cpu_usage.to_string()),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("System Memory").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_SYSTEM_MEMORY);
ui.add_sized([width, height], Label::new(sys.system_memory.to_string()));
ui.add_sized(
[width, height],
Label::new(RichText::new("System CPU Model").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_SYSTEM_CPU_MODEL);
ui.add_sized(
[width, height],
Label::new(sys.system_cpu_model.to_string()),
);
drop(sys);
})
});
// [P2Pool]
ui.group(|ui| {
ui.vertical(|ui| {
debug!("Status Tab | Rendering [P2Pool]");
ui.set_enabled(p2pool_alive);
ui.set_min_height(min_height);
ui.add_sized(
[width, height],
Label::new(
RichText::new("[P2Pool]")
.color(LIGHT_GRAY)
.text_style(TextStyle::Name("MonospaceLarge".into())),
),
)
.on_hover_text("P2Pool is online")
.on_disabled_hover_text("P2Pool is offline");
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
let height = height / 1.4;
let api = lock!(p2pool_api);
ui.add_sized(
[width, height],
Label::new(RichText::new("Uptime").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_UPTIME);
ui.add_sized([width, height], Label::new(format!("{}", api.uptime)));
ui.add_sized(
[width, height],
Label::new(RichText::new("Shares Found").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_SHARES);
ui.add_sized([width, height], Label::new(format!("{}", api.shares_found)));
ui.add_sized(
[width, height],
Label::new(RichText::new("Payouts").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_PAYOUTS);
ui.add_sized(
[width, height],
Label::new(format!("Total: {}", api.payouts)),
);
ui.add_sized(
[width, height],
Label::new(format!(
"[{:.7}/hour]\n[{:.7}/day]\n[{:.7}/month]",
api.payouts_hour, api.payouts_day, api.payouts_month
)),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("XMR Mined").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_XMR);
ui.add_sized(
[width, height],
Label::new(format!("Total: {:.13} XMR", api.xmr)),
);
ui.add_sized(
[width, height],
Label::new(format!(
"[{:.7}/hour]\n[{:.7}/day]\n[{:.7}/month]",
api.xmr_hour, api.xmr_day, api.xmr_month
)),
);
ui.add_sized(
[width, height],
Label::new(
RichText::new("Hashrate (15m/1h/24h)")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_P2POOL_HASHRATE);
ui.add_sized(
[width, height],
Label::new(format!(
"[{} H/s] [{} H/s] [{} H/s]",
api.hashrate_15m, api.hashrate_1h, api.hashrate_24h
)),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("Miners Connected").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_CONNECTIONS);
ui.add_sized([width, height], Label::new(format!("{}", api.connections)));
ui.add_sized(
[width, height],
Label::new(RichText::new("Effort").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_EFFORT);
ui.add_sized(
[width, height],
Label::new(format!(
"[Average: {}] [Current: {}]",
api.average_effort, api.current_effort
)),
);
let img = lock!(p2pool_img);
ui.add_sized(
[width, height],
Label::new(RichText::new("Monero Node").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_MONERO_NODE);
ui.add_sized(
[width, height],
Label::new(format!(
"[IP: {}]\n[RPC: {}] [ZMQ: {}]",
&img.host, &img.rpc, &img.zmq
)),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("Sidechain").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_POOL);
ui.add_sized([width, height], Label::new(&img.mini));
ui.add_sized(
[width, height],
Label::new(RichText::new("Address").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_ADDRESS);
ui.add_sized([width, height], Label::new(&img.address));
drop(img);
drop(api);
})
});
// [XMRig]
ui.group(|ui| {
ui.vertical(|ui| {
debug!("Status Tab | Rendering [XMRig]");
ui.set_enabled(xmrig_alive);
ui.set_min_height(min_height);
ui.add_sized(
[width, height],
Label::new(
RichText::new("[XMRig]")
.color(LIGHT_GRAY)
.text_style(TextStyle::Name("MonospaceLarge".into())),
),
)
.on_hover_text("XMRig is online")
.on_disabled_hover_text("XMRig is offline");
let api = lock!(xmrig_api);
ui.add_sized(
[width, height],
Label::new(RichText::new("Uptime").underline().color(BONE)),
)
.on_hover_text(STATUS_XMRIG_UPTIME);
ui.add_sized([width, height], Label::new(format!("{}", api.uptime)));
ui.add_sized(
[width, height],
Label::new(
RichText::new("CPU Load (10s/60s/15m)")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_XMRIG_CPU);
ui.add_sized([width, height], Label::new(format!("{}", api.resources)));
ui.add_sized(
[width, height],
Label::new(
RichText::new("Hashrate (10s/60s/15m)")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_XMRIG_HASHRATE);
ui.add_sized([width, height], Label::new(format!("{}", api.hashrate)));
ui.add_sized(
[width, height],
Label::new(RichText::new("Difficulty").underline().color(BONE)),
)
.on_hover_text(STATUS_XMRIG_DIFFICULTY);
ui.add_sized([width, height], Label::new(format!("{}", api.diff)));
ui.add_sized(
[width, height],
Label::new(RichText::new("Shares").underline().color(BONE)),
)
.on_hover_text(STATUS_XMRIG_SHARES);
ui.add_sized(
[width, height],
Label::new(format!(
"[Accepted: {}] [Rejected: {}]",
api.accepted, api.rejected
)),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("Pool").underline().color(BONE)),
)
.on_hover_text(STATUS_XMRIG_POOL);
ui.add_sized([width, height], Label::new(&lock!(xmrig_img).url));
ui.add_sized(
[width, height],
Label::new(RichText::new("Threads").underline().color(BONE)),
)
.on_hover_text(STATUS_XMRIG_THREADS);
ui.add_sized(
[width, height],
Label::new(format!("{}/{}", &lock!(xmrig_img).threads, max_threads)),
);
drop(api);
})
});
});
}
}

View file

@ -15,8 +15,13 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::disk::pool::Pool;
use crate::disk::state::Xmrig;
use crate::helper::xmrig::PubXmrigApi;
use crate::helper::Process;
use crate::regex::REGEXES;
use crate::{constants::*, disk::*, macros::*, Process, PubXmrigApi, Regexes};
use crate::utils::regex::Regexes;
use crate::{constants::*, macros::*};
use egui::{
Button, Checkbox, ComboBox, Label, RichText, SelectableLabel, Slider, TextEdit, TextStyle::*,
};
@ -24,7 +29,7 @@ use log::*;
use std::sync::{Arc, Mutex};
impl crate::disk::Xmrig {
impl Xmrig {
#[inline(always)] // called once
#[allow(clippy::too_many_arguments)]
pub fn show(

View file

@ -2,7 +2,7 @@ use egui::{Hyperlink, Image};
use crate::constants::{BYTES_XVB, SPACE};
impl crate::disk::Xvb {
impl crate::disk::state::Xvb {
#[inline(always)] // called once
pub fn show(width: f32, height: f32, _ctx: &egui::Context, ui: &mut egui::Ui) {
let website_height = height / 10.0;

4
src/app/panels/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod bottom;
pub mod middle;
pub mod quit_error;
pub mod top;

View file

@ -0,0 +1,362 @@
use std::process::exit;
use crate::app::keys::KeyPressed;
use crate::disk::node::Node;
use crate::disk::state::State;
use crate::utils::constants::*;
use crate::utils::errors::ErrorState;
use crate::utils::ferris::*;
use crate::utils::macros::{arc_mut, flip, lock, lock2};
use crate::utils::resets::{reset_nodes, reset_state};
use crate::utils::sudo::SudoState;
use egui::TextStyle::Name;
use egui::*;
impl crate::app::App {
pub(in crate::app) fn quit_error_panel(
&mut self,
ctx: &egui::Context,
p2pool_is_alive: bool,
xmrig_is_alive: bool,
key: &KeyPressed,
) {
CentralPanel::default().show(ctx, |ui| {
ui.vertical_centered(|ui| {
// Set width/height/font
let width = self.width;
let height = self.height / 4.0;
ui.style_mut().override_text_style = Some(Name("MonospaceLarge".into()));
// Display ferris
use crate::utils::errors::ErrorButtons;
use crate::utils::errors::ErrorButtons::*;
use crate::utils::errors::ErrorFerris;
use crate::utils::errors::ErrorFerris::*;
let ferris = match self.error_state.ferris {
Happy => Image::from_bytes("bytes://happy.png", FERRIS_HAPPY),
Cute => Image::from_bytes("bytes://cute.png", FERRIS_CUTE),
Oops => Image::from_bytes("bytes://oops.png", FERRIS_OOPS),
Error => Image::from_bytes("bytes://error.png", FERRIS_ERROR),
Panic => Image::from_bytes("bytes://panic.png", FERRIS_PANIC),
ErrorFerris::Sudo => Image::from_bytes("bytes://panic.png", FERRIS_SUDO),
};
match self.error_state.buttons {
ErrorButtons::Debug => ui.add_sized(
[width, height / 4.0],
Label::new("--- Debug Info ---\n\nPress [ESC] to quit"),
),
_ => ui.add_sized(Vec2::new(width, height), ferris),
};
// Error/Quit screen
match self.error_state.buttons {
StayQuit => {
let mut text = "".to_string();
if *lock2!(self.update, updating) {
text = format!(
"{}\nUpdate is in progress...! Quitting may cause file corruption!",
text
);
}
if p2pool_is_alive {
text = format!("{}\nP2Pool is online...!", text);
}
if xmrig_is_alive {
text = format!("{}\nXMRig is online...!", text);
}
ui.add_sized(
[width, height],
Label::new("--- Are you sure you want to quit? ---"),
);
ui.add_sized([width, height], Label::new(text))
}
ResetState => {
ui.add_sized(
[width, height],
Label::new(format!(
"--- Gupax has encountered an error! ---\n{}",
&self.error_state.msg
)),
);
ui.add_sized(
[width, height],
Label::new("Reset Gupax state? (Your settings)"),
)
}
ResetNode => {
ui.add_sized(
[width, height],
Label::new(format!(
"--- Gupax has encountered an error! ---\n{}",
&self.error_state.msg
)),
);
ui.add_sized([width, height], Label::new("Reset the manual node list?"))
}
ErrorButtons::Sudo => {
let text = format!(
"Why does XMRig need admin privilege?\n{}",
XMRIG_ADMIN_REASON
);
let height = height / 4.0;
ui.add_sized(
[width, height],
Label::new(format!(
"--- Gupax needs sudo/admin privilege for XMRig! ---\n{}",
&self.error_state.msg
)),
);
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
ui.add_sized([width / 2.0, height], Label::new(text));
ui.add_sized(
[width, height],
Hyperlink::from_label_and_url(
"Click here for more info.",
"https://xmrig.com/docs/miner/randomx-optimization-guide",
),
)
}
Debug => {
egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| {
let width = ui.available_width();
let height = ui.available_height();
egui::ScrollArea::vertical()
.max_width(width)
.max_height(height)
.auto_shrink([false; 2])
.show_viewport(ui, |ui, _| {
ui.add_sized(
[width - 20.0, height],
TextEdit::multiline(&mut self.error_state.msg.as_str()),
);
});
});
ui.label("")
}
_ => {
match self.error_state.ferris {
Panic => ui.add_sized(
[width, height],
Label::new("--- Gupax has encountered an unrecoverable error! ---"),
),
Happy => ui.add_sized([width, height], Label::new("--- Success! ---")),
_ => ui.add_sized(
[width, height],
Label::new("--- Gupax has encountered an error! ---"),
),
};
let height = height / 2.0;
// Show GitHub rant link for Windows admin problems.
if cfg!(windows) && self.error_state.buttons == ErrorButtons::WindowsAdmin {
ui.add_sized([width, height], Hyperlink::from_label_and_url(
"[Why does Gupax need to be Admin? (on Windows)]",
"https://github.com/hinto-janai/gupax/tree/main/src#why-does-gupax-need-to-be-admin-on-windows"
));
ui.add_sized([width, height], Label::new(&self.error_state.msg))
} else {
ui.add_sized([width, height], Label::new(&self.error_state.msg))
}
}
};
let height = ui.available_height();
match self.error_state.buttons {
YesNo => {
if ui
.add_sized([width, height / 2.0], Button::new("Yes"))
.clicked()
{
self.error_state.reset()
}
// If [Esc] was pressed, assume [No]
if key.is_esc()
|| ui
.add_sized([width, height / 2.0], Button::new("No"))
.clicked()
{
exit(0);
}
}
StayQuit => {
// If [Esc] was pressed, assume [Stay]
if key.is_esc()
|| ui
.add_sized([width, height / 2.0], Button::new("Stay"))
.clicked()
{
self.error_state = ErrorState::new();
}
if ui
.add_sized([width, height / 2.0], Button::new("Quit"))
.clicked()
{
if self.state.gupax.save_before_quit {
self.save_before_quit();
}
exit(0);
}
}
// This code handles the [state.toml/node.toml] resetting, [panic!]'ing if it errors once more
// Another error after this either means an IO error or permission error, which Gupax can't fix.
// [Yes/No] buttons
ResetState => {
if ui
.add_sized([width, height / 2.0], Button::new("Yes"))
.clicked()
{
match reset_state(&self.state_path) {
Ok(_) => match State::get(&self.state_path) {
Ok(s) => {
self.state = s;
self.og = arc_mut!(self.state.clone());
self.error_state.set(
"State read OK",
ErrorFerris::Happy,
ErrorButtons::Okay,
);
}
Err(e) => self.error_state.set(
format!("State read fail: {}", e),
ErrorFerris::Panic,
ErrorButtons::Quit,
),
},
Err(e) => self.error_state.set(
format!("State reset fail: {}", e),
ErrorFerris::Panic,
ErrorButtons::Quit,
),
};
}
if key.is_esc()
|| ui
.add_sized([width, height / 2.0], Button::new("No"))
.clicked()
{
self.error_state.reset()
}
}
ResetNode => {
if ui
.add_sized([width, height / 2.0], Button::new("Yes"))
.clicked()
{
match reset_nodes(&self.node_path) {
Ok(_) => match Node::get(&self.node_path) {
Ok(s) => {
self.node_vec = s;
self.og_node_vec = self.node_vec.clone();
self.error_state.set(
"Node read OK",
ErrorFerris::Happy,
ErrorButtons::Okay,
);
}
Err(e) => self.error_state.set(
format!("Node read fail: {}", e),
ErrorFerris::Panic,
ErrorButtons::Quit,
),
},
Err(e) => self.error_state.set(
format!("Node reset fail: {}", e),
ErrorFerris::Panic,
ErrorButtons::Quit,
),
};
}
if key.is_esc()
|| ui
.add_sized([width, height / 2.0], Button::new("No"))
.clicked()
{
self.error_state.reset()
}
}
ErrorButtons::Sudo => {
let sudo_width = width / 10.0;
let height = ui.available_height() / 4.0;
let mut sudo = lock!(self.sudo);
let hide = sudo.hide;
if sudo.testing {
ui.add_sized([width, height], Spinner::new().size(height));
ui.set_enabled(false);
} else {
ui.add_sized([width, height], Label::new(&sudo.msg));
}
ui.add_space(height);
let height = ui.available_height() / 5.0;
// Password input box with a hider.
ui.horizontal(|ui| {
let response = ui.add_sized(
[sudo_width * 8.0, height],
TextEdit::hint_text(
TextEdit::singleline(&mut sudo.pass).password(hide),
PASSWORD_TEXT,
),
);
let box_width = (ui.available_width() / 2.0) - 5.0;
if (response.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)))
|| ui
.add_sized([box_width, height], Button::new("Enter"))
.on_hover_text(PASSWORD_ENTER)
.clicked()
{
response.request_focus();
if !sudo.testing {
SudoState::test_sudo(
self.sudo.clone(),
&self.helper.clone(),
&self.state.xmrig,
&self.state.gupax.absolute_xmrig_path,
);
}
}
let color = if hide { BLACK } else { BRIGHT_YELLOW };
if ui
.add_sized(
[box_width, height],
Button::new(RichText::new("👁").color(color)),
)
.on_hover_text(PASSWORD_HIDE)
.clicked()
{
flip!(sudo.hide);
}
});
if (key.is_esc() && !sudo.testing)
|| ui
.add_sized([width, height * 4.0], Button::new("Leave"))
.on_hover_text(PASSWORD_LEAVE)
.clicked()
{
self.error_state.reset();
};
// If [test_sudo()] finished, reset error state.
if sudo.success {
self.error_state.reset();
}
}
crate::app::ErrorButtons::Okay | crate::app::ErrorButtons::WindowsAdmin => {
if key.is_esc()
|| ui.add_sized([width, height], Button::new("Okay")).clicked()
{
self.error_state.reset();
}
}
Debug => {
if key.is_esc() {
self.error_state.reset();
}
}
Quit => {
if ui.add_sized([width, height], Button::new("Quit")).clicked() {
exit(1);
}
}
}
})
});
}
}

79
src/app/panels/top.rs Normal file
View file

@ -0,0 +1,79 @@
use egui::TextStyle::Name;
use egui::{SelectableLabel, TopBottomPanel};
use log::debug;
use crate::{app::Tab, utils::constants::SPACE};
impl crate::app::App {
pub fn top_panel(&mut self, ctx: &egui::Context) {
debug!("App | Rendering TOP tabs");
TopBottomPanel::top("top").show(ctx, |ui| {
let width = (self.width - (SPACE * 11.0)) / 6.0;
let height = self.height / 15.0;
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.style_mut().override_text_style = Some(Name("Tab".into()));
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.tab == Tab::About, "About"),
)
.clicked()
{
self.tab = Tab::About;
}
ui.separator();
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.tab == Tab::Status, "Status"),
)
.clicked()
{
self.tab = Tab::Status;
}
ui.separator();
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.tab == Tab::Gupax, "Gupax"),
)
.clicked()
{
self.tab = Tab::Gupax;
}
ui.separator();
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.tab == Tab::P2pool, "P2Pool"),
)
.clicked()
{
self.tab = Tab::P2pool;
}
ui.separator();
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.tab == Tab::Xmrig, "XMRig"),
)
.clicked()
{
self.tab = Tab::Xmrig;
}
ui.separator();
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.tab == Tab::Xvb, "XvB"),
)
.clicked()
{
self.tab = Tab::Xvb;
}
});
ui.add_space(4.0);
});
}
}

48
src/app/quit.rs Normal file
View file

@ -0,0 +1,48 @@
use log::info;
use crate::errors::ErrorButtons;
use crate::errors::ErrorFerris;
use super::App;
impl App {
pub(super) fn quit(&mut self, ctx: &egui::Context) {
// If closing.
// Used to be `eframe::App::on_close_event(&mut self) -> bool`.
let close_signal = ctx.input(|input| {
use egui::viewport::ViewportCommand;
if !input.viewport().close_requested() {
return None;
}
info!("quit");
if self.state.gupax.ask_before_quit {
// If we're already on the [ask_before_quit] screen and
// the user tried to exit again, exit.
if self.error_state.quit_twice {
if self.state.gupax.save_before_quit {
self.save_before_quit();
}
return Some(ViewportCommand::Close);
}
// Else, set the error
self.error_state
.set("", ErrorFerris::Oops, ErrorButtons::StayQuit);
self.error_state.quit_twice = true;
Some(ViewportCommand::CancelClose)
// Else, just quit.
} else {
if self.state.gupax.save_before_quit {
self.save_before_quit();
}
Some(ViewportCommand::Close)
}
});
// This will either:
// 1. Cancel a close signal
// 2. Close the program
if let Some(cmd) = close_signal {
ctx.send_viewport_cmd(cmd);
}
}
}

77
src/app/resize.rs Normal file
View file

@ -0,0 +1,77 @@
use crate::inits::init_text_styles;
use crate::SPACE;
use egui::Color32;
use log::debug;
use log::info;
use super::App;
impl App {
pub fn resize(&mut self, ctx: &egui::Context) {
// This resizes fonts/buttons/etc globally depending on the width.
// This is separate from the [self.width != available_width] logic above
// because placing [init_text_styles()] above would mean calling it 60x a second
// while the user was readjusting the frame. It's a pretty heavy operation and looks
// buggy when calling it that many times. Looking for a [must_resize] in addition to
// checking if the user is hovering over the app means that we only have call it once.
debug!("App | Checking if we need to resize");
if self.must_resize && ctx.is_pointer_over_area() {
self.resizing = true;
self.must_resize = false;
}
// This (ab)uses [Area] and [TextEdit] to overlay a full black layer over whatever UI we had before.
// It incrementally becomes more opaque until [self.alpha] >= 250, when we just switch to pure black (no alpha).
// When black, we're safe to [init_text_styles()], and then incrementally go transparent, until we remove the layer.
if self.resizing {
egui::Area::new("resize_layer")
.order(egui::Order::Foreground)
.anchor(egui::Align2::CENTER_CENTER, (0.0, 0.0))
.show(ctx, |ui| {
if self.alpha < 250 {
egui::Frame::none()
.fill(Color32::from_rgba_premultiplied(0, 0, 0, self.alpha))
.show(ui, |ui| {
ui.add_sized(
[ui.available_width() + SPACE, ui.available_height() + SPACE],
egui::TextEdit::multiline(&mut ""),
);
});
ctx.request_repaint();
self.alpha += 10;
} else {
egui::Frame::none()
.fill(Color32::from_rgb(0, 0, 0))
.show(ui, |ui| {
ui.add_sized(
[ui.available_width() + SPACE, ui.available_height() + SPACE],
egui::TextEdit::multiline(&mut ""),
);
});
ctx.request_repaint();
info!(
"App | Resizing frame to match new internal resolution: [{}x{}]",
self.width, self.height
);
init_text_styles(ctx, self.width, self.state.gupax.selected_scale);
self.resizing = false;
}
});
} else if self.alpha != 0 {
egui::Area::new("resize_layer")
.order(egui::Order::Foreground)
.anchor(egui::Align2::CENTER_CENTER, (0.0, 0.0))
.show(ctx, |ui| {
egui::Frame::none()
.fill(Color32::from_rgba_premultiplied(0, 0, 0, self.alpha))
.show(ui, |ui| {
ui.add_sized(
[ui.available_width() + SPACE, ui.available_height() + SPACE],
egui::TextEdit::multiline(&mut ""),
);
})
});
self.alpha -= 10;
ctx.request_repaint();
}
}
}

112
src/components/gupax.rs Normal file
View file

@ -0,0 +1,112 @@
// Gupax - GUI Uniting P2Pool And XMRig
//
// Copyright (c) 2022-2023 hinto-janai
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::{
disk::state::*,
utils::macros::{arc_mut, lock},
};
use log::*;
use serde::{Deserialize, Serialize};
use std::{
sync::{Arc, Mutex},
thread,
};
//---------------------------------------------------------------------------------------------------- FileWindow
// Struct for writing/reading the path state.
// The opened file picker is started in a new
// thread so main() needs to be in sync.
pub struct FileWindow {
pub thread: bool, // Is there already a FileWindow thread?
pub picked_p2pool: bool, // Did the user pick a path for p2pool?
pub picked_xmrig: bool, // Did the user pick a path for xmrig?
pub p2pool_path: String, // The picked p2pool path
pub xmrig_path: String, // The picked p2pool path
}
impl FileWindow {
pub fn new() -> Arc<Mutex<Self>> {
arc_mut!(Self {
thread: false,
picked_p2pool: false,
picked_xmrig: false,
p2pool_path: String::new(),
xmrig_path: String::new(),
})
}
}
#[derive(Debug, Clone)]
pub enum FileType {
P2pool,
Xmrig,
}
//---------------------------------------------------------------------------------------------------- Ratio Lock
// Enum for the lock ratio in the advanced tab.
#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)]
pub enum Ratio {
Width,
Height,
None,
}
//---------------------------------------------------------------------------------------------------- Gupax
impl Gupax {
// Checks if a path is a valid path to a file.
pub fn path_is_file(path: &str) -> bool {
let path = path.to_string();
match crate::disk::into_absolute_path(path) {
Ok(path) => path.is_file(),
_ => false,
}
}
#[cold]
#[inline(never)]
pub fn spawn_file_window_thread(file_window: &Arc<Mutex<FileWindow>>, 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;
});
}
}

3
src/components/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod gupax;
pub mod node;
pub mod update;

View file

@ -15,6 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::components::update::Pkg;
use crate::{constants::*, macros::*};
use egui::Color32;
use hyper::{client::HttpConnector, Body, Client, Request};
@ -409,7 +410,7 @@ impl Ping {
let client: Client<HttpConnector> = Client::builder().build(HttpConnector::new());
// Random User Agent
let rand_user_agent = crate::Pkg::get_user_agent();
let rand_user_agent = Pkg::get_user_agent();
// Handle vector
let mut handles = Vec::with_capacity(REMOTE_NODE_LENGTH);
let node_vec = arc_mut!(Vec::with_capacity(REMOTE_NODE_LENGTH));
@ -506,13 +507,17 @@ impl Ping {
lock!(node_vec).push(NodeData { ip, ms, color });
}
}
//---------------------------------------------------------------------------------------------------- TESTS
//---------------------------------------------------------------------------------------------------- NODE
#[cfg(test)]
mod test {
use crate::components::node::{
format_ip, REMOTE_NODES, REMOTE_NODE_LENGTH, REMOTE_NODE_MAX_CHARS,
};
use crate::components::update::Pkg;
#[test]
fn validate_node_ips() {
for (ip, location, rpc, zmq) in crate::REMOTE_NODES {
for (ip, location, rpc, zmq) in REMOTE_NODES {
assert!(ip.len() < 255);
assert!(ip.is_ascii());
assert!(!location.is_empty());
@ -524,8 +529,8 @@ mod test {
#[test]
fn spacing() {
for (ip, _, _, _) in crate::REMOTE_NODES {
assert!(crate::format_ip(ip).len() <= crate::REMOTE_NODE_MAX_CHARS);
for (ip, _, _, _) in REMOTE_NODES {
assert!(format_ip(ip).len() <= REMOTE_NODE_MAX_CHARS);
}
}
@ -534,7 +539,6 @@ mod test {
#[tokio::test]
#[ignore]
async fn full_ping() {
use crate::{REMOTE_NODES, REMOTE_NODE_LENGTH};
use hyper::{client::HttpConnector, Client, Request};
use serde::{Deserialize, Serialize};
@ -548,7 +552,7 @@ mod test {
let client: Client<HttpConnector> = Client::builder().build(HttpConnector::new());
// Random User Agent
let rand_user_agent = crate::Pkg::get_user_agent();
let rand_user_agent = Pkg::get_user_agent();
// Only fail this test if >50% of nodes fail.
const HALF_REMOTE_NODES: usize = REMOTE_NODE_LENGTH / 2;

View file

@ -24,9 +24,17 @@
// b. auto-update at startup
//---------------------------------------------------------------------------------------------------- Imports
use crate::components::update::Name::*;
use crate::{
constants::GUPAX_VERSION, disk::*, macros::*, update::Name::*, ErrorButtons, ErrorFerris,
ErrorState, Restart,
app::Restart,
constants::GUPAX_VERSION,
disk::{
state::{State, Version},
*,
},
macros::*,
miscs::get_exe_dir,
utils::errors::{ErrorButtons, ErrorFerris, ErrorState},
};
use anyhow::{anyhow, Error};
use arti_client::TorClient;
@ -310,7 +318,7 @@ impl Update {
.take(10)
.map(char::from)
.collect();
let base = crate::get_exe_dir()?;
let base = get_exe_dir()?;
#[cfg(target_os = "windows")]
let tmp_dir = format!("{}{}{}{}", base, r"\gupax_update_", rand_string, r"\");
#[cfg(target_family = "unix")]
@ -365,7 +373,7 @@ impl Update {
// code only needs to be edited once, here.
pub fn spawn_thread(
og: &Arc<Mutex<State>>,
gupax: &crate::disk::Gupax,
gupax: &crate::disk::state::Gupax,
state_path: &Path,
update: &Arc<Mutex<Update>>,
error_state: &mut ErrorState,

File diff suppressed because it is too large Load diff

58
src/disk/consts.rs Normal file
View file

@ -0,0 +1,58 @@
//---------------------------------------------------------------------------------------------------- Const
// State file
pub const ERROR: &str = "Disk error";
pub const PATH_ERROR: &str = "PATH for state directory could not be not found";
#[cfg(target_os = "windows")]
pub const DIRECTORY: &str = r#"Gupax\"#;
#[cfg(target_os = "macos")]
pub const DIRECTORY: &str = "Gupax/";
#[cfg(target_os = "linux")]
pub const DIRECTORY: &str = "gupax/";
// File names
pub const STATE_TOML: &str = "state.toml";
pub const NODE_TOML: &str = "node.toml";
pub const POOL_TOML: &str = "pool.toml";
// P2Pool API
// Lives within the Gupax OS data directory.
// ~/.local/share/gupax/p2pool/
// ├─ payout_log // Raw log lines of payouts received
// ├─ payout // Single [u64] representing total payouts
// ├─ xmr // Single [u64] representing total XMR mined in atomic units
#[cfg(target_os = "windows")]
pub const GUPAX_P2POOL_API_DIRECTORY: &str = r"p2pool\";
#[cfg(target_family = "unix")]
pub const GUPAX_P2POOL_API_DIRECTORY: &str = "p2pool/";
pub const GUPAX_P2POOL_API_LOG: &str = "log";
pub const GUPAX_P2POOL_API_PAYOUT: &str = "payout";
pub const GUPAX_P2POOL_API_XMR: &str = "xmr";
pub const GUPAX_P2POOL_API_FILE_ARRAY: [&str; 3] = [
GUPAX_P2POOL_API_LOG,
GUPAX_P2POOL_API_PAYOUT,
GUPAX_P2POOL_API_XMR,
];
#[cfg(target_os = "windows")]
pub const DEFAULT_P2POOL_PATH: &str = r"P2Pool\p2pool.exe";
#[cfg(target_os = "macos")]
pub const DEFAULT_P2POOL_PATH: &str = "p2pool/p2pool";
#[cfg(target_os = "windows")]
pub const DEFAULT_XMRIG_PATH: &str = r"XMRig\xmrig.exe";
#[cfg(target_os = "macos")]
pub const DEFAULT_XMRIG_PATH: &str = "xmrig/xmrig";
// Default to [/usr/bin/] for Linux distro builds.
#[cfg(target_os = "linux")]
#[cfg(not(feature = "distro"))]
pub const DEFAULT_P2POOL_PATH: &str = "p2pool/p2pool";
#[cfg(target_os = "linux")]
#[cfg(not(feature = "distro"))]
pub const DEFAULT_XMRIG_PATH: &str = "xmrig/xmrig";
#[cfg(target_os = "linux")]
#[cfg(feature = "distro")]
pub const DEFAULT_P2POOL_PATH: &str = "/usr/bin/p2pool";
#[cfg(target_os = "linux")]
#[cfg(feature = "distro")]
pub const DEFAULT_XMRIG_PATH: &str = "/usr/bin/xmrig";

39
src/disk/errors.rs Normal file
View file

@ -0,0 +1,39 @@
use super::*;
//---------------------------------------------------------------------------------------------------- Custom Error [TomlError]
#[derive(Debug)]
pub enum TomlError {
Io(std::io::Error),
Path(String),
Serialize(toml::ser::Error),
Deserialize(toml::de::Error),
Merge(figment::Error),
Format(std::fmt::Error),
Parse(&'static str),
}
impl Display for TomlError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use TomlError::*;
match self {
Io(err) => write!(f, "{}: IO | {}", ERROR, err),
Path(err) => write!(f, "{}: Path | {}", ERROR, err),
Serialize(err) => write!(f, "{}: Serialize | {}", ERROR, err),
Deserialize(err) => write!(f, "{}: Deserialize | {}", ERROR, err),
Merge(err) => write!(f, "{}: Merge | {}", ERROR, err),
Format(err) => write!(f, "{}: Format | {}", ERROR, err),
Parse(err) => write!(f, "{}: Parse | {}", ERROR, err),
}
}
}
impl From<std::io::Error> for TomlError {
fn from(err: std::io::Error) -> Self {
TomlError::Io(err)
}
}
impl From<std::fmt::Error> for TomlError {
fn from(err: std::fmt::Error) -> Self {
TomlError::Format(err)
}
}

View file

@ -0,0 +1,266 @@
use super::*;
//---------------------------------------------------------------------------------------------------- Gupax-P2Pool API
#[derive(Clone, Debug)]
pub struct GupaxP2poolApi {
pub log: String, // Log file only containing full payout lines
pub log_rev: String, // Same as above but reversed based off lines
pub payout: HumanNumber, // Human-friendly display of payout count
pub payout_u64: u64, // [u64] version of above
pub payout_ord: PayoutOrd, // Ordered Vec of payouts, see [PayoutOrd]
pub payout_low: String, // A pre-allocated/computed [String] of the above Vec from low payout to high
pub payout_high: String, // Same as above but high -> low
pub xmr: AtomicUnit, // XMR stored as atomic units
pub path_log: PathBuf, // Path to [log]
pub path_payout: PathBuf, // Path to [payout]
pub path_xmr: PathBuf, // Path to [xmr]
}
impl Default for GupaxP2poolApi {
fn default() -> Self {
Self::new()
}
}
impl GupaxP2poolApi {
//---------------------------------------------------------------------------------------------------- Init, these pretty much only get called once
pub fn new() -> Self {
Self {
log: String::new(),
log_rev: String::new(),
payout: HumanNumber::unknown(),
payout_u64: 0,
payout_ord: PayoutOrd::new(),
payout_low: String::new(),
payout_high: String::new(),
xmr: AtomicUnit::new(),
path_xmr: PathBuf::new(),
path_payout: PathBuf::new(),
path_log: PathBuf::new(),
}
}
pub fn fill_paths(&mut self, gupax_p2pool_dir: &Path) {
let mut path_log = gupax_p2pool_dir.to_path_buf();
let mut path_payout = gupax_p2pool_dir.to_path_buf();
let mut path_xmr = gupax_p2pool_dir.to_path_buf();
path_log.push(GUPAX_P2POOL_API_LOG);
path_payout.push(GUPAX_P2POOL_API_PAYOUT);
path_xmr.push(GUPAX_P2POOL_API_XMR);
*self = Self {
path_log,
path_payout,
path_xmr,
..std::mem::take(self)
};
}
pub fn create_all_files(gupax_p2pool_dir: &Path) -> Result<(), TomlError> {
use std::io::Write;
for file in GUPAX_P2POOL_API_FILE_ARRAY {
let mut path = gupax_p2pool_dir.to_path_buf();
path.push(file);
if path.exists() {
info!(
"GupaxP2poolApi | [{}] already exists, skipping...",
path.display()
);
continue;
}
match std::fs::File::create(&path) {
Ok(mut f) => {
match file {
GUPAX_P2POOL_API_PAYOUT | GUPAX_P2POOL_API_XMR => writeln!(f, "0")?,
_ => (),
}
info!("GupaxP2poolApi | [{}] create ... OK", path.display());
}
Err(e) => {
warn!(
"GupaxP2poolApi | [{}] create ... FAIL: {}",
path.display(),
e
);
return Err(TomlError::Io(e));
}
}
}
Ok(())
}
pub fn read_all_files_and_update(&mut self) -> Result<(), TomlError> {
let payout_u64 = match read_to_string(File::Payout, &self.path_payout)?
.trim()
.parse::<u64>()
{
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::<u64>()
{
Ok(o) => AtomicUnit::from_u64(o),
Err(e) => {
warn!("GupaxP2poolApi | [xmr] parse error: {}", e);
return Err(TomlError::Parse("xmr"));
}
};
let payout = HumanNumber::from_u64(payout_u64);
let log = read_to_string(File::Log, &self.path_log)?;
self.payout_ord.update_from_payout_log(&log);
self.update_payout_strings();
*self = Self {
log,
payout,
payout_u64,
xmr,
..std::mem::take(self)
};
self.update_log_rev();
Ok(())
}
// Completely delete the [p2pool] folder and create defaults.
pub fn create_new(path: &PathBuf) -> Result<(), TomlError> {
info!(
"GupaxP2poolApi | Deleting old folder at [{}]...",
path.display()
);
std::fs::remove_dir_all(path)?;
info!(
"GupaxP2poolApi | Creating new default folder at [{}]...",
path.display()
);
create_gupax_p2pool_dir(path)?;
Self::create_all_files(path)?;
Ok(())
}
//---------------------------------------------------------------------------------------------------- Live, functions that actually update/write live stats
pub fn update_log_rev(&mut self) {
let mut log_rev = String::with_capacity(self.log.len());
for line in self.log.lines().rev() {
log_rev.push_str(line);
log_rev.push('\n');
}
self.log_rev = log_rev;
}
pub fn format_payout(date: &str, atomic_unit: &AtomicUnit, block: &HumanNumber) -> String {
format!("{} | {} XMR | Block {}", date, atomic_unit, block)
}
pub fn append_log(&mut self, formatted_log_line: &str) {
self.log.push_str(formatted_log_line);
self.log.push('\n');
}
pub fn append_head_log_rev(&mut self, formatted_log_line: &str) {
self.log_rev = format!("{}\n{}", formatted_log_line, self.log_rev);
}
pub fn update_payout_low(&mut self) {
self.payout_ord.sort_payout_low_to_high();
self.payout_low = self.payout_ord.to_string();
}
pub fn update_payout_high(&mut self) {
self.payout_ord.sort_payout_high_to_low();
self.payout_high = self.payout_ord.to_string();
}
pub fn update_payout_strings(&mut self) {
self.update_payout_low();
self.update_payout_high();
}
// Takes the (date, atomic_unit, block) and updates [self] and the [PayoutOrd]
pub fn add_payout(
&mut self,
formatted_log_line: &str,
date: String,
atomic_unit: AtomicUnit,
block: HumanNumber,
) {
self.append_log(formatted_log_line);
self.append_head_log_rev(formatted_log_line);
self.payout_u64 += 1;
self.payout = HumanNumber::from_u64(self.payout_u64);
self.xmr = self.xmr.add_self(atomic_unit);
self.payout_ord.push(date, atomic_unit, block);
self.update_payout_strings();
}
pub fn write_to_all_files(&self, formatted_log_line: &str) -> Result<(), TomlError> {
Self::disk_overwrite(&self.payout_u64.to_string(), &self.path_payout)?;
Self::disk_overwrite(&self.xmr.to_string(), &self.path_xmr)?;
Self::disk_append(formatted_log_line, &self.path_log)?;
Ok(())
}
pub fn disk_append(formatted_log_line: &str, path: &PathBuf) -> Result<(), TomlError> {
use std::io::Write;
let mut file = match fs::OpenOptions::new().append(true).create(true).open(path) {
Ok(f) => f,
Err(e) => {
error!(
"GupaxP2poolApi | Append [{}] ... FAIL: {}",
path.display(),
e
);
return Err(TomlError::Io(e));
}
};
match writeln!(file, "{}", formatted_log_line) {
Ok(_) => {
debug!("GupaxP2poolApi | Append [{}] ... OK", path.display());
Ok(())
}
Err(e) => {
error!(
"GupaxP2poolApi | Append [{}] ... FAIL: {}",
path.display(),
e
);
Err(TomlError::Io(e))
}
}
}
pub fn disk_overwrite(string: &str, path: &PathBuf) -> Result<(), TomlError> {
use std::io::Write;
let mut file = match fs::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)
{
Ok(f) => f,
Err(e) => {
error!(
"GupaxP2poolApi | Overwrite [{}] ... FAIL: {}",
path.display(),
e
);
return Err(TomlError::Io(e));
}
};
match writeln!(file, "{}", string) {
Ok(_) => {
debug!("GupaxP2poolApi | Overwrite [{}] ... OK", path.display());
Ok(())
}
Err(e) => {
error!(
"GupaxP2poolApi | Overwrite [{}] ... FAIL: {}",
path.display(),
e
);
Err(TomlError::Io(e))
}
}
}
}

222
src/disk/mod.rs Normal file
View file

@ -0,0 +1,222 @@
// Gupax - GUI Uniting P2Pool And XMRig
//
// Copyright (c) 2022-2023 hinto-janai
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// This handles reading/writing the disk files:
// - [state.toml] -> [App] state
// - [nodes.toml] -> [Manual Nodes] list
// The TOML format is used. This struct hierarchy
// directly translates into the TOML parser:
// State/
// ├─ Gupax/
// │ ├─ ...
// ├─ P2pool/
// │ ├─ ...
// ├─ Xmrig/
// │ ├─ ...
// ├─ Version/
// ├─ ...
use crate::disk::consts::*;
use crate::{app::Tab, components::gupax::Ratio, constants::*, human::*, macros::*, xmr::*};
use figment::providers::{Format, Toml};
use figment::Figment;
use log::*;
use serde::{Deserialize, Serialize};
#[cfg(target_family = "unix")]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::{
fmt::Display,
fmt::Write,
fs,
path::PathBuf,
result::Result,
sync::{Arc, Mutex},
};
use self::errors::TomlError;
pub mod consts;
pub mod errors;
pub mod gupax_p2pool_api;
pub mod node;
pub mod pool;
pub mod state;
pub mod status;
pub mod tests;
//---------------------------------------------------------------------------------------------------- General functions for all [File]'s
// get_file_path() | Return absolute path to OS data path + filename
// read_to_string() | Convert the file at a given path into a [String]
// create_new() | Write a default TOML Struct into the appropriate file (in OS data path)
// into_absolute_path() | Convert relative -> absolute path
pub fn get_gupax_data_path() -> Result<PathBuf, TomlError> {
// Get OS data folder
// Linux | $XDG_DATA_HOME or $HOME/.local/share/gupax | /home/alice/.local/state/gupax
// macOS | $HOME/Library/Application Support/Gupax | /Users/Alice/Library/Application Support/Gupax
// Windows | {FOLDERID_RoamingAppData}\Gupax | C:\Users\Alice\AppData\Roaming\Gupax
match dirs::data_dir() {
Some(mut path) => {
path.push(DIRECTORY);
info!("OS | Data path ... {}", path.display());
create_gupax_dir(&path)?;
let mut gupax_p2pool_dir = path.clone();
gupax_p2pool_dir.push(GUPAX_P2POOL_API_DIRECTORY);
create_gupax_p2pool_dir(&gupax_p2pool_dir)?;
Ok(path)
}
None => {
error!("OS | Data path ... FAIL");
Err(TomlError::Path(PATH_ERROR.to_string()))
}
}
}
pub fn set_unix_750_perms(path: &PathBuf) -> Result<(), TomlError> {
#[cfg(target_os = "windows")]
return Ok(());
#[cfg(target_family = "unix")]
match fs::set_permissions(path, fs::Permissions::from_mode(0o750)) {
Ok(_) => {
info!(
"OS | Unix 750 permissions on path [{}] ... OK",
path.display()
);
Ok(())
}
Err(e) => {
error!(
"OS | Unix 750 permissions on path [{}] ... FAIL ... {}",
path.display(),
e
);
Err(TomlError::Io(e))
}
}
}
pub fn set_unix_660_perms(path: &PathBuf) -> Result<(), TomlError> {
#[cfg(target_os = "windows")]
return Ok(());
#[cfg(target_family = "unix")]
match fs::set_permissions(path, fs::Permissions::from_mode(0o660)) {
Ok(_) => {
info!(
"OS | Unix 660 permissions on path [{}] ... OK",
path.display()
);
Ok(())
}
Err(e) => {
error!(
"OS | Unix 660 permissions on path [{}] ... FAIL ... {}",
path.display(),
e
);
Err(TomlError::Io(e))
}
}
}
pub fn get_gupax_p2pool_path(os_data_path: &Path) -> PathBuf {
let mut gupax_p2pool_dir = os_data_path.to_path_buf();
gupax_p2pool_dir.push(GUPAX_P2POOL_API_DIRECTORY);
gupax_p2pool_dir
}
pub fn create_gupax_dir(path: &PathBuf) -> Result<(), TomlError> {
// Create Gupax directory
match fs::create_dir_all(path) {
Ok(_) => info!("OS | Create data path ... OK"),
Err(e) => {
error!("OS | Create data path ... FAIL ... {}", e);
return Err(TomlError::Io(e));
}
}
set_unix_750_perms(path)
}
pub fn create_gupax_p2pool_dir(path: &PathBuf) -> Result<(), TomlError> {
// Create Gupax directory
match fs::create_dir_all(path) {
Ok(_) => {
info!(
"OS | Create Gupax-P2Pool API path [{}] ... OK",
path.display()
);
Ok(())
}
Err(e) => {
error!(
"OS | Create Gupax-P2Pool API path [{}] ... FAIL ... {}",
path.display(),
e
);
Err(TomlError::Io(e))
}
}
}
// Convert a [File] path to a [String]
pub fn read_to_string(file: File, path: &PathBuf) -> Result<String, TomlError> {
match fs::read_to_string(path) {
Ok(string) => {
info!("{:?} | Read ... OK", file);
Ok(string)
}
Err(err) => {
warn!("{:?} | Read ... FAIL", file);
Err(TomlError::Io(err))
}
}
}
// Write str to console with [info!] surrounded by "---"
pub fn print_dash(toml: &str) {
info!("{}", HORIZONTAL);
for i in toml.lines() {
info!("{}", i);
}
info!("{}", HORIZONTAL);
}
// Turn relative paths into absolute paths
pub fn into_absolute_path(path: String) -> Result<PathBuf, TomlError> {
let path = PathBuf::from(path);
if path.is_relative() {
let mut dir = std::env::current_exe()?;
dir.pop();
dir.push(path);
Ok(dir)
} else {
Ok(path)
}
}
//---------------------------------------------------------------------------------------------------- [File] Enum (for matching which file)
#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)]
pub enum File {
// State files
State, // state.toml | Gupax state
Node, // node.toml | P2Pool manual node selector
Pool, // pool.toml | XMRig manual pool selector
// Gupax-P2Pool API
Log, // log | Raw log lines of P2Pool payouts received
Payout, // payout | Single [u64] representing total payouts
Xmr, // xmr | Single [u64] representing total XMR mined in atomic units
}

164
src/disk/node.rs Normal file
View file

@ -0,0 +1,164 @@
use crate::disk::TomlError;
use crate::disk::*;
use serde::{Deserialize, Serialize};
//---------------------------------------------------------------------------------------------------- [Node] Impl
impl Node {
pub fn localhost() -> Self {
Self {
ip: "localhost".to_string(),
rpc: "18081".to_string(),
zmq: "18083".to_string(),
}
}
pub fn new_vec() -> Vec<(String, Self)> {
vec![("Local Monero Node".to_string(), Self::localhost())]
}
pub fn new_tuple() -> (String, Self) {
("Local Monero Node".to_string(), Self::localhost())
}
// Convert [String] to [Node] Vec
pub fn from_str_to_vec(string: &str) -> Result<Vec<(String, Self)>, TomlError> {
let nodes: toml::map::Map<String, toml::Value> = 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<String, TomlError> {
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<Vec<(String, Self)>, 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<String, Self>].
pub fn create_new(path: &PathBuf) -> Result<Vec<(String, Self)>, TomlError> {
info!("Node | Creating new default...");
let new = Self::new_vec();
let string = Self::to_string(&Self::new_vec())?;
fs::write(path, string)?;
info!("Node | Write ... OK");
Ok(new)
}
// Save [Node] onto disk file [node.toml]
pub fn save(vec: &[(String, Self)], path: &PathBuf) -> Result<(), TomlError> {
info!("Node | Saving to disk ... [{}]", path.display());
let string = Self::to_string(vec)?;
match fs::write(path, string) {
Ok(_) => {
info!("Node | Save ... OK");
Ok(())
}
Err(err) => {
error!("Node | Couldn't overwrite file");
Err(TomlError::Io(err))
}
}
}
// pub fn merge(old: &String) -> Result<Self, TomlError> {
// info!("Node | Starting TOML merge...");
// let default = match toml::ser::to_string(&Self::new()) {
// Ok(string) => { info!("Node | Default TOML parse ... OK"); string },
// Err(err) => { error!("Node | Couldn't parse default TOML into string"); return Err(TomlError::Serialize(err)) },
// };
// let mut new: Self = match Figment::new().merge(Toml::string(&old)).merge(Toml::string(&default)).extract() {
// Ok(new) => { info!("Node | TOML merge ... OK"); new },
// Err(err) => { error!("Node | Couldn't merge default + old TOML"); return Err(TomlError::Merge(err)) },
// };
// // Attempt save
// Self::save(&mut new)?;
// Ok(new)
// }
}
//---------------------------------------------------------------------------------------------------- [Node] Struct
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
pub struct Node {
pub ip: String,
pub rpc: String,
pub zmq: String,
}

137
src/disk/pool.rs Normal file
View file

@ -0,0 +1,137 @@
use super::*;
//---------------------------------------------------------------------------------------------------- [Pool] impl
impl Pool {
pub fn p2pool() -> Self {
Self {
rig: GUPAX_VERSION_UNDERSCORE.to_string(),
ip: "localhost".to_string(),
port: "3333".to_string(),
}
}
pub fn new_vec() -> Vec<(String, Self)> {
vec![("Local P2Pool".to_string(), Self::p2pool())]
}
pub fn new_tuple() -> (String, Self) {
("Local P2Pool".to_string(), Self::p2pool())
}
pub fn from_str_to_vec(string: &str) -> Result<Vec<(String, Self)>, TomlError> {
let pools: toml::map::Map<String, toml::Value> = 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<String, TomlError> {
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<Vec<(String, Self)>, 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<Vec<(String, Self)>, TomlError> {
info!("Pool | Creating new default...");
let new = Self::new_vec();
let string = Self::to_string(&Self::new_vec())?;
fs::write(path, string)?;
info!("Pool | Write ... OK");
Ok(new)
}
pub fn save(vec: &[(String, Self)], path: &PathBuf) -> Result<(), TomlError> {
info!("Pool | Saving to disk ... [{}]", path.display());
let string = Self::to_string(vec)?;
match fs::write(path, string) {
Ok(_) => {
info!("Pool | Save ... OK");
Ok(())
}
Err(err) => {
error!("Pool | Couldn't overwrite file");
Err(TomlError::Io(err))
}
}
}
}
//---------------------------------------------------------------------------------------------------- [Pool] Struct
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
pub struct Pool {
pub rig: String,
pub ip: String,
pub port: String,
}

362
src/disk/state.rs Normal file
View file

@ -0,0 +1,362 @@
use super::*;
use crate::{components::node::RemoteNode, disk::status::*};
//---------------------------------------------------------------------------------------------------- [State] Impl
impl Default for State {
fn default() -> Self {
Self::new()
}
}
impl State {
pub fn new() -> Self {
let max_threads = benri::threads!();
let current_threads = if max_threads == 1 { 1 } else { max_threads / 2 };
Self {
status: Status::default(),
gupax: Gupax::default(),
p2pool: P2pool::default(),
xmrig: Xmrig::with_threads(max_threads, current_threads),
xvb: Xvb::default(),
version: arc_mut!(Version::default()),
}
}
pub fn update_absolute_path(&mut self) -> Result<(), TomlError> {
self.gupax.absolute_p2pool_path = into_absolute_path(self.gupax.p2pool_path.clone())?;
self.gupax.absolute_xmrig_path = into_absolute_path(self.gupax.xmrig_path.clone())?;
Ok(())
}
// Convert [&str] to [State]
pub fn from_str(string: &str) -> Result<Self, TomlError> {
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<String, TomlError> {
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<Self, TomlError> {
// 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<Self, TomlError> {
info!("State | Creating new default...");
let new = Self::new();
let string = Self::to_string(&new)?;
fs::write(path, string)?;
info!("State | Write ... OK");
Ok(new)
}
// Save [State] onto disk file [gupax.toml]
pub fn save(&mut self, path: &PathBuf) -> Result<(), TomlError> {
info!("State | Saving to disk...");
// Convert path to absolute
self.gupax.absolute_p2pool_path = into_absolute_path(self.gupax.p2pool_path.clone())?;
self.gupax.absolute_xmrig_path = into_absolute_path(self.gupax.xmrig_path.clone())?;
let string = match toml::ser::to_string(&self) {
Ok(string) => {
info!("State | Parse ... OK");
print_dash(&string);
string
}
Err(err) => {
error!("State | Couldn't parse TOML into string ... FAIL");
return Err(TomlError::Serialize(err));
}
};
match fs::write(path, string) {
Ok(_) => {
info!("State | Save ... OK");
Ok(())
}
Err(err) => {
error!("State | Couldn't overwrite TOML file ... FAIL");
Err(TomlError::Io(err))
}
}
}
// Take [String] as input, merge it with whatever the current [default] is,
// leaving behind old keys+values and updating [default] with old valid ones.
pub fn merge(old: &str) -> Result<Self, TomlError> {
let default = toml::ser::to_string(&Self::new()).unwrap();
let new: Self = match Figment::from(Toml::string(&default))
.merge(Toml::string(old))
.extract()
{
Ok(new) => {
info!("State | TOML merge ... OK");
new
}
Err(err) => {
error!("State | Couldn't merge default + old TOML");
return Err(TomlError::Merge(err));
}
};
Ok(new)
}
}
//---------------------------------------------------------------------------------------------------- [State] Struct
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct State {
pub status: Status,
pub gupax: Gupax,
pub p2pool: P2pool,
pub xmrig: Xmrig,
pub xvb: Xvb,
pub version: Arc<Mutex<Version>>,
}
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
pub struct Status {
pub submenu: Submenu,
pub payout_view: PayoutView,
pub monero_enabled: bool,
pub manual_hash: bool,
pub hashrate: f64,
pub hash_metric: Hash,
}
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
pub struct Gupax {
pub simple: bool,
pub auto_update: bool,
pub auto_p2pool: bool,
pub auto_xmrig: bool,
// pub auto_monero: bool,
pub ask_before_quit: bool,
pub save_before_quit: bool,
pub update_via_tor: bool,
pub p2pool_path: String,
pub xmrig_path: String,
pub absolute_p2pool_path: PathBuf,
pub absolute_xmrig_path: PathBuf,
pub selected_width: u16,
pub selected_height: u16,
pub selected_scale: f32,
pub tab: Tab,
pub ratio: Ratio,
}
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
pub struct P2pool {
pub simple: bool,
pub mini: bool,
pub auto_ping: bool,
pub auto_select: bool,
pub backup_host: bool,
pub out_peers: u16,
pub in_peers: u16,
pub log_level: u8,
pub node: String,
pub arguments: String,
pub address: String,
pub name: String,
pub ip: String,
pub rpc: String,
pub zmq: String,
pub selected_index: usize,
pub selected_name: String,
pub selected_ip: String,
pub selected_rpc: String,
pub selected_zmq: String,
}
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
pub struct Xmrig {
pub simple: bool,
pub pause: u8,
pub simple_rig: String,
pub arguments: String,
pub tls: bool,
pub keepalive: bool,
pub max_threads: usize,
pub current_threads: usize,
pub address: String,
pub api_ip: String,
pub api_port: String,
pub name: String,
pub rig: String,
pub ip: String,
pub port: String,
pub selected_index: usize,
pub selected_name: String,
pub selected_rig: String,
pub selected_ip: String,
pub selected_port: String,
}
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize, Default)]
pub struct Xvb {
pub token_confirmed: String,
pub token_inserted: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Version {
pub gupax: String,
pub p2pool: String,
pub xmrig: String,
}
//---------------------------------------------------------------------------------------------------- [State] Defaults
impl Default for Status {
fn default() -> Self {
Self {
submenu: Submenu::default(),
payout_view: PayoutView::default(),
monero_enabled: false,
manual_hash: false,
hashrate: 1.0,
hash_metric: Hash::default(),
}
}
}
impl Default for Gupax {
fn default() -> Self {
Self {
simple: true,
auto_update: true,
auto_p2pool: false,
auto_xmrig: false,
ask_before_quit: true,
save_before_quit: true,
update_via_tor: true,
p2pool_path: DEFAULT_P2POOL_PATH.to_string(),
xmrig_path: DEFAULT_XMRIG_PATH.to_string(),
absolute_p2pool_path: into_absolute_path(DEFAULT_P2POOL_PATH.to_string()).unwrap(),
absolute_xmrig_path: into_absolute_path(DEFAULT_XMRIG_PATH.to_string()).unwrap(),
selected_width: APP_DEFAULT_WIDTH as u16,
selected_height: APP_DEFAULT_HEIGHT as u16,
selected_scale: APP_DEFAULT_SCALE,
ratio: Ratio::Width,
tab: Tab::About,
}
}
}
impl Default for P2pool {
fn default() -> Self {
Self {
simple: true,
mini: true,
auto_ping: true,
auto_select: true,
backup_host: true,
out_peers: 10,
in_peers: 10,
log_level: 3,
node: RemoteNode::new().to_string(),
arguments: String::new(),
address: String::with_capacity(96),
name: "Local Monero Node".to_string(),
ip: "localhost".to_string(),
rpc: "18081".to_string(),
zmq: "18083".to_string(),
selected_index: 0,
selected_name: "Local Monero Node".to_string(),
selected_ip: "localhost".to_string(),
selected_rpc: "18081".to_string(),
selected_zmq: "18083".to_string(),
}
}
}
impl Xmrig {
fn with_threads(max_threads: usize, current_threads: usize) -> Self {
let xmrig = Self::default();
Self {
max_threads,
current_threads,
..xmrig
}
}
}
impl Default for Xmrig {
fn default() -> Self {
Self {
simple: true,
pause: 0,
simple_rig: String::with_capacity(30),
arguments: String::with_capacity(300),
address: String::with_capacity(96),
name: "Local P2Pool".to_string(),
rig: GUPAX_VERSION_UNDERSCORE.to_string(),
ip: "localhost".to_string(),
port: "3333".to_string(),
selected_index: 0,
selected_name: "Local P2Pool".to_string(),
selected_ip: "localhost".to_string(),
selected_rig: GUPAX_VERSION_UNDERSCORE.to_string(),
selected_port: "3333".to_string(),
api_ip: "localhost".to_string(),
api_port: "18088".to_string(),
tls: false,
keepalive: false,
current_threads: 1,
max_threads: 1,
}
}
}
impl Default for Version {
fn default() -> Self {
Self {
gupax: GUPAX_VERSION.to_string(),
p2pool: P2POOL_VERSION.to_string(),
xmrig: XMRIG_VERSION.to_string(),
}
}
}

117
src/disk/status.rs Normal file
View file

@ -0,0 +1,117 @@
use super::*;
//---------------------------------------------------------------------------------------------------- [Submenu] enum for [Status] tab
#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)]
pub enum Submenu {
Processes,
P2pool,
Benchmarks,
}
impl Default for Submenu {
fn default() -> Self {
Self::Processes
}
}
impl Display for Submenu {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use Submenu::*;
match self {
P2pool => write!(f, "P2Pool"),
_ => write!(f, "{:?}", self),
}
}
}
//---------------------------------------------------------------------------------------------------- [PayoutView] enum for [Status/P2Pool] tab
// The enum buttons for selecting which "view" to sort the payout log in.
#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)]
pub enum PayoutView {
Latest, // Shows the most recent logs first
Oldest, // Shows the oldest logs first
Biggest, // Shows highest to lowest payouts
Smallest, // Shows lowest to highest payouts
}
impl PayoutView {
fn new() -> Self {
Self::Latest
}
}
impl Default for PayoutView {
fn default() -> Self {
Self::new()
}
}
impl Display for PayoutView {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
//---------------------------------------------------------------------------------------------------- [Hash] enum for [Status/P2Pool]
#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)]
#[allow(clippy::enum_variant_names)]
pub enum Hash {
Hash,
Kilo,
Mega,
Giga,
}
impl Default for Hash {
fn default() -> Self {
Self::Hash
}
}
impl Hash {
pub fn convert_to_hash(f: f64, from: Self) -> f64 {
match from {
Self::Hash => f,
Self::Kilo => f * 1_000.0,
Self::Mega => f * 1_000_000.0,
Self::Giga => f * 1_000_000_000.0,
}
}
pub fn convert(f: f64, og: Self, new: Self) -> f64 {
match og {
Self::Hash => match new {
Self::Hash => f,
Self::Kilo => f / 1_000.0,
Self::Mega => f / 1_000_000.0,
Self::Giga => f / 1_000_000_000.0,
},
Self::Kilo => match new {
Self::Hash => f * 1_000.0,
Self::Kilo => f,
Self::Mega => f / 1_000.0,
Self::Giga => f / 1_000_000.0,
},
Self::Mega => match new {
Self::Hash => f * 1_000_000.0,
Self::Kilo => f * 1_000.0,
Self::Mega => f,
Self::Giga => f / 1_000.0,
},
Self::Giga => match new {
Self::Hash => f * 1_000_000_000.0,
Self::Kilo => f * 1_000_000.0,
Self::Mega => f * 1_000.0,
Self::Giga => f,
},
}
}
}
impl Display for Hash {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Hash::Hash => write!(f, "Hash"),
_ => write!(f, "{:?}hash", self),
}
}
}

302
src/disk/tests.rs Normal file
View file

@ -0,0 +1,302 @@
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod test {
use crate::disk::node::Node;
use crate::disk::pool::Pool;
use crate::disk::state::State;
#[test]
fn serde_default_state() {
let state = State::new();
let string = State::to_string(&state).unwrap();
State::from_str(&string).unwrap();
}
#[test]
fn serde_default_node() {
let node = Node::new_vec();
let string = Node::to_string(&node).unwrap();
Node::from_str_to_vec(&string).unwrap();
}
#[test]
fn serde_default_pool() {
let pool = Pool::new_vec();
let string = Pool::to_string(&pool).unwrap();
Pool::from_str_to_vec(&string).unwrap();
}
#[test]
fn serde_custom_state() {
let state = r#"
[gupax]
simple = true
auto_update = true
auto_p2pool = false
auto_xmrig = false
ask_before_quit = true
save_before_quit = true
update_via_tor = true
p2pool_path = "p2pool/p2pool"
xmrig_path = "xmrig/xmrig"
absolute_p2pool_path = "/home/hinto/p2pool/p2pool"
absolute_xmrig_path = "/home/hinto/xmrig/xmrig"
selected_width = 1280
selected_height = 960
selected_scale = 0.0
tab = "About"
ratio = "Width"
[status]
submenu = "P2pool"
payout_view = "Oldest"
monero_enabled = true
manual_hash = false
hashrate = 1241.23
hash_metric = "Hash"
[p2pool]
simple = true
mini = true
auto_ping = true
auto_select = true
backup_host = true
out_peers = 10
in_peers = 450
log_level = 3
node = "Seth"
arguments = ""
address = "44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW"
name = "Local Monero Node"
ip = "192.168.1.123"
rpc = "18089"
zmq = "18083"
selected_index = 0
selected_name = "Local Monero Node"
selected_ip = "192.168.1.123"
selected_rpc = "18089"
selected_zmq = "18083"
[xmrig]
simple = true
pause = 0
simple_rig = ""
arguments = ""
tls = false
keepalive = false
max_threads = 32
current_threads = 16
address = ""
api_ip = "localhost"
api_port = "18088"
name = "linux"
rig = "Gupax"
ip = "192.168.1.122"
port = "3333"
selected_index = 1
selected_name = "linux"
selected_rig = "Gupax"
selected_ip = "192.168.1.122"
selected_port = "3333"
[xvb]
token_confirmed = ""
token_inserted = ""
[version]
gupax = "v1.3.0"
p2pool = "v2.5"
xmrig = "v6.18.0"
"#;
let state = State::from_str(state).unwrap();
State::to_string(&state).unwrap();
}
#[test]
fn serde_custom_node() {
let node = r#"
['Local Monero Node']
ip = "localhost"
rpc = "18081"
zmq = "18083"
['asdf-_. ._123']
ip = "localhost"
rpc = "11"
zmq = "1234"
['aaa bbb']
ip = "192.168.2.333"
rpc = "1"
zmq = "65535"
"#;
let node = Node::from_str_to_vec(node).unwrap();
Node::to_string(&node).unwrap();
}
#[test]
fn serde_custom_pool() {
let pool = r#"
['Local P2Pool']
rig = "Gupax_v1.0.0"
ip = "localhost"
port = "3333"
['aaa xx .. -']
rig = "Gupax"
ip = "192.168.22.22"
port = "1"
[' a']
rig = "Gupax_v1.0.0"
ip = "127.0.0.1"
port = "65535"
"#;
let pool = Pool::from_str_to_vec(pool).unwrap();
Pool::to_string(&pool).unwrap();
}
// Make sure we keep the user's old values that are still
// valid but discard the ones that don't exist anymore.
#[test]
fn merge_state() {
let bad_state = r#"
[gupax]
SETTING_THAT_DOESNT_EXIST_ANYMORE = 123123
simple = false
auto_update = true
auto_p2pool = false
auto_xmrig = false
ask_before_quit = true
save_before_quit = true
update_via_tor = true
p2pool_path = "p2pool/p2pool"
xmrig_path = "xmrig/xmrig"
absolute_p2pool_path = ""
absolute_xmrig_path = ""
selected_width = 0
selected_height = 0
tab = "About"
ratio = "Width"
[p2pool]
SETTING_THAT_DOESNT_EXIST_ANYMORE = "String"
simple = true
mini = true
auto_ping = true
auto_select = true
out_peers = 10
in_peers = 450
log_level = 6
node = "Seth"
arguments = ""
address = "44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW"
name = "Local Monero Node"
ip = "localhost"
rpc = "18081"
zmq = "18083"
selected_index = 0
selected_name = "Local Monero Node"
selected_ip = "localhost"
selected_rpc = "18081"
selected_zmq = "18083"
[xmrig]
SETTING_THAT_DOESNT_EXIST_ANYMORE = true
simple = true
pause = 0
simple_rig = ""
arguments = ""
tls = false
keepalive = false
max_threads = 32
current_threads = 16
address = ""
api_ip = "localhost"
api_port = "18088"
name = "Local P2Pool"
rig = "Gupax_v1.0.0"
ip = "localhost"
port = "3333"
selected_index = 0
selected_name = "Local P2Pool"
selected_rig = "Gupax_v1.0.0"
selected_ip = "localhost"
selected_port = "3333"
[xvb]
token = ""
[version]
gupax = "v1.0.0"
p2pool = "v2.5"
xmrig = "v6.18.0"
"#.to_string();
let merged_state = State::merge(&bad_state).unwrap();
let merged_state = State::to_string(&merged_state).unwrap();
println!("{}", merged_state);
assert!(merged_state.contains("simple = false"));
assert!(merged_state.contains("in_peers = 450"));
assert!(merged_state.contains("log_level = 6"));
assert!(merged_state.contains(r#"node = "Seth""#));
assert!(!merged_state.contains("SETTING_THAT_DOESNT_EXIST_ANYMORE"));
assert!(merged_state.contains("44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW"));
assert!(merged_state.contains("backup_host = true"));
}
#[test]
fn create_and_serde_gupax_p2pool_api() {
use crate::disk::gupax_p2pool_api::GupaxP2poolApi;
use crate::xmr::AtomicUnit;
use crate::xmr::PayoutOrd;
// Get API dir, fill paths.
let mut api = GupaxP2poolApi::new();
let mut path = crate::disk::get_gupax_data_path().unwrap();
path.push(crate::disk::GUPAX_P2POOL_API_DIRECTORY);
GupaxP2poolApi::fill_paths(&mut api, &path);
println!("{:#?}", api);
// Create, write some fake data.
GupaxP2poolApi::create_all_files(&path).unwrap();
api.log = "NOTICE 2022-01-27 01:30:23.1377 P2Pool You received a payout of 0.000000000001 XMR in block 2642816".to_string();
api.payout_u64 = 1;
api.xmr = AtomicUnit::from_u64(2);
let (date, atomic_unit, block) = PayoutOrd::parse_raw_payout_line(&api.log);
let formatted_log_line = GupaxP2poolApi::format_payout(&date, &atomic_unit, &block);
GupaxP2poolApi::write_to_all_files(&api, &formatted_log_line).unwrap();
println!("AFTER WRITE: {:#?}", api);
// Read
GupaxP2poolApi::read_all_files_and_update(&mut api).unwrap();
println!("AFTER READ: {:#?}", api);
// Assert that the file read mutated the internal struct correctly.
assert_eq!(api.payout_u64, 1);
assert_eq!(api.xmr.to_u64(), 2);
assert!(!api.payout_ord.is_empty());
assert!(api
.log
.contains("2022-01-27 01:30:23.1377 | 0.000000000001 XMR | Block 2,642,816"));
}
#[test]
fn convert_hash() {
use crate::disk::status::Hash;
let hash = 1.0;
assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Hash), 1.0);
assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Kilo), 0.001);
assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Mega), 0.000_001);
assert_eq!(Hash::convert(hash, Hash::Hash, Hash::Giga), 0.000_000_001);
let hash = 1.0;
assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Hash), 1_000.0);
assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Kilo), 1.0);
assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Mega), 0.001);
assert_eq!(Hash::convert(hash, Hash::Kilo, Hash::Giga), 0.000_001);
let hash = 1.0;
assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Hash), 1_000_000.0);
assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Kilo), 1_000.0);
assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Mega), 1.0);
assert_eq!(Hash::convert(hash, Hash::Mega, Hash::Giga), 0.001);
let hash = 1.0;
assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Hash), 1_000_000_000.0);
assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Kilo), 1_000_000.0);
assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Mega), 1_000.0);
assert_eq!(Hash::convert(hash, Hash::Giga, Hash::Giga), 1.0);
}
}

View file

@ -1,18 +0,0 @@
// Free functions.
//---------------------------------------------------------------------------------------------------- Use
use crate::constants::*;
//----------------------------------------------------------------------------------------------------
#[cold]
#[inline(never)]
// Clamp the scaling resolution `f32` to a known good `f32`.
pub fn clamp_scale(scale: f32) -> f32 {
// Make sure it is finite.
if !scale.is_finite() {
return APP_DEFAULT_SCALE;
}
// Clamp between valid range.
scale.clamp(APP_MIN_SCALE, APP_MAX_SCALE)
}

File diff suppressed because it is too large Load diff

955
src/helper/mod.rs Normal file
View file

@ -0,0 +1,955 @@
// Gupax - GUI Uniting P2Pool And XMRig
//
// Copyright (c) 2022-2023 hinto-janai
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// This file represents the "helper" thread, which is the full separate thread
// that runs alongside the main [App] GUI thread. It exists for the entire duration
// of Gupax so that things can be handled without locking up the GUI thread.
//
// This thread is a continual 1 second loop, collecting available jobs on the
// way down and (if possible) asynchronously executing them at the very end.
//
// The main GUI thread will interface with this thread by mutating the Arc<Mutex>'s
// found here, e.g: User clicks [Stop P2Pool] -> Arc<Mutex<ProcessSignal> is set
// indicating to this thread during its loop: "I should stop P2Pool!", e.g:
//
// if lock!(p2pool).signal == ProcessSignal::Stop {
// stop_p2pool(),
// }
//
// This also includes all things related to handling the child processes (P2Pool/XMRig):
// piping their stdout/stderr/stdin, accessing their APIs (HTTP + disk files), etc.
//---------------------------------------------------------------------------------------------------- Import
use crate::helper::{
p2pool::{ImgP2pool, PubP2poolApi},
xmrig::{ImgXmrig, PubXmrigApi},
};
use crate::{constants::*, disk::gupax_p2pool_api::GupaxP2poolApi, human::*, macros::*};
use log::*;
use std::path::Path;
use std::{
path::PathBuf,
sync::{Arc, Mutex},
thread,
time::*,
};
pub mod p2pool;
pub mod xmrig;
//---------------------------------------------------------------------------------------------------- Constants
// The max amount of bytes of process output we are willing to
// hold in memory before it's too much and we need to reset.
const MAX_GUI_OUTPUT_BYTES: usize = 500_000;
// Just a little leeway so a reset will go off before the [String] allocates more memory.
const GUI_OUTPUT_LEEWAY: usize = MAX_GUI_OUTPUT_BYTES - 1000;
// Some constants for generating hashrate/difficulty.
const MONERO_BLOCK_TIME_IN_SECONDS: u64 = 120;
const P2POOL_BLOCK_TIME_IN_SECONDS: u64 = 10;
//---------------------------------------------------------------------------------------------------- [Helper] Struct
// A meta struct holding all the data that gets processed in this thread
pub struct Helper {
pub instant: Instant, // Gupax start as an [Instant]
pub uptime: HumanTime, // Gupax uptime formatting for humans
pub pub_sys: Arc<Mutex<Sys>>, // The public API for [sysinfo] that the [Status] tab reads from
pub p2pool: Arc<Mutex<Process>>, // P2Pool process state
pub xmrig: Arc<Mutex<Process>>, // XMRig process state
pub gui_api_p2pool: Arc<Mutex<PubP2poolApi>>, // P2Pool API state (for GUI thread)
pub gui_api_xmrig: Arc<Mutex<PubXmrigApi>>, // XMRig API state (for GUI thread)
pub img_p2pool: Arc<Mutex<ImgP2pool>>, // A static "image" of the data P2Pool started with
pub img_xmrig: Arc<Mutex<ImgXmrig>>, // A static "image" of the data XMRig started with
pub_api_p2pool: Arc<Mutex<PubP2poolApi>>, // P2Pool API state (for Helper/P2Pool thread)
pub_api_xmrig: Arc<Mutex<PubXmrigApi>>, // XMRig API state (for Helper/XMRig thread)
pub gupax_p2pool_api: Arc<Mutex<GupaxP2poolApi>>, //
}
// The communication between the data here and the GUI thread goes as follows:
// [GUI] <---> [Helper] <---> [Watchdog] <---> [Private Data only available here]
//
// Both [GUI] and [Helper] own their separate [Pub*Api] structs.
// Since P2Pool & XMRig will be updating their information out of sync,
// it's the helpers job to lock everything, and move the watchdog [Pub*Api]s
// on a 1-second interval into the [GUI]'s [Pub*Api] struct, atomically.
//----------------------------------------------------------------------------------------------------
#[derive(Debug, Clone)]
pub struct Sys {
pub gupax_uptime: String,
pub gupax_cpu_usage: String,
pub gupax_memory_used_mb: String,
pub system_cpu_model: String,
pub system_memory: String,
pub system_cpu_usage: String,
}
impl Sys {
pub fn new() -> Self {
Self {
gupax_uptime: "0 seconds".to_string(),
gupax_cpu_usage: "???%".to_string(),
gupax_memory_used_mb: "??? megabytes".to_string(),
system_cpu_usage: "???%".to_string(),
system_memory: "???GB / ???GB".to_string(),
system_cpu_model: "???".to_string(),
}
}
}
impl Default for Sys {
fn default() -> Self {
Self::new()
}
}
//---------------------------------------------------------------------------------------------------- [Process] Struct
// This holds all the state of a (child) process.
// The main GUI thread will use this to display console text, online state, etc.
#[derive(Debug)]
pub struct Process {
pub name: ProcessName, // P2Pool or XMRig?
pub state: ProcessState, // The state of the process (alive, dead, etc)
pub signal: ProcessSignal, // Did the user click [Start/Stop/Restart]?
// STDIN Problem:
// - User can input many many commands in 1 second
// - The process loop only processes every 1 second
// - If there is only 1 [String] holding the user input,
// the user could overwrite their last input before
// the loop even has a chance to process their last command
// STDIN Solution:
// - When the user inputs something, push it to a [Vec]
// - In the process loop, loop over every [Vec] element and
// send each one individually to the process stdin
//
pub input: Vec<String>,
// 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<Arc<Mutex<Box<dyn portable_pty::Child + Send + std::marker::Sync>>>>, // STDOUT/STDERR is combined automatically thanks to this PTY, nice
// stdin: Option<Box<dyn portable_pty::MasterPty + Send>>, // 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<Mutex<String>>,
output_pub: Arc<Mutex<String>>,
// Start time of process.
start: std::time::Instant,
}
//---------------------------------------------------------------------------------------------------- [Process] Impl
impl Process {
pub fn new(name: ProcessName, _args: String, _path: PathBuf) -> Self {
Self {
name,
state: ProcessState::Dead,
signal: ProcessSignal::None,
start: Instant::now(),
// stdin: Option::None,
// child: Option::None,
output_parse: arc_mut!(String::with_capacity(500)),
output_pub: arc_mut!(String::with_capacity(500)),
input: vec![String::new()],
}
}
// Borrow a [&str], return an owned split collection
#[inline]
pub fn parse_args(args: &str) -> Vec<String> {
args.split_whitespace().map(|s| s.to_owned()).collect()
}
#[inline]
// Convenience functions
pub fn is_alive(&self) -> bool {
self.state == ProcessState::Alive
|| self.state == ProcessState::Middle
|| self.state == ProcessState::Syncing
|| self.state == ProcessState::NotMining
}
#[inline]
pub fn is_waiting(&self) -> bool {
self.state == ProcessState::Middle || self.state == ProcessState::Waiting
}
#[inline]
pub fn is_syncing(&self) -> bool {
self.state == ProcessState::Syncing
}
#[inline]
pub fn is_not_mining(&self) -> bool {
self.state == ProcessState::NotMining
}
}
//---------------------------------------------------------------------------------------------------- [Process*] Enum
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum ProcessState {
Alive, // Process is online, GREEN!
Dead, // Process is dead, BLACK!
Failed, // Process is dead AND exited with a bad code, RED!
Middle, // Process is in the middle of something ([re]starting/stopping), YELLOW!
Waiting, // Process was successfully killed by a restart, and is ready to be started again, YELLOW!
// Only for P2Pool, ORANGE.
Syncing,
// Only for XMRig, ORANGE.
NotMining,
}
impl Default for ProcessState {
fn default() -> Self {
Self::Dead
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum ProcessSignal {
None,
Start,
Stop,
Restart,
}
impl Default for ProcessSignal {
fn default() -> Self {
Self::None
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum ProcessName {
P2pool,
Xmrig,
}
impl std::fmt::Display for ProcessState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:#?}", self)
}
}
impl std::fmt::Display for ProcessSignal {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:#?}", self)
}
}
impl std::fmt::Display for ProcessName {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
ProcessName::P2pool => write!(f, "P2Pool"),
ProcessName::Xmrig => write!(f, "XMRig"),
}
}
}
//---------------------------------------------------------------------------------------------------- [Helper]
impl Helper {
//---------------------------------------------------------------------------------------------------- General Functions
#[allow(clippy::too_many_arguments)]
pub fn new(
instant: std::time::Instant,
pub_sys: Arc<Mutex<Sys>>,
p2pool: Arc<Mutex<Process>>,
xmrig: Arc<Mutex<Process>>,
gui_api_p2pool: Arc<Mutex<PubP2poolApi>>,
gui_api_xmrig: Arc<Mutex<PubXmrigApi>>,
img_p2pool: Arc<Mutex<ImgP2pool>>,
img_xmrig: Arc<Mutex<ImgXmrig>>,
gupax_p2pool_api: Arc<Mutex<GupaxP2poolApi>>,
) -> Self {
Self {
instant,
pub_sys,
uptime: HumanTime::into_human(instant.elapsed()),
pub_api_p2pool: arc_mut!(PubP2poolApi::new()),
pub_api_xmrig: arc_mut!(PubXmrigApi::new()),
// These are created when initializing [App], since it needs a handle to it as well
p2pool,
xmrig,
gui_api_p2pool,
gui_api_xmrig,
img_p2pool,
img_xmrig,
gupax_p2pool_api,
}
}
// Reset output if larger than max bytes.
// This will also append a message showing it was reset.
fn check_reset_gui_output(output: &mut String, name: ProcessName) {
let len = output.len();
if len > GUI_OUTPUT_LEEWAY {
info!(
"{} Watchdog | Output is nearing {} bytes, resetting!",
name, MAX_GUI_OUTPUT_BYTES
);
let text = format!("{}\n{} GUI log is exceeding the maximum: {} bytes!\nI've reset the logs for you!\n{}\n\n\n\n", HORI_CONSOLE, name, MAX_GUI_OUTPUT_BYTES, HORI_CONSOLE);
output.clear();
output.push_str(&text);
debug!("{} Watchdog | Resetting GUI output ... OK", name);
} else {
debug!(
"{} Watchdog | GUI output reset not needed! Current byte length ... {}",
name, len
);
}
}
// Read P2Pool/XMRig's API file to a [String].
fn path_to_string(
path: &Path,
name: ProcessName,
) -> std::result::Result<String, std::io::Error> {
match std::fs::read_to_string(path) {
Ok(s) => Ok(s),
Err(e) => {
warn!("{} API | [{}] read error: {}", name, path.display(), e);
Err(e)
}
}
}
//---------------------------------------------------------------------------------------------------- The "helper"
#[inline(always)] // called once
fn update_pub_sys_from_sysinfo(
sysinfo: &sysinfo::System,
pub_sys: &mut Sys,
pid: &sysinfo::Pid,
helper: &Helper,
max_threads: usize,
) {
let gupax_uptime = helper.uptime.to_string();
let cpu = &sysinfo.cpus()[0];
let gupax_cpu_usage = format!(
"{:.2}%",
sysinfo.process(*pid).unwrap().cpu_usage() / (max_threads as f32)
);
let gupax_memory_used_mb =
HumanNumber::from_u64(sysinfo.process(*pid).unwrap().memory() / 1_000_000);
let gupax_memory_used_mb = format!("{} megabytes", gupax_memory_used_mb);
let system_cpu_model = format!("{} ({}MHz)", cpu.brand(), cpu.frequency());
let system_memory = {
let used = (sysinfo.used_memory() as f64) / 1_000_000_000.0;
let total = (sysinfo.total_memory() as f64) / 1_000_000_000.0;
format!("{:.3} GB / {:.3} GB", used, total)
};
let system_cpu_usage = {
let mut total: f32 = 0.0;
for cpu in sysinfo.cpus() {
total += cpu.cpu_usage();
}
format!("{:.2}%", total / (max_threads as f32))
};
*pub_sys = Sys {
gupax_uptime,
gupax_cpu_usage,
gupax_memory_used_mb,
system_cpu_usage,
system_memory,
system_cpu_model,
};
}
#[cold]
#[inline(never)]
// The "helper" thread. Syncs data between threads here and the GUI.
#[allow(clippy::await_holding_lock)]
pub fn spawn_helper(
helper: &Arc<Mutex<Self>>,
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<Mutex>] the other
// thread is already locking. Yes, I figured this out the hard way, hence the vast amount of debug!() messages.
// Example of different order (BAD!):
//
// GUI Main -> locks [p2pool] first
// Helper -> locks [gui_api_p2pool] first
// GUI Status Tab -> tries to lock [gui_api_p2pool] -> CAN'T
// Helper -> tries to lock [p2pool] -> CAN'T
//
// These two threads are now in a deadlock because both
// are trying to access locks the other one already has.
//
// The locking order here must be in the same chronological
// order as the main GUI thread (top to bottom).
let helper = Arc::clone(helper);
let lock = lock!(helper);
let p2pool = Arc::clone(&lock.p2pool);
let xmrig = Arc::clone(&lock.xmrig);
let pub_sys = Arc::clone(&lock.pub_sys);
let gui_api_p2pool = Arc::clone(&lock.gui_api_p2pool);
let gui_api_xmrig = Arc::clone(&lock.gui_api_xmrig);
let pub_api_p2pool = Arc::clone(&lock.pub_api_p2pool);
let pub_api_xmrig = Arc::clone(&lock.pub_api_xmrig);
drop(lock);
let sysinfo_cpu = sysinfo::CpuRefreshKind::everything();
let sysinfo_processes = sysinfo::ProcessRefreshKind::new().with_cpu();
thread::spawn(move || {
info!("Helper | Hello from helper thread! Entering loop where I will spend the rest of my days...");
// Begin loop
loop {
// 1. Loop init timestamp
let start = Instant::now();
debug!("Helper | ----------- Start of loop -----------");
// Ignore the invasive [debug!()] messages on the right side of the code.
// The reason why they are there are so that it's extremely easy to track
// down the culprit of an [Arc<Mutex>] deadlock. I know, they're ugly.
// 2. Lock... EVERYTHING!
let mut lock = lock!(helper);
debug!("Helper | Locking (1/9) ... [helper]");
let p2pool = lock!(p2pool);
debug!("Helper | Locking (2/9) ... [p2pool]");
let xmrig = lock!(xmrig);
debug!("Helper | Locking (3/9) ... [xmrig]");
let mut lock_pub_sys = lock!(pub_sys);
debug!("Helper | Locking (5/9) ... [pub_sys]");
let mut gui_api_p2pool = lock!(gui_api_p2pool);
debug!("Helper | Locking (6/9) ... [gui_api_p2pool]");
let mut gui_api_xmrig = lock!(gui_api_xmrig);
debug!("Helper | Locking (7/9) ... [gui_api_xmrig]");
let mut pub_api_p2pool = lock!(pub_api_p2pool);
debug!("Helper | Locking (8/9) ... [pub_api_p2pool]");
let mut pub_api_xmrig = lock!(pub_api_xmrig);
debug!("Helper | Locking (9/9) ... [pub_api_xmrig]");
// Calculate Gupax's uptime always.
lock.uptime = HumanTime::into_human(lock.instant.elapsed());
// If [P2Pool] is alive...
if p2pool.is_alive() {
debug!("Helper | P2Pool is alive! Running [combine_gui_pub_api()]");
PubP2poolApi::combine_gui_pub_api(&mut gui_api_p2pool, &mut pub_api_p2pool);
} else {
debug!("Helper | P2Pool is dead! Skipping...");
}
// If [XMRig] is alive...
if xmrig.is_alive() {
debug!("Helper | XMRig is alive! Running [combine_gui_pub_api()]");
PubXmrigApi::combine_gui_pub_api(&mut gui_api_xmrig, &mut pub_api_xmrig);
} else {
debug!("Helper | XMRig is dead! Skipping...");
}
// 2. Selectively refresh [sysinfo] for only what we need (better performance).
sysinfo.refresh_cpu_specifics(sysinfo_cpu);
debug!("Helper | Sysinfo refresh (1/3) ... [cpu]");
sysinfo.refresh_processes_specifics(sysinfo_processes);
debug!("Helper | Sysinfo refresh (2/3) ... [processes]");
sysinfo.refresh_memory();
debug!("Helper | Sysinfo refresh (3/3) ... [memory]");
debug!("Helper | Sysinfo OK, running [update_pub_sys_from_sysinfo()]");
Self::update_pub_sys_from_sysinfo(
&sysinfo,
&mut lock_pub_sys,
&pid,
&lock,
max_threads,
);
// 3. Drop... (almost) EVERYTHING... IN REVERSE!
drop(lock_pub_sys);
debug!("Helper | Unlocking (1/9) ... [pub_sys]");
drop(xmrig);
debug!("Helper | Unlocking (2/9) ... [xmrig]");
drop(p2pool);
debug!("Helper | Unlocking (3/9) ... [p2pool]");
drop(pub_api_xmrig);
debug!("Helper | Unlocking (4/9) ... [pub_api_xmrig]");
drop(pub_api_p2pool);
debug!("Helper | Unlocking (5/9) ... [pub_api_p2pool]");
drop(gui_api_xmrig);
debug!("Helper | Unlocking (6/9) ... [gui_api_xmrig]");
drop(gui_api_p2pool);
debug!("Helper | Unlocking (7/9) ... [gui_api_p2pool]");
drop(lock);
debug!("Helper | Unlocking (8/9) ... [helper]");
// 4. Calculate if we should sleep or not.
// If we should sleep, how long?
let elapsed = start.elapsed().as_millis();
if elapsed < 1000 {
// Casting from u128 to u64 should be safe here, because [elapsed]
// is less than 1000, meaning it can fit into a u64 easy.
let sleep = (1000 - elapsed) as u64;
debug!("Helper | END OF LOOP - Sleeping for [{}]ms...", sleep);
sleep!(sleep);
} else {
debug!("Helper | END OF LOOP - Not sleeping!");
}
// 5. End loop
}
});
}
}
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod test {
use crate::helper::p2pool::{PrivP2poolLocalApi, PrivP2poolNetworkApi};
use super::*;
#[test]
fn reset_gui_output() {
let max = crate::helper::GUI_OUTPUT_LEEWAY;
let mut string = String::with_capacity(max);
for _ in 0..=max {
string.push('0');
}
Helper::check_reset_gui_output(&mut string, ProcessName::P2pool);
// Some text gets added, so just check for less than 500 bytes.
assert!(string.len() < 500);
}
#[test]
fn combine_gui_pub_p2pool_api() {
use crate::helper::PubP2poolApi;
let mut gui_api = PubP2poolApi::new();
let mut pub_api = PubP2poolApi::new();
pub_api.payouts = 1;
pub_api.payouts_hour = 2.0;
pub_api.payouts_day = 3.0;
pub_api.payouts_month = 4.0;
pub_api.xmr = 1.0;
pub_api.xmr_hour = 2.0;
pub_api.xmr_day = 3.0;
pub_api.xmr_month = 4.0;
println!("BEFORE - GUI_API: {:#?}\nPUB_API: {:#?}", gui_api, pub_api);
assert_ne!(gui_api, pub_api);
PubP2poolApi::combine_gui_pub_api(&mut gui_api, &mut pub_api);
println!("AFTER - GUI_API: {:#?}\nPUB_API: {:#?}", gui_api, pub_api);
assert_eq!(gui_api, pub_api);
pub_api.xmr = 2.0;
PubP2poolApi::combine_gui_pub_api(&mut gui_api, &mut pub_api);
assert_eq!(gui_api, pub_api);
assert_eq!(gui_api.xmr, 2.0);
assert_eq!(pub_api.xmr, 2.0);
}
#[test]
fn calc_payouts_and_xmr_from_output_p2pool() {
use crate::helper::PubP2poolApi;
use std::sync::{Arc, Mutex};
let public = Arc::new(Mutex::new(PubP2poolApi::new()));
let output_parse = Arc::new(Mutex::new(String::from(
r#"payout of 5.000000000001 XMR in block 1111
payout of 5.000000000001 XMR in block 1112
payout of 5.000000000001 XMR in block 1113"#,
)));
let output_pub = Arc::new(Mutex::new(String::new()));
let elapsed = std::time::Duration::from_secs(60);
let process = Arc::new(Mutex::new(Process::new(
ProcessName::P2pool,
"".to_string(),
PathBuf::new(),
)));
PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process);
let public = public.lock().unwrap();
println!("{:#?}", public);
assert_eq!(public.payouts, 3);
assert_eq!(public.payouts_hour, 180.0);
assert_eq!(public.payouts_day, 4320.0);
assert_eq!(public.payouts_month, 129600.0);
assert_eq!(public.xmr, 15.000000000003);
assert_eq!(public.xmr_hour, 900.00000000018);
assert_eq!(public.xmr_day, 21600.00000000432);
assert_eq!(public.xmr_month, 648000.0000001296);
}
#[test]
fn set_p2pool_synchronized() {
use crate::helper::PubP2poolApi;
use std::sync::{Arc, Mutex};
let public = Arc::new(Mutex::new(PubP2poolApi::new()));
let output_parse = Arc::new(Mutex::new(String::from(
r#"payout of 5.000000000001 XMR in block 1111
NOTICE 2021-12-27 21:42:17.2008 SideChain SYNCHRONIZED
payout of 5.000000000001 XMR in block 1113"#,
)));
let output_pub = Arc::new(Mutex::new(String::new()));
let elapsed = std::time::Duration::from_secs(60);
let process = Arc::new(Mutex::new(Process::new(
ProcessName::P2pool,
"".to_string(),
PathBuf::new(),
)));
// It only gets checked if we're `Syncing`.
process.lock().unwrap().state = ProcessState::Syncing;
PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process);
println!("{:#?}", process);
assert!(process.lock().unwrap().state == ProcessState::Alive);
}
#[test]
fn p2pool_synchronized_false_positive() {
use crate::helper::PubP2poolApi;
use std::sync::{Arc, Mutex};
let public = Arc::new(Mutex::new(PubP2poolApi::new()));
// The SideChain that is "SYNCHRONIZED" in this output is
// probably not main/mini, but the sidechain started on height 1,
// so this should _not_ trigger alive state.
let output_parse = Arc::new(Mutex::new(String::from(
r#"payout of 5.000000000001 XMR in block 1111
SideChain new chain tip: next height = 1
NOTICE 2021-12-27 21:42:17.2008 SideChain SYNCHRONIZED
payout of 5.000000000001 XMR in block 1113"#,
)));
let output_pub = Arc::new(Mutex::new(String::new()));
let elapsed = std::time::Duration::from_secs(60);
let process = Arc::new(Mutex::new(Process::new(
ProcessName::P2pool,
"".to_string(),
PathBuf::new(),
)));
// It only gets checked if we're `Syncing`.
process.lock().unwrap().state = ProcessState::Syncing;
PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process);
println!("{:#?}", process);
assert!(process.lock().unwrap().state == ProcessState::Syncing); // still syncing
}
#[test]
fn p2pool_synchronized_double_synchronized() {
use crate::helper::PubP2poolApi;
use std::sync::{Arc, Mutex};
let public = Arc::new(Mutex::new(PubP2poolApi::new()));
// The 1st SideChain that is "SYNCHRONIZED" in this output is
// the sidechain started on height 1, but there is another one
// which means the real main/mini is probably synced,
// so this _should_ trigger alive state.
let output_parse = Arc::new(Mutex::new(String::from(
r#"payout of 5.000000000001 XMR in block 1111
SideChain new chain tip: next height = 1
NOTICE 2021-12-27 21:42:17.2008 SideChain SYNCHRONIZED
payout of 5.000000000001 XMR in block 1113
NOTICE 2021-12-27 21:42:17.2100 SideChain SYNCHRONIZED"#,
)));
let output_pub = Arc::new(Mutex::new(String::new()));
let elapsed = std::time::Duration::from_secs(60);
let process = Arc::new(Mutex::new(Process::new(
ProcessName::P2pool,
"".to_string(),
PathBuf::new(),
)));
// It only gets checked if we're `Syncing`.
process.lock().unwrap().state = ProcessState::Syncing;
PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process);
println!("{:#?}", process);
assert!(process.lock().unwrap().state == ProcessState::Alive);
}
#[test]
fn update_pub_p2pool_from_local_network_pool() {
use crate::helper::p2pool::PoolStatistics;
use crate::helper::p2pool::PrivP2poolLocalApi;
use crate::helper::p2pool::PrivP2poolNetworkApi;
use crate::helper::p2pool::PrivP2poolPoolApi;
use crate::helper::PubP2poolApi;
use std::sync::{Arc, Mutex};
let public = Arc::new(Mutex::new(PubP2poolApi::new()));
let local = PrivP2poolLocalApi {
hashrate_15m: 10_000,
hashrate_1h: 20_000,
hashrate_24h: 30_000,
shares_found: 1000,
average_effort: 100.000,
current_effort: 200.000,
connections: 1234,
};
let network = PrivP2poolNetworkApi {
difficulty: 300_000_000_000,
hash: "asdf".to_string(),
height: 1234,
reward: 2345,
timestamp: 3456,
};
let pool = PrivP2poolPoolApi {
pool_statistics: PoolStatistics {
hashRate: 1_000_000, // 1 MH/s
miners: 1_000,
},
};
// Update Local
PubP2poolApi::update_from_local(&public, local);
let p = public.lock().unwrap();
println!("AFTER LOCAL: {:#?}", p);
assert_eq!(p.hashrate_15m.to_string(), "10,000");
assert_eq!(p.hashrate_1h.to_string(), "20,000");
assert_eq!(p.hashrate_24h.to_string(), "30,000");
assert_eq!(p.shares_found.to_string(), "1,000");
assert_eq!(p.average_effort.to_string(), "100.00%");
assert_eq!(p.current_effort.to_string(), "200.00%");
assert_eq!(p.connections.to_string(), "1,234");
assert_eq!(p.user_p2pool_hashrate_u64, 20000);
drop(p);
// Update Network + Pool
PubP2poolApi::update_from_network_pool(&public, network, pool);
let p = public.lock().unwrap();
println!("AFTER NETWORK+POOL: {:#?}", p);
assert_eq!(p.monero_difficulty.to_string(), "300,000,000,000");
assert_eq!(p.monero_hashrate.to_string(), "2.500 GH/s");
assert_eq!(p.hash.to_string(), "asdf");
assert_eq!(p.height.to_string(), "1,234");
assert_eq!(p.reward.to_u64(), 2345);
assert_eq!(p.p2pool_difficulty.to_string(), "10,000,000");
assert_eq!(p.p2pool_hashrate.to_string(), "1.000 MH/s");
assert_eq!(p.miners.to_string(), "1,000");
assert_eq!(
p.solo_block_mean.to_string(),
"5 months, 21 days, 9 hours, 52 minutes"
);
assert_eq!(
p.p2pool_block_mean.to_string(),
"3 days, 11 hours, 20 minutes"
);
assert_eq!(p.p2pool_share_mean.to_string(), "8 minutes, 20 seconds");
assert_eq!(p.p2pool_percent.to_string(), "0.040000%");
assert_eq!(p.user_p2pool_percent.to_string(), "2.000000%");
assert_eq!(p.user_monero_percent.to_string(), "0.000800%");
drop(p);
}
#[test]
fn set_xmrig_mining() {
use crate::helper::PubXmrigApi;
use std::sync::{Arc, Mutex};
let public = Arc::new(Mutex::new(PubXmrigApi::new()));
let output_parse = Arc::new(Mutex::new(String::from(
"[2022-02-12 12:49:30.311] net no active pools, stop mining",
)));
let output_pub = Arc::new(Mutex::new(String::new()));
let elapsed = std::time::Duration::from_secs(60);
let process = Arc::new(Mutex::new(Process::new(
ProcessName::Xmrig,
"".to_string(),
PathBuf::new(),
)));
process.lock().unwrap().state = ProcessState::Alive;
PubXmrigApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process);
println!("{:#?}", process);
assert!(process.lock().unwrap().state == ProcessState::NotMining);
let output_parse = Arc::new(Mutex::new(String::from("[2022-02-12 12:49:30.311] net new job from 192.168.2.1:3333 diff 402K algo rx/0 height 2241142 (11 tx)")));
PubXmrigApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &process);
assert!(process.lock().unwrap().state == ProcessState::Alive);
}
#[test]
fn serde_priv_p2pool_local_api() {
let data = r#"{
"hashrate_15m": 12,
"hashrate_1h": 11111,
"hashrate_24h": 468967,
"total_hashes": 2019283840922394082390,
"shares_found": 289037,
"average_effort": 915.563,
"current_effort": 129.297,
"connections": 123,
"incoming_connections": 96
}"#;
let priv_api = PrivP2poolLocalApi::from_str(data).unwrap();
let json = serde_json::ser::to_string_pretty(&priv_api).unwrap();
println!("{}", json);
let data_after_ser = r#"{
"hashrate_15m": 12,
"hashrate_1h": 11111,
"hashrate_24h": 468967,
"shares_found": 289037,
"average_effort": 915.563,
"current_effort": 129.297,
"connections": 123
}"#;
assert_eq!(data_after_ser, json)
}
#[test]
fn serde_priv_p2pool_network_api() {
let data = r#"{
"difficulty": 319028180924,
"hash": "22ae1b83d727bb2ff4efc17b485bc47bc8bf5e29a7b3af65baf42213ac70a39b",
"height": 2776576,
"reward": 600499860000,
"timestamp": 1670953659
}"#;
let priv_api = PrivP2poolNetworkApi::from_str(data).unwrap();
let json = serde_json::ser::to_string_pretty(&priv_api).unwrap();
println!("{}", json);
let data_after_ser = r#"{
"difficulty": 319028180924,
"hash": "22ae1b83d727bb2ff4efc17b485bc47bc8bf5e29a7b3af65baf42213ac70a39b",
"height": 2776576,
"reward": 600499860000,
"timestamp": 1670953659
}"#;
assert_eq!(data_after_ser, json)
}
#[test]
fn serde_priv_p2pool_pool_api() {
let data = r#"{
"pool_list": ["pplns"],
"pool_statistics": {
"hashRate": 10225772,
"miners": 713,
"totalHashes": 487463929193948,
"lastBlockFoundTime": 1670453228,
"lastBlockFound": 2756570,
"totalBlocksFound": 4
}
}"#;
let priv_api = crate::helper::p2pool::PrivP2poolPoolApi::from_str(data).unwrap();
let json = serde_json::ser::to_string_pretty(&priv_api).unwrap();
println!("{}", json);
let data_after_ser = r#"{
"pool_statistics": {
"hashRate": 10225772,
"miners": 713
}
}"#;
assert_eq!(data_after_ser, json)
}
#[test]
fn serde_priv_xmrig_api() {
let data = r#"{
"id": "6226e3sd0cd1a6es",
"worker_id": "hinto",
"uptime": 123,
"restricted": true,
"resources": {
"memory": {
"free": 123,
"total": 123123,
"resident_set_memory": 123123123
},
"load_average": [10.97, 10.58, 10.47],
"hardware_concurrency": 12
},
"features": ["api", "asm", "http", "hwloc", "tls", "opencl", "cuda"],
"results": {
"diff_current": 123,
"shares_good": 123,
"shares_total": 123,
"avg_time": 123,
"avg_time_ms": 123,
"hashes_total": 123,
"best": [123, 123, 123, 13, 123, 123, 123, 123, 123, 123],
"error_log": []
},
"algo": "rx/0",
"connection": {
"pool": "localhost:3333",
"ip": "127.0.0.1",
"uptime": 123,
"uptime_ms": 123,
"ping": 0,
"failures": 0,
"tls": null,
"tls-fingerprint": null,
"algo": "rx/0",
"diff": 123,
"accepted": 123,
"rejected": 123,
"avg_time": 123,
"avg_time_ms": 123,
"hashes_total": 123,
"error_log": []
},
"version": "6.18.0",
"kind": "miner",
"ua": "XMRig/6.18.0 (Linux x86_64) libuv/2.0.0-dev gcc/10.2.1",
"cpu": {
"brand": "blah blah blah",
"family": 1,
"model": 2,
"stepping": 0,
"proc_info": 123,
"aes": true,
"avx2": true,
"x64": true,
"64_bit": true,
"l2": 123123,
"l3": 123123,
"cores": 12,
"threads": 24,
"packages": 1,
"nodes": 1,
"backend": "hwloc/2.8.0a1-git",
"msr": "ryzen_19h",
"assembly": "ryzen",
"arch": "x86_64",
"flags": ["aes", "vaes", "avx", "avx2", "bmi2", "osxsave", "pdpe1gb", "sse2", "ssse3", "sse4.1", "popcnt", "cat_l3"]
},
"donate_level": 0,
"paused": false,
"algorithms": ["cn/1", "cn/2", "cn/r", "cn/fast", "cn/half", "cn/xao", "cn/rto", "cn/rwz", "cn/zls", "cn/double", "cn/ccx", "cn-lite/1", "cn-heavy/0", "cn-heavy/tube", "cn-heavy/xhv", "cn-pico", "cn-pico/tlo", "cn/upx2", "rx/0", "rx/wow", "rx/arq", "rx/graft", "rx/sfx", "rx/keva", "argon2/chukwa", "argon2/chukwav2", "argon2/ninja", "astrobwt", "astrobwt/v2", "ghostrider"],
"hashrate": {
"total": [111.11, 111.11, 111.11],
"highest": 111.11,
"threads": [
[111.11, 111.11, 111.11]
]
},
"hugepages": true
}"#;
use crate::helper::xmrig::PrivXmrigApi;
let priv_api = serde_json::from_str::<PrivXmrigApi>(data).unwrap();
let json = serde_json::ser::to_string_pretty(&priv_api).unwrap();
println!("{}", json);
let data_after_ser = r#"{
"worker_id": "hinto",
"resources": {
"load_average": [
10.97,
10.58,
10.47
]
},
"connection": {
"diff": 123,
"accepted": 123,
"rejected": 123
},
"hashrate": {
"total": [
111.11,
111.11,
111.11
]
}
}"#;
assert_eq!(data_after_ser, json)
}
}

1252
src/helper/p2pool.rs Normal file

File diff suppressed because it is too large Load diff

782
src/helper/xmrig.rs Normal file
View file

@ -0,0 +1,782 @@
use crate::helper::{ProcessName, ProcessSignal, ProcessState};
use crate::regex::XMRIG_REGEX;
use crate::utils::sudo::SudoState;
use crate::{constants::*, human::*, macros::*};
use log::*;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::{
fmt::Write,
path::PathBuf,
process::Stdio,
sync::{Arc, Mutex},
thread,
time::*,
};
use super::{Helper, Process};
impl Helper {
#[cold]
#[inline(never)]
fn read_pty_xmrig(
output_parse: Arc<Mutex<String>>,
output_pub: Arc<Mutex<String>>,
reader: Box<dyn std::io::Read + Send>,
) {
use std::io::BufRead;
let mut stdout = std::io::BufReader::new(reader).lines();
// Run a ANSI escape sequence filter for the first few lines.
let mut i = 0;
while let Some(Ok(line)) = stdout.next() {
let line = strip_ansi_escapes::strip_str(line);
if let Err(e) = writeln!(lock!(output_parse), "{}", line) {
error!("XMRig PTY Parse | Output error: {}", e);
}
if let Err(e) = writeln!(lock!(output_pub), "{}", line) {
error!("XMRig PTY Pub | Output error: {}", e);
}
if i > 20 {
break;
} else {
i += 1;
}
}
while let Some(Ok(line)) = stdout.next() {
// println!("{}", line); // For debugging.
if let Err(e) = writeln!(lock!(output_parse), "{}", line) {
error!("XMRig PTY Parse | Output error: {}", e);
}
if let Err(e) = writeln!(lock!(output_pub), "{}", line) {
error!("XMRig PTY Pub | Output error: {}", e);
}
}
}
//---------------------------------------------------------------------------------------------------- XMRig specific, most functions are very similar to P2Pool's
#[cold]
#[inline(never)]
// If processes are started with [sudo] on macOS, they must also
// be killed with [sudo] (even if I have a direct handle to it as the
// parent process...!). This is only needed on macOS, not Linux.
fn sudo_kill(pid: u32, sudo: &Arc<Mutex<SudoState>>) -> bool {
// Spawn [sudo] to execute [kill] on the given [pid]
let mut child = std::process::Command::new("sudo")
.args(["--stdin", "kill", "-9", &pid.to_string()])
.stdin(Stdio::piped())
.spawn()
.unwrap();
// Write the [sudo] password to STDIN.
let mut stdin = child.stdin.take().unwrap();
use std::io::Write;
if let Err(e) = writeln!(stdin, "{}\n", lock!(sudo).pass) {
error!("Sudo Kill | STDIN error: {}", e);
}
// Return exit code of [sudo/kill].
child.wait().unwrap().success()
}
#[cold]
#[inline(never)]
// Just sets some signals for the watchdog thread to pick up on.
pub fn stop_xmrig(helper: &Arc<Mutex<Self>>) {
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<Mutex<Self>>,
state: &crate::disk::state::Xmrig,
path: &Path,
sudo: Arc<Mutex<SudoState>>,
) {
info!("XMRig | Attempting to restart...");
lock2!(helper, xmrig).signal = ProcessSignal::Restart;
lock2!(helper, xmrig).state = ProcessState::Middle;
let helper = Arc::clone(helper);
let state = state.clone();
let path = path.to_path_buf();
// This thread lives to wait, start xmrig then die.
thread::spawn(move || {
while lock2!(helper, xmrig).state != ProcessState::Waiting {
warn!("XMRig | Want to restart but process is still alive, waiting...");
sleep!(1000);
}
// Ok, process is not alive, start the new one!
info!("XMRig | Old process seems dead, starting new one!");
Self::start_xmrig(&helper, &state, &path, sudo);
});
info!("XMRig | Restart ... OK");
}
#[cold]
#[inline(never)]
pub fn start_xmrig(
helper: &Arc<Mutex<Self>>,
state: &crate::disk::state::Xmrig,
path: &Path,
sudo: Arc<Mutex<SudoState>>,
) {
lock2!(helper, xmrig).state = ProcessState::Middle;
let (args, api_ip_port) = Self::build_xmrig_args_and_mutate_img(helper, state, path);
// Print arguments & user settings to console
crate::disk::print_dash(&format!("XMRig | Launch arguments: {:#?}", args));
info!("XMRig | Using path: [{}]", path.display());
// Spawn watchdog thread
let process = Arc::clone(&lock!(helper).xmrig);
let gui_api = Arc::clone(&lock!(helper).gui_api_xmrig);
let pub_api = Arc::clone(&lock!(helper).pub_api_xmrig);
let path = path.to_path_buf();
thread::spawn(move || {
Self::spawn_xmrig_watchdog(process, gui_api, pub_api, args, path, sudo, api_ip_port);
});
}
#[cold]
#[inline(never)]
// Takes in some [State/Xmrig] and parses it to build the actual command arguments.
// Returns the [Vec] of actual arguments, and mutates the [ImgXmrig] for the main GUI thread
// It returns a value... and mutates a deeply nested passed argument... this is some pretty bad code...
pub fn build_xmrig_args_and_mutate_img(
helper: &Arc<Mutex<Self>>,
state: &crate::disk::state::Xmrig,
path: &std::path::Path,
) -> (Vec<String>, String) {
let mut args = Vec::with_capacity(500);
let mut api_ip = String::with_capacity(15);
let mut api_port = String::with_capacity(5);
let path = path.to_path_buf();
// The actual binary we're executing is [sudo], technically
// the XMRig path is just an argument to sudo, so add it.
// Before that though, add the ["--prompt"] flag and set it
// to emptyness so that it doesn't show up in the output.
if cfg!(unix) {
args.push(r#"--prompt="#.to_string());
args.push("--".to_string());
args.push(path.display().to_string());
}
// [Simple]
if state.simple {
// Build the xmrig argument
let rig = if state.simple_rig.is_empty() {
GUPAX_VERSION_UNDERSCORE.to_string()
} else {
state.simple_rig.clone()
}; // Rig name
args.push("--url".to_string());
args.push("127.0.0.1:3333".to_string()); // Local P2Pool (the default)
args.push("--threads".to_string());
args.push(state.current_threads.to_string()); // Threads
args.push("--user".to_string());
args.push(rig); // Rig name
args.push("--no-color".to_string()); // No color
args.push("--http-host".to_string());
args.push("127.0.0.1".to_string()); // HTTP API IP
args.push("--http-port".to_string());
args.push("18088".to_string()); // HTTP API Port
if state.pause != 0 {
args.push("--pause-on-active".to_string());
args.push(state.pause.to_string());
} // Pause on active
*lock2!(helper, img_xmrig) = ImgXmrig {
threads: state.current_threads.to_string(),
url: "127.0.0.1:3333 (Local P2Pool)".to_string(),
};
api_ip = "127.0.0.1".to_string();
api_port = "18088".to_string();
// [Advanced]
} else {
// Overriding command arguments
if !state.arguments.is_empty() {
// This parses the input and attempts to fill out
// the [ImgXmrig]... This is pretty bad code...
let mut last = "";
let lock = lock!(helper);
let mut xmrig_image = lock!(lock.img_xmrig);
for arg in state.arguments.split_whitespace() {
match last {
"--threads" => xmrig_image.threads = arg.to_string(),
"--url" => xmrig_image.url = arg.to_string(),
"--http-host" => {
api_ip = if arg == "localhost" {
"127.0.0.1".to_string()
} else {
arg.to_string()
}
}
"--http-port" => api_port = arg.to_string(),
_ => (),
}
args.push(if arg == "localhost" {
"127.0.0.1".to_string()
} else {
arg.to_string()
});
last = arg;
}
// Else, build the argument
} else {
// XMRig doesn't understand [localhost]
let ip = if state.ip == "localhost" || state.ip.is_empty() {
"127.0.0.1"
} else {
&state.ip
};
api_ip = if state.api_ip == "localhost" || state.api_ip.is_empty() {
"127.0.0.1".to_string()
} else {
state.api_ip.to_string()
};
api_port = if state.api_port.is_empty() {
"18088".to_string()
} else {
state.api_port.to_string()
};
let url = format!("{}:{}", ip, state.port); // Combine IP:Port into one string
args.push("--user".to_string());
args.push(state.address.clone()); // Wallet
args.push("--threads".to_string());
args.push(state.current_threads.to_string()); // Threads
args.push("--rig-id".to_string());
args.push(state.rig.to_string()); // Rig ID
args.push("--url".to_string());
args.push(url.clone()); // IP/Port
args.push("--http-host".to_string());
args.push(api_ip.to_string()); // HTTP API IP
args.push("--http-port".to_string());
args.push(api_port.to_string()); // HTTP API Port
args.push("--no-color".to_string()); // No color escape codes
if state.tls {
args.push("--tls".to_string());
} // TLS
if state.keepalive {
args.push("--keepalive".to_string());
} // Keepalive
if state.pause != 0 {
args.push("--pause-on-active".to_string());
args.push(state.pause.to_string());
} // Pause on active
*lock2!(helper, img_xmrig) = ImgXmrig {
url,
threads: state.current_threads.to_string(),
};
}
}
(args, format!("{}:{}", api_ip, api_port))
}
// We actually spawn [sudo] on Unix, with XMRig being the argument.
#[cfg(target_family = "unix")]
fn create_xmrig_cmd_unix(args: Vec<String>, 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<String>, path: PathBuf) -> portable_pty::CommandBuilder {
let mut cmd = portable_pty::cmdbuilder::CommandBuilder::new(path.clone());
cmd.args(args);
cmd.cwd(path.as_path().parent().unwrap());
cmd
}
#[cold]
#[inline(never)]
// The XMRig watchdog. Spawns 1 OS thread for reading a PTY (STDOUT+STDERR), and combines the [Child] with a PTY so STDIN actually works.
// This isn't actually async, a tokio runtime is unfortunately needed because [Hyper] is an async library (HTTP API calls)
#[tokio::main]
#[allow(clippy::await_holding_lock)]
async fn spawn_xmrig_watchdog(
process: Arc<Mutex<Process>>,
gui_api: Arc<Mutex<PubXmrigApi>>,
pub_api: Arc<Mutex<PubXmrigApi>>,
args: Vec<String>,
path: std::path::PathBuf,
sudo: Arc<Mutex<SudoState>>,
mut api_ip_port: String,
) {
// 1a. Create PTY
debug!("XMRig | Creating PTY...");
let pty = portable_pty::native_pty_system();
let pair = pty
.openpty(portable_pty::PtySize {
rows: 100,
cols: 1000,
pixel_width: 0,
pixel_height: 0,
})
.unwrap();
// 1b. Create command
debug!("XMRig | Creating command...");
#[cfg(target_os = "windows")]
let cmd = Self::create_xmrig_cmd_windows(args, path);
#[cfg(target_family = "unix")]
let cmd = Self::create_xmrig_cmd_unix(args, path);
// 1c. Create child
debug!("XMRig | Creating child...");
let child_pty = arc_mut!(pair.slave.spawn_command(cmd).unwrap());
drop(pair.slave);
let mut stdin = pair.master.take_writer().unwrap();
// 2. Input [sudo] pass, wipe, then drop.
if cfg!(unix) {
debug!("XMRig | Inputting [sudo] and wiping...");
// a) Sleep to wait for [sudo]'s non-echo prompt (on Unix).
// this prevents users pass from showing up in the STDOUT.
sleep!(3000);
if let Err(e) = writeln!(stdin, "{}", lock!(sudo).pass) {
error!("XMRig | Sudo STDIN error: {}", e);
};
SudoState::wipe(&sudo);
// b) Reset GUI STDOUT just in case.
debug!("XMRig | Clearing GUI output...");
lock!(gui_api).output.clear();
}
// 3. Set process state
debug!("XMRig | Setting process state...");
let mut lock = lock!(process);
lock.state = ProcessState::NotMining;
lock.signal = ProcessSignal::None;
lock.start = Instant::now();
let reader = pair.master.try_clone_reader().unwrap(); // Get STDOUT/STDERR before moving the PTY
drop(lock);
// 4. Spawn PTY read thread
debug!("XMRig | Spawning PTY read thread...");
let output_parse = Arc::clone(&lock!(process).output_parse);
let output_pub = Arc::clone(&lock!(process).output_pub);
thread::spawn(move || {
Self::read_pty_xmrig(output_parse, output_pub, reader);
});
let output_parse = Arc::clone(&lock!(process).output_parse);
let output_pub = Arc::clone(&lock!(process).output_pub);
let client: hyper::Client<hyper::client::HttpConnector> =
hyper::Client::builder().build(hyper::client::HttpConnector::new());
let start = lock!(process).start;
let api_uri = {
if !api_ip_port.ends_with('/') {
api_ip_port.push('/');
}
"http://".to_owned() + &api_ip_port + XMRIG_API_URI
};
info!("XMRig | Final API URI: {}", api_uri);
// Reset stats before loop
*lock!(pub_api) = PubXmrigApi::new();
*lock!(gui_api) = PubXmrigApi::new();
// 5. Loop as watchdog
info!("XMRig | Entering watchdog mode... woof!");
loop {
// Set timer
let now = Instant::now();
debug!("XMRig Watchdog | ----------- Start of loop -----------");
// Check if the process secretly died without us knowing :)
if let Ok(Some(code)) = lock!(child_pty).try_wait() {
debug!("XMRig Watchdog | Process secretly died on us! Getting exit status...");
let exit_status = match code.success() {
true => {
lock!(process).state = ProcessState::Dead;
"Successful"
}
false => {
lock!(process).state = ProcessState::Failed;
"Failed"
}
};
let uptime = HumanTime::into_human(start.elapsed());
info!(
"XMRig | Stopped ... Uptime was: [{}], Exit status: [{}]",
uptime, exit_status
);
if let Err(e) = writeln!(
lock!(gui_api).output,
"{}\nXMRig stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n",
HORI_CONSOLE,
uptime,
exit_status,
HORI_CONSOLE
) {
error!(
"XMRig Watchdog | GUI Uptime/Exit status write failed: {}",
e
);
}
lock!(process).signal = ProcessSignal::None;
debug!("XMRig Watchdog | Secret dead process reap OK, breaking");
break;
}
// Stop on [Stop/Restart] SIGNAL
let signal = lock!(process).signal;
if signal == ProcessSignal::Stop || signal == ProcessSignal::Restart {
debug!("XMRig Watchdog | Stop/Restart SIGNAL caught");
// macOS requires [sudo] again to kill [XMRig]
if cfg!(target_os = "macos") {
// If we're at this point, that means the user has
// entered their [sudo] pass again, after we wiped it.
// So, we should be able to find it in our [Arc<Mutex<SudoState>>].
Self::sudo_kill(lock!(child_pty).process_id().unwrap(), &sudo);
// And... wipe it again (only if we're stopping full).
// If we're restarting, the next start will wipe it for us.
if signal != ProcessSignal::Restart {
SudoState::wipe(&sudo);
}
} else if let Err(e) = lock!(child_pty).kill() {
error!("XMRig Watchdog | Kill error: {}", e);
}
let exit_status = match lock!(child_pty).wait() {
Ok(e) => {
let mut process = lock!(process);
if e.success() {
if process.signal == ProcessSignal::Stop {
process.state = ProcessState::Dead;
}
"Successful"
} else {
if process.signal == ProcessSignal::Stop {
process.state = ProcessState::Failed;
}
"Failed"
}
}
_ => {
let mut process = lock!(process);
if process.signal == ProcessSignal::Stop {
process.state = ProcessState::Failed;
}
"Unknown Error"
}
};
let uptime = HumanTime::into_human(start.elapsed());
info!(
"XMRig | Stopped ... Uptime was: [{}], Exit status: [{}]",
uptime, exit_status
);
if let Err(e) = writeln!(
lock!(gui_api).output,
"{}\nXMRig stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n\n\n",
HORI_CONSOLE,
uptime,
exit_status,
HORI_CONSOLE
) {
error!(
"XMRig Watchdog | GUI Uptime/Exit status write failed: {}",
e
);
}
let mut process = lock!(process);
match process.signal {
ProcessSignal::Stop => process.signal = ProcessSignal::None,
ProcessSignal::Restart => process.state = ProcessState::Waiting,
_ => (),
}
debug!("XMRig Watchdog | Stop/Restart SIGNAL done, breaking");
break;
}
// Check vector of user input
{
let mut lock = lock!(process);
if !lock.input.is_empty() {
let input = std::mem::take(&mut lock.input);
for line in input {
if line.is_empty() {
continue;
}
debug!(
"XMRig Watchdog | User input not empty, writing to STDIN: [{}]",
line
);
#[cfg(target_os = "windows")]
if let Err(e) = write!(stdin, "{}\r\n", line) {
error!("XMRig Watchdog | STDIN error: {}", e);
}
#[cfg(target_family = "unix")]
if let Err(e) = writeln!(stdin, "{}", line) {
error!("XMRig Watchdog | STDIN error: {}", e);
}
// Flush.
if let Err(e) = stdin.flush() {
error!("XMRig Watchdog | STDIN flush error: {}", e);
}
}
}
}
// Check if logs need resetting
debug!("XMRig Watchdog | Attempting GUI log reset check");
{
let mut lock = lock!(gui_api);
Self::check_reset_gui_output(&mut lock.output, ProcessName::Xmrig);
}
// Always update from output
debug!("XMRig Watchdog | Starting [update_from_output()]");
PubXmrigApi::update_from_output(
&pub_api,
&output_pub,
&output_parse,
start.elapsed(),
&process,
);
// Send an HTTP API request
debug!("XMRig Watchdog | Attempting HTTP API request...");
if let Ok(priv_api) = PrivXmrigApi::request_xmrig_api(client.clone(), &api_uri).await {
debug!("XMRig Watchdog | HTTP API request OK, attempting [update_from_priv()]");
PubXmrigApi::update_from_priv(&pub_api, priv_api);
} else {
warn!(
"XMRig Watchdog | Could not send HTTP API request to: {}",
api_uri
);
}
// Sleep (only if 900ms hasn't passed)
let elapsed = now.elapsed().as_millis();
// Since logic goes off if less than 1000, casting should be safe
if elapsed < 900 {
let sleep = (900 - elapsed) as u64;
debug!(
"XMRig Watchdog | END OF LOOP - Sleeping for [{}]ms...",
sleep
);
sleep!(sleep);
} else {
debug!("XMRig Watchdog | END OF LOOP - Not sleeping!");
}
}
// 5. If loop broke, we must be done here.
info!("XMRig Watchdog | Watchdog thread exiting... Goodbye!");
}
}
//---------------------------------------------------------------------------------------------------- [ImgXmrig]
#[derive(Debug, Clone)]
pub struct ImgXmrig {
pub threads: String,
pub url: String,
}
impl Default for ImgXmrig {
fn default() -> Self {
Self::new()
}
}
impl ImgXmrig {
pub fn new() -> Self {
Self {
threads: "???".to_string(),
url: "???".to_string(),
}
}
}
//---------------------------------------------------------------------------------------------------- Public XMRig API
#[derive(Debug, Clone)]
pub struct PubXmrigApi {
pub output: String,
pub uptime: HumanTime,
pub worker_id: String,
pub resources: HumanNumber,
pub hashrate: HumanNumber,
pub diff: HumanNumber,
pub accepted: HumanNumber,
pub rejected: HumanNumber,
pub hashrate_raw: f32,
}
impl Default for PubXmrigApi {
fn default() -> Self {
Self::new()
}
}
impl PubXmrigApi {
pub fn new() -> Self {
Self {
output: String::new(),
uptime: HumanTime::new(),
worker_id: "???".to_string(),
resources: HumanNumber::unknown(),
hashrate: HumanNumber::unknown(),
diff: HumanNumber::unknown(),
accepted: HumanNumber::unknown(),
rejected: HumanNumber::unknown(),
hashrate_raw: 0.0,
}
}
#[inline]
pub(super) fn combine_gui_pub_api(gui_api: &mut Self, pub_api: &mut Self) {
let output = std::mem::take(&mut gui_api.output);
let buf = std::mem::take(&mut pub_api.output);
*gui_api = Self {
output,
..std::mem::take(pub_api)
};
if !buf.is_empty() {
gui_api.output.push_str(&buf);
}
}
// This combines the buffer from the PTY thread [output_pub]
// with the actual [PubApiXmrig] output field.
pub(super) fn update_from_output(
public: &Arc<Mutex<Self>>,
output_parse: &Arc<Mutex<String>>,
output_pub: &Arc<Mutex<String>>,
elapsed: std::time::Duration,
process: &Arc<Mutex<Process>>,
) {
// 1. Take the process's current output buffer and combine it with Pub (if not empty)
let mut output_pub = lock!(output_pub);
{
let mut public = lock!(public);
if !output_pub.is_empty() {
public.output.push_str(&std::mem::take(&mut *output_pub));
}
// Update uptime
public.uptime = HumanTime::into_human(elapsed);
}
// 2. Check for "new job"/"no active...".
let mut output_parse = lock!(output_parse);
if XMRIG_REGEX.new_job.is_match(&output_parse) {
lock!(process).state = ProcessState::Alive;
} else if XMRIG_REGEX.not_mining.is_match(&output_parse) {
lock!(process).state = ProcessState::NotMining;
}
// 3. Throw away [output_parse]
output_parse.clear();
drop(output_parse);
}
// Formats raw private data into ready-to-print human readable version.
fn update_from_priv(public: &Arc<Mutex<Self>>, private: PrivXmrigApi) {
let mut public = lock!(public);
let hashrate_raw = match private.hashrate.total.first() {
Some(Some(h)) => *h,
_ => 0.0,
};
*public = Self {
worker_id: private.worker_id,
resources: HumanNumber::from_load(private.resources.load_average),
hashrate: HumanNumber::from_hashrate(private.hashrate.total),
diff: HumanNumber::from_u128(private.connection.diff),
accepted: HumanNumber::from_u128(private.connection.accepted),
rejected: HumanNumber::from_u128(private.connection.rejected),
hashrate_raw,
..std::mem::take(&mut *public)
}
}
}
//---------------------------------------------------------------------------------------------------- Private XMRig API
// This matches to some JSON stats in the HTTP call [summary],
// e.g: [wget -qO- localhost:18085/1/summary].
// XMRig doesn't initialize stats at 0 (or 0.0) and instead opts for [null]
// which means some elements need to be wrapped in an [Option] or else serde will [panic!].
#[derive(Debug, Serialize, Deserialize, Clone)]
pub(super) struct PrivXmrigApi {
worker_id: String,
resources: Resources,
connection: Connection,
hashrate: Hashrate,
}
impl PrivXmrigApi {
fn new() -> Self {
Self {
worker_id: String::new(),
resources: Resources::new(),
connection: Connection::new(),
hashrate: Hashrate::new(),
}
}
#[inline]
// Send an HTTP request to XMRig's API, serialize it into [Self] and return it
async fn request_xmrig_api(
client: hyper::Client<hyper::client::HttpConnector>,
api_uri: &str,
) -> std::result::Result<Self, anyhow::Error> {
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::<Self>(&body)?)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
struct Resources {
load_average: [Option<f32>; 3],
}
impl Resources {
fn new() -> Self {
Self {
load_average: [Some(0.0), Some(0.0), Some(0.0)],
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Connection {
diff: u128,
accepted: u128,
rejected: u128,
}
impl Connection {
fn new() -> Self {
Self {
diff: 0,
accepted: 0,
rejected: 0,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
struct Hashrate {
total: [Option<f32>; 3],
}
impl Hashrate {
fn new() -> Self {
Self {
total: [Some(0.0), Some(0.0), Some(0.0)],
}
}
}

209
src/inits.rs Normal file
View file

@ -0,0 +1,209 @@
use std::io::Write;
use crate::components::update::Update;
use crate::helper::{Helper, ProcessSignal};
use crate::utils::constants::{APP_MIN_WIDTH, APP_MIN_HEIGHT, APP_MAX_WIDTH, APP_MAX_HEIGHT, BYTES_ICON};
use crate::utils::regex::Regexes;
//---------------------------------------------------------------------------------------------------- Init functions
use crate::{components::node::Ping, miscs::clamp_scale};
use crate::app::App;
use std::sync::Arc;
use std::time::Instant;
use eframe::NativeOptions;
use env_logger::fmt::style::Style;
use env_logger::{Builder, WriteStyle};
use log::LevelFilter;
use egui::TextStyle::{Body, Button, Monospace, Heading, Name};
use crate::{disk::state::*, utils::macros::lock};
use egui::TextStyle::Small;
use crate::{info, warn};
use egui::*;
#[cold]
#[inline(never)]
pub fn init_text_styles(ctx: &egui::Context, width: f32, pixels_per_point: f32) {
let scale = width / 35.5;
let mut style = (*ctx.style()).clone();
style.text_styles = [
(Small, FontId::new(scale / 3.0, egui::FontFamily::Monospace)),
(Body, FontId::new(scale / 2.0, egui::FontFamily::Monospace)),
(
Button,
FontId::new(scale / 2.0, egui::FontFamily::Monospace),
),
(
Monospace,
FontId::new(scale / 2.0, egui::FontFamily::Monospace),
),
(
Heading,
FontId::new(scale / 1.5, egui::FontFamily::Monospace),
),
(
Name("Tab".into()),
FontId::new(scale * 1.2, egui::FontFamily::Monospace),
),
(
Name("Bottom".into()),
FontId::new(scale / 2.0, egui::FontFamily::Monospace),
),
(
Name("MonospaceSmall".into()),
FontId::new(scale / 2.5, egui::FontFamily::Monospace),
),
(
Name("MonospaceLarge".into()),
FontId::new(scale / 1.5, egui::FontFamily::Monospace),
),
]
.into();
style.spacing.icon_width_inner = width / 35.0;
style.spacing.icon_width = width / 25.0;
style.spacing.icon_spacing = 20.0;
style.spacing.scroll = egui::style::ScrollStyle {
bar_width: width / 150.0,
..egui::style::ScrollStyle::solid()
};
ctx.set_style(style);
// Make sure scale f32 is a regular number.
let pixels_per_point = clamp_scale(pixels_per_point);
ctx.set_pixels_per_point(pixels_per_point);
ctx.request_repaint();
}
#[cold]
#[inline(never)]
pub fn init_logger(now: Instant) {
let filter_env = std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".to_string());
let filter = match filter_env.as_str() {
"error" | "Error" | "ERROR" => LevelFilter::Error,
"warn" | "Warn" | "WARN" => LevelFilter::Warn,
"debug" | "Debug" | "DEBUG" => LevelFilter::Debug,
"trace" | "Trace" | "TRACE" => LevelFilter::Trace,
_ => LevelFilter::Info,
};
std::env::set_var("RUST_LOG", format!("off,gupax={}", filter_env));
Builder::new()
.format(move |buf, record| {
let level = record.level();
let level_style = buf.default_level_style(level);
let dimmed = Style::new().dimmed();
writeln!(
buf,
"{level_style}[{}]{level_style:#} [{dimmed}{:.3}{dimmed:#}] [{dimmed}{}{dimmed:#}:{dimmed}{}{dimmed:#}] {}",
level,
now.elapsed().as_secs_f32(),
record.file().unwrap_or("???"),
record.line().unwrap_or(0),
record.args(),
)
})
.filter_level(filter)
.write_style(WriteStyle::Always)
.parse_default_env()
.format_timestamp_millis()
.init();
info!("init_logger() ... OK");
info!("Log level ... {}", filter);
}
#[cold]
#[inline(never)]
pub fn init_options(initial_window_size: Option<Vec2>) -> NativeOptions {
let mut options = eframe::NativeOptions::default();
options.viewport.min_inner_size = Some(Vec2::new(APP_MIN_WIDTH, APP_MIN_HEIGHT));
options.viewport.max_inner_size = Some(Vec2::new(APP_MAX_WIDTH, APP_MAX_HEIGHT));
options.viewport.inner_size = initial_window_size;
options.follow_system_theme = false;
options.default_theme = eframe::Theme::Dark;
let icon = image::load_from_memory(BYTES_ICON)
.expect("Failed to read icon bytes")
.to_rgba8();
let (icon_width, icon_height) = icon.dimensions();
options.viewport.icon = Some(Arc::new(egui::viewport::IconData {
rgba: icon.into_raw(),
width: icon_width,
height: icon_height,
}));
info!("init_options() ... OK");
options
}
#[cold]
#[inline(never)]
pub fn init_auto(app: &mut App) {
// Return early if [--no-startup] was not passed
if app.no_startup {
info!("[--no-startup] flag passed, skipping init_auto()...");
return;
} else if app.error_state.error {
info!("App error detected, skipping init_auto()...");
return;
} else {
info!("Starting init_auto()...");
}
// [Auto-Update]
#[cfg(not(feature = "distro"))]
if app.state.gupax.auto_update {
Update::spawn_thread(
&app.og,
&app.state.gupax,
&app.state_path,
&app.update,
&mut app.error_state,
&app.restart,
);
} else {
info!("Skipping auto-update...");
}
// [Auto-Ping]
if app.state.p2pool.auto_ping && app.state.p2pool.simple {
Ping::spawn_thread(&app.ping)
} else {
info!("Skipping auto-ping...");
}
// [Auto-P2Pool]
if app.state.gupax.auto_p2pool {
if !Regexes::addr_ok(&app.state.p2pool.address) {
warn!("Gupax | P2Pool address is not valid! Skipping auto-p2pool...");
} else if !Gupax::path_is_file(&app.state.gupax.p2pool_path) {
warn!("Gupax | P2Pool path is not a file! Skipping auto-p2pool...");
} else if !crate::components::update::check_p2pool_path(&app.state.gupax.p2pool_path) {
warn!("Gupax | P2Pool path is not valid! Skipping auto-p2pool...");
} else {
let backup_hosts = app.gather_backup_hosts();
Helper::start_p2pool(
&app.helper,
&app.state.p2pool,
&app.state.gupax.absolute_p2pool_path,
backup_hosts,
);
}
} else {
info!("Skipping auto-p2pool...");
}
// [Auto-XMRig]
if app.state.gupax.auto_xmrig {
if !Gupax::path_is_file(&app.state.gupax.xmrig_path) {
warn!("Gupax | XMRig path is not an executable! Skipping auto-xmrig...");
} else if !crate::components::update::check_xmrig_path(&app.state.gupax.xmrig_path) {
warn!("Gupax | XMRig path is not valid! Skipping auto-xmrig...");
} else if cfg!(windows) {
Helper::start_xmrig(
&app.helper,
&app.state.xmrig,
&app.state.gupax.absolute_xmrig_path,
Arc::clone(&app.sudo),
);
} else {
lock!(app.sudo).signal = ProcessSignal::Start;
app.error_state.ask_sudo(&app.sudo);
}
} else {
info!("Skipping auto-xmrig...");
}
}

File diff suppressed because it is too large Load diff

271
src/miscs.rs Normal file
View file

@ -0,0 +1,271 @@
//---------------------------------------------------------------------------------------------------- Misc functions
#[cold]
#[inline(never)]
pub fn parse_args<S: Into<String>>(mut app: App, panic: S) -> App {
info!("Parsing CLI arguments...");
let mut args: Vec<String> = env::args().collect();
if args.len() == 1 {
info!("No args ... OK");
return app;
} else {
args.remove(0);
info!("Args ... {:?}", args);
}
// [help/version], exit early
for arg in &args {
match arg.as_str() {
"--help" => {
println!("{}", ARG_HELP);
exit(0);
}
"--version" => {
println!("Gupax {} [OS: {}, Commit: {}]\nThis Gupax was originally bundled with:\n - P2Pool {}\n - XMRig {}\n\n{}", GUPAX_VERSION, OS_NAME, &COMMIT[..40], P2POOL_VERSION, XMRIG_VERSION, ARG_COPYRIGHT);
exit(0);
}
"--ferris" => {
println!("{}", FERRIS_ANSI);
exit(0);
}
_ => (),
}
}
// Abort on panic
let panic = panic.into();
if !panic.is_empty() {
info!("[Gupax error] {}", panic);
exit(1);
}
// Everything else
for arg in args {
match arg.as_str() {
"--state" => {
info!("Printing state...");
print_disk_file(&app.state_path);
}
"--nodes" => {
info!("Printing node list...");
print_disk_file(&app.node_path);
}
"--payouts" => {
info!("Printing payouts...\n");
print_gupax_p2pool_api(&app.gupax_p2pool_api);
}
"--reset-state" => {
if let Ok(()) = reset_state(&app.state_path) {
println!("\nState reset ... OK");
exit(0);
} else {
eprintln!("\nState reset ... FAIL");
exit(1)
}
}
"--reset-nodes" => {
if let Ok(()) = reset_nodes(&app.node_path) {
println!("\nNode reset ... OK");
exit(0)
} else {
eprintln!("\nNode reset ... FAIL");
exit(1)
}
}
"--reset-pools" => {
if let Ok(()) = reset_pools(&app.pool_path) {
println!("\nPool reset ... OK");
exit(0)
} else {
eprintln!("\nPool reset ... FAIL");
exit(1)
}
}
"--reset-payouts" => {
if let Ok(()) = reset_gupax_p2pool_api(&app.gupax_p2pool_api_path) {
println!("\nGupaxP2poolApi reset ... OK");
exit(0)
} else {
eprintln!("\nGupaxP2poolApi reset ... FAIL");
exit(1)
}
}
"--reset-all" => reset(
&app.os_data_path,
&app.state_path,
&app.node_path,
&app.pool_path,
&app.gupax_p2pool_api_path,
),
"--no-startup" => app.no_startup = true,
_ => {
eprintln!(
"\n[Gupax error] Invalid option: [{}]\nFor help, use: [--help]",
arg
);
exit(1);
}
}
}
app
}
// Get absolute [Gupax] binary path
#[cold]
#[inline(never)]
pub fn get_exe() -> Result<String, std::io::Error> {
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<String, std::io::Error> {
match std::env::current_exe() {
Ok(mut path) => {
path.pop();
Ok(path.display().to_string())
}
Err(err) => {
error!("Couldn't get exe basepath PATH");
Err(err)
}
}
}
// Clean any [gupax_update_.*] directories
// The trailing random bits must be exactly 10 alphanumeric characters
#[cold]
#[inline(never)]
pub fn clean_dir() -> Result<(), anyhow::Error> {
let regex = Regex::new("^gupax_update_[A-Za-z0-9]{10}$").unwrap();
for entry in std::fs::read_dir(get_exe_dir()?)? {
let entry = entry?;
if !entry.path().is_dir() {
continue;
}
if Regex::is_match(
&regex,
entry
.file_name()
.to_str()
.ok_or_else(|| anyhow::Error::msg("Basename failed"))?,
) {
let path = entry.path();
match std::fs::remove_dir_all(&path) {
Ok(_) => info!("Remove [{}] ... OK", path.display()),
Err(e) => warn!("Remove [{}] ... FAIL ... {}", path.display(), e),
}
}
}
Ok(())
}
// Print disk files to console
#[cold]
#[inline(never)]
fn print_disk_file(path: &PathBuf) {
match std::fs::read_to_string(path) {
Ok(string) => {
print!("{}", string);
exit(0);
}
Err(e) => {
error!("{}", e);
exit(1);
}
}
}
// Prints the GupaxP2PoolApi files.
#[cold]
#[inline(never)]
pub fn print_gupax_p2pool_api(gupax_p2pool_api: &Arc<Mutex<GupaxP2poolApi>>) {
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::<u64>() {
Ok(o) => crate::xmr::AtomicUnit::from_u64(o),
Err(e) => {
warn!("GupaxP2poolApi | [xmr] parse error: {}", e);
exit(1);
}
};
println!(
"{}\nTotal payouts | {}\nTotal XMR | {} ({} Atomic Units)",
log,
payout.trim(),
xmr,
xmr.to_u64()
);
exit(0);
}
#[inline]
pub fn cmp_f64(a: f64, b: f64) -> std::cmp::Ordering {
match (a <= b, a >= b) {
(false, true) => std::cmp::Ordering::Greater,
(true, false) => std::cmp::Ordering::Less,
(true, true) => std::cmp::Ordering::Equal,
_ => std::cmp::Ordering::Less,
}
}
// Free functions.
use crate::disk::gupax_p2pool_api::GupaxP2poolApi;
use crate::utils::macros::lock;
use log::error;
use log::warn;
use regex::Regex;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::{env, process::exit};
use log::info;
//---------------------------------------------------------------------------------------------------- Use
use crate::{
app::App,
constants::*,
utils::{
ferris::FERRIS_ANSI,
resets::{reset, reset_gupax_p2pool_api, reset_nodes, reset_pools, reset_state},
},
};
//----------------------------------------------------------------------------------------------------
#[cold]
#[inline(never)]
// Clamp the scaling resolution `f32` to a known good `f32`.
pub fn clamp_scale(scale: f32) -> f32 {
// Make sure it is finite.
if !scale.is_finite() {
return APP_DEFAULT_SCALE;
}
// Clamp between valid range.
scale.clamp(APP_MIN_SCALE, APP_MAX_SCALE)
}

View file

@ -1,620 +0,0 @@
// Gupax - GUI Uniting P2Pool And XMRig
//
// Copyright (c) 2022-2023 hinto-janai
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::regex::REGEXES;
use crate::{constants::*, disk::*, helper::*, macros::*, node::*, Regexes};
use egui::{
Button, Checkbox, Color32, ComboBox, Hyperlink, Label, ProgressBar, RichText, SelectableLabel,
Slider, Spinner, TextEdit, TextStyle::*,
};
use log::*;
use std::sync::{Arc, Mutex};
impl crate::disk::P2pool {
#[inline(always)] // called once
#[allow(clippy::too_many_arguments)]
pub fn show(
&mut self,
node_vec: &mut Vec<(String, Node)>,
_og: &Arc<Mutex<State>>,
ping: &Arc<Mutex<Ping>>,
process: &Arc<Mutex<Process>>,
api: &Arc<Mutex<PubP2poolApi>>,
buffer: &mut String,
width: f32,
height: f32,
_ctx: &egui::Context,
ui: &mut egui::Ui,
) {
let text_edit = height / 25.0;
//---------------------------------------------------------------------------------------------------- [Simple] Console
debug!("P2Pool Tab | Rendering [Console]");
ui.group(|ui| {
if self.simple {
let height = height / 2.8;
let width = width - SPACE;
egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| {
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.max_width(width)
.max_height(height)
.auto_shrink([false; 2])
.show_viewport(ui, |ui, _| {
ui.add_sized(
[width, height],
TextEdit::multiline(&mut lock!(api).output.as_str()),
);
});
});
//---------------------------------------------------------------------------------------------------- [Advanced] Console
} else {
let height = height / 2.8;
let width = width - SPACE;
egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| {
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.max_width(width)
.max_height(height)
.auto_shrink([false; 2])
.show_viewport(ui, |ui, _| {
ui.add_sized(
[width, height],
TextEdit::multiline(&mut lock!(api).output.as_str()),
);
});
});
ui.separator();
let response = ui
.add_sized(
[width, text_edit],
TextEdit::hint_text(
TextEdit::singleline(buffer),
r#"Type a command (e.g "help" or "status") and press Enter"#,
),
)
.on_hover_text(P2POOL_INPUT);
// If the user pressed enter, dump buffer contents into the process STDIN
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
response.request_focus(); // Get focus back
let buffer = std::mem::take(buffer); // Take buffer
let mut process = lock!(process); // Lock
if process.is_alive() {
process.input.push(buffer);
} // Push only if alive
}
}
});
//---------------------------------------------------------------------------------------------------- Args
if !self.simple {
debug!("P2Pool Tab | Rendering [Arguments]");
ui.group(|ui| {
ui.horizontal(|ui| {
let width = (width / 10.0) - SPACE;
ui.add_sized([width, text_edit], Label::new("Command arguments:"));
ui.add_sized(
[ui.available_width(), text_edit],
TextEdit::hint_text(
TextEdit::singleline(&mut self.arguments),
r#"--wallet <...> --host <...>"#,
),
)
.on_hover_text(P2POOL_ARGUMENTS);
self.arguments.truncate(1024);
})
});
ui.set_enabled(self.arguments.is_empty());
}
//---------------------------------------------------------------------------------------------------- Address
debug!("P2Pool Tab | Rendering [Address]");
ui.group(|ui| {
let width = width - SPACE;
ui.spacing_mut().text_edit_width = (width) - (SPACE * 3.0);
let text;
let color;
let len = format!("{:02}", self.address.len());
if self.address.is_empty() {
text = format!("Monero Address [{}/95] ", len);
color = Color32::LIGHT_GRAY;
} else if Regexes::addr_ok(&self.address) {
text = format!("Monero Address [{}/95] ✔", len);
color = Color32::from_rgb(100, 230, 100);
} else {
text = format!("Monero Address [{}/95] ❌", len);
color = Color32::from_rgb(230, 50, 50);
}
ui.add_sized(
[width, text_edit],
Label::new(RichText::new(text).color(color)),
);
ui.add_sized(
[width, text_edit],
TextEdit::hint_text(TextEdit::singleline(&mut self.address), "4..."),
)
.on_hover_text(P2POOL_ADDRESS);
self.address.truncate(95);
});
//---------------------------------------------------------------------------------------------------- Simple
let height = ui.available_height();
if self.simple {
// [Node]
let height = height / 6.5;
ui.spacing_mut().slider_width = width - 8.0;
ui.spacing_mut().icon_width = width / 25.0;
// [Auto-select] if we haven't already.
// Using [Arc<Mutex<Ping>>] as an intermediary here
// saves me the hassle of wrapping [state: State] completely
// and [.lock().unwrap()]ing it everywhere.
// Two atomic bools = enough to represent this data
debug!("P2Pool Tab | Running [auto-select] check");
if self.auto_select {
let mut ping = lock!(ping);
// If we haven't auto_selected yet, auto-select and turn it off
if ping.pinged && !ping.auto_selected {
self.node = ping.fastest.to_string();
ping.auto_selected = true;
}
drop(ping);
}
ui.vertical(|ui| {
ui.horizontal(|ui| {
debug!("P2Pool Tab | Rendering [Ping List]");
// [Ping List]
let mut ms = 0;
let mut color = Color32::LIGHT_GRAY;
if lock!(ping).pinged {
for data in lock!(ping).nodes.iter() {
if data.ip == self.node {
ms = data.ms;
color = data.color;
break;
}
}
}
debug!("P2Pool Tab | Rendering [ComboBox] of Remote Nodes");
let ip_location = crate::node::format_ip_location(&self.node, false);
let text = RichText::new(format!("{}ms | {}", ms, ip_location)).color(color);
ComboBox::from_id_source("remote_nodes")
.selected_text(text)
.width(width)
.show_ui(ui, |ui| {
for data in lock!(ping).nodes.iter() {
let ms = crate::node::format_ms(data.ms);
let ip_location = crate::node::format_ip_location(data.ip, true);
let text = RichText::new(format!("{} | {}", ms, ip_location))
.color(data.color);
ui.selectable_value(&mut self.node, data.ip.to_string(), text);
}
});
});
ui.add_space(5.0);
debug!("P2Pool Tab | Rendering [Select fastest ... Ping] buttons");
ui.horizontal(|ui| {
let width = (width / 5.0) - 6.0;
// [Select random node]
if ui
.add_sized([width, height], Button::new("Select random node"))
.on_hover_text(P2POOL_SELECT_RANDOM)
.clicked()
{
self.node = RemoteNode::get_random(&self.node);
}
// [Select fastest node]
if ui
.add_sized([width, height], Button::new("Select fastest node"))
.on_hover_text(P2POOL_SELECT_FASTEST)
.clicked()
&& lock!(ping).pinged
{
self.node = lock!(ping).fastest.to_string();
}
// [Ping Button]
ui.add_enabled_ui(!lock!(ping).pinging, |ui| {
if ui
.add_sized([width, height], Button::new("Ping remote nodes"))
.on_hover_text(P2POOL_PING)
.clicked()
{
Ping::spawn_thread(ping);
}
});
// [Last <-]
if ui
.add_sized([width, height], Button::new("⬅ Last"))
.on_hover_text(P2POOL_SELECT_LAST)
.clicked()
{
let ping = lock!(ping);
match ping.pinged {
true => {
self.node = RemoteNode::get_last_from_ping(&self.node, &ping.nodes)
}
false => self.node = RemoteNode::get_last(&self.node),
}
drop(ping);
}
// [Next ->]
if ui
.add_sized([width, height], Button::new("Next ➡"))
.on_hover_text(P2POOL_SELECT_NEXT)
.clicked()
{
let ping = lock!(ping);
match ping.pinged {
true => {
self.node = RemoteNode::get_next_from_ping(&self.node, &ping.nodes)
}
false => self.node = RemoteNode::get_next(&self.node),
}
drop(ping);
}
});
ui.vertical(|ui| {
let height = height / 2.0;
let pinging = lock!(ping).pinging;
ui.set_enabled(pinging);
let prog = lock!(ping).prog.round();
let msg = RichText::new(format!("{} ... {}%", lock!(ping).msg, prog));
let height = height / 1.25;
ui.add_space(5.0);
ui.add_sized([width, height], Label::new(msg));
ui.add_space(5.0);
if pinging {
ui.add_sized([width, height], Spinner::new().size(height));
} else {
ui.add_sized([width, height], Label::new("..."));
}
ui.add_sized([width, height], ProgressBar::new(prog.round() / 100.0));
ui.add_space(5.0);
});
});
debug!("P2Pool Tab | Rendering [Auto-*] buttons");
ui.group(|ui| {
ui.horizontal(|ui| {
let width = (width / 3.0) - (SPACE * 1.75);
// [Auto-node]
ui.add_sized(
[width, height],
Checkbox::new(&mut self.auto_select, "Auto-select"),
)
.on_hover_text(P2POOL_AUTO_SELECT);
ui.separator();
// [Auto-node]
ui.add_sized(
[width, height],
Checkbox::new(&mut self.auto_ping, "Auto-ping"),
)
.on_hover_text(P2POOL_AUTO_NODE);
ui.separator();
// [Backup host]
ui.add_sized(
[width, height],
Checkbox::new(&mut self.backup_host, "Backup host"),
)
.on_hover_text(P2POOL_BACKUP_HOST_SIMPLE);
})
});
debug!("P2Pool Tab | Rendering warning text");
ui.add_sized([width, height/2.0], Hyperlink::from_label_and_url("WARNING: It is recommended to run/use your own Monero Node (hover for details)", "https://github.com/hinto-janai/gupax#running-a-local-monero-node")).on_hover_text(P2POOL_COMMUNITY_NODE_WARNING);
//---------------------------------------------------------------------------------------------------- Advanced
} else {
debug!("P2Pool Tab | Rendering [Node List] elements");
let mut incorrect_input = false; // This will disable [Add/Delete] on bad input
// [Monero node IP/RPC/ZMQ]
ui.horizontal(|ui| {
ui.group(|ui| {
let width = width/10.0;
ui.vertical(|ui| {
ui.spacing_mut().text_edit_width = width*3.32;
ui.horizontal(|ui| {
let text;
let color;
let len = format!("{:02}", self.name.len());
if self.name.is_empty() {
text = format!("Name [ {}/30 ]", len);
color = Color32::LIGHT_GRAY;
incorrect_input = true;
} else if REGEXES.name.is_match(&self.name) {
text = format!("Name [ {}/30 ]✔", len);
color = Color32::from_rgb(100, 230, 100);
} else {
text = format!("Name [ {}/30 ]❌", len);
color = Color32::from_rgb(230, 50, 50);
incorrect_input = true;
}
ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
ui.text_edit_singleline(&mut self.name).on_hover_text(P2POOL_NAME);
self.name.truncate(30);
});
ui.horizontal(|ui| {
let text;
let color;
let len = format!("{:03}", self.ip.len());
if self.ip.is_empty() {
text = format!(" IP [{}/255]", len);
color = Color32::LIGHT_GRAY;
incorrect_input = true;
} else if self.ip == "localhost" || REGEXES.ipv4.is_match(&self.ip) || REGEXES.domain.is_match(&self.ip) {
text = format!(" IP [{}/255]✔", len);
color = Color32::from_rgb(100, 230, 100);
} else {
text = format!(" IP [{}/255]❌", len);
color = Color32::from_rgb(230, 50, 50);
incorrect_input = true;
}
ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
ui.text_edit_singleline(&mut self.ip).on_hover_text(P2POOL_NODE_IP);
self.ip.truncate(255);
});
ui.horizontal(|ui| {
let text;
let color;
let len = self.rpc.len();
if self.rpc.is_empty() {
text = format!(" RPC [ {}/5 ]", len);
color = Color32::LIGHT_GRAY;
incorrect_input = true;
} else if REGEXES.port.is_match(&self.rpc) {
text = format!(" RPC [ {}/5 ]✔", len);
color = Color32::from_rgb(100, 230, 100);
} else {
text = format!(" RPC [ {}/5 ]❌", len);
color = Color32::from_rgb(230, 50, 50);
incorrect_input = true;
}
ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
ui.text_edit_singleline(&mut self.rpc).on_hover_text(P2POOL_RPC_PORT);
self.rpc.truncate(5);
});
ui.horizontal(|ui| {
let text;
let color;
let len = self.zmq.len();
if self.zmq.is_empty() {
text = format!(" ZMQ [ {}/5 ]", len);
color = Color32::LIGHT_GRAY;
incorrect_input = true;
} else if REGEXES.port.is_match(&self.zmq) {
text = format!(" ZMQ [ {}/5 ]✔", len);
color = Color32::from_rgb(100, 230, 100);
} else {
text = format!(" ZMQ [ {}/5 ]❌", len);
color = Color32::from_rgb(230, 50, 50);
incorrect_input = true;
}
ui.add_sized([width, text_edit], Label::new(RichText::new(text).color(color)));
ui.text_edit_singleline(&mut self.zmq).on_hover_text(P2POOL_ZMQ_PORT);
self.zmq.truncate(5);
});
});
ui.vertical(|ui| {
let width = ui.available_width();
ui.add_space(1.0);
// [Manual node selection]
ui.spacing_mut().slider_width = width - 8.0;
ui.spacing_mut().icon_width = width / 25.0;
// [Ping List]
debug!("P2Pool Tab | Rendering [Node List]");
let text = RichText::new(format!("{}. {}", self.selected_index+1, self.selected_name));
ComboBox::from_id_source("manual_nodes").selected_text(text).width(width).show_ui(ui, |ui| {
for (n, (name, node)) in node_vec.iter().enumerate() {
let text = RichText::new(format!("{}. {}\n IP: {}\n RPC: {}\n ZMQ: {}", n+1, name, node.ip, node.rpc, node.zmq));
if ui.add(SelectableLabel::new(self.selected_name == *name, text)).clicked() {
self.selected_index = n;
let node = node.clone();
self.selected_name = name.clone();
self.selected_ip = node.ip.clone();
self.selected_rpc = node.rpc.clone();
self.selected_zmq = node.zmq.clone();
self.name = name.clone();
self.ip = node.ip;
self.rpc = node.rpc;
self.zmq = node.zmq;
}
}
});
// [Add/Save]
let node_vec_len = node_vec.len();
let mut exists = false;
let mut save_diff = true;
let mut existing_index = 0;
for (name, node) in node_vec.iter() {
if *name == self.name {
exists = true;
if self.ip == node.ip && self.rpc == node.rpc && self.zmq == node.zmq {
save_diff = false;
}
break
}
existing_index += 1;
}
ui.horizontal(|ui| {
let text = if exists { LIST_SAVE } else { LIST_ADD };
let text = format!("{}\n Currently selected node: {}. {}\n Current amount of nodes: {}/1000", text, self.selected_index+1, self.selected_name, node_vec_len);
// If the node already exists, show [Save] and mutate the already existing node
if exists {
ui.set_enabled(!incorrect_input && save_diff);
if ui.add_sized([width, text_edit], Button::new("Save")).on_hover_text(text).clicked() {
let node = Node {
ip: self.ip.clone(),
rpc: self.rpc.clone(),
zmq: self.zmq.clone(),
};
node_vec[existing_index].1 = node;
self.selected_index = existing_index;
self.selected_ip = self.ip.clone();
self.selected_rpc = self.rpc.clone();
self.selected_zmq = self.zmq.clone();
info!("Node | S | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", existing_index+1, self.name, self.ip, self.rpc, self.zmq);
}
// Else, add to the list
} else {
ui.set_enabled(!incorrect_input && node_vec_len < 1000);
if ui.add_sized([width, text_edit], Button::new("Add")).on_hover_text(text).clicked() {
let node = Node {
ip: self.ip.clone(),
rpc: self.rpc.clone(),
zmq: self.zmq.clone(),
};
node_vec.push((self.name.clone(), node));
self.selected_index = node_vec_len;
self.selected_name = self.name.clone();
self.selected_ip = self.ip.clone();
self.selected_rpc = self.rpc.clone();
self.selected_zmq = self.zmq.clone();
info!("Node | A | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", node_vec_len, self.name, self.ip, self.rpc, self.zmq);
}
}
});
// [Delete]
ui.horizontal(|ui| {
ui.set_enabled(node_vec_len > 1);
let text = format!("{}\n Currently selected node: {}. {}\n Current amount of nodes: {}/1000", LIST_DELETE, self.selected_index+1, self.selected_name, node_vec_len);
if ui.add_sized([width, text_edit], Button::new("Delete")).on_hover_text(text).clicked() {
let new_name;
let new_node;
match self.selected_index {
0 => {
new_name = node_vec[1].0.clone();
new_node = node_vec[1].1.clone();
node_vec.remove(0);
}
_ => {
node_vec.remove(self.selected_index);
self.selected_index -= 1;
new_name = node_vec[self.selected_index].0.clone();
new_node = node_vec[self.selected_index].1.clone();
}
};
self.selected_name = new_name.clone();
self.selected_ip = new_node.ip.clone();
self.selected_rpc = new_node.rpc.clone();
self.selected_zmq = new_node.zmq.clone();
self.name = new_name;
self.ip = new_node.ip;
self.rpc = new_node.rpc;
self.zmq = new_node.zmq;
info!("Node | D | [index: {}, name: \"{}\", ip: \"{}\", rpc: {}, zmq: {}]", self.selected_index, self.selected_name, self.selected_ip, self.selected_rpc, self.selected_zmq);
}
});
ui.horizontal(|ui| {
ui.set_enabled(!self.name.is_empty() || !self.ip.is_empty() || !self.rpc.is_empty() || !self.zmq.is_empty());
if ui.add_sized([width, text_edit], Button::new("Clear")).on_hover_text(LIST_CLEAR).clicked() {
self.name.clear();
self.ip.clear();
self.rpc.clear();
self.zmq.clear();
}
});
});
});
});
ui.add_space(5.0);
debug!("P2Pool Tab | Rendering [Main/Mini/Peers/Log] elements");
// [Main/Mini]
ui.horizontal(|ui| {
let height = height / 4.0;
ui.group(|ui| {
ui.horizontal(|ui| {
let width = (width / 4.0) - SPACE;
let height = height + 6.0;
if ui
.add_sized(
[width, height],
SelectableLabel::new(!self.mini, "P2Pool Main"),
)
.on_hover_text(P2POOL_MAIN)
.clicked()
{
self.mini = false;
}
if ui
.add_sized(
[width, height],
SelectableLabel::new(self.mini, "P2Pool Mini"),
)
.on_hover_text(P2POOL_MINI)
.clicked()
{
self.mini = true;
}
})
});
// [Out/In Peers] + [Log Level]
ui.group(|ui| {
ui.vertical(|ui| {
let text = (ui.available_width() / 10.0) - SPACE;
let width = (text * 8.0) - SPACE;
let height = height / 3.0;
ui.style_mut().spacing.slider_width = width / 1.1;
ui.style_mut().spacing.interact_size.y = height;
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
ui.horizontal(|ui| {
ui.add_sized([text, height], Label::new("Out peers [10-450]:"));
ui.add_sized(
[width, height],
Slider::new(&mut self.out_peers, 10..=450),
)
.on_hover_text(P2POOL_OUT);
ui.add_space(ui.available_width() - 4.0);
});
ui.horizontal(|ui| {
ui.add_sized([text, height], Label::new(" In peers [10-450]:"));
ui.add_sized(
[width, height],
Slider::new(&mut self.in_peers, 10..=450),
)
.on_hover_text(P2POOL_IN);
});
ui.horizontal(|ui| {
ui.add_sized([text, height], Label::new(" Log level [0-6]:"));
ui.add_sized([width, height], Slider::new(&mut self.log_level, 0..=6))
.on_hover_text(P2POOL_LOG);
});
})
});
});
debug!("P2Pool Tab | Rendering Backup host button");
ui.group(|ui| {
let width = width - SPACE;
let height = ui.available_height() / 3.0;
// [Backup host]
ui.add_sized(
[width, height],
Checkbox::new(&mut self.backup_host, "Backup host"),
)
.on_hover_text(P2POOL_BACKUP_HOST_ADVANCED);
});
}
}
}

View file

@ -1,956 +0,0 @@
// Gupax - GUI Uniting P2Pool And XMRig
//
// Copyright (c) 2022-2023 hinto-janai
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::{
constants::*, human::HumanNumber, macros::*, Benchmark, GupaxP2poolApi, Hash, ImgP2pool,
ImgXmrig, PayoutView, PubP2poolApi, PubXmrigApi, Submenu, Sys,
};
use egui::{
Hyperlink, Label, ProgressBar, RichText, SelectableLabel, Slider, Spinner, TextEdit, TextStyle,
TextStyle::Name,
};
use log::*;
use std::sync::{Arc, Mutex};
impl crate::disk::Status {
#[inline(always)] // called once
#[allow(clippy::too_many_arguments)]
pub fn show(
&mut self,
sys: &Arc<Mutex<Sys>>,
p2pool_api: &Arc<Mutex<PubP2poolApi>>,
xmrig_api: &Arc<Mutex<PubXmrigApi>>,
p2pool_img: &Arc<Mutex<ImgP2pool>>,
xmrig_img: &Arc<Mutex<ImgXmrig>>,
p2pool_alive: bool,
xmrig_alive: bool,
max_threads: usize,
gupax_p2pool_api: &Arc<Mutex<GupaxP2poolApi>>,
benchmarks: &[Benchmark],
width: f32,
height: f32,
_ctx: &egui::Context,
ui: &mut egui::Ui,
) {
//---------------------------------------------------------------------------------------------------- [Processes]
if self.submenu == Submenu::Processes {
let width = (width / 3.0) - (SPACE * 1.666);
let min_height = height - SPACE;
let height = height / 25.0;
ui.horizontal(|ui| {
// [Gupax]
ui.group(|ui| {
ui.vertical(|ui| {
debug!("Status Tab | Rendering [Gupax]");
ui.set_min_height(min_height);
ui.add_sized(
[width, height],
Label::new(
RichText::new("[Gupax]")
.color(LIGHT_GRAY)
.text_style(TextStyle::Name("MonospaceLarge".into())),
),
)
.on_hover_text("Gupax is online");
let sys = lock!(sys);
ui.add_sized(
[width, height],
Label::new(RichText::new("Uptime").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_UPTIME);
ui.add_sized([width, height], Label::new(sys.gupax_uptime.to_string()));
ui.add_sized(
[width, height],
Label::new(RichText::new("Gupax CPU").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_CPU_USAGE);
ui.add_sized([width, height], Label::new(sys.gupax_cpu_usage.to_string()));
ui.add_sized(
[width, height],
Label::new(RichText::new("Gupax Memory").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_MEMORY_USAGE);
ui.add_sized(
[width, height],
Label::new(sys.gupax_memory_used_mb.to_string()),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("System CPU").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_SYSTEM_CPU_USAGE);
ui.add_sized(
[width, height],
Label::new(sys.system_cpu_usage.to_string()),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("System Memory").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_SYSTEM_MEMORY);
ui.add_sized([width, height], Label::new(sys.system_memory.to_string()));
ui.add_sized(
[width, height],
Label::new(RichText::new("System CPU Model").underline().color(BONE)),
)
.on_hover_text(STATUS_GUPAX_SYSTEM_CPU_MODEL);
ui.add_sized(
[width, height],
Label::new(sys.system_cpu_model.to_string()),
);
drop(sys);
})
});
// [P2Pool]
ui.group(|ui| {
ui.vertical(|ui| {
debug!("Status Tab | Rendering [P2Pool]");
ui.set_enabled(p2pool_alive);
ui.set_min_height(min_height);
ui.add_sized(
[width, height],
Label::new(
RichText::new("[P2Pool]")
.color(LIGHT_GRAY)
.text_style(TextStyle::Name("MonospaceLarge".into())),
),
)
.on_hover_text("P2Pool is online")
.on_disabled_hover_text("P2Pool is offline");
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
let height = height / 1.4;
let api = lock!(p2pool_api);
ui.add_sized(
[width, height],
Label::new(RichText::new("Uptime").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_UPTIME);
ui.add_sized([width, height], Label::new(format!("{}", api.uptime)));
ui.add_sized(
[width, height],
Label::new(RichText::new("Shares Found").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_SHARES);
ui.add_sized([width, height], Label::new(format!("{}", api.shares_found)));
ui.add_sized(
[width, height],
Label::new(RichText::new("Payouts").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_PAYOUTS);
ui.add_sized(
[width, height],
Label::new(format!("Total: {}", api.payouts)),
);
ui.add_sized(
[width, height],
Label::new(format!(
"[{:.7}/hour]\n[{:.7}/day]\n[{:.7}/month]",
api.payouts_hour, api.payouts_day, api.payouts_month
)),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("XMR Mined").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_XMR);
ui.add_sized(
[width, height],
Label::new(format!("Total: {:.13} XMR", api.xmr)),
);
ui.add_sized(
[width, height],
Label::new(format!(
"[{:.7}/hour]\n[{:.7}/day]\n[{:.7}/month]",
api.xmr_hour, api.xmr_day, api.xmr_month
)),
);
ui.add_sized(
[width, height],
Label::new(
RichText::new("Hashrate (15m/1h/24h)")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_P2POOL_HASHRATE);
ui.add_sized(
[width, height],
Label::new(format!(
"[{} H/s] [{} H/s] [{} H/s]",
api.hashrate_15m, api.hashrate_1h, api.hashrate_24h
)),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("Miners Connected").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_CONNECTIONS);
ui.add_sized([width, height], Label::new(format!("{}", api.connections)));
ui.add_sized(
[width, height],
Label::new(RichText::new("Effort").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_EFFORT);
ui.add_sized(
[width, height],
Label::new(format!(
"[Average: {}] [Current: {}]",
api.average_effort, api.current_effort
)),
);
let img = lock!(p2pool_img);
ui.add_sized(
[width, height],
Label::new(RichText::new("Monero Node").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_MONERO_NODE);
ui.add_sized(
[width, height],
Label::new(format!(
"[IP: {}]\n[RPC: {}] [ZMQ: {}]",
&img.host, &img.rpc, &img.zmq
)),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("Sidechain").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_POOL);
ui.add_sized([width, height], Label::new(&img.mini));
ui.add_sized(
[width, height],
Label::new(RichText::new("Address").underline().color(BONE)),
)
.on_hover_text(STATUS_P2POOL_ADDRESS);
ui.add_sized([width, height], Label::new(&img.address));
drop(img);
drop(api);
})
});
// [XMRig]
ui.group(|ui| {
ui.vertical(|ui| {
debug!("Status Tab | Rendering [XMRig]");
ui.set_enabled(xmrig_alive);
ui.set_min_height(min_height);
ui.add_sized(
[width, height],
Label::new(
RichText::new("[XMRig]")
.color(LIGHT_GRAY)
.text_style(TextStyle::Name("MonospaceLarge".into())),
),
)
.on_hover_text("XMRig is online")
.on_disabled_hover_text("XMRig is offline");
let api = lock!(xmrig_api);
ui.add_sized(
[width, height],
Label::new(RichText::new("Uptime").underline().color(BONE)),
)
.on_hover_text(STATUS_XMRIG_UPTIME);
ui.add_sized([width, height], Label::new(format!("{}", api.uptime)));
ui.add_sized(
[width, height],
Label::new(
RichText::new("CPU Load (10s/60s/15m)")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_XMRIG_CPU);
ui.add_sized([width, height], Label::new(format!("{}", api.resources)));
ui.add_sized(
[width, height],
Label::new(
RichText::new("Hashrate (10s/60s/15m)")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_XMRIG_HASHRATE);
ui.add_sized([width, height], Label::new(format!("{}", api.hashrate)));
ui.add_sized(
[width, height],
Label::new(RichText::new("Difficulty").underline().color(BONE)),
)
.on_hover_text(STATUS_XMRIG_DIFFICULTY);
ui.add_sized([width, height], Label::new(format!("{}", api.diff)));
ui.add_sized(
[width, height],
Label::new(RichText::new("Shares").underline().color(BONE)),
)
.on_hover_text(STATUS_XMRIG_SHARES);
ui.add_sized(
[width, height],
Label::new(format!(
"[Accepted: {}] [Rejected: {}]",
api.accepted, api.rejected
)),
);
ui.add_sized(
[width, height],
Label::new(RichText::new("Pool").underline().color(BONE)),
)
.on_hover_text(STATUS_XMRIG_POOL);
ui.add_sized([width, height], Label::new(&lock!(xmrig_img).url));
ui.add_sized(
[width, height],
Label::new(RichText::new("Threads").underline().color(BONE)),
)
.on_hover_text(STATUS_XMRIG_THREADS);
ui.add_sized(
[width, height],
Label::new(format!("{}/{}", &lock!(xmrig_img).threads, max_threads)),
);
drop(api);
})
});
});
//---------------------------------------------------------------------------------------------------- [P2Pool]
} else if self.submenu == Submenu::P2pool {
let api = lock!(gupax_p2pool_api);
let text = height / 25.0;
let log = height / 2.8;
// Payout Text + PayoutView buttons
ui.group(|ui| {
ui.horizontal(|ui| {
let width = (width / 3.0) - (SPACE * 4.0);
ui.add_sized(
[width, text],
Label::new(
RichText::new(format!("Total Payouts: {}", api.payout))
.underline()
.color(LIGHT_GRAY),
),
)
.on_hover_text(STATUS_SUBMENU_PAYOUT);
ui.separator();
ui.add_sized(
[width, text],
Label::new(
RichText::new(format!("Total XMR: {}", api.xmr))
.underline()
.color(LIGHT_GRAY),
),
)
.on_hover_text(STATUS_SUBMENU_XMR);
let width = width / 4.0;
ui.separator();
if ui
.add_sized(
[width, text],
SelectableLabel::new(self.payout_view == PayoutView::Latest, "Latest"),
)
.on_hover_text(STATUS_SUBMENU_LATEST)
.clicked()
{
self.payout_view = PayoutView::Latest;
}
ui.separator();
if ui
.add_sized(
[width, text],
SelectableLabel::new(self.payout_view == PayoutView::Oldest, "Oldest"),
)
.on_hover_text(STATUS_SUBMENU_OLDEST)
.clicked()
{
self.payout_view = PayoutView::Oldest;
}
ui.separator();
if ui
.add_sized(
[width, text],
SelectableLabel::new(
self.payout_view == PayoutView::Biggest,
"Biggest",
),
)
.on_hover_text(STATUS_SUBMENU_BIGGEST)
.clicked()
{
self.payout_view = PayoutView::Biggest;
}
ui.separator();
if ui
.add_sized(
[width, text],
SelectableLabel::new(
self.payout_view == PayoutView::Smallest,
"Smallest",
),
)
.on_hover_text(STATUS_SUBMENU_SMALLEST)
.clicked()
{
self.payout_view = PayoutView::Smallest;
}
});
ui.separator();
// Actual logs
egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| {
egui::ScrollArea::vertical()
.stick_to_bottom(self.payout_view == PayoutView::Oldest)
.max_width(width)
.max_height(log)
.auto_shrink([false; 2])
.show_viewport(ui, |ui, _| {
ui.style_mut().override_text_style =
Some(Name("MonospaceLarge".into()));
match self.payout_view {
PayoutView::Latest => ui.add_sized(
[width, log],
TextEdit::multiline(&mut api.log_rev.as_str()),
),
PayoutView::Oldest => ui.add_sized(
[width, log],
TextEdit::multiline(&mut api.log.as_str()),
),
PayoutView::Biggest => ui.add_sized(
[width, log],
TextEdit::multiline(&mut api.payout_high.as_str()),
),
PayoutView::Smallest => ui.add_sized(
[width, log],
TextEdit::multiline(&mut api.payout_low.as_str()),
),
};
});
});
});
drop(api);
// Payout/Share Calculator
let button = (width / 20.0) - (SPACE * 1.666);
ui.group(|ui| {
ui.horizontal(|ui| {
ui.set_min_width(width - SPACE);
if ui
.add_sized(
[button * 2.0, text],
SelectableLabel::new(!self.manual_hash, "Automatic"),
)
.on_hover_text(STATUS_SUBMENU_AUTOMATIC)
.clicked()
{
self.manual_hash = false;
}
ui.separator();
if ui
.add_sized(
[button * 2.0, text],
SelectableLabel::new(self.manual_hash, "Manual"),
)
.on_hover_text(STATUS_SUBMENU_MANUAL)
.clicked()
{
self.manual_hash = true;
}
ui.separator();
ui.set_enabled(self.manual_hash);
if ui
.add_sized(
[button, text],
SelectableLabel::new(self.hash_metric == Hash::Hash, "Hash"),
)
.on_hover_text(STATUS_SUBMENU_HASH)
.clicked()
{
self.hash_metric = Hash::Hash;
}
ui.separator();
if ui
.add_sized(
[button, text],
SelectableLabel::new(self.hash_metric == Hash::Kilo, "Kilo"),
)
.on_hover_text(STATUS_SUBMENU_KILO)
.clicked()
{
self.hash_metric = Hash::Kilo;
}
ui.separator();
if ui
.add_sized(
[button, text],
SelectableLabel::new(self.hash_metric == Hash::Mega, "Mega"),
)
.on_hover_text(STATUS_SUBMENU_MEGA)
.clicked()
{
self.hash_metric = Hash::Mega;
}
ui.separator();
if ui
.add_sized(
[button, text],
SelectableLabel::new(self.hash_metric == Hash::Giga, "Giga"),
)
.on_hover_text(STATUS_SUBMENU_GIGA)
.clicked()
{
self.hash_metric = Hash::Giga;
}
ui.separator();
ui.spacing_mut().slider_width = button * 11.5;
ui.add_sized(
[button * 14.0, text],
Slider::new(&mut self.hashrate, 1.0..=1_000.0),
);
})
});
// Actual stats
ui.set_enabled(p2pool_alive);
let text = height / 25.0;
let width = (width / 3.0) - (SPACE * 1.666);
let min_height = ui.available_height() / 1.3;
let api = lock!(p2pool_api);
ui.horizontal(|ui| {
ui.group(|ui| {
ui.vertical(|ui| {
ui.set_min_height(min_height);
ui.add_sized(
[width, text],
Label::new(RichText::new("Monero Difficulty").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_MONERO_DIFFICULTY);
ui.add_sized([width, text], Label::new(api.monero_difficulty.as_str()));
ui.add_sized(
[width, text],
Label::new(RichText::new("Monero Hashrate").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_MONERO_HASHRATE);
ui.add_sized([width, text], Label::new(api.monero_hashrate.as_str()));
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Difficulty").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_DIFFICULTY);
ui.add_sized([width, text], Label::new(api.p2pool_difficulty.as_str()));
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Hashrate").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_HASHRATE);
ui.add_sized([width, text], Label::new(api.p2pool_hashrate.as_str()));
})
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.set_min_height(min_height);
if self.manual_hash {
let hashrate =
Hash::convert_to_hash(self.hashrate, self.hash_metric) as u64;
let p2pool_share_mean = PubP2poolApi::calculate_share_or_block_time(
hashrate,
api.p2pool_difficulty_u64,
);
let solo_block_mean = PubP2poolApi::calculate_share_or_block_time(
hashrate,
api.monero_difficulty_u64,
);
ui.add_sized(
[width, text],
Label::new(
RichText::new("Manually Inputted Hashrate")
.underline()
.color(BONE),
),
);
ui.add_sized(
[width, text],
Label::new(format!("{} H/s", HumanNumber::from_u64(hashrate))),
);
ui.add_sized(
[width, text],
Label::new(
RichText::new("P2Pool Block Mean").underline().color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_BLOCK_MEAN);
ui.add_sized(
[width, text],
Label::new(api.p2pool_block_mean.to_string()),
);
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your P2Pool Share Mean")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_SHARE_MEAN);
ui.add_sized([width, text], Label::new(p2pool_share_mean.to_string()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your Solo Block Mean")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_SOLO_BLOCK_MEAN);
ui.add_sized([width, text], Label::new(solo_block_mean.to_string()));
} else {
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your P2Pool Hashrate")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_HASHRATE);
ui.add_sized(
[width, text],
Label::new(format!("{} H/s", api.hashrate_1h)),
);
ui.add_sized(
[width, text],
Label::new(
RichText::new("P2Pool Block Mean").underline().color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_BLOCK_MEAN);
ui.add_sized(
[width, text],
Label::new(api.p2pool_block_mean.to_string()),
);
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your P2Pool Share Mean")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_SHARE_MEAN);
ui.add_sized(
[width, text],
Label::new(api.p2pool_share_mean.to_string()),
);
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your Solo Block Mean")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_SOLO_BLOCK_MEAN);
ui.add_sized(
[width, text],
Label::new(api.solo_block_mean.to_string()),
);
}
})
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.set_min_height(min_height);
if self.manual_hash {
let hashrate =
Hash::convert_to_hash(self.hashrate, self.hash_metric) as u64;
let user_p2pool_percent = PubP2poolApi::calculate_dominance(
hashrate,
api.p2pool_hashrate_u64,
);
let user_monero_percent = PubP2poolApi::calculate_dominance(
hashrate,
api.monero_hashrate_u64,
);
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Miners").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_MINERS);
ui.add_sized([width, text], Label::new(api.miners.as_str()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("P2Pool Dominance").underline().color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_DOMINANCE);
ui.add_sized([width, text], Label::new(api.p2pool_percent.as_str()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your P2Pool Dominance")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_DOMINANCE);
ui.add_sized([width, text], Label::new(user_p2pool_percent.as_str()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your Monero Dominance")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_YOUR_MONERO_DOMINANCE);
ui.add_sized([width, text], Label::new(user_monero_percent.as_str()));
} else {
ui.add_sized(
[width, text],
Label::new(RichText::new("P2Pool Miners").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_MINERS);
ui.add_sized([width, text], Label::new(api.miners.as_str()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("P2Pool Dominance").underline().color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_P2POOL_DOMINANCE);
ui.add_sized([width, text], Label::new(api.p2pool_percent.as_str()));
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your P2Pool Dominance")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_YOUR_P2POOL_DOMINANCE);
ui.add_sized(
[width, text],
Label::new(api.user_p2pool_percent.as_str()),
);
ui.add_sized(
[width, text],
Label::new(
RichText::new("Your Monero Dominance")
.underline()
.color(BONE),
),
)
.on_hover_text(STATUS_SUBMENU_YOUR_MONERO_DOMINANCE);
ui.add_sized(
[width, text],
Label::new(api.user_monero_percent.as_str()),
);
}
})
});
});
// Tick bar
ui.add_sized(
[ui.available_width(), text],
Label::new(api.calculate_tick_bar()),
)
.on_hover_text(STATUS_SUBMENU_PROGRESS_BAR);
drop(api);
//---------------------------------------------------------------------------------------------------- [Benchmarks]
} else if self.submenu == Submenu::Benchmarks {
debug!("Status Tab | Rendering [Benchmarks]");
let text = height / 20.0;
let double = text * 2.0;
let log = height / 3.0;
// [0], The user's CPU (most likely).
let cpu = &benchmarks[0];
ui.horizontal(|ui| {
let width = (width / 2.0) - (SPACE * 1.666);
let min_height = log;
ui.group(|ui| {
ui.vertical(|ui| {
ui.set_min_height(min_height);
ui.add_sized(
[width, text],
Label::new(RichText::new("Your CPU").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_CPU);
ui.add_sized([width, text], Label::new(cpu.cpu.as_str()));
ui.add_sized(
[width, text],
Label::new(RichText::new("Total Benchmarks").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_BENCHMARKS);
ui.add_sized([width, text], Label::new(format!("{}", cpu.benchmarks)));
ui.add_sized(
[width, text],
Label::new(RichText::new("Rank").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_RANK);
ui.add_sized(
[width, text],
Label::new(format!("{}/{}", cpu.rank, &benchmarks.len())),
);
})
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.set_min_height(min_height);
ui.add_sized(
[width, text],
Label::new(RichText::new("High Hashrate").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_HIGH);
ui.add_sized(
[width, text],
Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.high))),
);
ui.add_sized(
[width, text],
Label::new(RichText::new("Average Hashrate").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_AVERAGE);
ui.add_sized(
[width, text],
Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.average))),
);
ui.add_sized(
[width, text],
Label::new(RichText::new("Low Hashrate").underline().color(BONE)),
)
.on_hover_text(STATUS_SUBMENU_YOUR_LOW);
ui.add_sized(
[width, text],
Label::new(format!("{} H/s", HumanNumber::from_f32(cpu.low))),
);
})
})
});
// User's CPU hashrate comparison (if XMRig is alive).
ui.scope(|ui| {
if xmrig_alive {
let api = lock!(xmrig_api);
let percent = (api.hashrate_raw / cpu.high) * 100.0;
let human = HumanNumber::to_percent(percent);
if percent > 100.0 {
ui.add_sized([width, double], Label::new(format!("Your CPU's is faster than the highest benchmark! It is [{}] faster @ {}!", human, api.hashrate)));
ui.add_sized([width, text], ProgressBar::new(1.0));
} else if api.hashrate_raw == 0.0 {
ui.add_sized([width, text], Label::new("Measuring hashrate..."));
ui.add_sized([width, text], Spinner::new().size(text));
ui.add_sized([width, text], ProgressBar::new(0.0));
} else {
ui.add_sized([width, double], Label::new(format!("Your CPU's hashrate is [{}] of the highest benchmark @ {}", human, api.hashrate)));
ui.add_sized([width, text], ProgressBar::new(percent / 100.0));
}
} else {
ui.set_enabled(xmrig_alive);
ui.add_sized([width, double], Label::new("XMRig is offline. Hashrate cannot be determined."));
ui.add_sized([width, text], ProgressBar::new(0.0));
}
});
// Comparison
ui.group(|ui| {
ui.add_sized(
[width, text],
Hyperlink::from_label_and_url("Other CPUs", "https://xmrig.com/benchmark"),
)
.on_hover_text(STATUS_SUBMENU_OTHER_CPUS);
});
egui::ScrollArea::both()
.scroll_bar_visibility(
egui::containers::scroll_area::ScrollBarVisibility::AlwaysVisible,
)
.max_width(width)
.max_height(height)
.auto_shrink([false; 2])
.show_viewport(ui, |ui, _| {
let width = width / 20.0;
let (cpu, bar, high, average, low, rank, bench) = (
width * 10.0,
width * 3.0,
width * 2.0,
width * 2.0,
width * 2.0,
width,
width * 2.0,
);
ui.group(|ui| {
ui.horizontal(|ui| {
ui.add_sized([cpu, double], Label::new("CPU"))
.on_hover_text(STATUS_SUBMENU_OTHER_CPU);
ui.separator();
ui.add_sized([bar, double], Label::new("Relative"))
.on_hover_text(STATUS_SUBMENU_OTHER_RELATIVE);
ui.separator();
ui.add_sized([high, double], Label::new("High"))
.on_hover_text(STATUS_SUBMENU_OTHER_HIGH);
ui.separator();
ui.add_sized([average, double], Label::new("Average"))
.on_hover_text(STATUS_SUBMENU_OTHER_AVERAGE);
ui.separator();
ui.add_sized([low, double], Label::new("Low"))
.on_hover_text(STATUS_SUBMENU_OTHER_LOW);
ui.separator();
ui.add_sized([rank, double], Label::new("Rank"))
.on_hover_text(STATUS_SUBMENU_OTHER_RANK);
ui.separator();
ui.add_sized([bench, double], Label::new("Benchmarks"))
.on_hover_text(STATUS_SUBMENU_OTHER_BENCHMARKS);
});
});
for benchmark in benchmarks[1..].iter() {
ui.group(|ui| {
ui.horizontal(|ui| {
ui.add_sized([cpu, text], Label::new(benchmark.cpu.as_str()));
ui.separator();
ui.add_sized(
[bar, text],
ProgressBar::new(benchmark.percent / 100.0),
)
.on_hover_text(HumanNumber::to_percent(benchmark.percent).as_str());
ui.separator();
ui.add_sized(
[high, text],
Label::new(HumanNumber::to_hashrate(benchmark.high).as_str()),
);
ui.separator();
ui.add_sized(
[average, text],
Label::new(
HumanNumber::to_hashrate(benchmark.average).as_str(),
),
);
ui.separator();
ui.add_sized(
[low, text],
Label::new(HumanNumber::to_hashrate(benchmark.low).as_str()),
);
ui.separator();
ui.add_sized(
[rank, text],
Label::new(HumanNumber::from_u16(benchmark.rank).as_str()),
);
ui.separator();
ui.add_sized(
[bench, text],
Label::new(
HumanNumber::from_u16(benchmark.benchmarks).as_str(),
),
);
})
});
}
});
}
}
}

View file

@ -49,11 +49,11 @@ pub const DISTRO_NO_UPDATE: &str = r#"This [Gupax] was compiled for use as a Lin
// Use macOS shaped icon for macOS
#[cfg(target_os = "macos")]
pub const BYTES_ICON: &[u8] = include_bytes!("../images/icons/icon@2x.png");
pub const BYTES_ICON: &[u8] = include_bytes!("../../assets/images/icons/icon@2x.png");
#[cfg(not(target_os = "macos"))]
pub const BYTES_ICON: &[u8] = include_bytes!("../images/icons/icon.png");
pub const BYTES_XVB: &[u8] = include_bytes!("../images/xvb.png");
pub const BYTES_BANNER: &[u8] = include_bytes!("../images/banner.png");
pub const BYTES_ICON: &[u8] = include_bytes!("../../assets/images/icons/icon.png");
pub const BYTES_XVB: &[u8] = include_bytes!("../../assets/images/xvb.png");
pub const BYTES_BANNER: &[u8] = include_bytes!("../../assets/images/banner.png");
pub const HORIZONTAL: &str = "--------------------------------------------";
pub const HORI_CONSOLE: &str = "---------------------------------------------------------------------------------------------------------------------------";
@ -387,6 +387,10 @@ pub const XMRIG_PATH_NOT_VALID: &str = "XMRig binary at the given PATH in the Gu
pub const XMRIG_PATH_OK: &str = "XMRig was found at the given PATH";
pub const XMRIG_PATH_EMPTY: &str = "XMRig PATH is empty! To fix: goto the [Gupax Advanced] tab, select [Open] and specify where XMRig is located.";
// XvB
pub const XVB_HELP: &str = "You need to register an account by clicking on the link above to get your token with the same p2pool XMR address you use for payment.";
pub const XVB_URL: &str = "https://xmrvsbeast.com";
// CLI argument messages
pub const ARG_HELP: &str = r#"USAGE: ./gupax [--flag]
@ -488,8 +492,7 @@ pub static VISUALS: Lazy<Visuals> = Lazy::new(|| {
..Visuals::dark()
}
});
//---------------------------------------------------------------------------------------------------- TESTS
//---------------------------------------------------------------------------------------------------- CONSTANTS
#[cfg(test)]
mod test {
#[test]

92
src/utils/errors.rs Normal file
View file

@ -0,0 +1,92 @@
use std::sync::{Arc, Mutex};
use super::sudo::SudoState;
//---------------------------------------------------------------------------------------------------- [ErrorState] struct
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ErrorButtons {
YesNo,
StayQuit,
ResetState,
ResetNode,
Okay,
Quit,
Sudo,
WindowsAdmin,
Debug,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ErrorFerris {
Happy,
Cute,
Oops,
Error,
Panic,
Sudo,
}
pub struct ErrorState {
pub error: bool, // Is there an error?
pub msg: String, // What message to display?
pub ferris: ErrorFerris, // Which ferris to display?
pub buttons: ErrorButtons, // Which buttons to display?
pub quit_twice: bool, // This indicates the user tried to quit on the [ask_before_quit] screen
}
impl Default for ErrorState {
fn default() -> Self {
Self::new()
}
}
impl ErrorState {
pub fn new() -> Self {
Self {
error: false,
msg: "Unknown Error".to_string(),
ferris: ErrorFerris::Oops,
buttons: ErrorButtons::Okay,
quit_twice: false,
}
}
// Convenience function to enable the [App] error state
pub fn set(&mut self, msg: impl Into<String>, ferris: ErrorFerris, buttons: ErrorButtons) {
if self.error {
// If a panic error is already set and there isn't an [Okay] confirm or another [Panic], return
if self.ferris == ErrorFerris::Panic
&& (buttons != ErrorButtons::Okay || ferris != ErrorFerris::Panic)
{
return;
}
}
*self = Self {
error: true,
msg: msg.into(),
ferris,
buttons,
quit_twice: false,
};
}
// Just sets the current state to new, resetting it.
pub fn reset(&mut self) {
*self = Self::new();
}
// Instead of creating a whole new screen and system, this (ab)uses ErrorState
// to ask for the [sudo] when starting XMRig. Yes, yes I know, it's called "ErrorState"
// but rewriting the UI code and button stuff might be worse.
// It also resets the current [SudoState]
pub fn ask_sudo(&mut self, state: &Arc<Mutex<SudoState>>) {
*self = Self {
error: true,
msg: String::new(),
ferris: ErrorFerris::Sudo,
buttons: ErrorButtons::Sudo,
quit_twice: false,
};
SudoState::reset(state)
}
}

View file

@ -17,12 +17,12 @@
// Some images of ferris in byte form for error messages, etc
pub const FERRIS_HAPPY: &[u8] = include_bytes!("../images/ferris/happy.png");
pub const FERRIS_CUTE: &[u8] = include_bytes!("../images/ferris/cute.png");
pub const FERRIS_OOPS: &[u8] = include_bytes!("../images/ferris/oops.png");
pub const FERRIS_ERROR: &[u8] = include_bytes!("../images/ferris/error.png");
pub const FERRIS_PANIC: &[u8] = include_bytes!("../images/ferris/panic.png"); // This isnt technically ferris but its ok since its spooky
pub const FERRIS_SUDO: &[u8] = include_bytes!("../images/ferris/sudo.png");
pub const FERRIS_HAPPY: &[u8] = include_bytes!("../../assets/images/ferris/happy.png");
pub const FERRIS_CUTE: &[u8] = include_bytes!("../../assets/images/ferris/cute.png");
pub const FERRIS_OOPS: &[u8] = include_bytes!("../../assets/images/ferris/oops.png");
pub const FERRIS_ERROR: &[u8] = include_bytes!("../../assets/images/ferris/error.png");
pub const FERRIS_PANIC: &[u8] = include_bytes!("../../assets/images/ferris/panic.png"); // This isnt technically ferris but its ok since its spooky
pub const FERRIS_SUDO: &[u8] = include_bytes!("../../assets/images/ferris/sudo.png");
// This is the ANSI representation of Ferris in string form.
// Calling [println!] on this straight up prints a 256-bit color Ferris to the terminal.

10
src/utils/mod.rs Normal file
View file

@ -0,0 +1,10 @@
pub mod constants;
pub mod errors;
pub mod ferris;
pub mod human;
pub mod macros;
pub mod panic;
pub mod regex;
pub mod resets;
pub mod sudo;
pub mod xmr;

View file

@ -123,8 +123,7 @@ impl XmrigRegex {
}
}
}
//---------------------------------------------------------------------------------------------------- TESTS
//---------------------------------------------------------------------------------------------------- TEST
#[cfg(test)]
mod test {
use super::*;

117
src/utils/resets.rs Normal file
View file

@ -0,0 +1,117 @@
//---------------------------------------------------------------------------------------------------- Reset functions
use crate::disk::create_gupax_dir;
use crate::disk::errors::TomlError;
use crate::disk::gupax_p2pool_api::GupaxP2poolApi;
use crate::disk::node::Node;
use crate::disk::pool::Pool;
use crate::disk::state::State;
use crate::info;
use log::error;
use std::path::PathBuf;
use std::process::exit;
#[cold]
#[inline(never)]
pub fn reset_state(path: &PathBuf) -> Result<(), TomlError> {
match State::create_new(path) {
Ok(_) => {
info!("Resetting [state.toml] ... OK");
Ok(())
}
Err(e) => {
error!("Resetting [state.toml] ... FAIL ... {}", e);
Err(e)
}
}
}
#[cold]
#[inline(never)]
pub fn reset_nodes(path: &PathBuf) -> Result<(), TomlError> {
match Node::create_new(path) {
Ok(_) => {
info!("Resetting [node.toml] ... OK");
Ok(())
}
Err(e) => {
error!("Resetting [node.toml] ... FAIL ... {}", e);
Err(e)
}
}
}
#[cold]
#[inline(never)]
pub fn reset_pools(path: &PathBuf) -> Result<(), TomlError> {
match Pool::create_new(path) {
Ok(_) => {
info!("Resetting [pool.toml] ... OK");
Ok(())
}
Err(e) => {
error!("Resetting [pool.toml] ... FAIL ... {}", e);
Err(e)
}
}
}
#[cold]
#[inline(never)]
pub fn reset_gupax_p2pool_api(path: &PathBuf) -> Result<(), TomlError> {
match GupaxP2poolApi::create_new(path) {
Ok(_) => {
info!("Resetting GupaxP2poolApi ... OK");
Ok(())
}
Err(e) => {
error!("Resetting GupaxP2poolApi folder ... FAIL ... {}", e);
Err(e)
}
}
}
#[cold]
#[inline(never)]
pub fn reset(
path: &PathBuf,
state: &PathBuf,
node: &PathBuf,
pool: &PathBuf,
gupax_p2pool_api: &PathBuf,
) {
let mut code = 0;
// Attempt to remove directory first
match std::fs::remove_dir_all(path) {
Ok(_) => info!("Removing OS data path ... OK"),
Err(e) => {
error!("Removing OS data path ... FAIL ... {}", e);
code = 1;
}
}
// Recreate
match create_gupax_dir(path) {
Ok(_) => (),
Err(_) => code = 1,
}
match reset_state(state) {
Ok(_) => (),
Err(_) => code = 1,
}
match reset_nodes(node) {
Ok(_) => (),
Err(_) => code = 1,
}
match reset_pools(pool) {
Ok(_) => (),
Err(_) => code = 1,
}
match reset_gupax_p2pool_api(gupax_p2pool_api) {
Ok(_) => (),
Err(_) => code = 1,
}
match code {
0 => println!("\nGupax reset ... OK"),
_ => eprintln!("\nGupax reset ... FAIL"),
}
exit(code);
}

View file

@ -19,7 +19,12 @@
// [zeroize] is used to wipe the memory after use.
// Only gets imported in [main.rs] for Unix.
use crate::{constants::*, disk::Xmrig, macros::*, Helper, ProcessSignal};
use crate::{
constants::*,
disk::state::Xmrig,
helper::{Helper, ProcessSignal},
macros::*,
};
use log::*;
use std::{
io::Write,