CH-J Toolspamamahala ng server sa pamamagitan ng SSH
Menu
Nailathalang source

CH-J Tools

I-browse ang mga direktoryo at file ng isang partikular na release ng application.

I-download ang source ZIP
CH-J / Server Manager 1704 file
0.1.2-beta Idinagdag: · Binuo:
process.rs Idinagdag: Hindi naitala ang petsa 23.0 KB · 474 linya
Code I-download ang file
1//! Local OS tools for diagnostics (port of `diagnostics/processTools.js`): single-packet
2//! ping probes with a bounded overlap (no root/flood needed) and traceroute/tracert.
3//! Arguments are passed without a shell; output is size-limited.
5use crate::core::diagnostics::common::{now_ms, stats, DiagError, Options};
6use crate::core::ssh::latency::parse_ping_replies;
7use once_cell::sync::Lazy;
8use regex::Regex;
9use serde_json::{json, Value};
10use std::net::IpAddr;
11use std::process::Stdio;
12use std::sync::Arc;
13use std::time::Duration;
14use tokio::io::AsyncReadExt;
15use tokio_util::sync::CancellationToken;
17static CONFIG_ERROR: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)not permitted|permission denied|invalid option|unknown option|invalid argument|out of range|illegal|usage:").unwrap());
18static TRANSIENT_ERROR: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)network is unreachable|no route to host|general failure|transmit failed|no buffer space|resource temporarily unavailable|host is down").unwrap());
19static EXPLICIT_TIMEOUT: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)Request timed out|Request timeout|Zeitüberschreitung|Vypršel.*časov[ýy].*limit").unwrap());
20const MAX_PING_IN_FLIGHT: u64 = 32;
21const MAX_CONSECUTIVE_PING_ERRORS: u64 = 50;
23pub struct ProcessResult {
24 pub code: Option<i32>,
25 pub stdout: Vec<u8>,
26 pub stderr: String,
29pub fn clean_env(command: &mut tokio::process::Command, inherit: bool) {
30 if !inherit {
31 command.env_clear();
32 for name in ["SystemRoot", "SYSTEMROOT", "WINDIR", "TEMP", "TMP", "TMPDIR"] {
33 if let Ok(value) = std::env::var(name) {
34 command.env(name, value);
35 }
36 }
37 }
38 command.env("LC_ALL", "C").env("LANG", "C");
41pub async fn run_process(
42 executable: &str,
43 args: &[String],
44 timeout_ms: u64,
45 cancel: &CancellationToken,
46 max_bytes: usize,
47 on_line: Option<&(dyn Fn(&str) + Send + Sync)>,
48 curated_env: bool,
49) -> Result<ProcessResult, DiagError> {
50 if cancel.is_cancelled() {
51 return Err(DiagError::new("CANCELLED"));
52 }
53 let mut command = tokio::process::Command::new(executable);
54 command.args(args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
55 clean_env(&mut command, !curated_env);
56 #[cfg(windows)]
57 command.creation_flags(0x0800_0000);
58 let mut child = command.spawn().map_err(|e| if e.kind() == std::io::ErrorKind::NotFound { DiagError::with_message("ENOENT", e.to_string()) } else { DiagError::from_io(e) })?;
59 let mut stdout = child.stdout.take().expect("stdout");
60 let mut stderr = child.stderr.take().expect("stderr");
61 let work = async {
62 let mut out = Vec::new();
63 let mut err = Vec::new();
64 let mut pending = String::new();
65 let mut buffer = [0u8; 8192];
66 let mut err_buffer = [0u8; 4096];
67 let mut out_open = true;
68 let mut err_open = true;
69 while out_open || err_open {
70 tokio::select! {
71 n = stdout.read(&mut buffer), if out_open => {
72 let n = n.map_err(DiagError::from_io)?;
73 if n == 0 { out_open = false; continue; }
74 if out.len() + err.len() + n > max_bytes { return Err(DiagError::new("OUTPUT_LIMIT")); }
75 out.extend_from_slice(&buffer[..n]);
76 if let Some(callback) = on_line {
77 pending.push_str(&String::from_utf8_lossy(&buffer[..n]));
78 while let Some(index) = pending.find('\n') {
79 let line: String = pending.drain(..=index).collect();
80 callback(line.trim_end_matches(['\r', '\n']));
81 }
82 }
83 }
84 n = stderr.read(&mut err_buffer), if err_open => {
85 let n = n.map_err(DiagError::from_io)?;
86 if n == 0 { err_open = false; continue; }
87 if out.len() + err.len() + n > max_bytes { return Err(DiagError::new("OUTPUT_LIMIT")); }
88 err.extend_from_slice(&err_buffer[..n]);
89 }
90 }
91 }
92 if let Some(callback) = on_line {
93 if !pending.is_empty() {
94 callback(pending.trim_end_matches('\r'));
95 }
96 }
97 let status = child.wait().await.map_err(DiagError::from_io)?;
98 Ok(ProcessResult { code: status.code(), stdout: out, stderr: String::from_utf8_lossy(&err).to_string() })
99 };
100 tokio::select! {
101 result = tokio::time::timeout(Duration::from_millis(timeout_ms), work) => result.map_err(|_| DiagError::new("TIMEOUT"))?,
102 _ = cancel.cancelled() => Err(DiagError::new("CANCELLED")),
103 }
106pub fn ping_command(address: &str, timeout_ms: u64, options: &Options) -> Result<(String, Vec<String>), DiagError> {
107 let ip: IpAddr = address.parse().map_err(|_| DiagError::new("INVALID_IP"))?;
108 let v6 = ip.is_ipv6();
109 let size = |flag: &str| options.packet_size.map(|s| vec![flag.to_string(), s.to_string()]).unwrap_or_default();
110 let hops = |flag: &str| options.ttl.map(|t| vec![flag.to_string(), t.to_string()]).unwrap_or_default();
111 let df = |flags: &[&str]| if options.dont_fragment && !v6 { flags.iter().map(|f| f.to_string()).collect() } else { Vec::new() };
112 let mut args: Vec<String>;
113 let executable: &str;
114 if cfg!(windows) {
115 executable = "ping.exe";
116 args = vec![if v6 { "-6" } else { "-4" }.into(), "-n".into(), "1".into(), "-w".into(), timeout_ms.to_string()];
117 args.extend(size("-l"));
118 args.extend(hops("-i"));
119 args.extend(df(&["-f"]));
120 } else if cfg!(target_os = "macos") {
121 executable = if v6 { "/sbin/ping6" } else { "/sbin/ping" };
122 args = vec!["-n".into(), "-c".into(), "1".into()];
123 if !v6 {
124 args.extend(["-W".to_string(), timeout_ms.to_string()]);
125 }
126 args.extend(size("-s"));
127 args.extend(hops(if v6 { "-h" } else { "-m" }));
128 args.extend(df(&["-D"]));
129 } else if cfg!(target_os = "linux") {
130 executable = "ping";
131 args = vec![if v6 { "-6" } else { "-4" }.into(), "-n".into(), "-c".into(), "1".into(), "-W".into(), timeout_ms.div_ceil(1000).to_string()];
132 args.extend(size("-s"));
133 args.extend(hops("-t"));
134 args.extend(df(&["-M", "do"]));
135 } else {
136 return Err(DiagError::new("PLATFORM_UNSUPPORTED"));
137 }
138 args.push(address.to_string());
139 Ok((executable.to_string(), args))
142pub fn parse_ping_reply(output: &str) -> Value {
143 let exact = parse_ping_replies(output).into_iter().find(|r| !r.less_than && r.ms > 0.0).map(|r| r.ms);
144 let upper = Regex::new(r"(?i)<\s*(\d+(?:[.,]\d+)?)\s*ms").unwrap().captures(output).and_then(|c| c[1].replace(',', ".").parse::<f64>().ok());
145 let ttl = Regex::new(r"(?i)(?:ttl|hlim|hop limit)[=:\s]+(\d+)").unwrap().captures(output).and_then(|c| c[1].parse::<u64>().ok());
146 json!({ "received": exact.is_some() || upper.is_some(), "rttMs": exact, "upperBoundMs": upper, "ttl": ttl })
149struct PingState {
150 packets: Vec<Value>,
151 rtts: Vec<Option<f64>>,
152 sent: u64,
153 received: u64,
154 error_packets: u64,
155 consecutive_errors: u64,
156 failure: Option<Value>,
159fn summary(state: &PingState) -> Value {
160 json!({
161 "sent": state.sent,
162 "received": state.received,
163 "errorPackets": state.error_packets,
164 "lossPercent": if state.sent > 0 { json!(100.0 * (state.sent - state.received) as f64 / state.sent as f64) } else { Value::Null },
165 "statistics": stats(&state.rtts)
166 })
169fn merge(mut base: Value, extra: Value) -> Value {
170 if let (Some(target), Value::Object(fields)) = (base.as_object_mut(), extra) {
171 target.extend(fields);
172 }
173 base
176pub async fn ping(address: &str, options: &Options, cancel: &CancellationToken, progress: Arc<dyn Fn(Value) + Send + Sync>) -> Result<Value, DiagError> {
177 let interval = options.interval_ms.max(1);
178 let timeout = options.ping_timeout_ms.unwrap_or(options.timeout_ms);
179 let total = if options.continuous { u64::MAX } else { options.count * options.repetitions };
180 let max_in_flight = (timeout + 500).div_ceil(interval).saturating_add(1).clamp(1, MAX_PING_IN_FLIGHT);
181 let state = Arc::new(parking_lot::Mutex::new(PingState { packets: Vec::new(), rtts: Vec::new(), sent: 0, received: 0, error_packets: 0, consecutive_errors: 0, failure: None }));
182 let (executable, _) = ping_command(address, timeout, options)?;
183 let _ = executable;
184 let mut in_flight = tokio::task::JoinSet::new();
185 let mut launched = 0u64;
186 let mut throttled = 0u64;
187 let started = now_ms();
188 let mut next_at = started;
189 let mut last_launch = started;
190 let base_options = options.clone();
191 while launched < total && state.lock().failure.is_none() {
192 if cancel.is_cancelled() {
193 in_flight.abort_all();
194 return Err(DiagError::new("CANCELLED"));
195 }
196 if in_flight.len() as u64 >= max_in_flight {
197 throttled += 1;
198 tokio::select! {
199 _ = in_flight.join_next() => {},
200 _ = cancel.cancelled() => { in_flight.abort_all(); return Err(DiagError::new("CANCELLED")); }
201 }
202 continue;
203 }
204 launched += 1;
205 let sequence = launched;
206 let address = address.to_string();
207 let state = state.clone();
208 let progress = progress.clone();
209 let cancel_probe = cancel.clone();
210 let probe_options = base_options.clone();
211 in_flight.spawn(async move {
212 let (exe, args) = match ping_command(&address, timeout, &probe_options) {
213 Ok(c) => c,
214 Err(e) => {
215 state.lock().failure = Some(json!({ "sequence": sequence, "address": address, "received": false, "rttMs": null, "status": "error", "code": e.code, "raw": e.message }));
216 return;
217 }
218 };
219 let mut packet = match run_process(&exe, &args, timeout + 500, &cancel_probe, 256 * 1024, None, false).await {
220 Ok(result) => {
221 let raw = String::from_utf8_lossy(&result.stdout).to_string();
222 let reply = parse_ping_reply(&raw);
223 let received = reply["received"] == true;
224 let status = if received { "success" } else if EXPLICIT_TIMEOUT.is_match(&raw) { "timeout" } else { "no-reply" };
225 let combined = format!("{raw}{}", result.stderr);
226 let mut packet = merge(json!({ "sequence": sequence, "address": address }), reply);
227 packet["status"] = json!(status);
228 packet["raw"] = json!(combined.chars().take(if received { 300 } else { 4096 }).collect::<String>());
229 let threshold = if cfg!(target_os = "macos") { 2 } else { 1 };
230 if !received && (result.code.map(|c| c > threshold).unwrap_or(false) || CONFIG_ERROR.is_match(&result.stderr) || TRANSIENT_ERROR.is_match(&result.stderr)) {
231 packet["status"] = json!("error");
232 packet["fatal"] = json!(CONFIG_ERROR.is_match(&result.stderr));
233 }
234 packet
235 }
236 Err(error) => {
237 if cancel_probe.is_cancelled() {
238 return;
239 }
240 let status = match error.code.as_str() { "ENOENT" => "unavailable", "TIMEOUT" => "timeout", _ => "error" };
241 json!({ "sequence": sequence, "address": address, "received": false, "rttMs": null, "status": status, "code": error.code, "raw": error.message.chars().take(400).collect::<String>() })
242 }
243 };
244 if cancel_probe.is_cancelled() {
245 return;
246 }
247 let mut guard = state.lock();
248 if guard.failure.is_some() {
249 return;
250 }
251 packet["measuredAt"] = json!(chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true));
252 let is_error = packet["status"] == "error";
253 let fatal = packet["status"] == "unavailable" || (is_error && (packet["fatal"] == true || guard.sent == 0 || guard.consecutive_errors + 1 >= MAX_CONSECUTIVE_PING_ERRORS));
254 if let Some(object) = packet.as_object_mut() {
255 object.remove("fatal");
256 }
257 if fatal {
258 if is_error && guard.sent > 0 && packet.get("code").is_none() {
259 packet["code"] = json!("PING_REPEATED_ERRORS");
260 }
261 guard.packets.push(packet.clone());
262 guard.failure = Some(packet);
263 return;
264 }
265 guard.consecutive_errors = if is_error { guard.consecutive_errors + 1 } else { 0 };
266 if is_error {
267 guard.error_packets += 1;
268 }
269 guard.sent += 1;
270 if packet["received"] == true {
271 guard.received += 1;
272 let index = (sequence - 1) as usize;
273 if guard.rtts.len() <= index {
274 guard.rtts.resize(index + 1, None);
275 }
276 guard.rtts[index] = packet["rttMs"].as_f64();
277 }
278 guard.packets.push(packet.clone());
279 if guard.packets.len() > 500 {
280 guard.packets.remove(0);
281 }
282 let event = merge(merge(json!({ "kind": "ping-packet" }), packet), summary(&guard));
283 drop(guard);
284 progress(event);
285 });
286 last_launch = now_ms();
287 next_at = (next_at + interval as f64).max(last_launch - interval as f64 / 2.0);
288 let wait = next_at - now_ms();
289 if launched < total && wait > 0.0 {
290 tokio::select! {
291 _ = tokio::time::sleep(Duration::from_secs_f64(wait / 1000.0)) => {},
292 _ = cancel.cancelled() => { in_flight.abort_all(); return Err(DiagError::new("CANCELLED")); }
293 }
294 }
295 }
296 while in_flight.join_next().await.is_some() {}
297 if cancel.is_cancelled() {
298 return Err(DiagError::new("CANCELLED"));
299 }
300 let mut guard = state.lock();
301 guard.packets.sort_by_key(|p| p["sequence"].as_u64().unwrap_or(0));
302 let timing = json!({
303 "intervalMs": interval,
304 "effectiveIntervalMs": if launched > 1 { json!((last_launch - started) / (launched - 1) as f64) } else { Value::Null },
305 "timeoutMs": timeout,
306 "maxInFlight": max_in_flight,
307 "throttled": throttled,
308 "packetSize": options.packet_size,
309 "ttl": options.ttl,
310 "dontFragment": options.dont_fragment
311 });
312 let sum = summary(&guard);
313 if let Some(failure) = guard.failure.clone() {
314 progress(merge(merge(json!({ "kind": "ping-packet" }), failure.clone()), sum.clone()));
315 return Ok(merge(
316 json!({ "status": failure["status"], "code": failure.get("code").cloned().unwrap_or(json!("PING_TOOL_ERROR")), "reason": failure["raw"], "address": address, "packets": guard.packets }),
317 merge(sum, json!({ "timing": timing, "note": "ICMP failure does not establish server unavailability or prove a firewall." })),
318 ));
319 }
320 Ok(merge(
321 json!({ "status": if guard.received > 0 { "success" } else { "warning" }, "address": address, "packets": std::mem::take(&mut guard.packets) }),
322 merge(sum, json!({ "timing": timing, "note": "No ICMP reply may mean filtering, ICMP rate limiting or loss; it does not establish server unavailability." })),
323 ))
326pub fn traceroute_command(address: &str, options: &Options) -> Result<(String, Vec<String>), DiagError> {
327 let ip: IpAddr = address.parse().map_err(|_| DiagError::new("INVALID_IP"))?;
328 let v6 = ip.is_ipv6();
329 let hops = options.max_hops.to_string();
330 if cfg!(windows) {
331 Ok(("tracert.exe".into(), vec![if v6 { "-6" } else { "-4" }.into(), "-d".into(), "-h".into(), hops, "-w".into(), options.timeout_ms.to_string(), address.into()]))
332 } else if cfg!(target_os = "macos") {
333 Ok(((if v6 { "/usr/sbin/traceroute6" } else { "/usr/sbin/traceroute" }).into(), vec!["-n".into(), "-m".into(), hops, "-w".into(), options.timeout_ms.div_ceil(1000).to_string(), address.into()]))
334 } else if cfg!(target_os = "linux") {
335 let wait = options.timeout_ms as f64 / 1000.0;
336 Ok(("traceroute".into(), vec![if v6 { "-6" } else { "-4" }.into(), "-n".into(), "-m".into(), hops, "-w".into(), if wait.fract() == 0.0 { format!("{}", wait as u64) } else { wait.to_string() }, address.into()]))
337 } else {
338 Err(DiagError::new("PLATFORM_UNSUPPORTED"))
339 }
342pub fn parse_hop(line: &str) -> Option<Value> {
343 let caps = Regex::new(r"^\s*(\d{1,2})\s+(.+)").unwrap().captures(line)?;
344 let rest = caps[2].to_string();
345 let mut addresses: Vec<String> = Vec::new();
346 for token in rest.split_whitespace() {
347 let cleaned: String = token.chars().filter(|c| !"()[],".contains(*c)).collect();
348 if cleaned.parse::<IpAddr>().is_ok() && !addresses.contains(&cleaned) {
349 addresses.push(cleaned);
350 }
351 }
352 let probe_re = Regex::new(r"(?i)(<\s*)?(\d+(?:[.,]\d+)?)\s*ms|\*").unwrap();
353 let probes: Vec<Value> = probe_re
354 .captures_iter(&rest)
355 .map(|m| {
356 if &m[0] == "*" {
357 json!({ "status": "timeout", "rttMs": null })
358 } else {
359 let value = m[2].replace(',', ".").parse::<f64>().unwrap_or(0.0);
360 if m.get(1).is_some() { json!({ "status": "success", "rttMs": null, "upperBoundMs": value }) } else { json!({ "status": "success", "rttMs": value, "upperBoundMs": null }) }
361 }
362 })
363 .collect();
364 if probes.is_empty() && addresses.is_empty() {
365 return None;
366 }
367 let rtts: Vec<Option<f64>> = probes.iter().map(|p| p["rttMs"].as_f64()).collect();
368 let timeouts = probes.iter().filter(|p| p["status"] == "timeout").count();
369 Some(json!({
370 "hop": caps[1].parse::<u64>().unwrap_or(0),
371 "addresses": addresses,
372 "probes": probes,
373 "averageMs": stats(&rtts)["average"],
374 "lossPercent": if probes.is_empty() { Value::Null } else { json!(100.0 * timeouts as f64 / probes.len() as f64) },
375 "raw": line.chars().take(1024).collect::<String>()
376 }))
379fn same_ip(a: &str, b: &str) -> bool {
380 match (a.parse::<IpAddr>(), b.parse::<IpAddr>()) {
381 (Ok(x), Ok(y)) => x == y,
382 _ => a == b,
383 }
386pub async fn traceroute(address: &str, options: &Options, cancel: &CancellationToken, progress: Arc<dyn Fn(Value) + Send + Sync>) -> Result<Value, DiagError> {
387 let mut routes: Vec<Value> = Vec::new();
388 for iteration in 0..options.repetitions {
389 if cancel.is_cancelled() {
390 return Err(DiagError::new("CANCELLED"));
391 }
392 let (exe, args) = traceroute_command(address, options)?;
393 let hops = Arc::new(parking_lot::Mutex::new(Vec::<Value>::new()));
394 let hops_cb = hops.clone();
395 let progress_cb = progress.clone();
396 let address_owned = address.to_string();
397 let on_line = move |line: &str| {
398 if let Some(hop) = parse_hop(line) {
399 hops_cb.lock().push(hop.clone());
400 progress_cb(merge(json!({ "kind": "trace-hop", "iteration": iteration, "address": address_owned }), hop));
401 }
402 };
403 let timeout = (options.max_hops * options.timeout_ms.div_ceil(1000) * 1000 * 3 + 2000).min(180_000);
404 match run_process(&exe, &args, timeout, cancel, 256 * 1024, Some(&on_line), false).await {
405 Ok(result) => {
406 let hops = hops.lock().clone();
407 routes.push(json!({ "iteration": iteration, "hops": hops, "exitCode": result.code, "raw": String::from_utf8_lossy(&result.stdout).chars().take(32000).collect::<String>(), "reason": result.stderr.chars().take(1000).collect::<String>() }));
408 }
409 Err(error) => {
410 if cancel.is_cancelled() {
411 return Err(error);
412 }
413 let hops = hops.lock().clone();
414 if !hops.is_empty() {
415 routes.push(json!({ "iteration": iteration, "hops": hops, "incomplete": true, "reason": error.message }));
416 }
417 return Ok(json!({ "status": if error.code == "ENOENT" { "unavailable" } else { "error" }, "code": error.code, "reason": error.message, "address": address, "routes": routes }));
418 }
419 }
420 }
421 let reached = routes.iter().any(|r| r["hops"].as_array().map(|h| h.iter().any(|hop| hop["addresses"].as_array().map(|a| a.iter().any(|ip| same_ip(ip.as_str().unwrap_or(""), address))).unwrap_or(false))).unwrap_or(false));
422 let all_failed = routes.iter().all(|r| r["exitCode"] != 0 && r["hops"].as_array().map(|h| h.is_empty()).unwrap_or(true));
423 let addresses_of = |route: &Value| route["hops"].as_array().map(|h| h.iter().map(|hop| hop["addresses"].clone()).collect::<Vec<_>>()).unwrap_or_default();
424 let changed = if routes.len() > 1 { json!(routes.iter().any(|r| addresses_of(r) != addresses_of(&routes[0]))) } else { Value::Null };
425 Ok(json!({
426 "status": if reached { "success" } else if all_failed { "error" } else { "warning" },
427 "address": address,
428 "routes": routes,
429 "effectiveProbeTimeoutMs": if cfg!(target_os = "macos") { options.timeout_ms.div_ceil(1000) * 1000 } else { options.timeout_ms },
430 "routeChanged": changed,
431 "method": if cfg!(windows) { "ICMP tracert" } else { "System UDP traceroute" },
432 "note": "Intermediate timeouts do not prove a broken route. Loss is the fraction of unanswered displayed probes, not end-to-end packet loss."
433 }))
436#[cfg(test)]
437mod tests {
438 use super::*;
440 #[test]
441 fn hop_parsing() {
442 let hop = parse_hop(" 3 10.0.0.1 1.234 ms * <1 ms").unwrap();
443 assert_eq!(hop["hop"], 3);
444 assert_eq!(hop["addresses"][0], "10.0.0.1");
445 assert_eq!(hop["probes"].as_array().unwrap().len(), 3);
446 assert_eq!(hop["probes"][2]["upperBoundMs"], 1.0);
447 assert!(parse_hop("traceroute to x").is_none());
448 }
450 #[test]
451 fn ping_reply_parsing() {
452 let reply = parse_ping_reply("64 bytes from 1.1.1.1: icmp_seq=0 ttl=57 time=12.5 ms");
453 assert_eq!(reply["rttMs"], 12.5);
454 assert_eq!(reply["ttl"], 57);
455 let windows = parse_ping_reply("Reply from 10.0.0.1: bytes=32 time<1ms TTL=128");
456 assert_eq!(windows["received"], true);
457 assert_eq!(windows["upperBoundMs"], 1.0);
458 }
460 #[tokio::test]
461 async fn process_limits_and_cancel() {
462 if cfg!(windows) {
463 return;
464 }
465 let cancel = CancellationToken::new();
466 let result = run_process("/bin/sh", &["-c".into(), "echo hi; echo err >&2".into()], 5000, &cancel, 1024, None, false).await.unwrap();
467 assert_eq!(String::from_utf8_lossy(&result.stdout), "hi\n");
468 assert_eq!(result.stderr, "err\n");
469 let limited = run_process("/bin/sh", &["-c".into(), "yes".into()], 5000, &cancel, 1024, None, false).await;
470 assert_eq!(limited.err().unwrap().code, "OUTPUT_LIMIT");
471 let timed = run_process("/bin/sh", &["-c".into(), "sleep 5".into()], 200, &cancel, 1024, None, false).await;
472 assert_eq!(timed.err().unwrap().code, "TIMEOUT");
473 }
SHA-25623353fe1be096dd1ff396be8a10b5126125647d1b107b88e7545692670a625e4

CH-J Proprietary Software License 1.14 · Ibinibigay ang source sa ilalim ng CH-J Proprietary Software License 1.14. Hindi binabago ng pagiging available nito ang mga tuntunin ng lisensya at hindi ito nagbibigay ng karagdagang karapatan.

SHA-256 ng archiveac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Mga file