CH-J Toolsمدیریت سرور از طریق SSH
منو
منبع منتشرشده

CH-J Tools

پوشه‌ها و فایل‌های یک نسخهٔ مشخص از برنامه را مرور کنید.

دانلود ZIP منبع
CH-J / Server Manager 1704 فایل
0.1.2-beta افزوده‌شده: · ساخته‌شده:
latency.rs افزوده‌شده: تاریخ ثبت نشده است ۹٫۵ KB · 217 خط
کد دانلود فایل
1//! Session latency (port of `sessions/latencyMonitor.js`). ICMP from this computer to the
2//! address the SSH connection uses and the SSH transport round trip are independent: a
3//! blocked ping never hides the SSH measurement and never marks SSH as broken.
5use crate::core::ssh::session::{SessionManager, SessionRecord};
6use once_cell::sync::Lazy;
7use regex::Regex;
8use serde_json::{json, Value};
9use std::net::IpAddr;
10use std::process::Stdio;
11use std::sync::Arc;
12use std::time::Duration;
14pub const PING_COUNT: usize = 3;
15const INTERVAL: Duration = Duration::from_secs(10);
16const SSH_SAMPLES: usize = 3;
17static REPLY_LINE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\b(?:ttl|hlim)\s*=\s*\d+").unwrap());
18static REPLY_TIME: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)([=<])\s*(\d+(?:[.,]\d+)?)\s*ms\b").unwrap());
20#[derive(Debug, Clone, PartialEq)]
21pub struct Reply {
22 pub ms: f64,
23 pub less_than: bool,
26pub 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 })
36/// Per-reply times from macOS, Linux (iputils) and localized Windows output; "<1ms" ← 1 ms, lessThan.
37pub 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 replies
53pub 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 }
70fn 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.")
87pub 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_WINDOW
98 }
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(&times),
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 f64
123 });
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 })
130async 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)
151pub(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 }
157pub 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);
196#[cfg(test)]
197mod 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 }
SHA-256c29e4d5add26ebd238c39fe5822a9f0d618ea814c7d4e26515e3d5b0a3e675a9

CH-J Proprietary Software License 1.14 · کد منبع تحت CH-J Proprietary Software License 1.14 ارائه می‌شود. در دسترس بودن آن شرایط مجوز را تغییر نمی‌دهد و حق اضافه‌ای اعطا نمی‌کند.

SHA-256 بایگانیac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
فایل‌ها