From fcdd25b186e6ecdf0b7a19945e6f31f3811e61f8 Mon Sep 17 00:00:00 2001
From: hinto-janaiyo <hinto.janaiyo@protonmail.com>
Date: Fri, 18 Nov 2022 14:25:13 -0500
Subject: [PATCH] p2pool: implement async node ping in GUI

---
 CHANGELOG.md     |  24 ++++++
 src/gupax.rs     |   2 +-
 src/node.rs      | 207 +++++++++++++----------------------------------
 src/p2pool.rs    |  13 ++-
 src/update.rs    |   2 +-
 utils/prepare.sh |  38 +++++++++
 6 files changed, 133 insertions(+), 153 deletions(-)
 create mode 100644 CHANGELOG.md
 create mode 100755 utils/prepare.sh

diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..bae8bf7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,24 @@
+## v0.1.0
+## Prototype Release
+* Added package updater (by default, via Tor using [`Arti`](https://blog.torproject.org/arti_100_released/))
+* Added [custom icons per OS](https://github.com/hinto-janaiyo/gupax/tree/main/images/icons) (File Explorer, Taskbar, Finder, App header, etc)
+* Added Monero node [`JSON-RPC ping`](https://github.com/hinto-janaiyo/gupax/blob/main/src/node.rs) system, not yet in GUI
+* Added `F11` fullscreen toggle
+* Implemented `Ask before quit`
+* Implemented `Auto-save`
+* Binaries for all platforms (Windows, macOS, Linux)
+* Added state file to save settings:
+    - Windows: `C:\Users\USER\AppData\Roaming\Gupax\gupax.toml`
+    - macOS: `/Users/USER/Library/Application Support/Gupax/gupax.toml`
+    - Linux: `/home/USER/.local/share/gupax/gupax.toml`
+
+
+---
+
+
+## v0.0.1
+## Prototype Release
+* Functional (slightly buggy) GUI external
+* Elements have state (buttons, sliders, etc)
+* No internals, no connections, no processes
+* Only binaries for x86_64 Windows/Linux for now
diff --git a/src/gupax.rs b/src/gupax.rs
index 266c86f..afdd2ad 100644
--- a/src/gupax.rs
+++ b/src/gupax.rs
@@ -187,7 +187,7 @@ impl Gupax {
 			}
 			ui.spacing_mut().text_edit_width = ui.available_width() - SPACE;
 			ui.set_enabled(!file_window.lock().unwrap().thread);
-			if ui.button("Open	").on_hover_text(GUPAX_SELECT).clicked() {
+			if ui.button("Open").on_hover_text(GUPAX_SELECT).clicked() {
 				file_window.lock().unwrap().thread = true;
 				let file_window = Arc::clone(file_window);
 				thread::spawn(move|| {
diff --git a/src/node.rs b/src/node.rs
index 18ee5ab..ffdf942 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -69,7 +69,7 @@ impl std::fmt::Display for NodeEnum {
 }
 
 //---------------------------------------------------------------------------------------------------- Node data
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct NodeData {
 	pub id: NodeEnum,
 	pub ip: &'static str,
@@ -119,6 +119,7 @@ impl Ping {
 }
 
 //---------------------------------------------------------------------------------------------------- IP <-> Enum functions
+use crate::NodeEnum::*;
 // Function for returning IP/Enum
 pub fn ip_to_enum(ip: &'static str) -> NodeEnum {
 	match ip {
@@ -193,11 +194,29 @@ pub fn format_enum(id: NodeEnum) -> String {
 }
 
 //---------------------------------------------------------------------------------------------------- Main Ping function
+// This is for pinging the community nodes to
+// find the fastest/slowest one for the user.
+// The process:
+//   - Send [get_info] JSON-RPC request over HTTP to all IPs
+//   - Measure each request in milliseconds
+//   - Timeout on requests over 5 seconds
+//   - Add data to appropriate struct
+//   - Sorting fastest to lowest is automatic (fastest nodes return ... the fastest)
+//
+// This used to be done 3x linearly but after testing, sending a single
+// JSON-RPC call to all IPs asynchronously resulted in the same data.
+//
+// <300ms  = GREEN
+// <1000ms = YELLOW
+// >1000ms = RED
+// timeout = BLACK
+// default = GRAY
 #[tokio::main]
-pub async fn start(ping: Arc<Mutex<Ping>>, og: Arc<Mutex<State>>) -> Result<Vec<NodeData>, anyhow::Error> {
+pub async fn ping(ping: Arc<Mutex<Ping>>, og: Arc<Mutex<State>>) -> Result<(), anyhow::Error> {
 	// Start ping
 	ping.lock().unwrap().pinging = true;
 	ping.lock().unwrap().prog = 0.0;
+	let percent = (100.0 / ((NODE_IPS.len()) as f32)).floor();
 
 	// Create HTTP client
 	let info = format!("{}", "Creating HTTP Client");
@@ -208,175 +227,65 @@ pub async fn start(ping: Arc<Mutex<Ping>>, og: Arc<Mutex<State>>) -> Result<Vec<
 	// Random User Agent
 	let rand_user_agent = crate::Pkg::get_user_agent();
 	// Handle vector
-//	let mut handles: Vec<tokio::task::JoinHandle<Result<NodeData, anyhow::Error>>> = vec![];
 	let mut handles = vec![];
+	let node_vec = Arc::new(Mutex::new(Vec::new()));
 
 	for ip in NODE_IPS {
 		let client = client.clone();
+		let ping = Arc::clone(&ping);
+		let node_vec = Arc::clone(&node_vec);
 		let request = hyper::Request::builder()
 			.method("POST")
 			.uri("http://".to_string() + ip + "/json_rpc")
 			.header("User-Agent", rand_user_agent)
 			.body(hyper::Body::from(r#"{"jsonrpc":"2.0","id":"0","method":"get_info"}"#))
 			.unwrap();
-		let handle = tokio::spawn(async move { response(client, request, ip).await });
-//		let handle: tokio::task::JoinHandle<Result<NodeData, anyhow::Error>> = tokio::spawn(async move { response(client, request, ip).await });
+		let handle = tokio::spawn(async move { response(client, request, ip, ping, percent, node_vec).await });
 		handles.push(handle);
 	}
 
-	let mut node_vec = vec![];
 	for handle in handles {
-		match handle.await {
-			Ok(data) => match data { Ok(data) => node_vec.push(data), _ => return Err(anyhow::Error::msg("fail")) },
-			_ => return Err(anyhow::Error::msg("fail")),
-		};
+		handle.await;
 	}
+	let node_vec = node_vec.lock().unwrap().clone();
 
-	Ok(node_vec)
+	let info = format!("Fastest node: {}ms ... {} @ {}", node_vec[0].ms, node_vec[0].id, node_vec[0].ip);
+	info!("Ping | {}", info);
+	let mut ping = ping.lock().unwrap();
+		ping.fastest = node_vec[0].id;
+		ping.nodes = node_vec;
+		ping.prog = 100.0;
+		ping.msg = info;
+		ping.pinging = false;
+		ping.pinged = true;
+		ping.auto_selected = false;
+		drop(ping);
+	Ok(())
 }
 
-async fn response(client: hyper::client::Client<hyper::client::HttpConnector>, request: hyper::Request<hyper::Body>, ip: &'static str) -> Result<NodeData, anyhow::Error> {
+async fn response(client: hyper::client::Client<hyper::client::HttpConnector>, request: hyper::Request<hyper::Body>, ip: &'static str, ping: Arc<Mutex<Ping>>, percent: f32, node_vec: Arc<Mutex<Vec<NodeData>>>) {
+	let id = ip_to_enum(ip);
 	let now = Instant::now();
-	let response = tokio::time::timeout(Duration::from_secs(5), client.request(request)).await?;
-	let ms = now.elapsed().as_millis();
-	let mut color = Color32::BLACK;
+	let ms;
+	match tokio::time::timeout(Duration::from_secs(5), client.request(request)).await {
+		Ok(_) => ms = now.elapsed().as_millis(),
+		Err(e) => { warn!("Ping | {}: {} ... FAIL ... {}", id, ip, e); ms = 5000; },
+	};
+	let color;
 	if ms < 300 {
-		// GREEN
-		color = Color32::from_rgb(100, 230, 100);
+		color = Color32::from_rgb(100, 230, 100); // GREEN
 	} else if ms < 1000 {
-		// YELLOW
-		color = Color32::from_rgb(230, 230, 100);
+		color = Color32::from_rgb(230, 230, 100); // YELLOW
 	} else if ms < 5000 {
-		// RED
-		color = Color32::from_rgb(230, 50, 50);
+		color = Color32::from_rgb(230, 50, 50); // RED
+	} else {
+		color = Color32::BLACK;
 	}
-	Ok(NodeData {
-		id: ip_to_enum(ip),
-		ip,
-		ms,
-		color,
-	})
-}
-
-
-// This is for pinging the community nodes to
-// find the fastest/slowest one for the user.
-// The process:
-//   - Send 3 [get_info] JSON-RPC requests over HTTP
-//   - Measure each request in milliseconds as [u128]
-//   - Timeout on requests over 5 seconds
-//   - Calculate average time
-//   - Add data to appropriate struct
-//   - Sort fastest to lowest
-//   - Return [PingResult] (data and fastest node)
-//
-// This is done linearly since per IP since
-// multi-threading might affect performance.
-//
-// <300ms  = GREEN
-// <1000ms = YELLOW
-// >1000ms = RED
-// timeout = BLACK
-// default = GRAY
-use crate::NodeEnum::*;
-pub fn ping(ping: Arc<Mutex<Ping>>, og: Arc<Mutex<State>>) {
-	// Start ping
-	ping.lock().unwrap().pinging = true;
-	ping.lock().unwrap().prog = 0.0;
-	let info = format!("{}", "Creating HTTPS Client");
+	let info = format!("{}ms ... {}: {}", ms, id, ip);
 	info!("Ping | {}", info);
-	ping.lock().unwrap().msg = info;
-	let percent = (100 / (NODE_IPS.len() - 1)) as f32 / 3.0;
-
-	// Create Node vector
-	let mut nodes = Vec::new();
-
-	// Create JSON request
-	let mut get_info = HashMap::new();
-	get_info.insert("jsonrpc", "2.0");
-	get_info.insert("id", "0");
-	get_info.insert("method", "get_info");
-
-	// Misc Settings
-	let mut vec: Vec<(NodeEnum, u128)> = Vec::new();
-
-	// Create HTTP Client
-	let timeout_sec = Duration::from_millis(5000);
-	let client = ClientBuilder::new();
-	let client = ClientBuilder::timeout(client, timeout_sec);
-	let client = ClientBuilder::build(client).unwrap();
-
-	for ip in NODE_IPS.iter() {
-		// Match IP
-		let id = ip_to_enum(ip);
-		// Misc
-		let mut timeout = 0;
-		let mut mid = Duration::new(0, 0);
-
-		// Start JSON-RPC request
-		for i in 1..=3 {
-			ping.lock().unwrap().msg = format!("{}: {} [{}/3]", id, ip, i);
-			let now = Instant::now();
-			let http = "http://".to_string() + &**ip + "/json_rpc";
-			match client.post(http).json(&get_info).send() {
-				Ok(_) => mid += now.elapsed(),
-				Err(_) => {
-					mid += timeout_sec;
-					timeout += 1;
-					let error = format!("Timeout [{}/3] ... {:#?} ... {}", timeout, id, ip);
-					error!("Ping | {}", error);
-					ping.lock().unwrap().msg = error;
-				},
-			};
-			ping.lock().unwrap().prog += percent;
-		}
-
-		// Calculate average
-		let ms = mid.as_millis() / 3;
-		vec.push((id, ms));
-		let info = format!("{}ms ... {}: {}", ms, id, ip);
-		info!("Ping | {}", info);
-		ping.lock().unwrap().msg = format!("{}", info);
-		let color: Color32;
-		if timeout == 3 {
-			color = Color32::BLACK;
-		} else if ms >= 1000 {
-			// RED
-			color = Color32::from_rgb(230, 50, 50);
-		} else if ms >= 300 {
-			// YELLOW
-			color = Color32::from_rgb(230, 230, 100);
-		} else {
-			// GREEN
-			color = Color32::from_rgb(100, 230, 100);
-		}
-		nodes.push(NodeData { id, ip, ms, color })
-	}
-
-	let percent = (100.0 - ping.lock().unwrap().prog) / 2.0;
-	ping.lock().unwrap().prog += percent;
-	ping.lock().unwrap().msg = "Calculating fastest node".to_string();
-	// Calculate fastest out of all nodes
-	let mut fastest: NodeEnum = vec[0].0;
-	let mut best_ms: u128 = vec[0].1;
-	for (id, ms) in vec.iter() {
-		if ms < &best_ms {
-			fastest = *id;
-			best_ms = *ms;
-		}
-	}
-	let info = format!("Fastest node: {}ms ... {} @ {}", best_ms, fastest, enum_to_ip(fastest));
-	info!("Ping | {}", info);
-	let mut guard = ping.lock().unwrap();
-		guard.nodes = nodes;
-		guard.fastest = fastest;
-		guard.prog = 100.0;
-		guard.msg = info;
-		guard.pinging = false;
-		guard.pinged = true;
-		drop(guard);
-	if og.lock().unwrap().p2pool.auto_select {
-		ping.lock().unwrap().auto_selected = false;
-	}
-	info!("Ping ... OK");
+	let mut ping = ping.lock().unwrap();
+	ping.msg = info;
+	ping.prog += percent;
+	drop(ping);
+	node_vec.lock().unwrap().push(NodeData { id: ip_to_enum(ip), ip, ms, color, });
 }
diff --git a/src/p2pool.rs b/src/p2pool.rs
index 3fdeba2..d31ebbe 100644
--- a/src/p2pool.rs
+++ b/src/p2pool.rs
@@ -141,11 +141,20 @@ impl P2pool {
 		// [Ping Button]
 		ui.set_enabled(!ping.lock().unwrap().pinging);
 		if ui.add_sized([width, height], Button::new("Ping community nodes")).on_hover_text(P2POOL_PING).clicked() {
-			let ping = Arc::clone(&ping);
+			let ping1 = Arc::clone(&ping);
+			let ping2 = Arc::clone(&ping);
 			let og = Arc::clone(og);
 			thread::spawn(move|| {
 				info!("Spawning ping thread...");
-				crate::node::ping(ping, og);
+				match crate::node::ping(ping1, og) {
+					Ok(_) => info!("Ping ... OK"),
+					Err(err) => {
+						error!("Ping ... FAIL ... {}", err);
+						ping2.lock().unwrap().pinged = false;
+						ping2.lock().unwrap().msg = err.to_string();
+					},
+				};
+				ping2.lock().unwrap().pinging = false;
 			});
 		}});
 
diff --git a/src/update.rs b/src/update.rs
index 550bbdd..05da782 100644
--- a/src/update.rs
+++ b/src/update.rs
@@ -676,7 +676,7 @@ impl Pkg {
 	pub fn get_user_agent() -> &'static str {
 		let rand = thread_rng().gen_range(0..50);
 		let user_agent = FAKE_USER_AGENT[rand];
-		info!("Update | Randomly selecting User-Agent ({}/50) ... {}", rand, user_agent);
+		info!("Randomly selected User-Agent ({}/50) ... {}", rand, user_agent);
 		user_agent
 	}
 
diff --git a/utils/prepare.sh b/utils/prepare.sh
new file mode 100755
index 0000000..5bea494
--- /dev/null
+++ b/utils/prepare.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+# prepare new [gupax] version in:
+# 1. README.md
+# 2. CHANGELOG.md
+# 3. Cargo.toml
+
+# $1 = new_version
+set -ex
+sudo -v
+[[ $1 = v* ]]
+[[ $PWD = */gupax ]]
+
+# get old GUPAX_VER
+OLD_VER="v$(grep -m1 "version" Cargo.toml | grep -o "[0-9].[0-9].[0-9]")"
+
+# sed change
+sed -i "s/$OLD_VER/$1/g" README.md
+sed -i "s/$OLD_VER/$1/" Cargo.toml
+
+# changelog
+cat << EOM > CHANGELOG.md.new
+# $1
+## Updates
+*
+
+## Fixes
+*
+
+---
+
+EOM
+cat CHANGELOG.md >> CHANGELOG.md.new
+mv -f CHANGELOG.md.new CHANGELOG.md
+
+# commit
+git add .
+git commit -m "prepare $1"