Skip to main content

zwire_host/
procs.rs

1//! Process tools: list (`ps`), control (`kill`), and PATH lookup (`which`).
2//!
3//! `ps`/`kill` reuse the already-present `sysinfo` crate, so they add no
4//! dependencies; `which` is plain PATH scanning. Together they turn the host
5//! into a small activity monitor any app can drive.
6use serde_json::{json, Value};
7use std::path::{Path, PathBuf};
8
9/// Dispatch a process command.
10pub fn handle(cmd: &str, req: &Value) -> Value {
11    match cmd {
12        "ps" => ps(req),
13        "kill" => kill(req),
14        "which" => which(req),
15        _ => json!({"ok": false, "err": "unknown_cmd"}),
16    }
17}
18
19/// `{"cmd":"ps","filter"?:sub,"limit"?:N}` — processes sorted by memory, biggest
20/// first. `filter` keeps only names containing the substring; `limit` caps the
21/// list (default 50).
22fn ps(req: &Value) -> Value {
23    use sysinfo::System;
24    let sys = System::new_all();
25    let filter = req["filter"].as_str();
26    let limit = req["limit"].as_u64().unwrap_or(50) as usize;
27
28    let mut procs: Vec<(u32, String, u64, f32)> = sys
29        .processes()
30        .values()
31        .map(|p| {
32            (
33                p.pid().as_u32(),
34                p.name().to_string_lossy().into_owned(),
35                p.memory(),
36                p.cpu_usage(),
37            )
38        })
39        .filter(|(_, name, _, _)| filter.is_none_or(|f| name.contains(f)))
40        .collect();
41    procs.sort_unstable_by_key(|p| std::cmp::Reverse(p.2));
42    procs.truncate(limit);
43
44    let list: Vec<Value> = procs
45        .into_iter()
46        .map(|(pid, name, mem, cpu)| {
47            json!({"pid": pid, "name": name, "mem": mem, "cpu": (cpu * 100.0).round() / 100.0})
48        })
49        .collect();
50    json!({"ok": true, "procs": list})
51}
52
53/// `{"cmd":"kill","pid":N,"signal"?:"term"|"kill"}` — signal a process. Defaults
54/// to a graceful `term`, falling back to a hard kill if the platform lacks the
55/// requested signal.
56fn kill(req: &Value) -> Value {
57    use sysinfo::{Pid, Signal, System};
58    let Some(pid) = req["pid"].as_u64() else {
59        return json!({"ok": false, "err": "no_pid"});
60    };
61    let pid = Pid::from_u32(pid as u32);
62    let sys = System::new_all();
63    let Some(proc_) = sys.process(pid) else {
64        return json!({"ok": false, "err": "no_such_process"});
65    };
66    let signal = match req["signal"].as_str() {
67        Some("kill") => Signal::Kill,
68        _ => Signal::Term,
69    };
70    let ok = proc_.kill_with(signal).unwrap_or_else(|| proc_.kill());
71    json!({ "ok": ok })
72}
73
74/// `{"cmd":"which","program":"git"}` — resolve a program to an absolute path via
75/// `$PATH` (or check it directly if it already contains a path separator).
76fn which(req: &Value) -> Value {
77    let Some(program) = req["program"].as_str() else {
78        return json!({"ok": false, "err": "no_program"});
79    };
80    let path = locate(program).map(|p| p.to_string_lossy().into_owned());
81    json!({"ok": true, "path": path})
82}
83
84fn has_separator(p: &str) -> bool {
85    p.contains('/') || (cfg!(windows) && p.contains('\\'))
86}
87
88fn locate(program: &str) -> Option<PathBuf> {
89    if has_separator(program) {
90        let p = crate::store::expand(program);
91        return is_executable(&p).then_some(p);
92    }
93    let path = std::env::var_os("PATH")?;
94    for dir in std::env::split_paths(&path) {
95        let cand = dir.join(program);
96        if is_executable(&cand) {
97            return Some(cand);
98        }
99        #[cfg(windows)]
100        for ext in ["exe", "cmd", "bat", "com"] {
101            let c = dir.join(format!("{program}.{ext}"));
102            if c.is_file() {
103                return Some(c);
104            }
105        }
106    }
107    None
108}
109
110fn is_executable(p: &Path) -> bool {
111    #[cfg(unix)]
112    {
113        use std::os::unix::fs::PermissionsExt;
114        std::fs::metadata(p)
115            .map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
116            .unwrap_or(false)
117    }
118    #[cfg(not(unix))]
119    {
120        p.is_file()
121    }
122}