mirror of
https://github.com/Cyrix126/gupaxx.git
synced 2025-01-09 07:29:25 +00:00
fix: green status p2pool when node offline (#59)
* fix: green status p2pool when node offline P2Pool state will change dynamically based on: - Node connection, - p2p network connection, - synchronization fix https://github.com/Cyrix126/gupaxx/issues/57
This commit is contained in:
parent
5c2abd3bac
commit
ee2f27c5da
4 changed files with 142 additions and 10 deletions
|
@ -29,9 +29,11 @@ use crate::helper::signal_end;
|
||||||
use crate::helper::sleep_end_loop;
|
use crate::helper::sleep_end_loop;
|
||||||
use crate::regex::P2POOL_REGEX;
|
use crate::regex::P2POOL_REGEX;
|
||||||
use crate::regex::contains_end_status;
|
use crate::regex::contains_end_status;
|
||||||
|
use crate::regex::contains_newchain_tip;
|
||||||
use crate::regex::contains_statuscommand;
|
use crate::regex::contains_statuscommand;
|
||||||
use crate::regex::contains_yourhashrate;
|
use crate::regex::contains_yourhashrate;
|
||||||
use crate::regex::contains_yourshare;
|
use crate::regex::contains_yourshare;
|
||||||
|
use crate::regex::contains_zmq_failure;
|
||||||
use crate::regex::estimated_hr;
|
use crate::regex::estimated_hr;
|
||||||
use crate::regex::nb_current_shares;
|
use crate::regex::nb_current_shares;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -222,13 +224,13 @@ impl Helper {
|
||||||
) {
|
) {
|
||||||
helper.lock().unwrap().p2pool.lock().unwrap().state = ProcessState::Middle;
|
helper.lock().unwrap().p2pool.lock().unwrap().state = ProcessState::Middle;
|
||||||
|
|
||||||
let (args, api_path_local, api_path_network, api_path_pool) =
|
let (args, api_path_local, api_path_network, api_path_pool, api_path_p2p) =
|
||||||
Self::build_p2pool_args_and_mutate_img(helper, state, path, backup_hosts);
|
Self::build_p2pool_args_and_mutate_img(helper, state, path, backup_hosts);
|
||||||
|
|
||||||
// Print arguments & user settings to console
|
// Print arguments & user settings to console
|
||||||
crate::disk::print_dash(&format!(
|
crate::disk::print_dash(&format!(
|
||||||
"P2Pool | Launch arguments: {:#?} | Local API Path: {:#?} | Network API Path: {:#?} | Pool API Path: {:#?}",
|
"P2Pool | Launch arguments: {:#?} | Local API Path: {:#?} | Network API Path: {:#?} | Pool API Path: {:#?} | P2P API Path {:#?}",
|
||||||
args, api_path_local, api_path_network, api_path_pool,
|
args, api_path_local, api_path_network, api_path_pool, api_path_p2p
|
||||||
));
|
));
|
||||||
|
|
||||||
// Spawn watchdog thread
|
// Spawn watchdog thread
|
||||||
|
@ -247,6 +249,7 @@ impl Helper {
|
||||||
api_path_local,
|
api_path_local,
|
||||||
api_path_network,
|
api_path_network,
|
||||||
api_path_pool,
|
api_path_pool,
|
||||||
|
api_path_p2p,
|
||||||
gupax_p2pool_api,
|
gupax_p2pool_api,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -272,7 +275,7 @@ impl Helper {
|
||||||
state: &P2pool,
|
state: &P2pool,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
backup_hosts: Option<Vec<PoolNode>>,
|
backup_hosts: Option<Vec<PoolNode>>,
|
||||||
) -> (Vec<String>, PathBuf, PathBuf, PathBuf) {
|
) -> (Vec<String>, PathBuf, PathBuf, PathBuf, PathBuf) {
|
||||||
let mut args = Vec::with_capacity(500);
|
let mut args = Vec::with_capacity(500);
|
||||||
let path = path.to_path_buf();
|
let path = path.to_path_buf();
|
||||||
let mut api_path = path;
|
let mut api_path = path;
|
||||||
|
@ -447,10 +450,18 @@ impl Helper {
|
||||||
let mut api_path_local = api_path.clone();
|
let mut api_path_local = api_path.clone();
|
||||||
let mut api_path_network = api_path.clone();
|
let mut api_path_network = api_path.clone();
|
||||||
let mut api_path_pool = api_path.clone();
|
let mut api_path_pool = api_path.clone();
|
||||||
|
let mut api_path_p2p = api_path.clone();
|
||||||
api_path_local.push(P2POOL_API_PATH_LOCAL);
|
api_path_local.push(P2POOL_API_PATH_LOCAL);
|
||||||
api_path_network.push(P2POOL_API_PATH_NETWORK);
|
api_path_network.push(P2POOL_API_PATH_NETWORK);
|
||||||
api_path_pool.push(P2POOL_API_PATH_POOL);
|
api_path_pool.push(P2POOL_API_PATH_POOL);
|
||||||
(args, api_path_local, api_path_network, api_path_pool)
|
api_path_p2p.push(P2POOL_API_PATH_P2P);
|
||||||
|
(
|
||||||
|
args,
|
||||||
|
api_path_local,
|
||||||
|
api_path_network,
|
||||||
|
api_path_pool,
|
||||||
|
api_path_p2p,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cold]
|
#[cold]
|
||||||
|
@ -469,6 +480,7 @@ impl Helper {
|
||||||
api_path_local: std::path::PathBuf,
|
api_path_local: std::path::PathBuf,
|
||||||
api_path_network: std::path::PathBuf,
|
api_path_network: std::path::PathBuf,
|
||||||
api_path_pool: std::path::PathBuf,
|
api_path_pool: std::path::PathBuf,
|
||||||
|
api_path_p2p: std::path::PathBuf,
|
||||||
gupax_p2pool_api: Arc<Mutex<GupaxP2poolApi>>,
|
gupax_p2pool_api: Arc<Mutex<GupaxP2poolApi>>,
|
||||||
) {
|
) {
|
||||||
// 1a. Create PTY
|
// 1a. Create PTY
|
||||||
|
@ -542,6 +554,26 @@ impl Helper {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
debug!("P2Pool | Cleaning old [p2p] API files...");
|
||||||
|
// Attempt to remove stale API file
|
||||||
|
match std::fs::remove_file(&api_path_p2p) {
|
||||||
|
Ok(_) => info!("P2Pool | Attempting to remove stale API file ... OK"),
|
||||||
|
Err(e) => warn!(
|
||||||
|
"P2Pool | Attempting to remove stale API file ... FAIL ... {}",
|
||||||
|
e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
// Attempt to create a default empty one.
|
||||||
|
if std::fs::File::create(&api_path_p2p).is_ok() {
|
||||||
|
let text = r#"{"connections":0,"incoming_connections":0,"peer_list_size":0,"peers":[],"uptime":0}"#;
|
||||||
|
match std::fs::write(&api_path_p2p, text) {
|
||||||
|
Ok(_) => info!("P2Pool | Creating default empty API file ... OK"),
|
||||||
|
Err(e) => warn!(
|
||||||
|
"P2Pool | Creating default empty API file ... FAIL ... {}",
|
||||||
|
e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
let start = process.lock().unwrap().start;
|
let start = process.lock().unwrap().start;
|
||||||
|
|
||||||
// Reset stats before loop
|
// Reset stats before loop
|
||||||
|
@ -608,6 +640,20 @@ impl Helper {
|
||||||
PubP2poolApi::update_from_local(&mut pub_api_lock, local_api);
|
PubP2poolApi::update_from_local(&mut pub_api_lock, local_api);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Read [p2p] API
|
||||||
|
// allows to know if p2p is synced and connected to a Node.
|
||||||
|
debug!("P2Pool Watchdog | Attempting [p2p] API file read");
|
||||||
|
if let Ok(string) = Self::path_to_string(&api_path_p2p, ProcessName::P2pool) {
|
||||||
|
// Deserialize
|
||||||
|
if let Ok(p2p_api) = PrivP2PoolP2PApi::from_str(&string) {
|
||||||
|
// Update the structs.
|
||||||
|
PubP2poolApi::update_from_p2p(&mut pub_api_lock, p2p_api);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if state must be changed based on local and p2p API
|
||||||
|
pub_api_lock.update_state(&mut process_lock);
|
||||||
|
|
||||||
// If more than 1 minute has passed, read the other API files.
|
// If more than 1 minute has passed, read the other API files.
|
||||||
let last_p2pool_request_expired =
|
let last_p2pool_request_expired =
|
||||||
last_p2pool_request.elapsed() >= Duration::from_secs(60);
|
last_p2pool_request.elapsed() >= Duration::from_secs(60);
|
||||||
|
@ -746,7 +792,7 @@ pub struct PubP2poolApi {
|
||||||
pub monero_difficulty: HumanNumber, // e.g: [15,000,000]
|
pub monero_difficulty: HumanNumber, // e.g: [15,000,000]
|
||||||
pub monero_hashrate: HumanNumber, // e.g: [1.000 GH/s]
|
pub monero_hashrate: HumanNumber, // e.g: [1.000 GH/s]
|
||||||
pub hash: String, // Current block hash
|
pub hash: String, // Current block hash
|
||||||
pub height: HumanNumber,
|
pub height: u32,
|
||||||
pub reward: AtomicUnit,
|
pub reward: AtomicUnit,
|
||||||
// Pool API
|
// Pool API
|
||||||
pub p2pool_difficulty: HumanNumber,
|
pub p2pool_difficulty: HumanNumber,
|
||||||
|
@ -763,6 +809,11 @@ pub struct PubP2poolApi {
|
||||||
// from status
|
// from status
|
||||||
pub sidechain_shares: u32,
|
pub sidechain_shares: u32,
|
||||||
pub sidechain_ehr: f32,
|
pub sidechain_ehr: f32,
|
||||||
|
// from height
|
||||||
|
pub synchronised: bool,
|
||||||
|
// from local/p2p
|
||||||
|
pub p2p_connected: u32,
|
||||||
|
pub node_connected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PubP2poolApi {
|
impl Default for PubP2poolApi {
|
||||||
|
@ -801,7 +852,7 @@ impl PubP2poolApi {
|
||||||
monero_difficulty: HumanNumber::unknown(),
|
monero_difficulty: HumanNumber::unknown(),
|
||||||
monero_hashrate: HumanNumber::unknown(),
|
monero_hashrate: HumanNumber::unknown(),
|
||||||
hash: String::from("???"),
|
hash: String::from("???"),
|
||||||
height: HumanNumber::unknown(),
|
height: 0,
|
||||||
reward: AtomicUnit::new(),
|
reward: AtomicUnit::new(),
|
||||||
p2pool_difficulty: HumanNumber::unknown(),
|
p2pool_difficulty: HumanNumber::unknown(),
|
||||||
p2pool_hashrate: HumanNumber::unknown(),
|
p2pool_hashrate: HumanNumber::unknown(),
|
||||||
|
@ -814,6 +865,9 @@ impl PubP2poolApi {
|
||||||
user_monero_percent: HumanNumber::unknown(),
|
user_monero_percent: HumanNumber::unknown(),
|
||||||
sidechain_shares: 0,
|
sidechain_shares: 0,
|
||||||
sidechain_ehr: 0.0,
|
sidechain_ehr: 0.0,
|
||||||
|
p2p_connected: 0,
|
||||||
|
synchronised: false,
|
||||||
|
node_connected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -876,7 +930,7 @@ impl PubP2poolApi {
|
||||||
// 2. Parse the full STDOUT
|
// 2. Parse the full STDOUT
|
||||||
let mut output_parse = output_parse.lock().unwrap();
|
let mut output_parse = output_parse.lock().unwrap();
|
||||||
let (payouts_new, xmr_new) = Self::calc_payouts_and_xmr(&output_parse);
|
let (payouts_new, xmr_new) = Self::calc_payouts_and_xmr(&output_parse);
|
||||||
// Check for "SYNCHRONIZED" only if we aren't already.
|
// Check for "SYNCHRONIZED" only if we aren't already. Works at level 0 and above.
|
||||||
if process.state == ProcessState::Syncing {
|
if process.state == ProcessState::Syncing {
|
||||||
// How many times the word was captured.
|
// How many times the word was captured.
|
||||||
let synchronized_captures = P2POOL_REGEX.synchronized.find_iter(&output_parse).count();
|
let synchronized_captures = P2POOL_REGEX.synchronized.find_iter(&output_parse).count();
|
||||||
|
@ -897,6 +951,15 @@ impl PubP2poolApi {
|
||||||
// just finding 1 instance of "SYNCHRONIZED".
|
// just finding 1 instance of "SYNCHRONIZED".
|
||||||
process.state = ProcessState::Alive;
|
process.state = ProcessState::Alive;
|
||||||
}
|
}
|
||||||
|
// if the p2pool node was synced but is not anymore due to faulty monero node and is synced again, the status must be alive again
|
||||||
|
// required log level 2 minimum
|
||||||
|
if contains_newchain_tip(&output_parse) {
|
||||||
|
process.state = ProcessState::Alive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if the node is offline, p2pool can not function properly. Requires at least p2pool log level 1
|
||||||
|
if process.state == ProcessState::Alive && contains_zmq_failure(&output_parse) {
|
||||||
|
process.state = ProcessState::Syncing;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Throw away [output_parse]
|
// 3. Throw away [output_parse]
|
||||||
|
@ -976,6 +1039,15 @@ impl PubP2poolApi {
|
||||||
..std::mem::take(&mut *public)
|
..std::mem::take(&mut *public)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Mutate [PubP2poolApi] with data from a [PrivP2PoolP2PApi] and the process output.
|
||||||
|
pub(super) fn update_from_p2p(public: &mut Self, p2p: PrivP2PoolP2PApi) {
|
||||||
|
*public = Self {
|
||||||
|
p2p_connected: p2p.connections,
|
||||||
|
// 30 seconds before concluding the monero node connection is lost
|
||||||
|
node_connected: p2p.zmq_last_active < 30,
|
||||||
|
..std::mem::take(&mut *public)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Mutate [PubP2poolApi] with data from a [PrivP2pool(Network|Pool)Api].
|
// Mutate [PubP2poolApi] with data from a [PrivP2pool(Network|Pool)Api].
|
||||||
pub(super) fn update_from_network_pool(
|
pub(super) fn update_from_network_pool(
|
||||||
|
@ -1033,7 +1105,7 @@ impl PubP2poolApi {
|
||||||
monero_difficulty: HumanNumber::from_u64(monero_difficulty),
|
monero_difficulty: HumanNumber::from_u64(monero_difficulty),
|
||||||
monero_hashrate: HumanNumber::from_u64_to_gigahash_3_point(monero_hashrate),
|
monero_hashrate: HumanNumber::from_u64_to_gigahash_3_point(monero_hashrate),
|
||||||
hash: net.hash,
|
hash: net.hash,
|
||||||
height: HumanNumber::from_u32(net.height),
|
height: net.height,
|
||||||
reward: AtomicUnit::from_u64(net.reward),
|
reward: AtomicUnit::from_u64(net.reward),
|
||||||
p2pool_difficulty: HumanNumber::from_u64(p2pool_difficulty),
|
p2pool_difficulty: HumanNumber::from_u64(p2pool_difficulty),
|
||||||
p2pool_hashrate: HumanNumber::from_u64_to_megahash_3_point(p2pool_hashrate),
|
p2pool_hashrate: HumanNumber::from_u64_to_megahash_3_point(p2pool_hashrate),
|
||||||
|
@ -1047,6 +1119,14 @@ impl PubP2poolApi {
|
||||||
..std::mem::take(&mut *public)
|
..std::mem::take(&mut *public)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/// Check if all conditions are met to be alive or if something is wrong
|
||||||
|
fn update_state(&self, process: &mut Process) {
|
||||||
|
if self.synchronised && self.node_connected && self.p2p_connected > 1 && self.height > 10 {
|
||||||
|
process.state = ProcessState::Alive;
|
||||||
|
} else {
|
||||||
|
process.state = ProcessState::Syncing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn calculate_share_or_block_time(hashrate: u64, difficulty: u64) -> HumanTime {
|
pub fn calculate_share_or_block_time(hashrate: u64, difficulty: u64) -> HumanTime {
|
||||||
|
@ -1273,3 +1353,35 @@ impl PoolStatistics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//---------------------------------------------------------------------------------------------------- Private P2Pool "Network" API
|
||||||
|
// This matches P2Pool's [local/p2p] JSON API file.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub(super) struct PrivP2PoolP2PApi {
|
||||||
|
pub connections: u32,
|
||||||
|
pub zmq_last_active: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PrivP2PoolP2PApi {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrivP2PoolP2PApi {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
connections: 0,
|
||||||
|
zmq_last_active: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn from_str(string: &str) -> std::result::Result<Self, serde_json::Error> {
|
||||||
|
match serde_json::from_str::<Self>(string) {
|
||||||
|
Ok(a) => Ok(a),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("P2Pool Network API | Could not deserialize API data: {}", e);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ mod test {
|
||||||
p2pool::{PrivP2poolLocalApi, PrivP2poolNetworkApi},
|
p2pool::{PrivP2poolLocalApi, PrivP2poolNetworkApi},
|
||||||
xvb::{priv_stats::RuntimeDonationLevel, priv_stats::RuntimeMode},
|
xvb::{priv_stats::RuntimeDonationLevel, priv_stats::RuntimeMode},
|
||||||
};
|
};
|
||||||
|
use crate::human::HumanNumber;
|
||||||
use crate::miscs::client;
|
use crate::miscs::client;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -308,7 +309,7 @@ Uptime = 0h 2m 4s
|
||||||
assert_eq!(p.monero_difficulty.to_string(), "300,000,000,000");
|
assert_eq!(p.monero_difficulty.to_string(), "300,000,000,000");
|
||||||
assert_eq!(p.monero_hashrate.to_string(), "2.500 GH/s");
|
assert_eq!(p.monero_hashrate.to_string(), "2.500 GH/s");
|
||||||
assert_eq!(p.hash.to_string(), "asdf");
|
assert_eq!(p.hash.to_string(), "asdf");
|
||||||
assert_eq!(p.height.to_string(), "1,234");
|
assert_eq!(HumanNumber::from_u32(p.height).to_string(), "1,234");
|
||||||
assert_eq!(p.reward.to_u64(), 2345);
|
assert_eq!(p.reward.to_u64(), 2345);
|
||||||
assert_eq!(p.p2pool_difficulty.to_string(), "10,000,000");
|
assert_eq!(p.p2pool_difficulty.to_string(), "10,000,000");
|
||||||
assert_eq!(p.p2pool_hashrate.to_string(), "1.000 MH/s");
|
assert_eq!(p.p2pool_hashrate.to_string(), "1.000 MH/s");
|
||||||
|
|
|
@ -81,12 +81,16 @@ pub const P2POOL_API_PATH_LOCAL: &str = r"local\stratum";
|
||||||
pub const P2POOL_API_PATH_NETWORK: &str = r"network\stats";
|
pub const P2POOL_API_PATH_NETWORK: &str = r"network\stats";
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub const P2POOL_API_PATH_POOL: &str = r"pool\stats";
|
pub const P2POOL_API_PATH_POOL: &str = r"pool\stats";
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
pub const P2POOL_API_PATH_P2P: &str = r"local\p2p";
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub const P2POOL_API_PATH_LOCAL: &str = "local/stratum";
|
pub const P2POOL_API_PATH_LOCAL: &str = "local/stratum";
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub const P2POOL_API_PATH_NETWORK: &str = "network/stats";
|
pub const P2POOL_API_PATH_NETWORK: &str = "network/stats";
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub const P2POOL_API_PATH_POOL: &str = "pool/stats";
|
pub const P2POOL_API_PATH_POOL: &str = "pool/stats";
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
pub const P2POOL_API_PATH_P2P: &str = "local/p2p";
|
||||||
pub const XMRIG_API_SUMMARY_URI: &str = "1/summary"; // The default relative URI of XMRig's API summary
|
pub const XMRIG_API_SUMMARY_URI: &str = "1/summary"; // The default relative URI of XMRig's API summary
|
||||||
// pub const XMRIG_API_CONFIG_URI: &str = "1/config"; // The default relative URI of XMRig's API config
|
// pub const XMRIG_API_CONFIG_URI: &str = "1/config"; // The default relative URI of XMRig's API config
|
||||||
// todo allow user to change the port of the http api for xmrig and xmrig-proxy
|
// todo allow user to change the port of the http api for xmrig and xmrig-proxy
|
||||||
|
|
|
@ -277,6 +277,21 @@ pub fn contains_end_status(l: &str) -> bool {
|
||||||
static LINE_SHARE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^Uptime ").unwrap());
|
static LINE_SHARE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^Uptime ").unwrap());
|
||||||
LINE_SHARE.is_match(l)
|
LINE_SHARE.is_match(l)
|
||||||
}
|
}
|
||||||
|
// P2Pool
|
||||||
|
/// if the node is disconnected
|
||||||
|
/// this error will be present if log > 1 and Node is disconnected
|
||||||
|
pub fn contains_zmq_failure(l: &str) -> bool {
|
||||||
|
static LINE_SHARE: Lazy<Regex> = Lazy::new(|| {
|
||||||
|
Regex::new(r"(p2pool with offline node: failed: error Error (empty response)|ZMQReader failed to connect to|P2Pool Couldn't restart ZMQ reader: exception Operation cannot be accomplished in current state)").unwrap()
|
||||||
|
});
|
||||||
|
LINE_SHARE.is_match(l)
|
||||||
|
}
|
||||||
|
/// a way to detect that p2pool is alive
|
||||||
|
pub fn contains_newchain_tip(l: &str) -> bool {
|
||||||
|
static LINE_SHARE: Lazy<Regex> = Lazy::new(|| Regex::new(r"new chain tip").unwrap());
|
||||||
|
LINE_SHARE.is_match(l)
|
||||||
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- TEST
|
//---------------------------------------------------------------------------------------------------- TEST
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
Loading…
Reference in a new issue