diff --git a/Cargo.lock b/Cargo.lock index 1d97e07..7a047f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.11.0" @@ -346,6 +355,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -427,6 +445,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cty" version = "0.2.2" @@ -440,7 +468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" dependencies = [ "byteorder", - "digest", + "digest 0.9.0", "rand_core 0.5.1", "subtle", "zeroize", @@ -490,6 +518,36 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -950,15 +1008,21 @@ name = "gupax" version = "0.1.0" dependencies = [ "chrono", + "dirs", "eframe", "egui", "egui_extras", "env_logger", + "hex-literal", "image", "log", "monero", "num_cpus", "regex", + "serde", + "serde_derive", + "sha2", + "toml", ] [[package]] @@ -1699,6 +1763,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.7", + "redox_syscall", + "thiserror", +] + [[package]] name = "regex" version = "1.6.0" @@ -1820,6 +1895,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.5", +] + [[package]] name = "shared_library" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index e80bacc..996ed95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,27 +4,33 @@ version = "0.1.0" edition = "2021" [dependencies] -egui = "0.19.0" -eframe = "0.19.0" -egui_extras = { version = "0.19.0", features = ["image"] } -image = { version = "0.24.4", features = ["png"] } -num_cpus = "1.13.1" -monero = "0.18.0" -regex = "1.6.0" -log = "0.4.17" -env_logger = "0.9.1" +dirs = "4.0.0" chrono = "0.4.22" +eframe = "0.19.0" +egui = "0.19.0" +egui_extras = { version = "0.19.0", features = ["image"] } +env_logger = "0.9.1" +hex-literal = "0.3.4" +image = { version = "0.24.4", features = ["png"] } +log = "0.4.17" +monero = "0.18.0" +num_cpus = "1.13.1" +regex = "1.6.0" +serde = "1.0.145" +serde_derive = "1.0.145" +sha2 = "0.10.6" +toml = "0.5.9" [profile.optimized] -inherits = "release" -strip = "debuginfo" -debug = false +codegen-units = 1 debug-assertions = false +debug = false +incremental = true +inherits = "release" lto = true overflow-checks = false -incremental = true -codegen-units = 1 rpath = false +strip = "debuginfo" [profile.optimized.package."*"] opt-level = 3 diff --git a/build.sh b/build.sh index baf276e..f5a1341 100755 --- a/build.sh +++ b/build.sh @@ -4,4 +4,17 @@ set -e [[ $PWD = */gupax ]] -RUSTFLAGS="-C target-cpu=native" cargo build --profile optimized && du -hs target/optimized/gupax +if [[ $1 = *all* ]]; then + echo "=== building all ===" + echo "=== windows ===" + cargo build --profile optimized --target x86_64-pc-windows-gnu +# echo "=== macos ===" +# cargo build --profile optimized --target x86_64-apple-darwin + echo "=== linux ===" + cargo build --profile optimized + du -hs target/x86_64-pc-windows-gnu/optimized/gupax target/x86_64-apple-darwin/optimized/gupax target/optimized/gupax +else + echo "=== building linux cpu optimized ===" + RUSTFLAGS="-C target-cpu=native" cargo build --profile optimized + du -hs target/optimized/gupax +fi diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..c082873 --- /dev/null +++ b/src/README.md @@ -0,0 +1,49 @@ +# Gupax source files +* [State](#State) +* [Structure](#Structure) +* [Bootstrap](#Bootstrap) + +## Structure +| File/Folder | Purpose | +|----------------|---------| +| `about.rs` | Struct/impl for `About` tab +| `constants.rs` | General constants needed in Gupax +| `gupax.rs` | Struct/impl for `Gupax` tab +| `main.rs` | Struct/enum/impl for `App/Tab/State`, init functions, main function +| `p2pool.rs` | Struct/impl for `P2Pool` tab +| `status.rs` | Struct/impl for `Status` tab +| `toml.rs` | Struct/impl for `gupax.toml`, the disk state +| `xmrig.rs` | Struct/impl for `XMRig` tab + +## Bootstrap +This is how Gupax works internally when starting up, divided into 3 sections. + +1. **INIT** + - Initialize custom console logging with `log`, `env_logger` || *warn!* + - Initialize misc data (structs, text styles, thread count, images, etc) || *panic!* + - Check for admin privilege (for XMRig) || *warn!* + - Attempt to read `gupax.toml` || *warn!*, *initialize config with default options* + - If errors were found, pop-up window + +2. **AUTO-UPDATE** + - If `auto_update` == `true`, pop-up auto-updating window || *info!*, *skip auto-update* + - Multi-threaded GitHub API check on Gupax -> P2Pool -> XMRig || *warn!*, *skip auto-update* + - Multi-threaded download if current version != new version || *warn!*, *skip auto-update* + - After download, atomically replace current binaries with new || *warn!*, *skip auto-update* + - Update version metadata || *warn!*, *skip auto-update* + +3. **MAIN** + - All data must be initialized at this point, either via `gupax.toml` or default options || *panic!* + - Start `App` frame || *panic!* + - Write state to `gupax.toml` on user clicking `Save` (after checking input for correctness) || *warn!* + - If `ask_before_quit` == `true`, check for running processes, unsaved state, and update connections before quitting + - Kill processes, kill connections, exit + +## State +Internal state is saved in the "OS data folder" as `gupax.toml`, using the [TOML](https://github.com/toml-lang/toml) format. If the version can't be parsed (not in the `vX.X.X` or `vX.X` format), the auto-updater will be skipped. [If not found, a default `gupax.toml` file will be created with `Toml::default`.](https://github.com/hinto-janaiyo/gupax/blob/main/src/toml.rs) + +| OS | Data Folder | Example | +|----------|----------------------------------------- |-----------------------------------------------------------| +| Windows | `{FOLDERID_LocalAppData}` | C:\Users\Alice\AppData\Roaming\Gupax\gupax.toml | +| macOS | `$HOME`/Library/Application Support | /Users/Alice/Library/Application Support/Gupax/gupax.toml | +| Linux | `$XDG_DATA_HOME` or `$HOME`/.local/share | /home/alice/.local/share/gupax/gupax.toml | diff --git a/src/constants.rs b/src/constants.rs index 3b13e42..77395cd 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -16,11 +16,17 @@ // along with this program. If not, see . use std::net::{Ipv4Addr,SocketAddrV4}; +use sha2::{Sha256,Digest}; +use hex_literal::hex; +use std::{io, fs}; + +// These are the versions bundled with Gupax. +pub const P2POOL_VERSION: &'static str = "v2.4"; +pub const XMRIG_VERSION: &'static str = "v6.18.0"; -// Compile-time constants pub const BYTES_ICON: &[u8] = include_bytes!("../images/png/icon.png"); pub const BYTES_BANNER: &[u8] = include_bytes!("../images/png/banner.png"); -pub const P2POOL_BASE_ARGS: &'static str = "--host 127.0.0.1 --rpc-port 18081 --zmq-port 18083 --loglevel 3 --out-peers 10 --in-peers 10"; +pub const P2POOL_BASE_ARGS: &'static str = ""; pub const XMRIG_BASE_ARGS: &'static str = "--http-host=127.0.0.1 --http-port=18088 --algo=rx/0 --coin=Monero --randomx-cache-qos"; // OS specific diff --git a/src/main.rs b/src/main.rs index cf0601e..d5824e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ // along with this program. If not, see . #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release -use eframe::egui; +use eframe::{egui,NativeOptions}; use egui::{Vec2,Pos2}; use std::process::exit; use std::thread; @@ -36,12 +36,13 @@ use std::io::Write; use std::time::Instant; mod constants; +mod toml; mod about; mod status; mod gupax; mod p2pool; mod xmrig; -use {constants::*,about::*,status::*,gupax::*,p2pool::*,xmrig::*}; +use {constants::*,crate::toml::*,about::*,status::*,gupax::*,p2pool::*,xmrig::*}; // The state of the outer [App]. // See the [State] struct for the @@ -60,6 +61,8 @@ pub struct App { xmrig: bool, state: State, og: State, + allowed_to_close: bool, + show_confirmation_dialog: bool, } impl App { @@ -79,12 +82,12 @@ impl App { let resolution = cc.integration_info.window_info.size; init_text_styles(&cc.egui_ctx, resolution[0] as f32); let banner = match RetainedImage::from_image_bytes("banner.png", BYTES_BANNER) { - Ok(banner) => { info!("Banner loading OK"); banner }, + Ok(banner) => { info!("Banner loading ... OK"); banner }, Err(err) => { error!("{}", err); panic!("{}", err); }, }; let mut state = State::new(); let mut og = State::new(); - info!("{:?}", resolution); + info!("Frame resolution ... {:#?}", resolution); Self { version, name_version, @@ -99,6 +102,8 @@ impl App { xmrig: false, state, og, + allowed_to_close: false, + show_confirmation_dialog: false, } } } @@ -169,48 +174,58 @@ fn init_text_styles(ctx: &egui::Context, width: f32) { // style.spacing.button_padding = Vec2::new(scale/2.0, scale/2.0); ctx.set_style(style); ctx.set_pixels_per_point(1.0); + ctx.request_repaint(); } -fn main() { +fn init_logger() { use env_logger::fmt::Color; Builder::new().format(|buf, record| { let level; let mut style = buf.style(); match record.level() { Level::Error => { style.set_color(Color::Red); level = "ERROR" }, - Level::Warn => { style.set_color(Color::Yellow); level = "WARN " }, - Level::Info => { style.set_color(Color::White); level = "INFO " }, + Level::Warn => { style.set_color(Color::Yellow); level = "WARN" }, + Level::Info => { style.set_color(Color::White); level = "INFO" }, Level::Debug => { style.set_color(Color::Blue); level = "DEBUG" }, Level::Trace => { style.set_color(Color::Magenta); level = "TRACE" }, }; writeln!( buf, - "| {} | {} | {}:{} | {}", + "[{}] [{}] [{}:{}] {}", style.set_bold(true).value(level), buf.style().set_dimmed(true).value(chrono::Local::now().format("%F %T%.3f")), buf.style().set_dimmed(true).value(record.file().unwrap_or("???")), buf.style().set_dimmed(true).value(record.line().unwrap_or(0)), record.args(), ) - }).filter_level(LevelFilter::Info).write_style(WriteStyle::Always).parse_default_env().format_timestamp_millis().try_init(); - info!("test"); - warn!("test"); - error!("test"); - debug!("test"); + }).filter_level(LevelFilter::Info).write_style(WriteStyle::Always).parse_default_env().format_timestamp_millis().init(); + info!("init_logger() ... OK"); +} +fn init_options() -> NativeOptions { let mut options = eframe::NativeOptions::default(); options.min_window_size = Option::from(Vec2::new(1280.0, 720.0)); options.max_window_size = Option::from(Vec2::new(3180.0, 2160.0)); options.initial_window_size = Option::from(Vec2::new(1280.0, 720.0)); 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 = image::load_from_memory(BYTES_ICON).expect("Failed to read icon bytes").to_rgba8(); let (icon_width, icon_height) = icon.dimensions(); options.icon_data = Some(eframe::IconData { rgba: icon.into_raw(), width: icon_width, height: icon_height, }); + info!("init_options() ... OK"); + options +} + +fn main() { + init_logger(); + let options = init_options(); + let toml = Toml::get(); + info!("Printing gupax.toml..."); + eprintln!("{:#?}", toml); let now = Instant::now(); eframe::run_native( "Gupax", @@ -220,7 +235,29 @@ fn main() { } impl eframe::App for App { - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + fn on_close_event(&mut self) -> bool { + self.show_confirmation_dialog = true; + self.allowed_to_close + } + fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + if self.show_confirmation_dialog { + // Show confirmation dialog: + 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("Are you sure you want to quit?")); + ui.group(|ui| { + if ui.add_sized([width, height/10.0], egui::Button::new("Yes")).clicked() { + exit(0); + } else if ui.add_sized([width, height/10.0], egui::Button::new("No")).clicked() { + self.show_confirmation_dialog = false; + } + }); + }); + return + } // Top: Tabs egui::CentralPanel::default().show(ctx, |ui| { init_text_styles(ctx, ui.available_width()); @@ -355,16 +392,3 @@ impl eframe::App for App { }); } } - -pub trait View { - fn ui(&mut self, ui: &mut egui::Ui); -} - -/// Something to view -pub trait Demo { - /// `&'static` so we can also use it as a key to store open/close state. - fn name(&self) -> &'static str; - - /// Show windows, etc - fn show(&mut self, ctx: &egui::Context, open: &mut bool); -} diff --git a/src/toml.rs b/src/toml.rs new file mode 100644 index 0000000..b2bd9a1 --- /dev/null +++ b/src/toml.rs @@ -0,0 +1,223 @@ +// 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: +// Toml/ +// ├─ Gupax/ +// │ ├─ ... +// ├─ P2pool/ +// │ ├─ ... +// ├─ Xmrig/ +// │ ├─ ... +// ├─ Version/ +// ├─ ... + +use std::{fs,env}; +use std::fmt::Display; +use std::path::{Path,PathBuf}; +use serde_derive::{Serialize,Deserialize}; +use log::*; + +//---------------------------------------------------------------------------------------------------- Impl +// Since [State] is already used in [main.rs] to represent +// working state, [Toml] is used to disk state. +impl Toml { + pub fn default() -> Self { + use crate::constants::{P2POOL_VERSION,XMRIG_VERSION}; + Self { + gupax: Gupax { + auto_update: true, + ask_before_quit: true, + p2pool_path: DEFAULT_P2POOL_PATH.to_string(), + xmrig_path: DEFAULT_XMRIG_PATH.to_string(), + }, + p2pool: P2pool { + simple: true, + mini: true, + out_peers: 10, + in_peers: 10, + log_level: 3, + monerod: "localhost".to_string(), + rpc: 18081, + zmq: 18083, + address: "".to_string(), + }, + xmrig: Xmrig { + simple: true, + tls: false, + nicehash: false, + keepalive: false, + threads: 1, + priority: 2, + pool: "localhost:3333".to_string(), + address: "".to_string(), + }, + version: Version { + p2pool: P2POOL_VERSION.to_string(), + xmrig: XMRIG_VERSION.to_string(), + }, + } + } + + pub fn get() -> 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(DIRECTORY); + info!("{}, OS data path ... OK", path.display()); + 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)?; + + // Attempt to read file, create default if not found + path.push(FILENAME); + let file = match fs::read_to_string(&path) { + Ok(file) => file, + Err(err) => { + error!("TOML not found, attempting to create default"); + let default = match toml::ser::to_string(&Toml::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"); + fs::read_to_string(&path)? + }, + }; + info!("TOML read ... OK"); + + // Attempt to parse, return Result + match toml::from_str(&file) { + Ok(file) => { info!("TOML parse ... OK"); Ok(file) }, + Err(err) => { error!("Couldn't parse TOML file"); Err(TomlError::Parse(err)) }, + } + } +} + +impl Display for TomlError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use TomlError::*; + match self { + Io(err) => write!(f, "{} | {}", ERROR, err), + Path(err) => write!(f, "{} | {}", ERROR, err), + Parse(err) => write!(f, "{} | {}", ERROR, err), + Serialize(err) => write!(f, "{} | {}", ERROR, err), + } + } +} + +impl From for TomlError { + fn from(err: std::io::Error) -> Self { + TomlError::Io(err) + } +} + +fn main() { + let state = match Toml::get() { + Ok(state) => { println!("OK"); state }, + Err(err) => panic!(), + }; +} + +//---------------------------------------------------------------------------------------------------- Const +const FILENAME: &'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 DIRECTORY: &'static str = "Gupax"; +#[cfg(target_os = "macos")] +const DIRECTORY: &'static str = "Gupax"; +#[cfg(target_os = "linux")] +const DIRECTORY: &'static str = "gupax"; +#[cfg(target_os = "windows")] +const DEFAULT_P2POOL_PATH: &'static str = r"P2Pool\p2pool.exe"; +#[cfg(target_os = "macos")] +const DEFAULT_P2POOL_PATH: &'static str = "P2Pool/p2pool"; +#[cfg(target_os = "linux")] +const DEFAULT_P2POOL_PATH: &'static str = "p2pool/p2pool"; +#[cfg(target_os = "windows")] +const DEFAULT_XMRIG_PATH: &'static str = r"XMRig\xmrig.exe"; +#[cfg(target_os = "macos")] +const DEFAULT_XMRIG_PATH: &'static str = "XMRig/xmrig"; +#[cfg(target_os = "linux")] +const DEFAULT_XMRIG_PATH: &'static str = "xmrig/xmrig"; + +//---------------------------------------------------------------------------------------------------- Error Enum +#[derive(Debug)] +pub enum TomlError { + Io(std::io::Error), + Path(String), + Parse(toml::de::Error), + Serialize(toml::ser::Error), +} + +//---------------------------------------------------------------------------------------------------- Structs +#[derive(Debug,Deserialize,Serialize)] +pub struct Toml { + gupax: Gupax, + p2pool: P2pool, + xmrig: Xmrig, + version: Version, +} + +#[derive(Debug,Deserialize,Serialize)] +struct Gupax { + auto_update: bool, + ask_before_quit: bool, + p2pool_path: String, + xmrig_path: String, +} + +#[derive(Debug,Deserialize,Serialize)] +struct P2pool { + simple: bool, + mini: bool, + out_peers: u8, + in_peers: u8, + log_level: u8, + monerod: String, + rpc: u16, + zmq: u16, + address: String, +} + +#[derive(Debug,Deserialize,Serialize)] +struct Xmrig { + simple: bool, + tls: bool, + nicehash: bool, + keepalive: bool, + threads: u16, + priority: u8, + pool: String, + address: String, +} + +#[derive(Debug,Deserialize,Serialize)] +struct Version { + p2pool: String, + xmrig: String, +}