شائع شدہ سورس
سورس ZIP ڈاؤن لوڈ کریں
CH-J Tools
ایپلیکیشن کی کسی مخصوص ریلیز کی ڈائریکٹریز اور فائلیں دیکھیں۔
CH-J / Server Manager
1704 فائلیں
0.1.2-beta
شامل کیا گیا: · بلڈ کیا گیا:
1
//! Session latency (port of `sessions/latencyMonitor.js`). ICMP from this computer to the2
//! address the SSH connection uses and the SSH transport round trip are independent: a3
//! blocked ping never hides the SSH measurement and never marks SSH as broken.5
use crate::core::ssh::session::{SessionManager, SessionRecord};6
use once_cell::sync::Lazy;7
use regex::Regex;8
use serde_json::{json, Value};9
use std::net::IpAddr;10
use std::process::Stdio;11
use std::sync::Arc;12
use std::time::Duration;14
pub const PING_COUNT: usize = 3;15
const INTERVAL: Duration = Duration::from_secs(10);16
const SSH_SAMPLES: usize = 3;17
static REPLY_LINE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\b(?:ttl|hlim)\s*=\s*\d+").unwrap());18
static REPLY_TIME: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)([=<])\s*(\d+(?:[.,]\d+)?)\s*ms\b").unwrap());20
#[derive(Debug, Clone, PartialEq)]21
pub struct Reply {22
pub ms: f64,23
pub less_than: bool,24
}26
pub fn median(values: &[f64]) -> Option<f64> {27
if values.is_empty() {28
return None;29
}30
let mut sorted = values.to_vec();31
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));32
let middle = sorted.len() / 2;33
Some(if sorted.len() % 2 == 1 { sorted[middle] } else { (sorted[middle - 1] + sorted[middle]) / 2.0 })34
}36
/// Per-reply times from macOS, Linux (iputils) and localized Windows output; "<1ms" ← 1 ms, lessThan.37
pub fn parse_ping_replies(stdout: &str) -> Vec<Reply> {38
let mut replies = Vec::new();39
for line in stdout.lines() {40
if !REPLY_LINE.is_match(line) {41
continue;42
}43
let Some(caps) = REPLY_TIME.captures(line) else { continue };44
let Ok(value) = caps[2].replace(',', ".").parse::<f64>() else { continue };45
if value.is_finite() && value >= 0.0 {46
let less = &caps[1] == "<";47
replies.push(Reply { ms: if less { value.max(1.0) } else { value }, less_than: less });48
}49
}50
replies51
}53
pub fn ping_command(address: &IpAddr) -> (String, Vec<String>) {54
let ipv6 = address.is_ipv6();55
let target = address.to_string();56
if cfg!(windows) {57
("ping.exe".into(), vec![if ipv6 { "-6" } else { "-4" }.into(), "-n".into(), PING_COUNT.to_string(), "-w".into(), "2000".into(), target])58
} else if cfg!(target_os = "macos") {59
let mut args = vec!["-n".to_string(), "-c".into(), PING_COUNT.to_string()];60
if !ipv6 {61
args.extend(["-W".to_string(), "2000".into()]);62
}63
args.push(target);64
((if ipv6 { "/sbin/ping6" } else { "/sbin/ping" }).into(), args)65
} else {66
("ping".into(), vec![if ipv6 { "-6" } else { "-4" }.into(), "-n".into(), "-c".into(), PING_COUNT.to_string(), "-W".into(), "2".into(), target])67
}68
}70
fn classify_failure(text: &str, not_found: bool) -> (&'static str, &'static str) {71
if not_found {72
return ("unsupported", "The ping utility is not available on this computer.");73
}74
let has = |pattern: &str| Regex::new(pattern).map(|r| r.is_match(text)).unwrap_or(false);75
if has(r"(?i)Operation not permitted|Permission denied|Access is denied|not allowed") {76
return ("denied", "The operating system denied sending ICMP (for example the macOS Local Network permission or a security policy).");77
}78
if has(r"(?i)unknown host|cannot resolve|could not find host|Name or service not known") {79
return ("error", "The address could not be resolved for ping.");80
}81
if has(r"(?i)No route to host|Network is unreachable|Destination (host|net) unreachable|unreachable|nedosažiteln|nicht erreichbar") {82
return ("unreachable", "The target is unreachable for ICMP (no route or an unreachable reply).");83
}84
("timeout", "No ICMP echo reply before the timeout; the server or a firewall may block ICMP.")85
}87
pub async fn measure_ping(address: &str) -> Value {88
let Ok(ip) = address.parse::<IpAddr>() else {89
return json!({ "ms": null, "status": "error", "reason": "No IP address is known for this session.", "family": null, "sent": 0, "received": 0, "loss": null });90
};91
let family = if ip.is_ipv4() { 4 } else { 6 };92
let (program, args) = ping_command(&ip);93
let mut command = tokio::process::Command::new(&program);94
command.args(&args).env("LC_ALL", "C").env("LANG", "C").stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);95
#[cfg(windows)]96
{97
command.creation_flags(0x0800_0000); // CREATE_NO_WINDOW98
}99
let output = match tokio::time::timeout(Duration::from_secs(9), command.output()).await {100
Ok(Ok(output)) => output,101
Ok(Err(error)) => {102
let (status, reason) = classify_failure(&error.to_string(), error.kind() == std::io::ErrorKind::NotFound);103
return json!({ "ms": null, "status": status, "reason": reason, "family": family, "sent": PING_COUNT, "received": 0, "loss": 1 });104
}105
Err(_) => {106
return json!({ "ms": null, "status": "timeout", "reason": classify_failure("", false).1, "family": family, "sent": PING_COUNT, "received": 0, "loss": null });107
}108
};109
let stdout = String::from_utf8_lossy(&output.stdout).to_string();110
let replies = parse_ping_replies(&stdout);111
let received = replies.len().min(PING_COUNT);112
if received > 0 {113
let times: Vec<f64> = replies.iter().map(|r| r.ms).collect();114
return json!({115
"ms": median(×),116
"lessThan": replies.iter().all(|r| r.less_than),117
"status": "ok",118
"reason": "",119
"family": family,120
"sent": PING_COUNT,121
"received": received,122
"loss": (PING_COUNT - received) as f64 / PING_COUNT as f64123
});124
}125
let text = format!("{stdout}\n{}", String::from_utf8_lossy(&output.stderr));126
let (status, reason) = classify_failure(&text, false);127
json!({ "ms": null, "status": status, "reason": reason, "family": family, "sent": PING_COUNT, "received": 0, "loss": 1 })128
}130
async fn measure_ssh(manager: &SessionManager, record: &Arc<SessionRecord>) -> (Option<f64>, Option<&'static str>, usize, Option<String>) {131
let mut samples = Vec::new();132
let mut method = None;133
let mut error = None;134
for _ in 0..SSH_SAMPLES {135
match manager.ssh_round_trip(record).await {136
Ok((Some(ms), used)) if ms > 0.0 => {137
samples.push(ms);138
method = Some(used);139
}140
Ok((_, used)) => method = Some(used),141
Err(cause) => {142
error = Some(cause.message);143
break;144
}145
}146
}147
let error = if samples.is_empty() { Some(error.unwrap_or_else(|| "SSH round trip failed".into())) } else { None };148
(median(&samples), method, samples.len(), error)149
}151
pub(crate) async fn measure_route_ping(address: &str, jump: bool) -> Value {152
if jump {153
json!({ "ms": null, "status": "jump", "reason": "Target reached through SSH forwarding.", "family": null })154
} else { measure_ping(address).await }155
}157
pub fn start(manager: Arc<SessionManager>, record: Arc<SessionRecord>) {158
let task_record = record.clone();159
let task = tokio::spawn(async move {160
let record = task_record;161
loop {162
if !record.is_connected() {163
break;164
}165
let address = record.address.lock().clone();166
let ping = measure_route_ping(&address, record.has_jump());167
let (ping, ssh) = tokio::join!(ping, measure_ssh(&manager, &record));168
if !record.is_connected() {169
break;170
}171
let valid = |v: Option<f64>| v.filter(|v| v.is_finite() && *v > 0.0);172
let family = ping.get("family").cloned().filter(|v| !v.is_null()).unwrap_or_else(|| {173
address.parse::<IpAddr>().map(|ip| json!(if ip.is_ipv4() { 4 } else { 6 })).unwrap_or(Value::Null)174
});175
let latency = json!({176
"pingMs": valid(ping["ms"].as_f64()),177
"pingStatus": ping["status"].as_str().unwrap_or("error"),178
"pingReason": ping["reason"].as_str().unwrap_or(""),179
"pingLoss": ping["loss"].as_f64(),180
"pingFamily": family,181
"pingLessThan": ping["lessThan"].as_bool() == Some(true),182
"sshRttMs": valid(ssh.0),183
"sshRttMethod": ssh.1,184
"sshRttSamples": ssh.2,185
"sshRttError": ssh.3,186
"measuredAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)187
});188
*record.latency.lock() = Some(latency);189
manager.publish_state(&record);190
tokio::time::sleep(INTERVAL).await;191
}192
});193
*record.stop_latency.lock() = Some(task);194
}196
#[cfg(test)]197
mod tests {198
use super::*;200
#[test]201
fn parses_platform_outputs() {202
let mac = "64 bytes from 1.1.1.1: icmp_seq=0 ttl=57 time=12.345 ms\n64 bytes from 1.1.1.1: icmp_seq=1 ttl=57 time=10.1 ms\nround-trip min/avg/max = 1/2/3 ms";203
assert_eq!(parse_ping_replies(mac).len(), 2);204
let windows_cs = "Odpověď od 10.0.0.1: bajty=32 čas<1ms TTL=128\nOdpověď od 10.0.0.1: bajty=32 čas=2ms TTL=128\nMinimum = 0ms";205
let replies = parse_ping_replies(windows_cs);206
assert_eq!(replies, vec![Reply { ms: 1.0, less_than: true }, Reply { ms: 2.0, less_than: false }]);207
assert_eq!(median(&[3.0, 1.0, 2.0]), Some(2.0));208
assert_eq!(median(&[1.0, 2.0]), Some(1.5));209
}211
#[test]212
fn argv_has_no_shell() {213
let (program, args) = ping_command(&"::1".parse().unwrap());214
assert!(program.contains("ping"));215
assert!(args.iter().all(|a| !a.contains(';')));216
}217
}SHA-256
c29e4d5add26ebd238c39fe5822a9f0d618ea814c7d4e26515e3d5b0a3e675a9CH-J Proprietary Software License 1.14 · سورس CH-J Proprietary Software License 1.14 کے تحت فراہم کیا جاتا ہے۔ اس کی دستیابی لائسنس کی شرائط نہیں بدلتی اور نہ ہی اضافی حقوق دیتی ہے۔
آرکائیو کا SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f