From bcef96560e6a3b421ca67476934545e30676a3bf Mon Sep 17 00:00:00 2001 From: mahesh bhatiya Date: Thu, 11 Sep 2025 22:25:25 +0530 Subject: [PATCH] feat(monitor): live eBPF program monitor with bpftool + DB Query bpftool progs/links\n- Merge with state; prefer live hook/type\n- Include DB programs not in state with status\n- Show Hook, PID, Status; 2s refresh\n- Remove comments; graceful on missing deps --- eclipta-cli/src/commands/system/monitor.rs | 142 +++++++++++++++++++-- 1 file changed, 133 insertions(+), 9 deletions(-) diff --git a/eclipta-cli/src/commands/system/monitor.rs b/eclipta-cli/src/commands/system/monitor.rs index 89991c6..b4dcd0c 100644 --- a/eclipta-cli/src/commands/system/monitor.rs +++ b/eclipta-cli/src/commands/system/monitor.rs @@ -16,6 +16,11 @@ use tui::{ use crate::utils::paths::default_state_path; use crate::utils::state::load_state; +use crate::utils::db::ensure_db_ready; +use crate::db::programs::list_programs; +use serde_json::Value; +use std::collections::HashMap; +use tokio::process::Command as TokioCommand; struct AttachmentRow { name: String, @@ -32,6 +37,91 @@ fn proc_alive(pid: u32) -> bool { p.exists() } +#[derive(Debug, Clone)] +struct LinkInfo { + prog_id: u32, + pid: Option, + attach_type: String, + target: Option, + hook: Option, +} + +async fn get_live_bpf_indices() -> Result<(HashMap, Vec), std::io::Error> { + let prog_out = TokioCommand::new("bpftool").args(["prog", "list", "-j"]).output().await; + let link_out = TokioCommand::new("bpftool").args(["link", "list", "-j"]).output().await; + + let mut name_to_id: HashMap = HashMap::new(); + let mut links: Vec = Vec::new(); + + if let Ok(o) = prog_out { + if o.status.success() { + if let Ok(v) = serde_json::from_slice::(&o.stdout) { + if let Some(arr) = v.as_array() { + for p in arr { + let id = p.get("id").and_then(|x| x.as_u64()).unwrap_or(0) as u32; + if id == 0 { continue; } + if let Some(name) = p.get("name").and_then(|x| x.as_str()) { + name_to_id.insert(name.to_string(), id); + } else if let Some(tag) = p.get("tag").and_then(|x| x.as_str()) { + name_to_id.insert(tag.to_string(), id); + } + } + } + } + } + } + + if let Ok(o) = link_out { + if o.status.success() { + if let Ok(v) = serde_json::from_slice::(&o.stdout) { + if let Some(arr) = v.as_array() { + for l in arr { + let prog_id = l.get("prog_id").and_then(|x| x.as_u64()).unwrap_or(0) as u32; + if prog_id == 0 { continue; } + let attach_type = l.get("type").and_then(|x| x.as_str()).unwrap_or("").to_string(); + let target = l.get("target_name").and_then(|x| x.as_str()).map(|s| s.to_string()); + let hook = l.get("tp_name").and_then(|x| x.as_str()).map(|s| s.to_string()); + let mut pid: Option = None; + if let Some(pid_val) = l.get("pid").and_then(|x| x.as_u64()) { + pid = Some(pid_val as u32); + } else if let Some(pids) = l.get("pids").and_then(|x| x.as_array()) { + for p in pids { + if let Some(pv) = p.get("pid").and_then(|x| x.as_u64()) { + pid = Some(pv as u32); + break; + } + } + } + links.push(LinkInfo { prog_id, pid, attach_type, target, hook }); + } + } + } + } + } + + Ok((name_to_id, links)) +} + +fn live_hook_for( + prog_index: &HashMap, + link_index: &Vec, + program_name: &str, + pid: u32, +) -> Option { + let prog_id = prog_index.get(program_name).copied()?; + let mut candidates: Vec<&LinkInfo> = link_index.iter().filter(|l| l.prog_id == prog_id).collect(); + if candidates.is_empty() { return None; } + if let Some(first_pid_match) = candidates.iter().find(|l| l.pid == Some(pid)) { + return Some(render_hook(first_pid_match)); + } + Some(render_hook(candidates[0])) +} + +fn render_hook(l: &LinkInfo) -> String { + let tgt = l.hook.clone().or_else(|| l.target.clone()).unwrap_or_default(); + if tgt.is_empty() { l.attach_type.clone() } else { format!("{}:{}", l.attach_type, tgt) } +} + pub async fn handle_monitor() -> io::Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); @@ -49,7 +139,12 @@ pub async fn handle_monitor() -> io::Result<()> { } let st = load_state(&default_state_path()); - let rows_data: Vec = st + + let (prog_index, link_index) = match get_live_bpf_indices().await { + Ok(v) => v, + Err(_) => (HashMap::new(), Vec::new()), + }; + let mut rows_data: Vec = st .attachments .iter() .map(|r| { @@ -62,6 +157,9 @@ pub async fn handle_monitor() -> io::Result<()> { .as_ref() .map(|p| if p.exists() { "yes" } else { "missing" }) .unwrap_or("no"); + let live_hook = live_hook_for(&prog_index, &link_index, &r.name, r.pid) + .unwrap_or(hook); + let status = if proc_alive(r.pid) { "online" } else { "offline" }; let created = DateTime::from_timestamp(r.created_at, 0) .unwrap_or(DateTime::::UNIX_EPOCH) @@ -71,7 +169,7 @@ pub async fn handle_monitor() -> io::Result<()> { AttachmentRow { name: r.name.clone(), kind: r.kind.clone(), - hook, + hook: live_hook, pid: r.pid.to_string(), status: status.to_string(), pinned: pinned.to_string(), @@ -80,6 +178,32 @@ pub async fn handle_monitor() -> io::Result<()> { }) .collect(); + if let Ok(pool) = ensure_db_ready().await { + if let Ok(programs) = list_programs(&pool).await { + let state_names: std::collections::HashSet = st.attachments.iter().map(|a| a.name.clone()).collect(); + let any_attached = link_index.iter().next(); + for p in programs { + if state_names.contains(&p.title) { continue; } + let (pid_str, hook_str, status_str) = if let Some(l) = any_attached { + let pid_str = l.pid.map(|x| x.to_string()).unwrap_or("-".to_string()); + let hook_str = render_hook(l); + (pid_str, hook_str, "attached".to_string()) + } else { + ("-".to_string(), "-".to_string(), "detached".to_string()) + }; + rows_data.push(AttachmentRow { + name: p.title, + kind: p.status, + hook: hook_str, + pid: pid_str, + status: status_str, + pinned: "n/a".to_string(), + created: "-".to_string(), + }); + } + } + } + terminal.draw(|f| { let size = f.size(); let chunks = Layout::default() @@ -134,13 +258,13 @@ pub async fn handle_monitor() -> io::Result<()> { .borders(Borders::ALL), ) .widths(&[ - Constraint::Length(18), // Program - Constraint::Length(12), // Kind - Constraint::Length(24), // Hook - Constraint::Length(8), // PID - Constraint::Length(10), // Status - Constraint::Length(8), // Pinned - Constraint::Length(20), // Created + Constraint::Length(18), + Constraint::Length(12), + Constraint::Length(24), + Constraint::Length(8), + Constraint::Length(10), + Constraint::Length(8), + Constraint::Length(20), ]) .column_spacing(1);