helper: p2pool - connect major [Helper] APIs to GUI thread

Lots of stuff in this commit:
1. Implement [Start/Stop/Restart] and make it not possible for
the GUI to interact with that UI if [Helper] is doing stuff.
This prevents the obviously bad situation where [Helper] is in
the middle of spawning P2Pool, but the user is still allowed to
start it again, which would spawn another P2Pool. The main GUI
matches on the state and disables the appropriate UI so the
user can't do this.

2. Sync P2Pool's [Priv] and [Pub] output so that the GUI thread
is only rendering it once a second. All of Gupax also refreshes
at least once a second now as well.

3. Match the [ProcessState] with some colors in the GUI

4. GUI thread no longer directly starts/stops/restarts a process.
It will call a function in [Helper] that acts as a proxy.

5. The tokio [async_spawn_p2pool_watchdog()] function that was
a clone of the PTY version (but had async stuff) and all of the
related functions like the async STDOUT/STDERR reader is now
completely gone. It doesn't make sense to write the same code
twice, both [Simple] and [Advanced] will have a PTY, only
difference being the [Simple] UI won't have an input box.

6. P2Pool's exit code is now examined, either success or failure

7. Output was moved into it's own [Arc<Mutex>]. This allows for
more efficient writing/reading since before I had to lock all of
[Helper], which caused some noticable deadlocks in the GUI.

8. New [tab] field in [State<Gupax>], and GUI option to select
the tab that Gupax will start on.
This commit is contained in:
hinto-janaiyo 2022-12-05 22:33:35 -05:00
parent 1f3a472869
commit 2a9ebd4cdf
No known key found for this signature in database
GPG key ID: B1C5A64B80691E45
6 changed files with 297 additions and 223 deletions

View file

@ -35,11 +35,27 @@ pub const BYTES_ICON: &[u8] = include_bytes!("../images/icons/icon@2x.png");
#[cfg(not(target_os = "macos"))]
pub const BYTES_ICON: &[u8] = include_bytes!("../images/icons/icon.png");
pub const BYTES_BANNER: &[u8] = include_bytes!("../images/banner.png");
pub const P2POOL_BASE_ARGS: &str = "";
pub const XMRIG_BASE_ARGS: &str = "--http-host=127.0.0.1 --http-port=18088 --algo=rx/0 --coin=Monero";
pub const HORIZONTAL: &str = "--------------------------------------------";
pub const HORI_DOUBLE: &str = "----------------------------------------------------------------------------------------";
// P2Pool & XMRig default API stuff
#[cfg(target_os = "windows")]
pub const P2POOL_API_PATH: &str = r"local\stats"; // The default relative FS path of P2Pool's local API
#[cfg(target_family = "unix")]
pub const P2POOL_API_PATH: &str = "local/stats";
pub const XMRIG_API_URI: &str = "1/summary"; // The default relative URI of XMRig's API
// Process state tooltips (online, offline, etc)
pub const P2POOL_ALIVE: &str = "P2Pool is online";
pub const P2POOL_DEAD: &str = "P2Pool is offline";
pub const P2POOL_FAILED: &str = "P2Pool is offline, and failed when exiting";
pub const P2POOL_MIDDLE: &str = "P2Pool is in the middle of (re)starting/stopping";
pub const XMRIG_ALIVE: &str = "XMRig is online";
pub const XMRIG_DEAD: &str = "XMRig is offline";
pub const XMRIG_FAILED: &str = "XMRig is offline, and failed when exiting";
pub const XMRIG_MIDDLE: &str = "XMRig is in the middle of (re)starting/stopping";
// This is the typical space added when using
// [ui.separator()] or [ui.group()]
// Used for subtracting the width/height so
@ -50,8 +66,10 @@ pub const SPACE: f32 = 10.0;
pub const RED: egui::Color32 = egui::Color32::from_rgb(230, 50, 50);
pub const GREEN: egui::Color32 = egui::Color32::from_rgb(100, 230, 100);
pub const YELLOW: egui::Color32 = egui::Color32::from_rgb(230, 230, 100);
pub const GRAY: egui::Color32 = egui::Color32::GRAY;
pub const LIGHT_GRAY: egui::Color32 = egui::Color32::LIGHT_GRAY;
pub const BLACK: egui::Color32 = egui::Color32::BLACK;
pub const DARK_GRAY: egui::Color32 = egui::Color32::from_rgb(18, 18, 18);
// [Duration] constants
pub const SECOND: std::time::Duration = std::time::Duration::from_secs(1);
@ -90,6 +108,12 @@ pub const GUPAX_LOCK_WIDTH: &str = "Automatically match the height against the w
pub const GUPAX_LOCK_HEIGHT: &str = "Automatically match the width against the height in a 16:10 ratio; aka WIDTH = HEIGHT * 1.6";
pub const GUPAX_NO_LOCK: &str = "Allow individual selection of width and height";
pub const GUPAX_SET: &str = "Set the width/height of the Gupax window to the current values";
pub const GUPAX_TAB_ABOUT: &str = "Set the tab Gupax starts on to: About";
pub const GUPAX_TAB_STATUS: &str = "Set the tab Gupax starts on to: Status";
pub const GUPAX_TAB_GUPAX: &str = "Set the tab Gupax starts on to: Gupax";
pub const GUPAX_TAB_P2POOL: &str = "Set the tab Gupax starts on to: P2Pool";
pub const GUPAX_TAB_XMRIG: &str = "Set the tab Gupax starts on to: XMRig";
pub const GUPAX_SIMPLE: &str =
r#"Use simple Gupax settings:
- Update button

View file

