Skip to main content

web_admin/
sysinfo.rs

1use alloc::{format, string::String, vec::Vec};
2use core::fmt::Write;
3use strat9_syscall::{call, flag};
4
5use crate::net;
6const WEBRTC_PATH: &str = "/srv/strate-webrtc/default";
7const WEBRTC_OP_SESSION_OPEN: u32 = 0x300;
8const WEBRTC_OP_SESSION_CLOSE: u32 = 0x301;
9const WEBRTC_OP_SESSION_INFO: u32 = 0x30A;
10
11// ---------------------------------------------------------------------------
12// Structured process info
13// ---------------------------------------------------------------------------
14
15pub struct ProcInfo {
16    pub pid: u32,
17    pub ppid: u32,
18    pub name: String,
19    pub state: String,
20    pub silo_id: String,
21    pub mem_used: String,
22}
23
24/// Parses proc status.
25fn parse_proc_status(raw: &str) -> ProcInfo {
26    let mut info = ProcInfo {
27        pid: 0,
28        ppid: 0,
29        name: String::new(),
30        state: String::new(),
31        silo_id: String::new(),
32        mem_used: String::new(),
33    };
34    for line in raw.lines() {
35        let (key, val) = match line.split_once(':') {
36            Some((k, v)) => (k.trim(), v.trim()),
37            None => continue,
38        };
39        match key {
40            "Pid" => info.pid = val.parse().unwrap_or(0),
41            "PPid" => info.ppid = val.parse().unwrap_or(0),
42            "Name" => info.name = val.into(),
43            "State" => info.state = val.into(),
44            "SiloId" => info.silo_id = val.into(),
45            "SiloMemUsed" => info.mem_used = val.into(),
46            _ => {}
47        }
48    }
49    info
50}
51
52// ---------------------------------------------------------------------------
53// /proc readers
54// ---------------------------------------------------------------------------
55
56/// Implements kernel version.
57pub fn kernel_version() -> String {
58    net::read_file_text("/proc/version")
59}
60
61/// Implements cpu info.
62pub fn cpu_info() -> String {
63    net::read_file_text("/proc/cpuinfo")
64}
65
66/// Implements mem info.
67pub fn mem_info() -> String {
68    net::read_file_text("/proc/meminfo")
69}
70
71/// Implements silo info.
72pub fn silo_info() -> String {
73    net::read_file_text("/proc/silos")
74}
75
76/// Implements net routes.
77pub fn net_routes() -> String {
78    net::read_file_text("/net/routes")
79}
80
81// ---------------------------------------------------------------------------
82// /net scheme readers
83// ---------------------------------------------------------------------------
84
85/// Implements net address.
86pub fn net_address() -> String {
87    net::read_file_text("/net/address").trim().into()
88}
89
90/// Implements net gateway.
91pub fn net_gateway() -> String {
92    net::read_file_text("/net/gateway").trim().into()
93}
94
95/// Implements net dns.
96pub fn net_dns() -> String {
97    net::read_file_text("/net/dns").trim().into()
98}
99
100/// Implements net ip.
101pub fn net_ip() -> String {
102    net::read_file_text("/net/ip").trim().into()
103}
104
105/// Implements net netmask.
106pub fn net_netmask() -> String {
107    net::read_file_text("/net/netmask").trim().into()
108}
109
110// ---------------------------------------------------------------------------
111// Process list via /proc + getdents (loops until all entries read)
112// ---------------------------------------------------------------------------
113
114/// Implements process list.
115pub fn process_list() -> Vec<ProcInfo> {
116    let fd = match call::openat(
117        0,
118        "/proc",
119        (flag::OpenFlags::RDONLY | flag::OpenFlags::DIRECTORY).bits() as usize,
120        0,
121    ) {
122        Ok(fd) => fd as usize,
123        Err(_) => return Vec::new(),
124    };
125
126    let mut pids = Vec::new();
127    let mut raw = [0u8; 4096];
128    const K_DIRENT_HDR: usize = 11; // ino(8) + type(1) + name_len(2)
129
130    loop {
131        let n = match call::getdents(fd, &mut raw) {
132            Ok(0) => break,
133            Ok(n) => n,
134            Err(_) => break,
135        };
136
137        let mut offset = 0usize;
138        while offset + K_DIRENT_HDR <= n {
139            let name_len = u16::from_le_bytes([raw[offset + 9], raw[offset + 10]]) as usize;
140            let entry_size = K_DIRENT_HDR.saturating_add(name_len).saturating_add(1);
141            if entry_size == 0 || offset + entry_size > n {
142                break;
143            }
144            let name_start = offset + K_DIRENT_HDR;
145            let name_end = name_start + name_len;
146            if let Ok(name_str) = core::str::from_utf8(&raw[name_start..name_end]) {
147                if let Ok(pid) = name_str.parse::<u32>() {
148                    pids.push(pid);
149                }
150            }
151            offset += entry_size;
152        }
153    }
154    let _ = call::close(fd);
155
156    let mut result = Vec::with_capacity(pids.len());
157    for pid in pids {
158        let status_raw = net::read_file_text(&format!("/proc/{}/status", pid));
159        if !status_raw.is_empty() {
160            result.push(parse_proc_status(&status_raw));
161        }
162    }
163    result
164}
165
166/// Implements uptime secs.
167pub fn uptime_secs() -> u64 {
168    net::clock_gettime_ns() / 1_000_000_000
169}
170
171// ---------------------------------------------------------------------------
172// Kill a process by PID
173// ---------------------------------------------------------------------------
174
175/// Implements kill process.
176pub fn kill_process(pid: u32) -> bool {
177    if pid <= 2 {
178        return false;
179    }
180    call::kill(pid as isize, 9).is_ok()
181}
182
183// ---------------------------------------------------------------------------
184// JSON builders
185// ---------------------------------------------------------------------------
186
187/// Implements json health.
188pub fn json_health() -> String {
189    let pid = call::getpid().unwrap_or(0);
190    let up = uptime_secs();
191    format!(r#"{{"status":"ok","pid":{},"uptime_secs":{}}}"#, pid, up)
192}
193
194/// Implements json uptime.
195pub fn json_uptime() -> String {
196    let ns = net::clock_gettime_ns();
197    let secs = ns / 1_000_000_000;
198    let mins = secs / 60;
199    let hours = mins / 60;
200    let days = hours / 24;
201    format!(
202        r#"{{"uptime_ns":{},"secs":{},"human":"{}d {}h {}m {}s"}}"#,
203        ns,
204        secs,
205        days,
206        hours % 24,
207        mins % 60,
208        secs % 60
209    )
210}
211
212/// Implements json version.
213pub fn json_version() -> String {
214    format!(r#"{{"version":"{}"}}"#, json_escape(&kernel_version()))
215}
216
217/// Implements json cpuinfo.
218pub fn json_cpuinfo() -> String {
219    format!(r#"{{"cpuinfo":"{}"}}"#, json_escape(&cpu_info()))
220}
221
222/// Implements json meminfo.
223pub fn json_meminfo() -> String {
224    format!(r#"{{"meminfo":"{}"}}"#, json_escape(&mem_info()))
225}
226
227/// Implements json silos.
228pub fn json_silos() -> String {
229    format!(r#"{{"silos":"{}"}}"#, json_escape(&silo_info()))
230}
231
232/// Implements json network.
233pub fn json_network() -> String {
234    format!(
235        r#"{{"address":"{}","gateway":"{}","dns":"{}","ip":"{}","netmask":"{}"}}"#,
236        json_escape(&net_address()),
237        json_escape(&net_gateway()),
238        json_escape(&net_dns()),
239        json_escape(&net_ip()),
240        json_escape(&net_netmask()),
241    )
242}
243
244/// Implements json routes.
245pub fn json_routes() -> String {
246    format!(r#"{{"routes":"{}"}}"#, json_escape(&net_routes()))
247}
248
249/// Implements json processes.
250pub fn json_processes() -> String {
251    let procs = process_list();
252    let mut out = String::with_capacity(procs.len() * 120 + 32);
253    let _ = write!(out, r#"{{"count":{},"processes":["#, procs.len());
254    for (i, p) in procs.iter().enumerate() {
255        if i > 0 {
256            out.push(',');
257        }
258        let _ = write!(
259            out,
260            r#"{{"pid":{},"ppid":{},"name":"{}","state":"{}","silo":"{}","mem":"{}"}}"#,
261            p.pid,
262            p.ppid,
263            json_escape(&p.name),
264            json_escape(&p.state),
265            json_escape(&p.silo_id),
266            json_escape(&p.mem_used),
267        );
268    }
269    out.push_str("]}");
270    out
271}
272
273/// Implements json kill result.
274pub fn json_kill_result(pid: u32) -> String {
275    let ok = kill_process(pid);
276    format!(
277        r#"{{"pid":{},"killed":{},"error":{}}}"#,
278        pid,
279        ok,
280        if ok { "null" } else { "\"EPERM or ESRCH\"" }
281    )
282}
283
284/// Implements json all.
285pub fn json_all() -> String {
286    let mut out = String::with_capacity(4096);
287    out.push_str(r#"{"health":"#);
288    out.push_str(&json_health());
289    out.push_str(r#","uptime":"#);
290    out.push_str(&json_uptime());
291    out.push_str(r#","version":"#);
292    out.push_str(&json_version());
293    out.push_str(r#","cpuinfo":"#);
294    out.push_str(&json_cpuinfo());
295    out.push_str(r#","meminfo":"#);
296    out.push_str(&json_meminfo());
297    out.push_str(r#","silos":"#);
298    out.push_str(&json_silos());
299    out.push_str(r#","network":"#);
300    out.push_str(&json_network());
301    out.push_str(r#","routes":"#);
302    out.push_str(&json_routes());
303    out.push_str(r#","processes":"#);
304    out.push_str(&json_processes());
305    out.push('}');
306    out
307}
308
309pub fn json_graphics_open(sid: u32) -> String {
310    let mut payload = [0u8; 5];
311    payload[0..4].copy_from_slice(&sid.to_le_bytes());
312    payload[4] = 0;
313    let Some(reply) = net::ipc_call_path(WEBRTC_PATH, WEBRTC_OP_SESSION_OPEN, &payload) else {
314        return String::from(r#"{"ok":false,"error":"ipc_unreachable"}"#);
315    };
316    let status = u32::from_le_bytes([
317        reply.payload[0],
318        reply.payload[1],
319        reply.payload[2],
320        reply.payload[3],
321    ]);
322    if status != 0 {
323        return format!(r#"{{"ok":false,"status":{}}}"#, status);
324    }
325    let session = u64::from_le_bytes([
326        reply.payload[4],
327        reply.payload[5],
328        reply.payload[6],
329        reply.payload[7],
330        reply.payload[8],
331        reply.payload[9],
332        reply.payload[10],
333        reply.payload[11],
334    ]);
335    let token = u64::from_le_bytes([
336        reply.payload[12],
337        reply.payload[13],
338        reply.payload[14],
339        reply.payload[15],
340        reply.payload[16],
341        reply.payload[17],
342        reply.payload[18],
343        reply.payload[19],
344    ]);
345    let ttl = u32::from_le_bytes([
346        reply.payload[20],
347        reply.payload[21],
348        reply.payload[22],
349        reply.payload[23],
350    ]);
351    format!(
352        r#"{{"ok":true,"status":0,"session_id":{},"token":{},"ttl_sec":{}}}"#,
353        session, token, ttl
354    )
355}
356
357pub fn json_graphics_close(session_id: u64) -> String {
358    let payload = session_id.to_le_bytes();
359    let Some(reply) = net::ipc_call_path(WEBRTC_PATH, WEBRTC_OP_SESSION_CLOSE, &payload) else {
360        return String::from(r#"{"ok":false,"error":"ipc_unreachable"}"#);
361    };
362    let status = u32::from_le_bytes([
363        reply.payload[0],
364        reply.payload[1],
365        reply.payload[2],
366        reply.payload[3],
367    ]);
368    format!(r#"{{"ok":{},"status":{}}}"#, status == 0, status)
369}
370
371pub fn json_graphics_info(session_id: u64) -> String {
372    let payload = session_id.to_le_bytes();
373    let Some(reply) = net::ipc_call_path(WEBRTC_PATH, WEBRTC_OP_SESSION_INFO, &payload) else {
374        return String::from(r#"{"ok":false,"error":"ipc_unreachable"}"#);
375    };
376    let status = u32::from_le_bytes([
377        reply.payload[0],
378        reply.payload[1],
379        reply.payload[2],
380        reply.payload[3],
381    ]);
382    if status != 0 {
383        return format!(r#"{{"ok":false,"status":{}}}"#, status);
384    }
385    let sid = u32::from_le_bytes([
386        reply.payload[12],
387        reply.payload[13],
388        reply.payload[14],
389        reply.payload[15],
390    ]);
391    let token = u64::from_le_bytes([
392        reply.payload[16],
393        reply.payload[17],
394        reply.payload[18],
395        reply.payload[19],
396        reply.payload[20],
397        reply.payload[21],
398        reply.payload[22],
399        reply.payload[23],
400    ]);
401    let flags = u64::from_le_bytes([
402        reply.payload[24],
403        reply.payload[25],
404        reply.payload[26],
405        reply.payload[27],
406        reply.payload[28],
407        reply.payload[29],
408        reply.payload[30],
409        reply.payload[31],
410    ]);
411    let expires_at_ns = u64::from_le_bytes([
412        reply.payload[32],
413        reply.payload[33],
414        reply.payload[34],
415        reply.payload[35],
416        reply.payload[36],
417        reply.payload[37],
418        reply.payload[38],
419        reply.payload[39],
420    ]);
421    format!(
422        r#"{{"ok":true,"status":0,"silo_id":{},"token":{},"flags":{},"expires_at_ns":{}}}"#,
423        sid, token, flags, expires_at_ns
424    )
425}
426
427// ---------------------------------------------------------------------------
428// JSON string escaping
429// ---------------------------------------------------------------------------
430
431/// Implements json escape.
432pub fn json_escape(s: &str) -> String {
433    let mut out = String::with_capacity(s.len() + s.len() / 8);
434    for c in s.chars() {
435        match c {
436            '"' => out.push_str("\\\""),
437            '\\' => out.push_str("\\\\"),
438            '\n' => out.push_str("\\n"),
439            '\r' => out.push_str("\\r"),
440            '\t' => out.push_str("\\t"),
441            c if c.is_control() => {
442                let _ = write!(out, "\\u{:04x}", c as u32);
443            }
444            c => out.push(c),
445        }
446    }
447    out
448}