From 9faf0fc9f54420539eff54572f1369d7675668fb Mon Sep 17 00:00:00 2001 From: hinto-janaiyo Date: Sun, 13 Nov 2022 21:56:25 -0500 Subject: [PATCH] p2pool: add [Advanced], add [node.toml] database, add char limit --- src/README.md | 2 +- src/constants.rs | 45 +++- src/disk.rs | 537 +++++++++++++++++++++++++++++++++++++++++++++++ src/gupax.rs | 71 +++++-- src/main.rs | 75 +++++-- src/p2pool.rs | 378 +++++++++++++++++++++------------ src/state.rs | 350 ------------------------------ src/update.rs | 2 +- src/xmrig.rs | 2 +- 9 files changed, 925 insertions(+), 537 deletions(-) create mode 100644 src/disk.rs delete mode 100644 src/state.rs diff --git a/src/README.md b/src/README.md index b9bf19d..52f8f2b 100644 --- a/src/README.md +++ b/src/README.md @@ -8,12 +8,12 @@ | File/Folder | Purpose | |----------------|---------| | `constants.rs` | General constants needed in Gupax +| `disk.rs` | Code for writing to disk: `state.toml`, `nodes.toml`; This holds the structs for mutable [State] | `ferris.rs` | Cute crab `--ferris` | `gupax.rs` | `Gupax` tab | `main.rs` | `App/Tab/State` + misc functions | `node.rs` | Community node feature | `p2pool.rs` | `P2Pool` tab -| `state.rs` | `gupax.toml` config code. This holds the structs representing tabs with mutable state (Gupax/P2Pool/XMRig) | `status.rs` | `Status` tab | `update.rs` | Update code for the `Gupax` tab | `xmrig.rs` | `XMRig` tab diff --git a/src/constants.rs b/src/constants.rs index 6be837d..d43877d 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -66,13 +66,13 @@ pub const GUPAX_UPDATE_VIA_TOR: &'static str = "Update through the Tor network. pub const GUPAX_AUTO_NODE: &'static str = "Automatically ping the community Monero nodes and select the fastest at startup for P2Pool"; pub const GUPAX_ASK_BEFORE_QUIT: &'static str = "Ask before quitting if processes are still alive or if an update is in progress"; pub const GUPAX_SAVE_BEFORE_QUIT: &'static str = "Automatically save any changed settings before quitting"; -pub const GUPAX_PATH_P2POOL: &'static str = "The location of the P2Pool binary, both absolute and relative paths are accepted"; -pub const GUPAX_PATH_XMRIG: &'static str = "The location of the XMRig binary, both absolute and relative paths are accepted"; +pub const GUPAX_PATH_P2POOL: &'static str = "The location of the P2Pool binary: Both absolute and relative paths are accepted; A red [X] will appear if there is no file found at the given path"; +pub const GUPAX_PATH_XMRIG: &'static str = "The location of the XMRig binary: Both absolute and relative paths are accepted; A red [X] will appear if there is no file found at the given path"; // P2Pool pub const P2POOL_MAIN: &'static str = "Use the P2Pool main-chain. This P2Pool finds shares faster, but has a higher difficulty. Suitable for miners with more than 50kH/s"; pub const P2POOL_MINI: &'static str = "Use the P2Pool mini-chain. This P2Pool finds shares slower, but has a lower difficulty. Suitable for miners with less than 50kH/s"; -pub const P2POOL_OUT: &'static str = "How many out-bound peers (you connecting to others) to connect to?"; -pub const P2POOL_IN: &'static str = "How many in-bound peers (others connecting to you) to connect to?"; +pub const P2POOL_OUT: &'static str = "How many out-bound peers to connect to? (you connecting to others)"; +pub const P2POOL_IN: &'static str = "How many in-bound peers to allow? (others connecting to you)"; pub const P2POOL_LOG: &'static str = "Verbosity of the console log"; pub const P2POOL_COMMUNITY: &'static str = "Connect to a community trusted Monero node: This is convenient because you don't have to download the Monero blockchain but it comes at the cost of privacy"; pub const P2POOL_MANUAL: &'static str = "Manually specify your own Monero node settings"; @@ -81,6 +81,25 @@ pub const P2POOL_AUTO_SELECT: &'static str = "Automatically select the fastest c pub const P2POOL_SELECT_FASTEST: &'static str = "Select the fastest community Monero node"; pub const P2POOL_PING: &'static str = "Ping the built-in community Monero nodes"; pub const P2POOL_ADDRESS: &'static str = "You must use a primary Monero address to mine on P2Pool (starts with a 4). It is highly recommended to create a new wallet for P2Pool mining; wallet addresses are public on P2Pool!"; +pub const P2POOL_COMMAND: &'static str = "Start P2Pool with these arguments and override all below settings; If the [--data-api] flag is not given, Gupax will append it to the arguments automatically so that the [Status] tab can work"; +pub const P2POOL_SIMPLE: &'static str = +r#"Use simple settings: + - Remote community Monero node + - Default P2Pool settings + Mini"#; +pub const P2POOL_ADVANCED: &'static str = +r#"Use advanced settings: + - Overriding command arguments + - Manual node selection + - P2Pool Main/Mini selection + - Out/In peer setting + - Log level setting"#; +pub const P2POOL_NAME: &'static str = "Add a unique name to identify this node; Only [A-Za-z0-9-_] and spaces allowed; Max length = 30 characters"; +pub const P2POOL_NODE_IP: &'static str = "Specify the Monero Node IP to connect to with P2Pool; Max length = 255 characters"; +pub const P2POOL_RPC_PORT: &'static str = "Specify the RPC port of the Monero node; [0-65535]"; +pub const P2POOL_ZMQ_PORT: &'static str = "Specify the ZMQ port of the Monero node; [0-65535]"; +pub const P2POOL_ADD: &'static str = "Add the current values to the list"; +pub const P2POOL_DELETE: &'static str = "Delete the currently selected node"; +pub const P2POOL_CLEAR: &'static str = "Clear all current values"; // XMRig pub const XMRIG_P2POOL: &'static str = "Mine to your own P2Pool instance (localhost:3333)"; @@ -94,13 +113,19 @@ pub const XMRIG_PRIORITY: &'static str = "Set process priority (0 idle, 2 normal // CLI argument messages pub const ARG_HELP: &'static str = -r#"USAGE: gupax [--flags] +r#"USAGE: ./gupax [--flags] - -h | --help Print this help message - -v | --version Print versions - -n | --no-startup Disable auto-update/node connections at startup - -r | --reset Reset all Gupax configuration/state - -f | --ferris Print an extremely cute crab"#; + -h | --help Print this help message + -v | --version Print version and build info + -l | --node-list Print the manual node list + -s | --state Print Gupax state + -n | --no-startup Disable all auto-startup settings for this instance + -r | --reset Reset all Gupax state and the manual node list + -f | --ferris Print an extremely cute crab + +To view more detailed console debug information, start Gupax with +the environment variable [RUST_LOG] set to a log level like so: + RUST_LOG=(trace|debug|info|warn|error) ./gupax"#; pub const ARG_COPYRIGHT: &'static str = r#"Gupax is licensed under GPLv3. For more information, see link below: diff --git a/src/disk.rs b/src/disk.rs new file mode 100644 index 0000000..5ff3c7a --- /dev/null +++ b/src/disk.rs @@ -0,0 +1,537 @@ +// Gupax - GUI Uniting P2Pool And XMRig +// +// Copyright (c) 2022 hinto-janaiyo +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// This handles reading/writing the disk files: +// - [state.toml] -> [App] state +// - [nodes.toml] -> [Manual Nodes] list +// The TOML format is used. This struct hierarchy +// directly translates into the TOML parser: +// State/ +// ├─ Gupax/ +// │ ├─ ... +// ├─ P2pool/ +// │ ├─ ... +// ├─ Xmrig/ +// │ ├─ ... +// ├─ Version/ +// ├─ ... + +use std::{fs,env}; +use std::fmt::Display; +use std::path::{Path,PathBuf}; +use std::result::Result; +use std::sync::{Arc,Mutex}; +use std::collections::HashMap; +use std::fmt::Write; +use serde::{Serialize,Deserialize}; +use figment::Figment; +use figment::providers::{Format,Toml}; +use crate::constants::*; +use anyhow::Error; +use log::*; + +//---------------------------------------------------------------------------------------------------- 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_file_path(file: File) -> Result { + // Get OS data folder + // Linux | $XDG_DATA_HOME or $HOME/.local/share | /home/alice/.local/state + // macOS | $HOME/Library/Application Support | /Users/Alice/Library/Application Support + // Windows | {FOLDERID_RoamingAppData} | C:\Users\Alice\AppData\Roaming + let name = File::name(&file); + + let mut path = match dirs::data_dir() { + Some(mut path) => { + path.push(DIRECTORY); + info!("OS data path ... OK"); + path + }, + None => { error!("Couldn't get OS PATH for data"); return Err(TomlError::Path(PATH_ERROR.to_string())) }, + }; + // Create directory + fs::create_dir_all(&path)?; + path.push(name); + info!("{:?} path ... {}", file, path.display()); + Ok(path) +} + +// Convert a [File] path to a [String] +pub fn read_to_string(file: File, path: &PathBuf) -> Result { + match fs::read_to_string(&path) { + Ok(string) => { + info!("{:?} | Read ... OK", file); + Ok(string) + }, + Err(err) => { + warn!("{:?} | Read ... FAIL", file); + Err(TomlError::Io(err)) + }, + } +} + +// Write [String] to console with [info!] surrounded by "---" +pub fn print_toml(toml: &String) { + info!("{}", HORIZONTAL); + for i in toml.lines() { info!("{}", i); } + info!("{}", HORIZONTAL); +} + +// Turn relative paths into absolute paths +pub fn into_absolute_path(path: String) -> Result { + let path = PathBuf::from(path); + if path.is_relative() { + let mut dir = std::env::current_exe()?; + dir.pop(); + dir.push(path); + Ok(dir) + } else { + Ok(path) + } +} + +//---------------------------------------------------------------------------------------------------- [State] Impl +impl State { + pub fn new() -> Self { + let max_threads = num_cpus::get(); + let current_threads; + if max_threads == 1 { current_threads = 1; } else { current_threads = max_threads / 2; } + Self { + gupax: Gupax { + auto_update: true, + auto_node: true, + 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(), + }, + p2pool: P2pool { + simple: true, + mini: true, + auto_node: true, + auto_select: true, + out_peers: 10, + in_peers: 10, + log_level: 3, + node: crate::NodeEnum::C3pool, + arguments: String::new(), + address: String::with_capacity(95), + name: "Local Monero Node".to_string(), + ip: "localhost".to_string(), + rpc: "18081".to_string(), + zmq: "18083".to_string(), + selected_name: "Local Monero Node".to_string(), + selected_ip: "localhost".to_string(), + selected_rpc: "18081".to_string(), + selected_zmq: "18083".to_string(), + }, + xmrig: Xmrig { + simple: true, + tls: false, + nicehash: false, + keepalive: false, + hugepages_jit: true, + current_threads, + max_threads, + priority: 2, + pool: "localhost:3333".to_string(), + address: String::with_capacity(95), + }, + version: Arc::new(Mutex::new(Version { + p2pool: Arc::new(Mutex::new(P2POOL_VERSION.to_string())), + xmrig: Arc::new(Mutex::new(XMRIG_VERSION.to_string())), + })), + } + } + + // Convert [String] to [State] + pub fn from_string(string: String) -> Result { + match toml::de::from_str(&string) { + Ok(state) => { + info!("State parse ... OK"); + print_toml(&string); + Ok(state) + } + Err(err) => { + error!("State | String -> State ... FAIL ... {}", err); + Err(TomlError::Deserialize(err)) + }, + } + } + + // 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() -> Result { + // Read + let file = File::State; + let path = get_file_path(file)?; + let string = match read_to_string(file, &path) { + Ok(string) => string, + // Create + _ => { + let new = Self::create_new()?; + read_to_string(file, &path)? + }, + }; + // Deserialize + Self::from_string(string) + } + + // Completely overwrite current [state.toml] + // with a new default version, and return [Self]. + pub fn create_new() -> Result { + info!("State | Creating new default..."); + let new = Self::new(); + let path = get_file_path(File::State)?; + println!("{:#?}", new); + let string = match toml::ser::to_string(&new) { + Ok(o) => { info!("State | Serialization ... OK"); o }, + Err(e) => { error!("State | Couldn't serialize default file: {}", e); return Err(TomlError::Serialize(e)) }, + }; + fs::write(&path, &string)?; + info!("State | Write ... OK"); + Ok(new) + } + + // Save [State] onto disk file [gupax.toml] + pub fn save(&mut self) -> Result<(), TomlError> { + info!("Saving {:?} to disk...", self); + let path = get_file_path(File::State)?; + // 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!("TOML parse ... OK"); + print_toml(&string); + string + }, + Err(err) => { error!("Couldn't parse TOML into string"); return Err(TomlError::Serialize(err)) }, + }; + match fs::write(path, string) { + Ok(_) => { info!("TOML save ... OK"); Ok(()) }, + Err(err) => { error!("Couldn't overwrite TOML file"); return Err(TomlError::Io(err)) }, + } + } + + // Take [Self] as input, merge it with whatever the current [default] is, + // leaving behind old keys+values and updating [default] with old valid ones. + // Automatically overwrite current file. + pub fn merge(old: &Self) -> Result { + info!("Starting TOML merge..."); + let old = match toml::ser::to_string(&old) { + Ok(string) => { info!("Old TOML parse ... OK"); string }, + Err(err) => { error!("Couldn't parse old TOML into string"); return Err(TomlError::Serialize(err)) }, + }; + let default = match toml::ser::to_string(&Self::new()) { + Ok(string) => { info!("Default TOML parse ... OK"); string }, + Err(err) => { error!("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!("TOML merge ... OK"); new }, + Err(err) => { error!("Couldn't merge default + old TOML"); return Err(TomlError::Merge(err)) }, + }; + // Attempt save + Self::save(&mut new)?; + Ok(new) + } +} + +//---------------------------------------------------------------------------------------------------- [Node] Impl +impl Node { + pub fn new() -> Self { + Self { + ip: String::new(), + rpc: "18081".to_string(), + zmq: "18083".to_string(), + } + } + + pub fn localhost() -> Self { + Self { + ip: "localhost".to_string(), + rpc: "18081".to_string(), + zmq: "18083".to_string(), + } + } + + pub fn new_vec() -> Vec<(String, Self)> { + let mut vec = Vec::new(); + vec.push(("Local Monero Node".to_string(), Self::localhost())); + vec + } + + // Convert [String] to [Node] Vec + pub fn from_string(string: String) -> Result, TomlError> { + let nodes: HashMap = match toml::de::from_str(&string) { + Ok(map) => { + info!("Node | Parse ... OK"); + print_toml(&string); + 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() { + vec.push((key.clone(), values.clone())); + } + Ok(vec) + } + + // Convert [Vec<(String, Self)>] into [String] + // that can be written as a proper TOML file + pub fn into_string(vec: Vec<(String, Self)>) -> String { + let mut toml = String::new(); + for (key, value) in vec.iter() { + write!( + toml, + "[\'{}\']\nip = {:#?}\nrpc = {:#?}\nzmq = {:#?}\n", + key, + value.ip, + value.rpc, + value.zmq, + ); + } + 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() -> Result, TomlError> { + // Read + let file = File::Node; + let path = get_file_path(file)?; + let string = match read_to_string(file, &path) { + Ok(string) => string, + // Create + _ => { + let new = Self::create_new()?; + read_to_string(file, &path)? + }, + }; + // Deserialize + Self::from_string(string) + } + + // Completely overwrite current [node.toml] + // with a new default version, and return [Vec]. + pub fn create_new() -> Result, TomlError> { + info!("Node | Creating new default..."); + let new = Self::new_vec(); + let path = get_file_path(File::Node)?; + let string = Self::into_string(Self::new_vec()); + fs::write(&path, &string)?; + info!("Node | Write ... OK"); + Ok(new) + } + +// // Save [State] onto disk file [gupax.toml] +// pub fn save(&mut self) -> Result<(), TomlError> { +// info!("Saving {:?} to disk...", self); +// let path = get_file_path(File::State)?; +// // 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!("TOML parse ... OK"); +// print_toml(&string); +// string +// }, +// Err(err) => { error!("Couldn't parse TOML into string"); return Err(TomlError::Serialize(err)) }, +// }; +// match fs::write(path, string) { +// Ok(_) => { info!("TOML save ... OK"); Ok(()) }, +// Err(err) => { error!("Couldn't overwrite TOML file"); return Err(TomlError::Io(err)) }, +// } +// } +// +// // Take [Self] as input, merge it with whatever the current [default] is, +// // leaving behind old keys+values and updating [default] with old valid ones. +// // Automatically overwrite current file. +// pub fn merge(old: &Self) -> Result { +// info!("Starting TOML merge..."); +// let old = match toml::ser::to_string(&old) { +// Ok(string) => { info!("Old TOML parse ... OK"); string }, +// Err(err) => { error!("Couldn't parse old TOML into string"); return Err(TomlError::Serialize(err)) }, +// }; +// let default = match toml::ser::to_string(&Self::new()) { +// Ok(string) => { info!("Default TOML parse ... OK"); string }, +// Err(err) => { error!("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!("TOML merge ... OK"); new }, +// Err(err) => { error!("Couldn't merge default + old TOML"); return Err(TomlError::Merge(err)) }, +// }; +// // Attempt save +// Self::save(&mut new)?; +// Ok(new) +// } +} + +//---------------------------------------------------------------------------------------------------- Custom Error [TomlError] +impl Display for TomlError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use TomlError::*; + match self { + Io(err) => write!(f, "{}: {} | {}", ERROR, self, err), + Path(err) => write!(f, "{}: {} | {}", ERROR, self, err), + Serialize(err) => write!(f, "{}: {} | {}", ERROR, self, err), + Deserialize(err) => write!(f, "{}: {} | {}", ERROR, self, err), + Merge(err) => write!(f, "{}: {} | {}", ERROR, self, err), + } + } +} + +impl From for TomlError { + fn from(err: std::io::Error) -> Self { + TomlError::Io(err) + } +} + +//---------------------------------------------------------------------------------------------------- Const +// State file +const ERROR: &'static str = "Disk error"; +const PATH_ERROR: &'static str = "PATH for state directory could not be not found"; +#[cfg(target_os = "windows")] +const DIRECTORY: &'static str = r#"Gupax\"#; +#[cfg(target_os = "macos")] +const DIRECTORY: &'static str = "com.github.hinto-janaiyo.gupax/"; +#[cfg(target_os = "linux")] +const DIRECTORY: &'static str = "gupax/"; + +#[cfg(target_os = "windows")] +pub const DEFAULT_P2POOL_PATH: &'static str = r"P2Pool\p2pool.exe"; +#[cfg(target_family = "unix")] +pub const DEFAULT_P2POOL_PATH: &'static str = "p2pool/p2pool"; +#[cfg(target_os = "windows")] +pub const DEFAULT_XMRIG_PATH: &'static str = r"XMRig\xmrig.exe"; +#[cfg(target_family = "unix")] +pub const DEFAULT_XMRIG_PATH: &'static str = "xmrig/xmrig"; + +//---------------------------------------------------------------------------------------------------- Error Enum +#[derive(Debug)] +pub enum TomlError { + Io(std::io::Error), + Path(String), + Serialize(toml::ser::Error), + Deserialize(toml::de::Error), + Merge(figment::Error), +} + +//---------------------------------------------------------------------------------------------------- [File] Enum (for matching which file) +#[derive(Clone,Copy,Eq,PartialEq,Debug,Deserialize,Serialize)] +pub enum File { + State, + Node, +} + +impl File { + fn name(&self) -> &'static str { + match *self { + Self::State => "state.toml", + Self::Node => "node.toml", + } + } +} + +//---------------------------------------------------------------------------------------------------- [Node] Struct +#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)] +pub struct Node { + pub ip: String, + pub rpc: String, + pub zmq: String, +} + +//---------------------------------------------------------------------------------------------------- [State] Struct +#[derive(Clone,Debug,Deserialize,Serialize)] +pub struct State { + pub gupax: Gupax, + pub p2pool: P2pool, + pub xmrig: Xmrig, + pub version: Arc>, +} + +#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)] +pub struct Gupax { + pub auto_update: bool, + pub auto_node: 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, +} + +#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)] +pub struct P2pool { + pub simple: bool, + pub mini: bool, + pub auto_node: bool, + pub auto_select: bool, + pub out_peers: u16, + pub in_peers: u16, + pub log_level: u8, + pub node: crate::node::NodeEnum, + pub arguments: String, + pub address: String, + pub name: String, + pub ip: String, + pub rpc: String, + pub zmq: String, + 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 tls: bool, + pub nicehash: bool, + pub keepalive: bool, + pub hugepages_jit: bool, + pub max_threads: usize, + pub current_threads: usize, + pub priority: u8, + pub pool: String, + pub address: String, +} + +#[derive(Clone,Debug,Deserialize,Serialize)] +pub struct Version { + pub p2pool: Arc>, + pub xmrig: Arc>, +} diff --git a/src/gupax.rs b/src/gupax.rs index 35043de..54d298f 100644 --- a/src/gupax.rs +++ b/src/gupax.rs @@ -17,24 +17,29 @@ use std::path::Path; use crate::{App,State}; -use egui::TextStyle::Monospace; -use egui::RichText; +use egui::{ + TextStyle::Monospace, + Checkbox, + RichText, + Label, + Color32, +}; use crate::constants::*; -use crate::state::{Gupax,Version}; +use crate::disk::{Gupax,Version}; use crate::update::*; use std::thread; use std::sync::{Arc,Mutex}; use log::*; impl Gupax { - pub fn show(state: &mut Gupax, og: &Arc>, state_ver: &Arc>, update: &Arc>, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) { + pub fn show(&mut self, og: &Arc>, state_ver: &Arc>, update: &Arc>, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) { // Update button + Progress bar ui.group(|ui| { // These are in unnecessary [ui.vertical()]'s // because I need to use [ui.set_enabled]s, but I can't // find a way to use a [ui.xxx()] with [ui.add_sized()]. // I have to pick one. This one seperates them though. - let height = height/6.0; + let height = height/8.0; let width = width - SPACE; let updating = *update.lock().unwrap().updating.lock().unwrap(); ui.vertical(|ui| { @@ -87,36 +92,62 @@ impl Gupax { ui.horizontal(|ui| { ui.group(|ui| { - let width = (width - SPACE*9.8)/5.0; - let height = height/2.5; + let width = (width - SPACE*7.5)/4.0; + let height = height/8.0; let mut style = (*ctx.style()).clone(); - style.spacing.icon_width_inner = width / 6.0; - style.spacing.icon_width = width / 4.0; + style.spacing.icon_width_inner = width / 8.0; + style.spacing.icon_width = width / 6.0; style.spacing.icon_spacing = 20.0; ctx.set_style(style); - let height = height/2.5; - ui.add_sized([width, height], egui::Checkbox::new(&mut state.auto_update, "Auto-update")).on_hover_text(GUPAX_AUTO_UPDATE); + ui.add_sized([width, height], egui::Checkbox::new(&mut self.auto_update, "Auto-update")).on_hover_text(GUPAX_AUTO_UPDATE); ui.separator(); - ui.add_sized([width, height], egui::Checkbox::new(&mut state.auto_node, "Auto-node")).on_hover_text(GUPAX_AUTO_NODE); + ui.add_sized([width, height], egui::Checkbox::new(&mut self.update_via_tor, "Update via Tor")).on_hover_text(GUPAX_UPDATE_VIA_TOR); ui.separator(); - ui.add_sized([width, height], egui::Checkbox::new(&mut state.update_via_tor, "Update via Tor")).on_hover_text(GUPAX_UPDATE_VIA_TOR); + ui.add_sized([width, height], egui::Checkbox::new(&mut self.ask_before_quit, "Ask before quit")).on_hover_text(GUPAX_ASK_BEFORE_QUIT); ui.separator(); - ui.add_sized([width, height], egui::Checkbox::new(&mut state.ask_before_quit, "Ask before quit")).on_hover_text(GUPAX_ASK_BEFORE_QUIT); - ui.separator(); - ui.add_sized([width, height], egui::Checkbox::new(&mut state.save_before_quit, "Save before quit")).on_hover_text(GUPAX_SAVE_BEFORE_QUIT); + ui.add_sized([width, height], egui::Checkbox::new(&mut self.save_before_quit, "Save before quit")).on_hover_text(GUPAX_SAVE_BEFORE_QUIT); }); }); ui.add_space(SPACE); + ui.style_mut().override_text_style = Some(Monospace); + let height = height/20.0; + let text_edit = (ui.available_width()/10.0)-SPACE; ui.horizontal(|ui| { - ui.label("P2Pool binary path:"); + if self.p2pool_path.is_empty() { + ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ➖").color(Color32::LIGHT_GRAY))); + } else { + match crate::disk::into_absolute_path(self.p2pool_path.clone()) { + Ok(path) => { + if path.is_file() { + ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ✔").color(Color32::from_rgb(100, 230, 100)))) + } else { + ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ❌").color(Color32::from_rgb(230, 50, 50)))) + } + }, + _ => ui.add_sized([text_edit, height], Label::new(RichText::new("P2Pool Binary Path ❌").color(Color32::from_rgb(230, 50, 50)))), + }; + } ui.spacing_mut().text_edit_width = ui.available_width() - SPACE; - ui.text_edit_singleline(&mut state.p2pool_path).on_hover_text(GUPAX_PATH_P2POOL); + ui.text_edit_singleline(&mut self.p2pool_path).on_hover_text(GUPAX_PATH_P2POOL); }); ui.horizontal(|ui| { - ui.label("XMRig binary path: "); + if self.xmrig_path.is_empty() { + ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ➖").color(Color32::LIGHT_GRAY))); + } else { + match crate::disk::into_absolute_path(self.xmrig_path.clone()) { + Ok(path) => { + if path.is_file() { + ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ✔").color(Color32::from_rgb(100, 230, 100)))) + } else { + ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ❌").color(Color32::from_rgb(230, 50, 50)))) + } + }, + _ => ui.add_sized([text_edit, height], Label::new(RichText::new(" XMRig Binary Path ❌").color(Color32::from_rgb(230, 50, 50)))), + }; + } ui.spacing_mut().text_edit_width = ui.available_width() - SPACE; - ui.text_edit_singleline(&mut state.xmrig_path).on_hover_text(GUPAX_PATH_XMRIG); + ui.text_edit_singleline(&mut self.xmrig_path).on_hover_text(GUPAX_PATH_XMRIG); }); } } diff --git a/src/main.rs b/src/main.rs index a86fa6f..7fcfdfd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,14 +49,14 @@ use std::path::PathBuf; mod ferris; mod constants; mod node; -mod state; +mod disk; mod about; mod status; mod gupax; mod p2pool; mod xmrig; mod update; -use {ferris::*,constants::*,node::*,state::*,about::*,status::*,gupax::*,p2pool::*,xmrig::*,update::*}; +use {ferris::*,constants::*,node::*,disk::*,about::*,status::*,gupax::*,p2pool::*,xmrig::*,update::*}; //---------------------------------------------------------------------------------------------------- Struct + Impl // The state of the outer main [App]. @@ -74,6 +74,7 @@ pub struct App { og: Arc>, // og = Old state to compare against state: State, // state = Working state (current settings) update: Arc>, // State for update data [update.rs] + node_vec: Vec<(String, Node)>, // Manual Node database diff: bool, // This bool indicates state changes // Process/update state: // Doesn't make sense to save this on disk @@ -92,7 +93,7 @@ pub struct App { version: &'static str, // Gupax version name_version: String, // [Gupax vX.X.X] banner: RetainedImage, // Gupax banner image - addr_regex: Regex, // [4.*] Monero Address Regex + regex: Regexes, // Custom Struct holding pre-made [Regex]'s } impl App { @@ -113,9 +114,10 @@ impl App { ping: Arc::new(Mutex::new(Ping::new())), width: 1280.0, height: 720.0, - og: Arc::new(Mutex::new(State::default())), - state: State::default(), + og: Arc::new(Mutex::new(State::new())), + state: State::new(), update: Arc::new(Mutex::new(Update::new(String::new(), PathBuf::new(), PathBuf::new(), true))), + node_vec: Node::new_vec(), diff: false, p2pool: false, xmrig: false, @@ -129,7 +131,7 @@ impl App { version: GUPAX_VERSION, name_version: format!("Gupax {}", GUPAX_VERSION), banner: RetainedImage::from_image_bytes("banner.png", BYTES_BANNER).unwrap(), - addr_regex: Regex::new("^4[A-Za-z1-9]+$").unwrap(), + regex: Regexes::new(), }; // Apply arg state let mut app = parse_args(app); @@ -151,6 +153,8 @@ impl App { }; } app.state = app.og.lock().unwrap().clone(); + // Get node list + app.node_vec = Node::get().unwrap(); // Handle max threads app.og.lock().unwrap().xmrig.max_threads = num_cpus::get(); let current = app.og.lock().unwrap().xmrig.current_threads; @@ -167,7 +171,7 @@ impl App { } } -//---------------------------------------------------------------------------------------------------- Enum + Impl +//---------------------------------------------------------------------------------------------------- [Tab] Enum + Impl // The tabs inside [App]. #[derive(Clone, Copy, Debug, PartialEq)] enum Tab { @@ -184,6 +188,28 @@ impl Default for Tab { } } +//---------------------------------------------------------------------------------------------------- [Regexes] struct +#[derive(Clone, Debug)] +struct Regexes { + name: Regex, + address: Regex, + ipv4: Regex, + domain: Regex, + port: Regex, +} + +impl Regexes { + fn new() -> Self { + Regexes { + name: Regex::new("^[A-Za-z0-9-_]+( [A-Za-z0-9-_]+)*$").unwrap(), + address: Regex::new("^4[A-Za-z1-9]+$").unwrap(), // This still needs to check for (l, I, o, 0) + ipv4: Regex::new(r#"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$"#).unwrap(), + domain: Regex::new(r#"^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,3})$"#).unwrap(), + port: Regex::new(r#"^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"#).unwrap(), + } + } +} + //---------------------------------------------------------------------------------------------------- Init functions fn init_text_styles(ctx: &egui::Context, width: f32) { let scale = width / 26.666; @@ -196,6 +222,7 @@ fn init_text_styles(ctx: &egui::Context, width: f32) { (Heading, FontId::new(scale/1.5, Proportional)), (Name("Tab".into()), FontId::new(scale*1.2, Proportional)), (Name("Bottom".into()), FontId::new(scale/2.0, Proportional)), + (Name("MonospaceSmall".into()), FontId::new(scale/2.5, egui::FontFamily::Monospace)), ].into(); // style.visuals.selection.stroke = Stroke { width: 5.0, color: Color32::from_rgb(255, 255, 255) }; // style.spacing.slider_width = scale; @@ -301,7 +328,9 @@ fn init_auto(app: &App) { } // [Auto-Ping] - if app.og.lock().unwrap().p2pool.auto_node { + let auto_node = app.og.lock().unwrap().p2pool.auto_node; + let simple = app.og.lock().unwrap().p2pool.simple; + if auto_node && simple { let ping = Arc::clone(&app.ping); let og = Arc::clone(&app.og); thread::spawn(move|| { @@ -323,7 +352,7 @@ fn parse_args(mut app: App) -> App { match arg.as_str() { "-h"|"--help" => { println!("{}", ARG_HELP); exit(0); }, "-v"|"--version" => { - println!("Gupax {} (OS: {}, Commit: {})\n\n{}", GUPAX_VERSION, OS_NAME, &COMMIT[..40], ARG_COPYRIGHT); + println!("Gupax {} [OS: {}, Commit: {}]\n\n{}", GUPAX_VERSION, OS_NAME, &COMMIT[..40], ARG_COPYRIGHT); exit(0); }, "-f"|"--ferris" => { println!("{}", FERRIS); exit(0); }, @@ -333,9 +362,11 @@ fn parse_args(mut app: App) -> App { // Everything else for arg in args { match arg.as_str() { + "-l"|"--node-list" => { info!("Printing node list..."); print_disk_file(File::Node); } + "-s"|"--state" => { info!("Printing state..."); print_disk_file(File::State); } "-n"|"--no-startup" => { info!("Disabling startup..."); app.startup = false; } - "-r"|"--reset" => { info!("Resetting state..."); app.reset = true; } - _ => { eprintln!("[Gupax error] Invalid option: [{}]\nFor help, use: [--help]", arg); exit(1); }, + "-r"|"--reset" => { info!("Resetting state..."); app.reset = true; } + _ => { eprintln!("[Gupax error] Invalid option: [{}]\nFor help, use: [--help]", arg); exit(1); }, } } app @@ -373,6 +404,18 @@ pub fn clean_dir() -> Result<(), anyhow::Error> { Ok(()) } +// Print disk files to console +fn print_disk_file(file: File) { + let path = match get_file_path(file) { + Ok(path) => path, + Err(e) => { error!("{}", e); exit(1); }, + }; + match std::fs::read_to_string(&path) { + Ok(string) => { println!("{}", string); exit(0); }, + Err(e) => { error!("{}", e); exit(1); }, + } +} + //---------------------------------------------------------------------------------------------------- [App] frame for [Panic] situations fn panic_main(error: String) { error!("{}", error); @@ -560,7 +603,7 @@ impl eframe::App for App { // Top: Tabs egui::TopBottomPanel::top("top").show(ctx, |ui| { let width = (self.width - (SPACE*10.0))/5.0; - let height = self.height/10.0; + let height = self.height/12.0; ui.group(|ui| { ui.add_space(4.0); ui.horizontal(|ui| { @@ -584,7 +627,7 @@ impl eframe::App for App { // Bottom: app info + state/process buttons egui::TopBottomPanel::bottom("bottom").show(ctx, |ui| { - let height = self.height/18.0; + let height = self.height/20.0; ui.style_mut().override_text_style = Some(Name("Bottom".into())); ui.horizontal(|ui| { ui.group(|ui| { @@ -632,11 +675,11 @@ impl eframe::App for App { Tab::P2pool => { ui.group(|ui| { let width = width / 1.5; - if ui.add_sized([width, height], egui::SelectableLabel::new(!self.state.p2pool.simple, "Advanced")).clicked() { + if ui.add_sized([width, height], egui::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], egui::SelectableLabel::new(self.state.p2pool.simple, "Simple")).clicked() { + if ui.add_sized([width, height], egui::SelectableLabel::new(self.state.p2pool.simple, "Simple")).on_hover_text(P2POOL_SIMPLE).clicked() { self.state.p2pool.simple = true; } }); @@ -725,7 +768,7 @@ impl eframe::App for App { Gupax::show(&mut self.state.gupax, &self.og, &self.state.version, &self.update, self.width, self.height, ctx, ui); } Tab::P2pool => { - P2pool::show(&mut self.state.p2pool, &self.og, self.p2pool, &self.ping, &self.addr_regex, self.width, self.height, ctx, ui); + P2pool::show(&mut self.state.p2pool, &mut self.node_vec, &self.og, self.p2pool, &self.ping, &self.regex, self.width, self.height, ctx, ui); } Tab::Xmrig => { Xmrig::show(&mut self.state.xmrig, self.width, self.height, ctx, ui); diff --git a/src/p2pool.rs b/src/p2pool.rs index eb3c966..0fededb 100644 --- a/src/p2pool.rs +++ b/src/p2pool.rs @@ -15,33 +15,73 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::App; -use crate::constants::*; -use crate::state::*; -use crate::node::*; -use crate::node::NodeEnum::*; +use crate::{ + App, + Regexes, + constants::*, + disk::*, + node::* +}; +use egui::{ + TextEdit,SelectableLabel,ComboBox,Label,FontId,Button,Color32,RichText,Slider,Checkbox, + TextStyle::*, + FontFamily::Proportional, + TextBuffer, +}; use std::sync::{Arc,Mutex}; use std::thread; -use log::*; -use egui::{TextEdit,SelectableLabel,ComboBox,Label}; -use egui::TextStyle::*; -use egui::FontFamily::Proportional; -use egui::{FontId,Button,Color32,RichText}; use regex::Regex; +use log::*; impl P2pool { - pub fn show(&mut self, og: &Arc>, online: bool, ping: &Arc>, addr_regex: &Regex, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) { - let text_edit = height / 20.0; - // Console + pub fn show(&mut self, node_vec: &mut Vec<(String, Node)>, og: &Arc>, online: bool, ping: &Arc>, regex: &Regexes, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) { + let text_edit = height / 22.0; + //---------------------------------------------------------------------------------------------------- Console ui.group(|ui| { let height = height / SPACE; let width = width - SPACE; + ui.style_mut().override_text_style = Some(Monospace); ui.add_sized([width, height*3.0], TextEdit::multiline(&mut "".to_string())); ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(&mut "".to_string()), r#"Type a command (e.g "help" or "status") and press Enter"#)); }); + //---------------------------------------------------------------------------------------------------- Args + if ! self.simple { + ui.group(|ui| { ui.horizontal(|ui| { + let width = (width/10.0) - SPACE; + ui.style_mut().override_text_style = Some(Monospace); + 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_COMMAND); + self.arguments.truncate(1024); + })}); + ui.set_enabled(self.arguments.is_empty()); + } + + //---------------------------------------------------------------------------------------------------- Address + ui.group(|ui| { + let width = width - SPACE; + ui.spacing_mut().text_edit_width = (width)-(SPACE*3.0); + ui.style_mut().override_text_style = Some(Monospace); + 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 self.address.len() == 95 && Regex::is_match(®ex.address, &self.address) && ! self.address.contains("0") && ! self.address.contains("O") && ! self.address.contains("l") { + 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(); - // [Simple] if self.simple { // [Node] let height = height / 6.0; @@ -50,7 +90,7 @@ impl P2pool { ui.vertical(|ui| { ui.horizontal(|ui| { // [Ping List] - let id = og.lock().unwrap().p2pool.node; + let id = self.node; let ip = enum_to_ip(id); let mut ms = 0; let mut color = Color32::LIGHT_GRAY; @@ -61,13 +101,13 @@ impl P2pool { break } } - let text = RichText::new(format!("⏺ {}ms | {} | {}", ms, id, ip)).color(color); + let text = RichText::new(format!(" ⏺ {}ms | {} | {}", ms, id, ip)).color(color); ComboBox::from_id_source("nodes").selected_text(RichText::text_style(text, Monospace)).show_ui(ui, |ui| { for data in ping.lock().unwrap().nodes.iter() { let ms = crate::node::format_ms(data.ms); let id = crate::node::format_enum(data.id); - let text = RichText::text_style(RichText::new(format!("⏺ {} | {} | {}", ms, id, data.ip)).color(data.color), Monospace); - ui.selectable_value(&mut og.lock().unwrap().p2pool.node, data.id, text); + let text = RichText::text_style(RichText::new(format!(" ⏺ {} | {} | {}", ms, id, data.ip)).color(data.color), Monospace); + ui.selectable_value(&mut self.node, data.id, text); } }); }); @@ -130,131 +170,193 @@ impl P2pool { ui.add_sized([width, height], egui::Checkbox::new(&mut self.auto_node, "Auto-node")).on_hover_text(P2POOL_AUTO_NODE); })}); - // [Address] - let height = ui.available_height(); - ui.horizontal(|ui| { - if self.address.is_empty() { - ui.add_sized([width, text_edit], Label::new(RichText::new("Monero Payout Address ➖").color(Color32::LIGHT_GRAY))); - } else if self.address.len() == 95 && Regex::is_match(addr_regex, &self.address) { - ui.add_sized([width, text_edit], Label::new(RichText::new("Monero Payout Address ✔").color(Color32::from_rgb(100, 230, 100)))); - } else { - ui.add_sized([width, text_edit], Label::new(RichText::new("Monero Payout Address ❌").color(Color32::from_rgb(230, 50, 50)))); - } - }); - ui.spacing_mut().text_edit_width = (width)-(SPACE*3.0); - ui.style_mut().override_text_style = Some(Monospace); - ui.add_sized([width, text_edit], TextEdit::hint_text(TextEdit::singleline(&mut self.address), "4...")).on_hover_text(P2POOL_ADDRESS); - - // [Advanced] + //---------------------------------------------------------------------------------------------------- Advanced } else { - // TODO: - // ping code - // If ping was pressed, start thread -// if self.ping { -// self.ping = false; -// self.pinging = Arc::new(Mutex::new(true)); -// let node_clone = Arc::clone(&self.node); -// let pinging_clone = Arc::clone(&self.pinging); -// thread::spawn(move|| { -// let result = NodeStruct::ping(); -// *node_clone.lock().unwrap() = result.nodes; -// *pinging_clone.lock().unwrap() = false; -// }); -// } - - // If ping-ING, display stats -// if *self.pinging.lock().unwrap() { -// egui::CentralPanel::default().show(ctx, |ui| { -// let width = ui.available_width(); -// let width = width - 10.0; -// let height = ui.available_height(); -// init_text_styles(ctx, width); -// ui.add_sized([width, height/2.0], Label::new(format!("In progress: {}", *self.pinging.lock().unwrap()))); -// ui.group(|ui| { -// if ui.add_sized([width, height/10.0], egui::Button::new("Yes")).clicked() { -// info!("Quit confirmation = yes ... goodbye!"); -// exit(0); -// } else if ui.add_sized([width, height/10.0], egui::Button::new("No")).clicked() { -// info!("Quit confirmation = no ... returning!"); -// self.show_confirmation_dialog = false; -// } -// }); -// }); -// return -// } - - let width = width - 30.0; - let mut style = (*ctx.style()).clone(); - let height = ui.available_height()/1.2; + let mut incorrect_input = false; // This will disable [Add/Delete] on bad input + // [Monero node IP/RPC/ZMQ] ui.horizontal(|ui| { - ui.group(|ui| { ui.vertical(|ui| { - ui.group(|ui| { ui.horizontal(|ui| { - if ui.add_sized([width/4.0, height/5.0], egui::SelectableLabel::new(self.mini == false, "P2Pool Main")).on_hover_text(P2POOL_MAIN).clicked() { self.mini = false; }; - if ui.add_sized([width/4.0, height/5.0], egui::SelectableLabel::new(self.mini == true, "P2Pool Mini")).on_hover_text(P2POOL_MINI).clicked() { self.mini = true; }; - })}); - - let width = width/4.0; - style.spacing.slider_width = width*1.25; - ctx.set_style(style); - ui.horizontal(|ui| { - ui.add_sized([width/8.0, height/5.0], egui::Label::new("Out peers [10-450]:")); - ui.add_sized([width, height/5.0], egui::Slider::new(&mut self.out_peers, 10..=450)).on_hover_text(P2POOL_OUT); - }); - - ui.horizontal(|ui| { - ui.add_sized([width/8.0, height/5.0], egui::Label::new(" In peers [10-450]:")); - ui.add_sized([width, height/5.0], egui::Slider::new(&mut self.in_peers, 10..=450)).on_hover_text(P2POOL_IN); - }); - - ui.horizontal(|ui| { - ui.add_sized([width/8.0, height/5.0], egui::Label::new(" Log level [0-6]:")); - ui.add_sized([width, height/5.0], egui::Slider::new(&mut self.log_level, 0..=6)).on_hover_text(P2POOL_LOG); - }); - })}); - - ui.group(|ui| { ui.vertical(|ui| { - ui.group(|ui| { ui.horizontal(|ui| { - if ui.add_sized([width/4.0, height/5.0], egui::SelectableLabel::new(self.simple == false, "Community Monero Node")).on_hover_text(P2POOL_COMMUNITY).clicked() { self.simple = false; }; - if ui.add_sized([width/4.0, height/5.0], egui::SelectableLabel::new(self.simple == true, "Manual Monero Node")).on_hover_text(P2POOL_MANUAL).clicked() { self.simple = true; }; - })}); - ui.add_space(8.0); - ui.horizontal(|ui| { -// ui.add_sized([width/8.0, height/5.0], - egui::ComboBox::from_label(self.node.to_string()).selected_text(RINO).show_ui(ui, |ui| { - ui.selectable_value(&mut self.node, NodeEnum::Rino, RINO); - ui.selectable_value(&mut self.node, NodeEnum::Seth, SETH); - ui.selectable_value(&mut self.node, NodeEnum::Selsta1, SELSTA_1); - }); -// ); - }); - - if self.simple == false { ui.set_enabled(false); } - let width = (width/4.0); - ui.horizontal(|ui| { - ui.add_sized([width/8.0, height/7.8], egui::Label::new("Monero Node IP:")); - ui.spacing_mut().text_edit_width = ui.available_width() - 35.0; - ui.text_edit_singleline(&mut self.monerod); - }); - ui.horizontal(|ui| { - ui.add_sized([width/8.0, height/7.8], egui::Label::new("Monero Node RPC Port:")); - ui.spacing_mut().text_edit_width = ui.available_width() - 35.0; - ui.text_edit_singleline(&mut self.rpc.to_string()); - }); - ui.horizontal(|ui| { - ui.add_sized([width/8.0, height/7.8], egui::Label::new("Monero Node ZMQ Port:")); - ui.spacing_mut().text_edit_width = ui.available_width() - 35.0; - ui.text_edit_singleline(&mut self.zmq.to_string()); - }); - - })}); - - }); ui.group(|ui| { + let width = width/10.0; + ui.vertical(|ui| { + ui.style_mut().override_text_style = Some(Monospace); + 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 Regex::is_match(®ex.name, &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" || Regex::is_match(®ex.ipv4, &self.ip) || Regex::is_match(®ex.domain, &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 Regex::is_match(®ex.port, &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 Regex::is_match(®ex.port, &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] + let text = RichText::new(format!("{} | {}", self.selected_name, self.selected_ip)); + ComboBox::from_id_source("nodes").selected_text(RichText::text_style(text, Monospace)).show_ui(ui, |ui| { + for (name, node) in node_vec.iter() { + let text = RichText::text_style(RichText::new(format!("{}\n IP: {}\n RPC: {}\n ZMQ: {}", name, node.ip, node.rpc, node.zmq)), Monospace); + ui.selectable_value(&mut self.selected_name, name.clone(), text); +// println!("{}", value.ip); + } + }); + // [Add] + [Delete] + let node_vec_len = node_vec.len(); + ui.horizontal(|ui| { + let mut exists = false; + for (name, _) in node_vec.iter() { + if *name == self.name { exists = true; } + } + ui.set_enabled(!incorrect_input && !exists && node_vec_len < 100); + let text = format!("{}\n Max amount of nodes: 100\n Current amount: [{}/100]", P2POOL_ADD, node_vec_len); + 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)); + } + }); + ui.horizontal(|ui| { + ui.set_enabled(node_vec_len > 1); + let text = format!("{}\n Max amount of nodes: 100\n Current amount: [{}/100]", P2POOL_DELETE, node_vec_len); + if ui.add_sized([width, text_edit], Button::new("Delete")).on_hover_text(text).clicked() { + let mut n = 0; + for (name, _) in node_vec.iter() { + if *name == self.selected_name { + self.selected_name = node_vec[n-1].0.clone(); + node_vec.remove(n); + break + } + n += 1; + } + } + }); + 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(P2POOL_CLEAR).clicked() { + self.name.clear(); + self.ip.clear(); + self.rpc.clear(); + self.zmq.clear(); + } + }); + }); + }); + }); + ui.add_space(5.0); + + // [Main/Mini] ui.horizontal(|ui| { - ui.spacing_mut().text_edit_width = ui.available_width(); - ui.label("Address:"); - ui.text_edit_singleline(&mut self.address); + let height = height/3.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 == false, "P2Pool Main")).on_hover_text(P2POOL_MAIN).clicked() { self.mini = false; } + if ui.add_sized([width, height], SelectableLabel::new(self.mini == true, "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.2; + ui.style_mut().spacing.interact_size.y = height; + ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into())); +// ui.style_mut().override_text_style = Some(Monospace); + 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); + }); + })}); + }); } } } diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index 9fd367d..0000000 --- a/src/state.rs +++ /dev/null @@ -1,350 +0,0 @@ -// Gupax - GUI Uniting P2Pool And XMRig -// -// Copyright (c) 2022 hinto-janaiyo -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -// This handles reading/parsing the state file: [gupax.toml] -// The TOML format is used. This struct hierarchy directly -// translates into the TOML parser: -// State/ -// ├─ Gupax/ -// │ ├─ ... -// ├─ P2pool/ -// │ ├─ ... -// ├─ Xmrig/ -// │ ├─ ... -// ├─ Version/ -// ├─ ... - -use std::{fs,env}; -use std::fmt::Display; -use std::path::{Path,PathBuf}; -use std::result::Result; -use std::sync::{Arc,Mutex}; -use serde::{Serialize,Deserialize}; -use figment::Figment; -use figment::providers::{Format,Toml}; -use crate::constants::HORIZONTAL; -use log::*; - -//---------------------------------------------------------------------------------------------------- Impl -impl State { - pub fn default() -> Self { - use crate::constants::{P2POOL_VERSION,XMRIG_VERSION}; - let max_threads = num_cpus::get(); - let current_threads; - if max_threads == 1 { current_threads = 1; } else { current_threads = max_threads / 2; } - Self { - gupax: Gupax { - auto_update: true, - auto_node: true, - 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: Self::into_absolute_path(DEFAULT_P2POOL_PATH.to_string()).unwrap(), - absolute_xmrig_path: Self::into_absolute_path(DEFAULT_XMRIG_PATH.to_string()).unwrap(), - }, - p2pool: P2pool { - simple: true, - mini: true, - auto_node: true, - auto_select: true, - out_peers: 10, - in_peers: 10, - log_level: 3, - node: crate::NodeEnum::C3pool, - monerod: "localhost".to_string(), - rpc: 18081, - zmq: 18083, - address: "".to_string(), - }, - xmrig: Xmrig { - simple: true, - tls: false, - nicehash: false, - keepalive: false, - hugepages_jit: true, - current_threads, - max_threads, - priority: 2, - pool: "localhost:3333".to_string(), - address: "".to_string(), - }, - version: Arc::new(Mutex::new(Version { - p2pool: Arc::new(Mutex::new(P2POOL_VERSION.to_string())), - xmrig: Arc::new(Mutex::new(XMRIG_VERSION.to_string())), - })), - } - } - - pub fn get_path() -> Result { - // Get OS data folder - // Linux | $XDG_DATA_HOME or $HOME/.local/share | /home/alice/.local/state - // macOS | $HOME/Library/Application Support | /Users/Alice/Library/Application Support - // Windows | {FOLDERID_RoamingAppData} | C:\Users\Alice\AppData\Roaming - let mut path = match dirs::data_dir() { - Some(mut path) => { - path.push(STATE_DIRECTORY); - info!("OS data path ... OK"); - path - }, - None => { error!("Couldn't get OS PATH for data"); return Err(TomlError::Path(PATH_ERROR.to_string())) }, - }; - // Create directory - fs::create_dir_all(&path)?; - path.push(STATE_FILE); - info!("TOML path ... {}", path.display()); - Ok(path) - } - - // Attempts to read [gupax.toml] or - // attempts to create if not found. - pub fn read_or_create(path: PathBuf) -> Result { - // Attempt to read file, create default if not found - match fs::read_to_string(&path) { - Ok(string) => { - info!("TOML read ... OK"); - Ok(string) - }, - Err(err) => { - warn!("TOML not found, attempting to create default"); - let default = match toml::ser::to_string(&Self::default()) { - Ok(o) => { info!("TOML serialization ... OK"); o }, - Err(e) => { error!("Couldn't serialize default TOML file: {}", e); return Err(TomlError::Serialize(e)) }, - }; - fs::write(&path, &default)?; - info!("TOML write ... OK"); - Ok(default) - }, - } - } - - // Attempt to parse from String - // If failed, assume we're working with an old [State] - // and attempt to merge it with a new [State::default()]. - pub fn parse(string: String) -> Result { - match toml::de::from_str(&string) { - Ok(toml) => { - info!("TOML parse ... OK"); - Self::info(&toml); - Ok(toml) - }, - Err(err) => { - warn!("Couldn't parse TOML, assuming old [State], attempting merge..."); - Self::merge(&Self::default()) - }, - } - } - - // Last three functions combined - // get_path() -> read_or_create() -> parse() - pub fn get() -> Result { - Self::parse(Self::read_or_create(Self::get_path()?)?) - } - - // Completely overwrite current [gupax.toml] - // with a new default version, and return [Self]. - pub fn new_default() -> Result { - info!("Creating new default TOML..."); - let default = Self::default(); - let path = Self::get_path()?; - let string = match toml::ser::to_string(&default) { - Ok(o) => { info!("TOML serialization ... OK"); o }, - Err(e) => { error!("Couldn't serialize default TOML file: {}", e); return Err(TomlError::Serialize(e)) }, - }; - fs::write(&path, &string)?; - info!("TOML write ... OK"); - Ok(default) - } - - // Turn relative paths into absolute paths - fn into_absolute_path(path: String) -> Result { - let path = PathBuf::from(path); - if path.is_relative() { - let mut dir = std::env::current_exe()?; - dir.pop(); - dir.push(path); - Ok(dir) - } else { - Ok(path) - } - } - - // Save [State] onto disk file [gupax.toml] - pub fn save(&mut self) -> Result<(), TomlError> { - info!("Saving TOML to disk..."); - let path = Self::get_path()?; - // Convert path to absolute - self.gupax.absolute_p2pool_path = Self::into_absolute_path(self.gupax.p2pool_path.clone())?; - self.gupax.absolute_xmrig_path = Self::into_absolute_path(self.gupax.xmrig_path.clone())?; - let string = match toml::ser::to_string(&self) { - Ok(string) => { - info!("TOML parse ... OK"); - Self::info(&self); - string - }, - Err(err) => { error!("Couldn't parse TOML into string"); return Err(TomlError::Serialize(err)) }, - }; - match fs::write(path, string) { - Ok(_) => { info!("TOML save ... OK"); Ok(()) }, - Err(err) => { error!("Couldn't overwrite TOML file"); return Err(TomlError::Io(err)) }, - } - } - - // Take [Self] as input, merge it with whatever the current [default] is, - // leaving behind old keys+values and updating [default] with old valid ones. - // Automatically overwrite current file. - pub fn merge(old: &Self) -> Result { - info!("Starting TOML merge..."); - let old = match toml::ser::to_string(&old) { - Ok(string) => { info!("Old TOML parse ... OK"); string }, - Err(err) => { error!("Couldn't parse old TOML into string"); return Err(TomlError::Serialize(err)) }, - }; - let default = match toml::ser::to_string(&Self::default()) { - Ok(string) => { info!("Default TOML parse ... OK"); string }, - Err(err) => { error!("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!("TOML merge ... OK"); new }, - Err(err) => { error!("Couldn't merge default + old TOML"); return Err(TomlError::Merge(err)) }, - }; - // Attempt save - Self::save(&mut new)?; - Ok(new) - } - - // Write [Self] to console with - // [info!] surrounded by "---" - pub fn info(&self) -> Result<(), toml::ser::Error> { - info!("{}", HORIZONTAL); - for i in toml::ser::to_string(&self)?.lines() { info!("{}", i); } - info!("{}", HORIZONTAL); - Ok(()) - } -} - -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), - } - } -} - -impl From for TomlError { - fn from(err: std::io::Error) -> Self { - TomlError::Io(err) - } -} - -//---------------------------------------------------------------------------------------------------- Const -// State file -const STATE_FILE: &'static str = "gupax.toml"; -const ERROR: &'static str = "TOML Error"; -const PATH_ERROR: &'static str = "PATH for state directory could not be not found"; -#[cfg(target_os = "windows")] -const STATE_DIRECTORY: &'static str = "Gupax"; -#[cfg(target_os = "macos")] -const STATE_DIRECTORY: &'static str = "com.github.hinto-janaiyo.gupax"; -#[cfg(target_os = "linux")] -const STATE_DIRECTORY: &'static str = "gupax"; - -#[cfg(target_os = "windows")] -pub const DEFAULT_P2POOL_PATH: &'static str = r"P2Pool\p2pool.exe"; -#[cfg(target_family = "unix")] -pub const DEFAULT_P2POOL_PATH: &'static str = "p2pool/p2pool"; -#[cfg(target_os = "windows")] -pub const DEFAULT_XMRIG_PATH: &'static str = r"XMRig\xmrig.exe"; -#[cfg(target_family = "unix")] -pub const DEFAULT_XMRIG_PATH: &'static str = "xmrig/xmrig"; - -//---------------------------------------------------------------------------------------------------- Error Enum -#[derive(Debug)] -pub enum TomlError { - Io(std::io::Error), - Path(String), - Serialize(toml::ser::Error), - Deserialize(toml::de::Error), - Merge(figment::Error), -} - -//---------------------------------------------------------------------------------------------------- Structs -#[derive(Clone,Debug,Deserialize,Serialize)] -pub struct State { - pub gupax: Gupax, - pub p2pool: P2pool, - pub xmrig: Xmrig, - pub version: Arc>, -} - -#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)] -pub struct Gupax { - pub auto_update: bool, - pub auto_node: 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, -} - -#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)] -pub struct P2pool { - pub simple: bool, - pub mini: bool, - pub auto_node: bool, - pub auto_select: bool, - pub out_peers: u16, - pub in_peers: u16, - pub log_level: u8, - pub node: crate::node::NodeEnum, - pub monerod: String, - pub rpc: u16, - pub zmq: u16, - pub address: String, -// pub config: String, -// pub args: String, -} - -#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)] -pub struct Xmrig { - pub simple: bool, - pub tls: bool, - pub nicehash: bool, - pub keepalive: bool, - pub hugepages_jit: bool, - pub max_threads: usize, - pub current_threads: usize, - pub priority: u8, - pub pool: String, - pub address: String, -// pub config: String, -// pub args: String, -} - -#[derive(Clone,Debug,Deserialize,Serialize)] -pub struct Version { - pub p2pool: Arc>, - pub xmrig: Arc>, -} diff --git a/src/update.rs b/src/update.rs index ba05425..ca30856 100644 --- a/src/update.rs +++ b/src/update.rs @@ -30,7 +30,7 @@ use arti_hyper::*; use arti_hyper::*; use crate::constants::GUPAX_VERSION; //use crate::{Name::*,State}; -use crate::state::*; +use crate::disk::*; use crate::update::Name::*; use hyper::{Client,Body,Request}; use hyper::header::HeaderValue; diff --git a/src/xmrig.rs b/src/xmrig.rs index ec12d74..f2389fe 100644 --- a/src/xmrig.rs +++ b/src/xmrig.rs @@ -21,7 +21,7 @@ use std::str::FromStr; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use num_cpus; use crate::constants::*; -use crate::state::Xmrig; +use crate::disk::Xmrig; impl Xmrig { pub fn show(&mut self, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) {