@ -44,6 +44,7 @@ use figment::providers::{Format,Toml};
use crate::{
constants::*,
gupax::Ratio,
Tab,
};
use log::*;
@ -154,6 +155,7 @@ impl State {
selected_width: APP_DEFAULT_WIDTH as u16,
selected_height: APP_DEFAULT_HEIGHT as u16,
ratio: Ratio::Width,
tab: Tab::About,
},
p2pool: P2pool {
simple: true,
@ -586,6 +588,7 @@ pub struct Gupax {
pub absolute_xmrig_path: PathBuf,
pub selected_width: u16,
pub selected_height: u16,
pub tab: Tab,
pub ratio: Ratio,
}

View file

@ -29,6 +29,7 @@ use crate::{
update::*,
ErrorState,
Restart,
Tab,
};
use std::{
thread,
@ -86,7 +87,7 @@ impl Gupax {
// 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/8.0;
let height = if self.simple { height/5.0 } else { height/10.0 };
let width = width - SPACE;
let updating = *update.lock().unwrap().updating.lock().unwrap();
ui.vertical(|ui| {
@ -113,12 +114,7 @@ impl Gupax {
ui.horizontal(|ui| {
ui.group(|ui| {
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 / 8.0;
// style.spacing.icon_width = width / 6.0;
// style.spacing.icon_spacing = 20.0;
// ctx.set_style(style);
let height = height/10.0;
ui.add_sized([width, height], Checkbox::new(&mut self.auto_update, "Auto-update")).on_hover_text(GUPAX_AUTO_UPDATE);
ui.separator();
ui.add_sized([width, height], Checkbox::new(&mut self.update_via_tor, "Update via Tor")).on_hover_text(GUPAX_UPDATE_VIA_TOR);
@ -217,19 +213,36 @@ impl Gupax {
});
});
ui.style_mut().override_text_style = Some(egui::TextStyle::Button);
// Width/Height locks
ui.group(|ui| {
let width = (width/4.0)-(SPACE*1.5);
let height = ui.available_height()/2.0;
let height = ui.available_height()/4.0;
ui.horizontal(|ui| {
if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Ratio::Width, "Lock to width")).on_hover_text(GUPAX_LOCK_WIDTH).clicked() { self.ratio = Ratio::Width; }
use Ratio::*;
let width = (width/4.0)-(SPACE*1.5);
if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Width, "Lock to width")).on_hover_text(GUPAX_LOCK_WIDTH).clicked() { self.ratio = Width; }
ui.separator();
if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Ratio::Height, "Lock to height")).on_hover_text(GUPAX_LOCK_HEIGHT).clicked() { self.ratio = Ratio::Height; }
if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Height, "Lock to height")).on_hover_text(GUPAX_LOCK_HEIGHT).clicked() { self.ratio = Height; }
ui.separator();
if ui.add_sized([width, height], SelectableLabel::new(self.ratio == Ratio::None, "No lock")).on_hover_text(GUPAX_NO_LOCK).clicked() { self.ratio = Ratio::None; }
if ui.add_sized([width, height], SelectableLabel::new(self.ratio == None, "No lock")).on_hover_text(GUPAX_NO_LOCK).clicked() { self.ratio = None; }
if ui.add_sized([width, height], Button::new("Set")).on_hover_text(GUPAX_SET).clicked() {
frame.set_window_size(Vec2::new(self.selected_width as f32, self.selected_height as f32));
}
})});
// Saved [Tab]
ui.group(|ui| {
let height = ui.available_height()/2.0;
let width = (width/5.0)-(SPACE*1.8);
ui.horizontal(|ui| {
if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::About, "About")).on_hover_text(GUPAX_TAB_ABOUT).clicked() { self.tab = Tab::About; }
ui.separator();
if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::Status, "Status")).on_hover_text(GUPAX_TAB_STATUS).clicked() { self.tab = Tab::Status; }
ui.separator();
if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::Gupax, "Gupax")).on_hover_text(GUPAX_TAB_GUPAX).clicked() { self.tab = Tab::Gupax; }
ui.separator();
if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::P2pool, "P2Pool")).on_hover_text(GUPAX_TAB_P2POOL).clicked() { self.tab = Tab::P2pool; }
ui.separator();
if ui.add_sized([width, height], SelectableLabel::new(self.tab == Tab::Xmrig, "XMRig")).on_hover_text(GUPAX_TAB_XMRIG).clicked() { self.tab = Tab::Xmrig; }
})});
}
fn spawn_file_window_thread(file_window: &Arc<Mutex<FileWindow>>, file_type: FileType) {

View file

@ -88,21 +88,14 @@ pub struct Process {
// The below are the handles to the actual child process.
// [Simple] has no STDIN, but [Advanced] does. A PTY (pseudo-terminal) is
// required for P2Pool/XMRig to open their STDIN pipe, so whether [child]
// or [child_pty] actually has a [Some] depends on the users setting.
// [Simple]
child: Option<Arc<Mutex<tokio::process::Child>>>,
stdout: Option<tokio::process::ChildStdout>, // Handle to STDOUT pipe
stderr: Option<tokio::process::ChildStderr>, // Handle to STDERR pipe
// [Advanced] (PTY)
child_pty: Option<Arc<Mutex<Box<dyn portable_pty::Child + Send + std::marker::Sync>>>>, // STDOUT/STDERR is combined automatically thanks to this PTY, nice
// required for P2Pool/XMRig to open their STDIN pipe.
child: Option<Arc<Mutex<Box<dyn portable_pty::Child + Send + std::marker::Sync>>>>, // STDOUT/STDERR is combined automatically thanks to this PTY, nice
stdin: Option<Box<dyn portable_pty::MasterPty + Send>>, // A handle to the process's MasterPTY/STDIN
// This is the process's private output [String], used by both [Simple] and [Advanced].
// The "watchdog" threads mutate this, the "helper" thread synchronizes the [Pub*Api] structs
// so that the data in here is cloned there roughly once a second. GUI thread never touches this.
output: String,
output: Arc<Mutex<String>>,
}
//---------------------------------------------------------------------------------------------------- [Process] Impl
@ -115,15 +108,12 @@ impl Process {
signal: ProcessSignal::None,
start: now,
uptime: HumanTime::into_human(now.elapsed()),
stdout: Option::None,
stderr: Option::None,
stdin: Option::None,
child: Option::None,
child_pty: Option::None,
// P2Pool log level 1 produces a bit less than 100,000 lines a day.
// Assuming each line averages 80 UTF-8 scalars (80 bytes), then this
// initial buffer should last around a week (56MB) before resetting.
output: String::with_capacity(56_000_000),
output: Arc::new(Mutex::new(String::with_capacity(56_000_000))),
input: vec![String::new()],
}
}
@ -132,6 +122,15 @@ impl Process {
pub fn parse_args(args: &str) -> Vec<String> {
args.split_whitespace().map(|s| s.to_owned()).collect()
}
// Convenience functions
pub fn is_alive(&self) -> bool {
self.state == ProcessState::Alive || self.state == ProcessState::Middle
}
pub fn is_waiting(&self) -> bool {
self.state == ProcessState::Middle || self.state == ProcessState::Waiting
}
}
//---------------------------------------------------------------------------------------------------- [Process*] Enum
@ -140,7 +139,8 @@ pub enum ProcessState {
Alive, // Process is online, GREEN!
Dead, // Process is dead, BLACK!
Failed, // Process is dead AND exited with a bad code, RED!
Middle, // Process is in the middle of something (starting, stopping, etc), YELLOW!
Middle, // Process is in the middle of something ([re]starting/stopping), YELLOW!
Waiting, // Process was successfully killed by a restart, and is ready to be started again, YELLOW!
}
#[derive(Copy,Clone,Eq,PartialEq,Debug)]
@ -166,63 +166,80 @@ use tokio::io::{BufReader,AsyncBufReadExt};
impl Helper {
//---------------------------------------------------------------------------------------------------- General Functions
pub fn new(instant: std::time::Instant, pub_api_p2pool: Arc<Mutex<PubP2poolApi>>, pub_api_xmrig: Arc<Mutex<PubXmrigApi>>) -> Self {
pub fn new(instant: std::time::Instant, p2pool: Arc<Mutex<Process>>, xmrig: Arc<Mutex<Process>>, pub_api_p2pool: Arc<Mutex<PubP2poolApi>>, pub_api_xmrig: Arc<Mutex<PubXmrigApi>>) -> Self {
Self {
instant,
human_time: HumanTime::into_human(instant.elapsed()),
p2pool: Arc::new(Mutex::new(Process::new(ProcessName::P2pool, String::new(), PathBuf::new()))),
xmrig: Arc::new(Mutex::new(Process::new(ProcessName::Xmrig, String::new(), PathBuf::new()))),
priv_api_p2pool: Arc::new(Mutex::new(PrivP2poolApi::new())),
priv_api_xmrig: Arc::new(Mutex::new(PrivXmrigApi::new())),
// These are created when initializing [App], since it needs a handle to it as well
p2pool,
xmrig,
pub_api_p2pool,
pub_api_xmrig,
}
}
// The tokio runtime that blocks while async reading both STDOUT/STDERR
// Cheaper than spawning 2 OS threads just to read 2 pipes (...right? :D)
#[tokio::main]
async fn async_read_stdout_stderr(process: Arc<Mutex<Process>>) {
let process_stdout = Arc::clone(&process);
let process_stderr = Arc::clone(&process);
let stdout = process.lock().unwrap().child.as_ref().unwrap().lock().unwrap().stdout.take().unwrap();
let stderr = process.lock().unwrap().child.as_ref().unwrap().lock().unwrap().stderr.take().unwrap();
// Create STDOUT pipe job
let stdout_job = tokio::spawn(async move {
let mut reader = BufReader::new(stdout).lines();
while let Ok(Some(line)) = reader.next_line().await {
println!("{}", line); // For debugging.
writeln!(process_stdout.lock().unwrap().output, "{}", line);
}
});
// Create STDERR pipe job
let stderr_job = tokio::spawn(async move {
let mut reader = BufReader::new(stderr).lines();
while let Ok(Some(line)) = reader.next_line().await {
println!("{}", line); // For debugging.
writeln!(process_stderr.lock().unwrap().output, "{}", line);
}
});
// Block and read both until they are closed (automatic when process dies)
// The ordering of STDOUT/STDERR should be automatic thanks to the locks.
tokio::join![stdout_job, stderr_job];
}
// Reads a PTY which combines STDOUT/STDERR for me, yay
fn read_pty(process: Arc<Mutex<Process>>, reader: Box<dyn std::io::Read + Send>) {
fn read_pty(output: Arc<Mutex<String>>, reader: Box<dyn std::io::Read + Send>) {
use std::io::BufRead;
let mut stdout = std::io::BufReader::new(reader).lines();
while let Some(Ok(line)) = stdout.next() {
println!("{}", line); // For debugging.
writeln!(process.lock().unwrap().output, "{}", line);
// println!("{}", line); // For debugging.
writeln!(output.lock().unwrap(), "{}", line);
}
}
//---------------------------------------------------------------------------------------------------- P2Pool specific
// Intermediate function that parses the arguments, and spawns the P2Pool watchdog thread.
pub fn spawn_p2pool(helper: &Arc<Mutex<Self>>, state: &crate::disk::P2pool, path: std::path::PathBuf) {
// Read P2Pool's API file.
fn read_p2pool_api(path: &std::path::PathBuf) -> Result<String, std::io::Error> {
match std::fs::read_to_string(path) {
Ok(s) => Ok(s),
Err(e) => { warn!("P2Pool API | [{}] read error: {}", path.display(), e); Err(e) },
}
}
// Deserialize the above [String] into a [PrivP2poolApi]
fn str_to_priv_p2pool_api(string: &str) -> Result<PrivP2poolApi, serde_json::Error> {
match serde_json::from_str::<PrivP2poolApi>(string) {
Ok(a) => Ok(a),
Err(e) => { warn!("P2Pool API | Could not deserialize API data: {}", e); Err(e) },
}
}
// Just sets some signals for the watchdog thread to pick up on.
pub fn stop_p2pool(helper: &Arc<Mutex<Self>>) {
info!("P2Pool | Attempting stop...");
helper.lock().unwrap().p2pool.lock().unwrap().signal = ProcessSignal::Stop;
helper.lock().unwrap().p2pool.lock().unwrap().state = ProcessState::Middle;
}
// The "restart frontend" to a "frontend" function.
// Basically calls to kill the current p2pool, waits a little, then starts the below function in a a new thread, then exit.
pub fn restart_p2pool(helper: &Arc<Mutex<Self>>, state: &crate::disk::P2pool, path: std::path::PathBuf) {
info!("P2Pool | Attempting restart...");
helper.lock().unwrap().p2pool.lock().unwrap().signal = ProcessSignal::Restart;
helper.lock().unwrap().p2pool.lock().unwrap().state = ProcessState::Middle;
let helper = Arc::clone(&helper);
let state = state.clone();
let path = path.clone();
// This thread lives to wait, start p2pool then die.
thread::spawn(move || {
while helper.lock().unwrap().p2pool.lock().unwrap().is_alive() {
warn!("P2Pool Restart | Process still alive, waiting...");
thread::sleep(SECOND);
}
// Ok, process is not alive, start the new one!
Self::start_p2pool(&helper, &state, path);
});
info!("P2Pool | Restart ... OK");
}
// The "frontend" function that parses the arguments, and spawns either the [Simple] or [Advanced] P2Pool watchdog thread.
pub fn start_p2pool(helper: &Arc<Mutex<Self>>, state: &crate::disk::P2pool, path: std::path::PathBuf) {
helper.lock().unwrap().p2pool.lock().unwrap().state = ProcessState::Middle;
let mut args = Vec::with_capacity(500);
let path = path.clone();
let mut api_path = path.clone();
@ -268,81 +285,29 @@ impl Helper {
crate::disk::print_dash(&format!("P2Pool | Launch arguments ... {:#?}", args));
// Spawn watchdog thread
let simple = !state.simple; // Will this process need a PTY (STDIN)?
let process = Arc::clone(&helper.lock().unwrap().p2pool);
let pub_api = Arc::clone(&helper.lock().unwrap().pub_api_p2pool);
let priv_api = Arc::clone(&helper.lock().unwrap().priv_api_p2pool);
thread::spawn(move || {
if simple {
Self::spawn_simple_p2pool_watchdog(process, pub_api, priv_api, args, path);
} else {
Self::spawn_pty_p2pool_watchdog(process, pub_api, priv_api, args, path);
}
Self::spawn_p2pool_watchdog(process, pub_api, priv_api, args, path);
});
}
// The [Simple] P2Pool watchdog tokio runtime, using async features with no PTY (STDIN).
// The P2Pool watchdog. Spawns 1 OS thread for reading a PTY (STDOUT+STDERR), and combines the [Child] with a PTY so STDIN actually works.
#[tokio::main]
async fn spawn_simple_p2pool_watchdog(process: Arc<Mutex<Process>>, pub_api: Arc<Mutex<PubP2poolApi>>, priv_api: Arc<Mutex<PrivP2poolApi>>, args: Vec<String>, path: std::path::PathBuf) {
// 1a. Create command
let child = Arc::new(Mutex::new(tokio::process::Command::new(path)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::piped())
.spawn().unwrap()));
// 2. Set process state
let mut lock = process.lock().unwrap();
lock.state = ProcessState::Alive;
lock.signal = ProcessSignal::None;
lock.start = Instant::now();
lock.child = Some(Arc::clone(&child));
drop(lock);
// 3. Spawn STDOUT/STDERR thread
let process_clone = Arc::clone(&process);
thread::spawn(move || {
Self::async_read_stdout_stderr(process_clone);
});
// 4. Loop forever as watchdog until process dies
loop {
// a. Watch user SIGNAL
if process.lock().unwrap().signal == ProcessSignal::Stop {
process.lock().unwrap().child.as_mut().unwrap().lock().unwrap().kill().await;
process.lock().unwrap().signal = ProcessSignal::None;
}
// let signal = match process.lock().unwrap().signal {
// ProcessSignal::Stop => { crate::disk::print_dash("KILLING P2POOL"); process.lock().unwrap().child.as_mut().unwrap().lock().unwrap().kill().await.unwrap() },
// ProcessSignal::Restart => process.lock().unwrap().child.as_mut().unwrap().lock().unwrap().kill().await,
// _ => Ok(()),
// };
// b. Create STDIN task
if !process.lock().unwrap().input.is_empty() { /* process it */ }
// c. Create API task
let async_file_read = { /* tokio async file read job */ };
// d. Execute async tasks
// tokio::join![signal];
// f. Sleep (900ms)
std::thread::sleep(MILLI_900);
}
}
// The [Advanced] P2Pool watchdog. Spawns 1 OS thread for reading a PTY (STDOUT+STDERR), and combines the [Child] with a PTY so STDIN actually works.
#[tokio::main]
async fn spawn_pty_p2pool_watchdog(process: Arc<Mutex<Process>>, pub_api: Arc<Mutex<PubP2poolApi>>, priv_api: Arc<Mutex<PrivP2poolApi>>, args: Vec<String>, path: std::path::PathBuf) {
async fn spawn_p2pool_watchdog(process: Arc<Mutex<Process>>, pub_api: Arc<Mutex<PubP2poolApi>>, priv_api: Arc<Mutex<PrivP2poolApi>>, args: Vec<String>, mut path: std::path::PathBuf) {
// 1a. Create PTY
let pty = portable_pty::native_pty_system();
let mut pair = pty.openpty(portable_pty::PtySize {
let pair = pty.openpty(portable_pty::PtySize {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
}).unwrap();
// 1b. Create command
let mut cmd = portable_pty::CommandBuilder::new(path);
let mut cmd = portable_pty::CommandBuilder::new(path.as_path());
cmd.args(args);
cmd.cwd(path.as_path().parent().unwrap());
// 1c. Create child
let child_pty = Arc::new(Mutex::new(pair.slave.spawn_command(cmd).unwrap()));
@ -351,17 +316,22 @@ impl Helper {
lock.state = ProcessState::Alive;
lock.signal = ProcessSignal::None;
lock.start = Instant::now();
lock.child_pty = Some(Arc::clone(&child_pty));
lock.child = Some(Arc::clone(&child_pty));
let reader = pair.master.try_clone_reader().unwrap(); // Get STDOUT/STDERR before moving the PTY
lock.stdin = Some(pair.master);
drop(lock);
// 3. Spawn PTY read thread
let process_clone = Arc::clone(&process);
let output_clone = Arc::clone(&process.lock().unwrap().output);
thread::spawn(move || {
Self::read_pty(process_clone, reader);
Self::read_pty(output_clone, reader);
});
path.pop();
path.push(P2POOL_API_PATH);
let regex = P2poolRegex::new();
let output = Arc::clone(&process.lock().unwrap().output);
// 4. Loop as watchdog
loop {
// Set timer
@ -371,14 +341,42 @@ impl Helper {
if process.lock().unwrap().signal == ProcessSignal::Stop {
child_pty.lock().unwrap().kill(); // This actually sends a SIGHUP to p2pool (closes the PTY, hangs up on p2pool)
// Wait to get the exit status
let mut lock = process.lock().unwrap();
let exit_status = match child_pty.lock().unwrap().wait() {
Ok(e) => if e.success() { lock.state = ProcessState::Dead; "Successful" } else { lock.state = ProcessState::Failed; "Failed" },
_ => { lock.state = ProcessState::Failed; "Unknown Error" },
};
let uptime = lock.uptime.clone();
info!("P2Pool | Stopped ... Uptime was: [{}], Exit status: [{}]", uptime, exit_status);
// This is written directly into the public API, because sometimes the 900ms event loop can't catch it.
writeln!(pub_api.lock().unwrap().output, "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n", HORI_DOUBLE, uptime, exit_status, HORI_DOUBLE);
lock.signal = ProcessSignal::None;
break
} else if process.lock().unwrap().signal == ProcessSignal::Restart {
child_pty.lock().unwrap().kill(); // This actually sends a SIGHUP to p2pool (closes the PTY, hangs up on p2pool)
// Wait to get the exit status
let mut lock = process.lock().unwrap();
let exit_status = match child_pty.lock().unwrap().wait() {
Ok(e) => if e.success() { "Successful" } else { "Failed" },
_ => "Unknown Error",
};
let mut lock = process.lock().unwrap();
let uptime = lock.uptime.clone();
info!("P2Pool | Stopped ... Uptime was: [{}], Exit status: [{}]", uptime, exit_status);
writeln!(lock.output, "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n", HORI_DOUBLE, uptime, exit_status, HORI_DOUBLE);
// This is written directly into the public API, because sometimes the 900ms event loop can't catch it.
writeln!(pub_api.lock().unwrap().output, "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n", HORI_DOUBLE, uptime, exit_status, HORI_DOUBLE);
lock.state = ProcessState::Waiting;
break
// Check if the process is secretly died without us knowing :)
} else if let Ok(Some(code)) = child_pty.lock().unwrap().try_wait() {
let mut lock = process.lock().unwrap();
let exit_status = match code.success() {
true => { lock.state = ProcessState::Dead; "Successful" },
false => { lock.state = ProcessState::Failed; "Failed" },
};
let uptime = lock.uptime.clone();
info!("P2Pool | Stopped ... Uptime was: [{}], Exit status: [{}]", uptime, exit_status);
// This is written directly into the public API, because sometimes the 900ms event loop can't catch it.
writeln!(pub_api.lock().unwrap().output, "{}\nP2Pool stopped | Uptime: [{}] | Exit status: [{}]\n{}\n\n", HORI_DOUBLE, uptime, exit_status, HORI_DOUBLE);
lock.signal = ProcessSignal::None;
break
}
@ -393,6 +391,15 @@ impl Helper {
}
drop(lock);
// Read API file into string
if let Ok(string) = Self::read_p2pool_api(&path) {
// Deserialize
if let Ok(s) = Self::str_to_priv_p2pool_api(&string) {
// Update the structs.
PubP2poolApi::update_from_priv(&pub_api, &priv_api, &output, process.lock().unwrap().start.elapsed().as_secs_f64(), &regex);
}
}
// Sleep (only if 900ms hasn't passed)
let elapsed = now.elapsed().as_millis();
// Since logic goes off if less than 1000, casting should be safe
@ -400,12 +407,12 @@ impl Helper {
}
// 5. If loop broke, we must be done here.
info!("P2Pool | Watchdog thread exiting... Goodbye!");
info!("P2Pool | Advanced watchdog thread exiting... Goodbye!");
}
//---------------------------------------------------------------------------------------------------- XMRig specific
// Intermediate function that parses the arguments, and spawns the XMRig watchdog thread.
pub fn spawn_xmrig(state: &crate::disk::Xmrig, api_path: &std::path::Path) {
pub fn spawn_xmrig(helper: &Arc<Mutex<Self>>, state: &crate::disk::Xmrig, path: std::path::PathBuf) {
let mut args = Vec::with_capacity(500);
if state.simple {
let rig_name = if state.simple_rig.is_empty() { GUPAX_VERSION.to_string() } else { state.simple_rig.clone() }; // Rig name
@ -661,26 +668,31 @@ impl HumanNumber {
// The following STDLIB implementation takes [0.003~] seconds to find all matches given a [String] with 30k lines:
// let mut n = 0;
// for line in P2POOL_OUTPUT.lines() {
// if line.contains("[0-9].[0-9]+ XMR") { n += 1; }
// if line.contains("You received a payout of [0-9].[0-9]+ XMR") { n += 1; }
// }
//
// This regex function takes [0.0003~] seconds (10x faster):
// let regex = Regex::new("[0-9].[0-9]+ XMR").unwrap();
// let regex = Regex::new("You received a payout of [0-9].[0-9]+ XMR").unwrap();
// let n = regex.find_iter(P2POOL_OUTPUT).count();
//
// Both are nominally fast enough where it doesn't matter too much but meh, why not use regex.
struct P2poolRegex {
xmr: regex::Regex,
payout: regex::Regex,
float: regex::Regex,
}
impl P2poolRegex {
fn new() -> Self {
Self { xmr: regex::Regex::new("[0-9].[0-9]+ XMR").unwrap(), }
Self {
payout: regex::Regex::new("You received a payout of [0-9].[0-9]+ XMR").unwrap(),
float: regex::Regex::new("[0-9].[0-9]+").unwrap(),
}
}
}
//---------------------------------------------------------------------------------------------------- Public P2Pool API
// GUI thread interfaces with this.
#[derive(Debug, Clone)]
pub struct PubP2poolApi {
// One off
pub mini: bool,
@ -728,20 +740,23 @@ impl PubP2poolApi {
}
}
// Mutate [PubP2poolApi] with data from a [PrivP2poolApi].
fn update_from_priv(self, output: String, regex: P2poolRegex, private: PrivP2poolApi, uptime: f64) -> Self {
// Mutate [PubP2poolApi] with data from a [PrivP2poolApi] and the process output.
fn update_from_priv(public: &Arc<Mutex<Self>>, private: &Arc<Mutex<PrivP2poolApi>>, output: &Arc<Mutex<String>>, start: f64, regex: &P2poolRegex) {
let public_clone = public.lock().unwrap().clone();
let output = output.lock().unwrap().clone();
// 1. Parse STDOUT
let (payouts, xmr) = Self::calc_payouts_and_xmr(&output, &regex);
let stdout_parse = Self {
output: output.clone(),
output,
payouts,
xmr,
..self // <- So useful
..public_clone // <- So useful
};
// 2. Time calculations
let hour_day_month = Self::update_hour_day_month(stdout_parse, uptime);
let hour_day_month = Self::update_hour_day_month(stdout_parse, start);
// 3. Final priv -> pub conversion
Self {
let private = private.lock().unwrap();
*public.lock().unwrap() = Self {
hashrate_15m: HumanNumber::from_u128(private.hashrate_15m),
hashrate_1h: HumanNumber::from_u128(private.hashrate_1h),
hashrate_24h: HumanNumber::from_u128(private.hashrate_24h),
@ -756,16 +771,13 @@ impl PubP2poolApi {
// Essentially greps the output for [x.xxxxxxxxxxxx XMR] where x = a number.
// It sums each match and counts along the way, handling an error by not adding and printing to console.
fn calc_payouts_and_xmr(output: &str, regex: &P2poolRegex) -> (u128 /* payout count */, f64 /* total xmr */) {
let mut iter = regex.xmr.find_iter(output);
let iter = regex.payout.find_iter(output);
let mut result: f64 = 0.0;
let mut count: u128 = 0;
for i in iter {
if let Some(text) = i.as_str().split_whitespace().next() {
match text.parse::<f64>() {
Ok(num) => result += num,
Err(e) => error!("P2Pool | Total XMR sum calculation error: [{}]", e),
}
count += 1;
match regex.float.find(i.as_str()).unwrap().as_str().parse::<f64>() {
Ok(num) => { result += num; count += 1; },
Err(e) => error!("P2Pool | Total XMR sum calculation error: [{}]", e),
}
}
(count, result)
@ -799,7 +811,7 @@ impl PubP2poolApi {
// This is the data the "watchdog" threads mutate.
// It matches directly to P2Pool's [local/stats] JSON API file (excluding a few stats).
// P2Pool seems to initialize all stats at 0 (or 0.0), so no [Option] wrapper seems needed.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
struct PrivP2poolApi {
hashrate_15m: u128,
hashrate_1h: u128,
@ -825,6 +837,7 @@ impl PrivP2poolApi {
}
//---------------------------------------------------------------------------------------------------- Public XMRig API
#[derive(Debug, Clone)]
pub struct PubXmrigApi {
output: String,
worker_id: String,
@ -870,7 +883,7 @@ impl PubXmrigApi {
// e.g: [wget -qO- localhost:18085/1/summary].
// XMRig doesn't initialize stats at 0 (or 0.0) and instead opts for [null]
// which means some elements need to be wrapped in an [Option] or else serde will [panic!].
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
struct PrivXmrigApi {
worker_id: String,
resources: Resources,
@ -889,7 +902,7 @@ impl PrivXmrigApi {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
struct Resources {
load_average: [Option<f32>; 3],
}
@ -901,7 +914,7 @@ impl Resources {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Connection {
pool: String,
diff: u128,
@ -919,7 +932,7 @@ impl Connection {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
struct Hashrate {
total: [Option<f32>; 3],
}

View file

@ -32,14 +32,13 @@ use egui::{
};
use egui_extras::RetainedImage;
use eframe::{egui,NativeOptions};
// Logging
use log::*;
use env_logger::{Builder,WriteStyle};
// Regex
use regex::Regex;
// Serde
use serde::{Serialize,Deserialize};
// std
use std::{
env,
@ -49,7 +48,6 @@ use std::{
time::Instant,
path::PathBuf,
};
// Modules
mod ferris;
mod constants;
@ -106,16 +104,11 @@ pub struct App {
// Helper/API State:
// This holds everything related to the data processed by the "helper thread".
// This includes the "helper" threads public P2Pool/XMRig's API.
helper: Arc<Mutex<Helper>>,
p2pool_api: Arc<Mutex<PubP2poolApi>>,
xmrig_api: Arc<Mutex<PubXmrigApi>>,
// Fix-me.
// These shouldn't exist
// Just for debugging.
p2pool: bool,
xmrig: bool,
helper: Arc<Mutex<Helper>>, // [Helper] state, mostly for Gupax uptime
p2pool: Arc<Mutex<Process>>, // [P2Pool] process state
xmrig: Arc<Mutex<Process>>, // [XMRig] process state
p2pool_api: Arc<Mutex<PubP2poolApi>>, // Public ready-to-print P2Pool API made by the "helper" thread
xmrig_api: Arc<Mutex<PubXmrigApi>>, // Public ready-to-print XMRig API made by the "helper" thread
// State from [--flags]
no_startup: bool,
// Static stuff
@ -146,6 +139,8 @@ impl App {
fn new(now: Instant) -> Self {
info!("Initializing App Struct...");
let p2pool = Arc::new(Mutex::new(Process::new(ProcessName::P2pool, String::new(), PathBuf::new())));
let xmrig = Arc::new(Mutex::new(Process::new(ProcessName::Xmrig, String::new(), PathBuf::new())));
let p2pool_api = Arc::new(Mutex::new(PubP2poolApi::new()));
let xmrig_api = Arc::new(Mutex::new(PubXmrigApi::new()));
let mut app = Self {
@ -165,17 +160,13 @@ impl App {
restart: Arc::new(Mutex::new(Restart::No)),
diff: false,
error_state: ErrorState::new(),
helper: Arc::new(Mutex::new(Helper::new(now, p2pool_api.clone(), xmrig_api.clone()))),
helper: Arc::new(Mutex::new(Helper::new(now, p2pool.clone(), xmrig.clone(), p2pool_api.clone(), xmrig_api.clone()))),
p2pool,
xmrig,
p2pool_api,
xmrig_api,
// TODO
// these p2pool/xmrig bools are here for debugging purposes
// they represent the online/offline status.
// fix this later when [Helper] is integrated.
resizing: false,
alpha: 0,
p2pool: false,
xmrig: false,
no_startup: false,
now,
exe: String::new(),
@ -319,6 +310,8 @@ impl App {
// Set state version as compiled in version
og.version.lock().unwrap().gupax = GUPAX_VERSION.to_string();
app.state.version.lock().unwrap().gupax = GUPAX_VERSION.to_string();
// Set saved [Tab]
app.tab = app.state.gupax.tab;
drop(og); // Unlock [og]
info!("App ... OK");
app
@ -327,8 +320,8 @@ impl App {
//---------------------------------------------------------------------------------------------------- [Tab] Enum + Impl
// The tabs inside [App].
#[derive(Clone, Copy, Debug, PartialEq)]
enum Tab {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Tab {
About,
Status,
Gupax,
@ -719,6 +712,9 @@ impl eframe::App for App {
frame.set_fullscreen(!info.window_info.fullscreen);
}
// Refresh AT LEAST once a second
ctx.request_repaint_after(SECOND);
// This sets the top level Ui dimensions.
// Used as a reference for other uis.
CentralPanel::default().show(ctx, |ui| {
@ -797,8 +793,8 @@ impl eframe::App for App {
StayQuit => {
let mut text = "".to_string();
if *self.update.lock().unwrap().updating.lock().unwrap() { text = format!("{}\nUpdate is in progress...!", text); }
if self.p2pool { text = format!("{}\nP2Pool is online...!", text); }
if self.xmrig { text = format!("{}\nXMRig is online...!", text); }
if self.p2pool.lock().unwrap().is_alive() { text = format!("{}\nP2Pool is online...!", text); }
if self.xmrig.lock().unwrap().is_alive() { text = format!("{}\nXMRig is online...!", text); }
ui.add_sized([width, height], Label::new("--- Are you sure you want to quit? ---"));
ui.add_sized([width, height], Label::new(text))
},
@ -939,17 +935,20 @@ impl eframe::App for App {
ui.add_sized([width, height], Label::new(self.os));
ui.separator();
// [P2Pool/XMRig] Status
if self.p2pool {
ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(GREEN))).on_hover_text("P2Pool is online");
} else {
ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(RED))).on_hover_text("P2Pool is offline");
}
use ProcessState::*;
match self.p2pool.lock().unwrap().state {
Alive => ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(GREEN))).on_hover_text(P2POOL_ALIVE),
Dead => ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(GRAY))).on_hover_text(P2POOL_DEAD),
Failed => ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(RED))).on_hover_text(P2POOL_FAILED),
Middle|Waiting => ui.add_sized([width, height], Label::new(RichText::new("P2Pool ⏺").color(YELLOW))).on_hover_text(P2POOL_MIDDLE),
};
ui.separator();
if self.xmrig {
ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(GREEN))).on_hover_text("XMRig is online");
} else {
ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(RED))).on_hover_text("XMRig is offline");
}
match self.xmrig.lock().unwrap().state {
Alive => ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(GREEN))).on_hover_text(XMRIG_ALIVE),
Dead => ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(GRAY))).on_hover_text(XMRIG_DEAD),
Failed => ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(RED))).on_hover_text(XMRIG_FAILED),
Middle|Waiting => ui.add_sized([width, height], Label::new(RichText::new("XMRig ⏺").color(YELLOW))).on_hover_text(XMRIG_MIDDLE),
};
});
// [Save/Reset]
@ -1019,29 +1018,30 @@ impl eframe::App for App {
});
ui.group(|ui| {
let width = (ui.available_width()/3.0)-5.0;
// if self.p2pool {
// if ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart P2Pool").clicked() { self.p2pool = false; }
// if ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop P2Pool").clicked() { self.p2pool = false; }
// ui.add_enabled_ui(false, |ui| {
// if ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start P2Pool").clicked() {
// Helper::spawn_p2pool(&self.helper, &self.state.p2pool, self.state.gupax.absolute_p2pool_path.clone());
// }
// });
// } else {
// ui.add_enabled_ui(false, |ui| {
// ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart P2Pool");
// ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop P2Pool");
// });
// if ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start P2Pool").clicked() {
// Helper::spawn_p2pool(&self.helper, &self.state.p2pool, self.state.gupax.absolute_p2pool_path.clone());
// }
// }
ui.add_sized([width, height], Button::new("")).on_hover_text("Restart P2Pool");
if ui.add_sized([width, height], Button::new("")).on_hover_text("Stop P2Pool").clicked() {
self.helper.lock().unwrap().p2pool.lock().unwrap().signal = ProcessSignal::Stop;
}
if ui.add_sized([width, height], Button::new("")).on_hover_text("Start P2Pool").clicked() {
Helper::spawn_p2pool(&self.helper, &self.state.p2pool, self.state.gupax.absolute_p2pool_path.clone());
if self.p2pool.lock().unwrap().is_waiting() {
ui.add_enabled_ui(false, |ui| {
ui.add_sized([width, height], Button::new("")).on_hover_text("Restart P2Pool");
ui.add_sized([width, height], Button::new("")).on_hover_text("Stop P2Pool");
ui.add_sized([width, height], Button::new("")).on_hover_text("Start P2Pool");
});
} else if self.p2pool.lock().unwrap().is_alive() {
if ui.add_sized([width, height], Button::new("")).on_hover_text("Restart P2Pool").clicked() {
Helper::restart_p2pool(&self.helper, &self.state.p2pool, self.state.gupax.absolute_p2pool_path.clone());
}
if ui.add_sized([width, height], Button::new("")).on_hover_text("Stop P2Pool").clicked() {
Helper::stop_p2pool(&self.helper);
}
ui.add_enabled_ui(false, |ui| {
ui.add_sized([width, height], Button::new("")).on_hover_text("Start P2Pool");
});
} else {
ui.add_enabled_ui(false, |ui| {
ui.add_sized([width, height], Button::new("")).on_hover_text("Restart P2Pool");
ui.add_sized([width, height], Button::new("")).on_hover_text("Stop P2Pool");
});
if ui.add_sized([width, height], Button::new("")).on_hover_text("Start P2Pool").clicked() {
Helper::start_p2pool(&self.helper, &self.state.p2pool, self.state.gupax.absolute_p2pool_path.clone());
}
}
});
},
@ -1058,9 +1058,13 @@ impl eframe::App for App {
});
ui.group(|ui| {
let width = (ui.available_width()/3.0)-5.0;
if self.xmrig {
if ui.add_sized([width, height], Button::new("")).on_hover_text("Restart XMRig").clicked() { self.xmrig = false; }
if ui.add_sized([width, height], Button::new("")).on_hover_text("Stop XMRig").clicked() { self.xmrig = false; }
if self.xmrig.lock().unwrap().is_alive() {
if ui.add_sized([width, height], Button::new("")).on_hover_text("Restart XMRig").clicked() {
self.xmrig.lock().unwrap().state = ProcessState::Middle;
}
if ui.add_sized([width, height], Button::new("")).on_hover_text("Stop XMRig").clicked() {
self.xmrig.lock().unwrap().state = ProcessState::Dead;
}
ui.add_enabled_ui(false, |ui| {
ui.add_sized([width, height], Button::new("")).on_hover_text("Start XMRig");
});
@ -1069,7 +1073,9 @@ impl eframe::App for App {
ui.add_sized([width, height], Button::new("")).on_hover_text("Restart XMRig");
ui.add_sized([width, height], Button::new("")).on_hover_text("Stop XMRig");
});
if ui.add_sized([width, height], Button::new("")).on_hover_text("Start XMRig").clicked() { self.xmrig = true; }
if ui.add_sized([width, height], Button::new("")).on_hover_text("Start XMRig").clicked() {
Helper::spawn_xmrig(&self.helper, &self.state.xmrig, self.state.gupax.absolute_xmrig_path.clone());
}
}
});
},
@ -1104,6 +1110,7 @@ impl eframe::App for App {
ui.hyperlink_to("Made by hinto-janaiyo".to_string(), "https://gupax.io");
ui.label("egui is licensed under MIT & Apache-2.0");
ui.label("Gupax, P2Pool, and XMRig are licensed under GPLv3");
if cfg!(debug_assertions) { ui.label(format!("{}", self.now.elapsed().as_secs_f64())); }
});
}
Tab::Status => {
@ -1113,7 +1120,7 @@ impl eframe::App for App {
Gupax::show(&mut self.state.gupax, &self.og, &self.state_path, &self.update, &self.file_window, &mut self.error_state, &self.restart, self.width, self.height, frame, ctx, ui);
}
Tab::P2pool => {
P2pool::show(&mut self.state.p2pool, &mut self.node_vec, &self.og, self.p2pool, &self.ping, &self.regex, &self.helper, &self.p2pool_api, self.width, self.height, ctx, ui);
P2pool::show(&mut self.state.p2pool, &mut self.node_vec, &self.og, &self.ping, &self.regex, &self.helper, &self.p2pool_api, self.width, self.height, ctx, ui);
}
Tab::Xmrig => {
Xmrig::show(&mut self.state.xmrig, &mut self.pool_vec, &self.regex, self.width, self.height, ctx, ui);

View file

@ -32,24 +32,38 @@ use regex::Regex;
use log::*;
impl P2pool {
pub fn show(&mut self, node_vec: &mut Vec<(String, Node)>, og: &Arc<Mutex<State>>, _online: bool, ping: &Arc<Mutex<Ping>>, regex: &Regexes, helper: &Arc<Mutex<Helper>>, api: &Arc<Mutex<PubP2poolApi>>, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) {
pub fn show(&mut self, node_vec: &mut Vec<(String, Node)>, og: &Arc<Mutex<State>>, ping: &Arc<Mutex<Ping>>, regex: &Regexes, helper: &Arc<Mutex<Helper>>, api: &Arc<Mutex<PubP2poolApi>>, width: f32, height: f32, ctx: &egui::Context, ui: &mut egui::Ui) {
let text_edit = height / 22.0;
//---------------------------------------------------------------------------------------------------- Console
//---------------------------------------------------------------------------------------------------- [Simple] Console
if self.simple {
ui.group(|ui| {
let height = height / 2.5;
let width = width - SPACE;
ui.style_mut().override_text_style = Some(Monospace);
egui::Frame::none().fill(Color32::from_rgb(18, 18, 18)).show(ui, |ui| {
egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| {
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
egui::ScrollArea::vertical().stick_to_bottom(true).max_width(width).max_height(height).auto_shrink([false; 2]).show_viewport(ui, |ui, _| {
let lock = api.lock().unwrap();
ui.add_sized([width, height], TextEdit::multiline(&mut lock.output.as_str()));
// if lock.p2pool.lock().unwrap().state == ProcessState::Alive { ctx.request_repaint(); }
});
});
// ui.separator();
// 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"#));
});
//---------------------------------------------------------------------------------------------------- [Advanced] Console
} else {
ui.group(|ui| {
let height = height / 3.0;
let width = width - SPACE;
ui.style_mut().override_text_style = Some(Monospace);
egui::Frame::none().fill(DARK_GRAY).show(ui, |ui| {
ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into()));
egui::ScrollArea::vertical().stick_to_bottom(true).max_width(width).max_height(height).auto_shrink([false; 2]).show_viewport(ui, |ui, _| {
ui.add_sized([width, height], TextEdit::multiline(&mut api.lock().unwrap().output.as_str()));
});
});
ui.separator();
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 {