Skip to main content

peek_core/
lib.rs

1//! Core library for peek: process snapshot types, collection orchestration, and extended data.
2//!
3//! Provides `ProcessInfo`, `CollectOptions`, `collect()`, and `collect_extended()`; delegates to
4//! peek-proc-reader, kernel-explainer, resource-sampler, network-inspector, and signal-engine.
5
6pub mod config;
7pub mod proc;
8pub mod ringbuf;
9
10use serde::{Deserialize, Serialize};
11
12// ─── Core process snapshot ────────────────────────────────────────────────────
13
14#[derive(Debug, Serialize, Deserialize, Clone)]
15pub struct ProcessInfo {
16    pub pid: i32,
17    pub name: String,
18    pub cmdline: String,
19    /// Resolved executable path from `/proc/<pid>/exe`, when available.
20    pub exe: Option<String>,
21    pub state: String,
22    pub ppid: i32,
23    pub uid: u32,
24    pub gid: u32,
25    pub started_at: Option<chrono::DateTime<chrono::Local>>,
26    pub threads: i32,
27    pub vm_size_kb: u64,
28    pub rss_kb: u64,
29    /// Proportional set size (KB), from smaps_rollup. Linux, extended only.
30    pub pss_kb: Option<u64>,
31    /// Swap used (KB), from status VmSwap. Linux, extended only.
32    pub swap_kb: Option<u64>,
33    // Extended resource fields
34    pub cpu_percent: Option<f64>,
35    pub io_read_bytes: Option<u64>,
36    pub io_write_bytes: Option<u64>,
37    pub fd_count: Option<usize>,
38    // Optional rich sections
39    pub kernel: Option<KernelInfo>,
40    pub network: Option<NetworkInfo>,
41    pub open_files: Option<Vec<OpenFile>>,
42    pub env_vars: Option<Vec<EnvVar>>,
43    pub process_tree: Option<ProcessNode>,
44    pub gpu: Option<Vec<GpuInfo>>,
45}
46
47// ─── Kernel context ──────────────────────────────────────────────────────────
48
49#[derive(Debug, Serialize, Deserialize, Clone)]
50pub struct KernelInfo {
51    pub sched_policy: String,
52    pub nice: i32,
53    pub priority: i32,
54    pub oom_score: i32,
55    pub oom_score_adj: i32,
56    pub cgroup: String,
57    pub namespaces: Vec<NamespaceEntry>,
58    pub cap_permitted: String,
59    pub cap_effective: String,
60    pub seccomp: u32,
61    pub voluntary_ctxt_switches: Option<u64>,
62    pub nonvoluntary_ctxt_switches: Option<u64>,
63    /// Optional LSM security label (e.g. AppArmor/SELinux).
64    pub security_label: Option<String>,
65}
66
67#[derive(Debug, Serialize, Deserialize, Clone)]
68pub struct NamespaceEntry {
69    pub ns_type: String,
70    pub inode: String,
71}
72
73// ─── Network ─────────────────────────────────────────────────────────────────
74
75#[derive(Debug, Serialize, Deserialize, Clone)]
76pub struct NetworkInfo {
77    pub listening_tcp: Vec<SocketEntry>,
78    pub listening_udp: Vec<SocketEntry>,
79    pub connections: Vec<ConnectionEntry>,
80    /// Unix socket paths for this process (from /proc/net/unix + fd inodes).
81    pub unix_sockets: Option<Vec<UnixSocketEntry>>,
82    /// RX bytes/sec in process network namespace (from /proc/<pid>/net/dev delta).
83    /// Sampling window is controlled by `PEEK_NET_SAMPLE_MS` (default 1000ms; 0 disables sampling).
84    pub traffic_rx_bytes_per_sec: Option<u64>,
85    /// TX bytes/sec in process network namespace.
86    pub traffic_tx_bytes_per_sec: Option<u64>,
87}
88
89#[derive(Debug, Serialize, Deserialize, Clone)]
90pub struct UnixSocketEntry {
91    pub path: String,
92}
93
94#[derive(Debug, Serialize, Deserialize, Clone)]
95pub struct SocketEntry {
96    pub protocol: String,
97    pub local_addr: String,
98    pub local_port: u16,
99}
100
101#[derive(Debug, Serialize, Deserialize, Clone)]
102pub struct ConnectionEntry {
103    pub protocol: String,
104    pub local_addr: String,
105    pub local_port: u16,
106    pub remote_addr: String,
107    pub remote_port: u16,
108    pub state: String,
109}
110
111// ─── Files ───────────────────────────────────────────────────────────────────
112
113#[derive(Debug, Serialize, Deserialize, Clone)]
114pub struct OpenFile {
115    pub fd: u32,
116    pub fd_type: String,
117    pub description: String,
118}
119
120// ─── Environment ─────────────────────────────────────────────────────────────
121
122#[derive(Debug, Serialize, Deserialize, Clone)]
123pub struct EnvVar {
124    pub key: String,
125    pub value: String,
126    pub redacted: bool,
127}
128
129// ─── Process tree ─────────────────────────────────────────────────────────────
130
131#[derive(Debug, Serialize, Deserialize, Clone)]
132pub struct ProcessNode {
133    pub pid: i32,
134    pub name: String,
135    pub uid: u32,
136    pub rss_kb: u64,
137    pub children: Vec<ProcessNode>,
138}
139
140// ─── GPU ─────────────────────────────────────────────────────────────────────
141
142pub use resource_sampler::gpu::GpuInfo;
143
144// ─── Signal impact pre-flight ─────────────────────────────────────────────────
145pub use signal_engine::impact::SignalImpact;
146
147// ─── FD leak detection ───────────────────────────────────────────────────────
148
149#[derive(Debug, Serialize, Deserialize, Clone)]
150pub struct FdLeakWarning {
151    /// FD count at the start of the observation window.
152    pub start_count: usize,
153    /// FD count at the end.
154    pub end_count: usize,
155    /// How many consecutive samples showed an increase.
156    pub consecutive_increases: usize,
157}
158
159// ─── Errors ──────────────────────────────────────────────────────────────────
160
161#[derive(Debug, thiserror::Error)]
162pub enum PeekError {
163    #[error("process {0} not found")]
164    NotFound(i32),
165
166    #[error("failed to read /proc for pid {pid}: {source}")]
167    ProcIo {
168        pid: i32,
169        #[source]
170        source: std::io::Error,
171    },
172
173    #[error("failed to parse /proc for pid {pid}: {msg}")]
174    ProcParse { pid: i32, msg: String },
175}
176
177impl From<peek_proc_reader::ProcReaderError> for PeekError {
178    fn from(e: peek_proc_reader::ProcReaderError) -> Self {
179        let pid = e.pid().unwrap_or(-1);
180        match e {
181            peek_proc_reader::ProcReaderError::NotFound(pid) => PeekError::NotFound(pid),
182            peek_proc_reader::ProcReaderError::Io { source, .. } => {
183                PeekError::ProcIo { pid, source }
184            }
185            peek_proc_reader::ProcReaderError::Parse { msg, .. } => {
186                PeekError::ProcParse { pid, msg }
187            }
188        }
189    }
190}
191
192pub type Result<T> = std::result::Result<T, PeekError>;
193
194// ─── Collect options ─────────────────────────────────────────────────────────
195
196#[derive(Debug, Default, Clone)]
197pub struct CollectOptions {
198    pub resources: bool,
199    pub kernel: bool,
200    pub network: bool,
201    pub files: bool,
202    pub env: bool,
203    pub tree: bool,
204    pub gpu: bool,
205}
206
207// ─── Public API ──────────────────────────────────────────────────────────────
208
209/// Fast baseline snapshot (no CPU sampling, no extended sections).
210pub fn collect(pid: i32) -> Result<ProcessInfo> {
211    proc::collect_process(pid, false)
212}
213
214/// Full snapshot gated by `opts`. On Linux includes kernel, network, files, env, tree, GPU; elsewhere baseline only.
215pub fn collect_extended(pid: i32, opts: &CollectOptions) -> Result<ProcessInfo> {
216    let mut info = proc::collect_process(pid, opts.resources)?;
217
218    #[cfg(target_os = "linux")]
219    {
220        if opts.resources {
221            info.io_read_bytes = proc::resources::read_io(pid).map(|io| io.0).ok();
222            info.io_write_bytes = proc::resources::read_io(pid).map(|io| io.1).ok();
223            info.fd_count = proc::files::count_fds(pid).ok();
224            if let Some((_rss, pss, swap)) = resource_sampler::memory::sample_memory(pid) {
225                info.pss_kb = Some(pss);
226                info.swap_kb = Some(swap);
227            }
228        }
229        if opts.kernel {
230            info.kernel = proc::kernel::collect_kernel(pid).ok();
231        }
232        if opts.network {
233            info.network = proc::network::collect_network(pid).ok();
234        }
235        if opts.files {
236            info.open_files = proc::files::collect_files(pid).ok();
237        }
238        if opts.env {
239            info.env_vars = proc::env::collect_env(pid).ok();
240        }
241        if opts.tree {
242            info.process_tree = proc::tree::build_tree(pid).ok();
243        }
244        if opts.gpu {
245            info.gpu = Some(proc::gpu::collect_gpu(pid));
246        }
247    }
248
249    Ok(info)
250}
251
252/// Pre-flight signal impact analysis. Linux only.
253pub fn signal_impact(_pid: i32) -> anyhow::Result<SignalImpact> {
254    #[cfg(target_os = "linux")]
255    {
256        signal_engine::impact::analyze_impact(_pid)
257    }
258    #[cfg(not(target_os = "linux"))]
259    {
260        anyhow::bail!("Signal impact analysis is only available on Linux")
261    }
262}
263
264/// Optional human-readable description for well-known process names.
265pub fn binary_description(name: &str) -> Option<String> {
266    kernel_explainer::well_known::binary_description(name).map(|s| s.to_string())
267}
268
269/// Human-readable OOM kill likelihood band (low / moderate / high / critical).
270pub fn oom_description(score: i32) -> &'static str {
271    kernel_explainer::oom::oom_description(score)
272}
273
274/// Soft "Max open files" limit from `/proc/<pid>/limits`, if available.
275#[cfg(target_os = "linux")]
276pub fn fd_soft_limit(pid: i32) -> Option<u64> {
277    use peek_proc_reader::limits::read_limits;
278
279    let limits = read_limits(pid).ok()?;
280    limits.max_open_files_soft
281}
282
283#[cfg(not(target_os = "linux"))]
284pub fn fd_soft_limit(_pid: i32) -> Option<u64> {
285    None
286}
287
288/// Current syscall name and description from `/proc/<pid>/syscall` (x86_64).
289/// Returns `None` if unreadable or syscall number unknown.
290#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
291pub fn current_syscall(pid: i32) -> Option<(String, String)> {
292    let (num, _) = peek_proc_reader::current::read_syscall(pid)?;
293    let name = kernel_explainer::syscalls::syscall_name_x86_64(num)?;
294    let desc = kernel_explainer::syscalls::syscall_description(name);
295    Some((name.to_string(), desc.to_string()))
296}
297
298#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
299pub fn current_syscall(_pid: i32) -> Option<(String, String)> {
300    None
301}
302
303/// Best-effort reverse DNS for an address (e.g. "192.168.1.1:443"). Time-bounded; for CLI/TUI only.
304pub fn resolve_remote(addr: &str) -> Option<String> {
305    network_inspector::resolver::resolve(addr)
306}