state.rs: add State::merge()

This commit is contained in:
hinto-janaiyo 2022-10-18 15:26:21 -04:00
parent 6a1db35c10
commit 5e65d07470
No known key found for this signature in database
GPG key ID: D7483F6CA27D1B1D
8 changed files with 353 additions and 66 deletions

43
Cargo.lock generated
View file

@ -89,6 +89,15 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]]
name = "atomic"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "atomic_refcell" name = "atomic_refcell"
version = "0.1.8" version = "0.1.8"
@ -765,6 +774,19 @@ dependencies = [
"instant", "instant",
] ]
[[package]]
name = "figment"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e56602b469b2201400dec66a66aec5a9b8761ee97cd1b8c96ab2483fcc16cc9"
dependencies = [
"atomic",
"serde",
"toml",
"uncased",
"version_check",
]
[[package]] [[package]]
name = "fixed-hash" name = "fixed-hash"
version = "0.7.0" version = "0.7.0"
@ -1079,12 +1101,14 @@ dependencies = [
"egui", "egui",
"egui_extras", "egui_extras",
"env_logger", "env_logger",
"figment",
"hex-literal", "hex-literal",
"image", "image",
"log", "log",
"monero", "monero",
"num-format", "num-format",
"num_cpus", "num_cpus",
"openssl",
"rand", "rand",
"regex", "regex",
"reqwest", "reqwest",
@ -1800,6 +1824,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.22.0+1.1.1q"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.76" version = "0.9.76"
@ -1809,6 +1842,7 @@ dependencies = [
"autocfg", "autocfg",
"cc", "cc",
"libc", "libc",
"openssl-src",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]
@ -2620,6 +2654,15 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "uncased"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.8" version = "0.3.8"

View file

@ -10,6 +10,7 @@ eframe = "0.19.0"
egui = "0.19.0" egui = "0.19.0"
egui_extras = { version = "0.19.0", features = ["image"] } egui_extras = { version = "0.19.0", features = ["image"] }
env_logger = "0.9.1" env_logger = "0.9.1"
figment = { version = "0.10.8", features = ["toml"] }
hex-literal = "0.3.4" hex-literal = "0.3.4"
image = { version = "0.24.4", features = ["png"] } image = { version = "0.24.4", features = ["png"] }
log = "0.4.17" log = "0.4.17"
@ -23,6 +24,7 @@ serde = "1.0.145"
serde_derive = "1.0.145" serde_derive = "1.0.145"
sha2 = "0.10.6" sha2 = "0.10.6"
toml = "0.5.9" toml = "0.5.9"
openssl = { version = "*", features = ["vendored"] }
[profile.optimized] [profile.optimized]
codegen-units = 1 codegen-units = 1

View file

@ -26,6 +26,11 @@ 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"; pub const XMRIG_BASE_ARGS: &'static str = "--http-host=127.0.0.1 --http-port=18088 --algo=rx/0 --coin=Monero --randomx-cache-qos";
pub const HORIZONTAL: &'static str = "--------------------------------------------"; pub const HORIZONTAL: &'static str = "--------------------------------------------";
// Update data
pub const GITHUB_METADATA_GUPAX: &'static str = "https://api.github.com/repos/hinto-janaiyo/gupax/releases/latest";
pub const GITHUB_METADATA_P2POOL: &'static str = "https://api.github.com/repos/SChernykh/p2pool/releases/latest";
pub const GITHUB_METADATA_XMRIG: &'static str = "https://api.github.com/repos/xmrig/xmrig/releases/latest";
// OS specific // OS specific
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub const OS: &'static str = " Windows"; pub const OS: &'static str = " Windows";
@ -80,7 +85,8 @@ r#"USAGE: gupax [--flags]
-h | --help Print this help message -h | --help Print this help message
-v | --version Print versions -v | --version Print versions
-n | --no-startup Disable auto-update/node connections at startup -n | --no-startup Disable auto-update/node connections at startup
-r | --reset Reset all Gupax configuration/state"#; -r | --reset Reset all Gupax configuration/state
-f | --ferris Print an extremely cute crab"#;
pub const ARG_COPYRIGHT: &'static str = pub const ARG_COPYRIGHT: &'static str =
r#"Gupax, P2Pool, and XMRig are licensed under GPLv3. r#"Gupax, P2Pool, and XMRig are licensed under GPLv3.
For more information, see here: For more information, see here:

81
src/ferris.rs Normal file

File diff suppressed because one or more lines are too long

View file

@ -41,6 +41,7 @@ use std::time::Instant;
use std::path::PathBuf; use std::path::PathBuf;
// Modules // Modules
mod ferris;
mod constants; mod constants;
mod node; mod node;
mod state; mod state;
@ -49,7 +50,7 @@ mod status;
mod gupax; mod gupax;
mod p2pool; mod p2pool;
mod xmrig; mod xmrig;
use {constants::*,node::*,state::*,about::*,status::*,gupax::*,p2pool::*,xmrig::*}; use {ferris::*,constants::*,node::*,state::*,about::*,status::*,gupax::*,p2pool::*,xmrig::*};
//---------------------------------------------------------------------------------------------------- Struct + Impl //---------------------------------------------------------------------------------------------------- Struct + Impl
// The state of the outer main [App]. // The state of the outer main [App].
@ -72,13 +73,13 @@ pub struct App {
// the [diff] bool will be the signal for [Reset/Save]. // the [diff] bool will be the signal for [Reset/Save].
og: State, og: State,
state: State, state: State,
// update: Update, // State for update data [update.rs]
diff: bool, diff: bool,
// Process/update state: // Process/update state:
// Doesn't make sense to save this on disk // Doesn't make sense to save this on disk
// so it's represented as a bool here. // so it's represented as a bool here.
p2pool: bool, // Is p2pool online? p2pool: bool, // Is p2pool online?
xmrig: bool, // Is xmrig online? xmrig: bool, // Is xmrig online?
updating: bool, // Is an update in progress?
// State from [--flags] // State from [--flags]
startup: bool, startup: bool,
reset: bool, reset: bool,
@ -113,10 +114,10 @@ impl App {
node: Arc::new(Mutex::new(NodeStruct::default())), node: Arc::new(Mutex::new(NodeStruct::default())),
og: State::default(), og: State::default(),
state: State::default(), state: State::default(),
// update: Update::default(),
diff: false, diff: false,
p2pool: false, p2pool: false,
xmrig: false, xmrig: false,
updating: false,
startup: true, startup: true,
reset: false, reset: false,
now: Instant::now(), now: Instant::now(),
@ -242,18 +243,6 @@ fn init_options() -> NativeOptions {
} }
//---------------------------------------------------------------------------------------------------- Misc functions //---------------------------------------------------------------------------------------------------- Misc functions
fn into_absolute_path(path: String) -> Result<PathBuf, std::io::Error> {
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)
}
}
fn parse_args(mut app: App) -> App { fn parse_args(mut app: App) -> App {
info!("Parsing CLI arguments..."); info!("Parsing CLI arguments...");
let mut args: Vec<String> = env::args().collect(); let mut args: Vec<String> = env::args().collect();
@ -266,6 +255,7 @@ fn parse_args(mut app: App) -> App {
println!("Gupax | {}\nP2Pool | {}\nXMRig | {}\n\nOS: [{}], Commit: [{}]\n\n{}", GUPAX_VERSION, P2POOL_VERSION, XMRIG_VERSION, OS_NAME, &COMMIT[..40], ARG_COPYRIGHT); println!("Gupax | {}\nP2Pool | {}\nXMRig | {}\n\nOS: [{}], Commit: [{}]\n\n{}", GUPAX_VERSION, P2POOL_VERSION, XMRIG_VERSION, OS_NAME, &COMMIT[..40], ARG_COPYRIGHT);
exit(0); exit(0);
}, },
"-f"|"--ferris" => { println!("{}", FERRIS); exit(0); },
_ => (), _ => (),
} }
} }
@ -349,50 +339,76 @@ impl eframe::App for App {
// Close confirmation. // Close confirmation.
if self.quit { if self.quit {
egui::TopBottomPanel::bottom("quit").show(ctx, |ui| { // If [ask_before_quit == true]
let width = self.width; if self.state.gupax.ask_before_quit {
let height = self.height/8.0; egui::TopBottomPanel::bottom("quit").show(ctx, |ui| {
ui.group(|ui| { let width = self.width;
if ui.add_sized([width, height], egui::Button::new("Yes")).clicked() { let height = self.height/8.0;
if self.state.gupax.save_before_quit { ui.group(|ui| {
info!("Saving before quit..."); if ui.add_sized([width, height], egui::Button::new("Yes")).clicked() {
match self.state.save() { if self.state.gupax.save_before_quit {
Err(err) => { error!("{}", err); exit(1); }, if self.diff {
_ => (), info!("Saving before quit...");
}; match self.state.save() {
Err(err) => { error!("{}", err); exit(1); },
_ => (),
};
} else {
info!("No changed detected, not saving...");
}
}
info!("Quit confirmation = yes ... goodbye!");
exit(0);
} else if ui.add_sized([width, height], egui::Button::new("No")).clicked() {
self.quit = false;
} }
info!("Quit confirmation = yes ... goodbye!"); });
self.quit_confirm = true; });
egui::CentralPanel::default().show(ctx, |ui| {
let width = self.width;
let height = ui.available_height();
let ten = height/10.0;
// Detect processes or update
ui.add_space(ten);
// || self.update.updating
if self.p2pool || self.xmrig {
ui.add_sized([width, height/4.0], Label::new("Are you sure you want to quit?"));
// if self.update.updating { ui.add_sized([width, ten], Label::new("Update is in progress...!")); }
if self.p2pool { ui.add_sized([width, ten], Label::new("P2Pool is online...!")); }
if self.xmrig { ui.add_sized([width, ten], Label::new("XMRig is online...!")); }
// Else, just quit
} else {
if self.state.gupax.save_before_quit {
if self.diff {
info!("Saving before quit...");
match self.state.save() {
Err(err) => { error!("{}", err); exit(1); },
_ => (),
};
} else {
info!("No changed detected, not saving...");
}
}
info!("No processes or update in progress ... goodbye!");
exit(0); exit(0);
} else if ui.add_sized([width, height], egui::Button::new("No")).clicked() {
self.quit = false;
} }
}); });
}); // Else, quit (save if [save_before_quit == true]
egui::CentralPanel::default().show(ctx, |ui| { } else {
let width = self.width; if self.state.gupax.save_before_quit {
let height = ui.available_height(); if self.diff {
let ten = height/10.0;
// Detect processes or update
ui.add_space(ten);
if self.p2pool || self.xmrig || self.updating {
ui.add_sized([width, height/4.0], Label::new("Are you sure you want to quit?"));
if self.updating { ui.add_sized([width, ten], Label::new("Update is in progress...!")); }
if self.p2pool { ui.add_sized([width, ten], Label::new("P2Pool is online...!")); }
if self.xmrig { ui.add_sized([width, ten], Label::new("XMRig is online...!")); }
// Else, just quit
} else {
if self.state.gupax.save_before_quit {
info!("Saving before quit..."); info!("Saving before quit...");
match self.state.save() { match self.state.save() {
Err(err) => { error!("{}", err); exit(1); }, Err(err) => { error!("{}", err); exit(1); },
_ => (), _ => (),
}; };
} else {
info!("No changed detected, not saving...");
} }
info!("No processes or update in progress ... goodbye!");
exit(0);
} }
}); info!("Quit confirmation = yes ... goodbye!");
exit(0);
}
return return
} }

View file

@ -110,7 +110,7 @@ impl NodeStruct {
// - Add data to appropriate struct // - Add data to appropriate struct
// - Sort fastest to lowest // - Sort fastest to lowest
// - Return [PingResult(NodeStruct, NodeEnum)] (data and fastest node) // - Return [PingResult(NodeStruct, NodeEnum)] (data and fastest node)
// //
// This is done linearly since per IP since // This is done linearly since per IP since
// multi-threading might affect performance. // multi-threading might affect performance.
// //

View file

@ -33,6 +33,8 @@ use std::fmt::Display;
use std::path::{Path,PathBuf}; use std::path::{Path,PathBuf};
use std::result::Result; use std::result::Result;
use serde_derive::{Serialize,Deserialize}; use serde_derive::{Serialize,Deserialize};
use figment::Figment;
use figment::providers::{Format,Toml};
use crate::constants::HORIZONTAL; use crate::constants::HORIZONTAL;
use log::*; use log::*;
@ -51,6 +53,8 @@ impl State {
save_before_quit: true, save_before_quit: true,
p2pool_path: DEFAULT_P2POOL_PATH.to_string(), p2pool_path: DEFAULT_P2POOL_PATH.to_string(),
xmrig_path: DEFAULT_XMRIG_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 { p2pool: P2pool {
simple: true, simple: true,
@ -114,7 +118,7 @@ impl State {
}, },
Err(err) => { Err(err) => {
warn!("TOML not found, attempting to create default"); warn!("TOML not found, attempting to create default");
let default = match toml::ser::to_string(&State::default()) { let default = match toml::ser::to_string(&Self::default()) {
Ok(o) => { info!("TOML serialization ... OK"); o }, Ok(o) => { info!("TOML serialization ... OK"); o },
Err(e) => { error!("Couldn't serialize default TOML file: {}", e); return Err(TomlError::Serialize(e)) }, Err(e) => { error!("Couldn't serialize default TOML file: {}", e); return Err(TomlError::Serialize(e)) },
}; };
@ -126,35 +130,67 @@ impl State {
} }
// Attempt to parse from String // Attempt to parse from String
pub fn parse(string: String) -> Result<State, TomlError> { // 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<Self, TomlError> {
match toml::de::from_str(&string) { match toml::de::from_str(&string) {
Ok(toml) => { Ok(toml) => {
info!("TOML parse ... OK"); info!("TOML parse ... OK");
info!("{}", HORIZONTAL); Self::info(&toml);
for i in string.lines() { info!("{}", i); }
info!("{}", HORIZONTAL);
Ok(toml) Ok(toml)
}, },
Err(err) => { error!("Couldn't parse TOML from string"); Err(TomlError::Deserialize(err)) }, Err(err) => {
warn!("Couldn't parse TOML, assuming old [State], attempting merge...");
Self::merge(&Self::default())
},
} }
} }
// Last three functions combined // Last three functions combined
// get_path() -> read_or_create() -> parse() // get_path() -> read_or_create() -> parse()
pub fn get() -> Result<State, TomlError> { pub fn get() -> Result<Self, TomlError> {
Self::parse(Self::read_or_create(Self::get_path()?)?) Self::parse(Self::read_or_create(Self::get_path()?)?)
} }
// Save [State] onto disk file [gupax.toml] // Completely overwrite current [gupax.toml]
pub fn save(&self) -> Result<(), TomlError> { // with a new default version, and return [Self].
pub fn new_default() -> Result<Self, TomlError> {
info!("Creating new default TOML...");
let default = Self::default();
let path = Self::get_path()?; 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<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)
}
}
// Save [State] onto disk file [gupax.toml]
pub fn save(&mut self) -> Result<(), TomlError> {
info!("Starting TOML overwrite..."); info!("Starting TOML overwrite...");
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) { let string = match toml::ser::to_string(&self) {
Ok(string) => { Ok(string) => {
info!("TOML parse ... OK"); info!("TOML parse ... OK");
info!("{}", HORIZONTAL); Self::info(&self);
for i in string.lines() { info!("{}", i); }
info!("{}", HORIZONTAL);
string string
}, },
Err(err) => { error!("Couldn't parse TOML into string"); return Err(TomlError::Serialize(err)) }, Err(err) => { error!("Couldn't parse TOML into string"); return Err(TomlError::Serialize(err)) },
@ -164,16 +200,49 @@ impl State {
Err(err) => { error!("Couldn't overwrite TOML file"); return Err(TomlError::Io(err)) }, 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<Self, TomlError> {
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
info!("Attempting to save to disk...");
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 { impl Display for TomlError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use TomlError::*; use TomlError::*;
match self { match self {
Io(err) => write!(f, "{} | {}", ERROR, err), Io(err) => write!(f, "{}: Io | {}", ERROR, err),
Path(err) => write!(f, "{} | {}", ERROR, err), Path(err) => write!(f, "{}: Path | {}", ERROR, err),
Serialize(err) => write!(f, "{} | {}", ERROR, err), Serialize(err) => write!(f, "{}: Serialize | {}", ERROR, err),
Deserialize(err) => write!(f, "{} | {}", ERROR, err), Deserialize(err) => write!(f, "{}: Deserialize | {}", ERROR, err),
Merge(err) => write!(f, "{}: Merge | {}", ERROR, err),
} }
} }
} }
@ -214,6 +283,7 @@ pub enum TomlError {
Path(String), Path(String),
Serialize(toml::ser::Error), Serialize(toml::ser::Error),
Deserialize(toml::de::Error), Deserialize(toml::de::Error),
Merge(figment::Error),
} }
//---------------------------------------------------------------------------------------------------- Structs //---------------------------------------------------------------------------------------------------- Structs
@ -233,6 +303,8 @@ pub struct Gupax {
pub save_before_quit: bool, pub save_before_quit: bool,
pub p2pool_path: String, pub p2pool_path: String,
pub xmrig_path: String, pub xmrig_path: String,
pub absolute_p2pool_path: PathBuf,
pub absolute_xmrig_path: PathBuf,
} }
#[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)] #[derive(Clone,Eq,PartialEq,Debug,Deserialize,Serialize)]

67
src/update.rs Normal file
View file

@ -0,0 +1,67 @@
// 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 <https://www.gnu.org/licenses/>.
use crate::State;
struct Update {
new_gupax: String,
new_p2pool: String,
new_xmrig: String,
path_gupax: String,
path_p2pool: String,
path_xmrig: String,
updating: Arc<Mutex<bool>> // Is the update in progress?
update_prog: u8, // Not an [f32] because [Eq] doesn't work
}
impl Update {
fn new(path_p2pool: String, path_xmrig: String) -> Result<Self, Error> {
let path_gupax = std::env::current_exe()?;
Self {
new_gupax: "?".to_string(),
new_p2pool: "?".to_string(),
new_xmrig: "?".to_string(),
path_gupax,
path_p2pool,
path_xmrig,
updating: Arc::new(Mutex::new(false)),
update_prog: 0,
}
}
fn update(state: &mut State) -> Result((), Error) {
}
#[derive(Debug, Serialize, Deserialize)]
struct TagName {
tag_name: String,
}
#[derive(Debug, Serialize, Deserialize)]
enum Error {
Io(std::io::Error),
Serialize(toml::ser::Error),
Deserialize(toml::de::Error),
}
#[derive(Debug, Serialize, Deserialize)]
enum Package {
Gupax,
P2pool,
Xmrig,
}