initial check for process summary screen
This check in offers alpha support for per process metrics, you can view threads, process CPU usage over time, IO, memory, CPU time, parent process, command, uptime and journal entries. This is unfinished but all major functionality is available and I wanted to make it available to feedback and testing.
This commit is contained in:
@@ -2,7 +2,10 @@
|
||||
|
||||
use crate::gpu::collect_all_gpus;
|
||||
use crate::state::AppState;
|
||||
use crate::types::{DiskInfo, Metrics, NetworkInfo, ProcessInfo, ProcessesPayload};
|
||||
use crate::types::{
|
||||
DetailedProcessInfo, DiskInfo, JournalEntry, JournalResponse, LogLevel, Metrics, NetworkInfo,
|
||||
ProcessInfo, ProcessMetricsResponse, ProcessesPayload,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::collections::HashMap;
|
||||
@@ -10,13 +13,55 @@ use std::collections::HashMap;
|
||||
use std::fs;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::io;
|
||||
use std::process::Command;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration as StdDuration;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate};
|
||||
use tracing::warn;
|
||||
|
||||
// NOTE: CPU normalization env removed; non-Linux now always reports per-process share (0..100) as given by sysinfo.
|
||||
|
||||
// Helper functions to get CPU time from /proc/stat on Linux
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_cpu_time_user(pid: u32) -> u64 {
|
||||
if let Ok(stat) = fs::read_to_string(format!("/proc/{pid}/stat")) {
|
||||
let fields: Vec<&str> = stat.split_whitespace().collect();
|
||||
if fields.len() > 13 {
|
||||
// Field 13 (0-indexed) is utime (user CPU time in clock ticks)
|
||||
if let Ok(utime) = fields[13].parse::<u64>() {
|
||||
// Convert clock ticks to milliseconds (assuming 100 Hz)
|
||||
return utime * 10; // 1 tick = 10ms at 100 Hz
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_cpu_time_system(pid: u32) -> u64 {
|
||||
if let Ok(stat) = fs::read_to_string(format!("/proc/{pid}/stat")) {
|
||||
let fields: Vec<&str> = stat.split_whitespace().collect();
|
||||
if fields.len() > 14 {
|
||||
// Field 14 (0-indexed) is stime (system CPU time in clock ticks)
|
||||
if let Ok(stime) = fields[14].parse::<u64>() {
|
||||
// Convert clock ticks to milliseconds (assuming 100 Hz)
|
||||
return stime * 10; // 1 tick = 10ms at 100 Hz
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn get_cpu_time_user(_pid: u32) -> u64 {
|
||||
0 // Not implemented for non-Linux platforms
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn get_cpu_time_system(_pid: u32) -> u64 {
|
||||
0 // Not implemented for non-Linux platforms
|
||||
}
|
||||
// Runtime toggles (read once)
|
||||
fn gpu_enabled() -> bool {
|
||||
static ON: OnceCell<bool> = OnceCell::new();
|
||||
@@ -549,3 +594,616 @@ pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload {
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
/// Lightweight child process enumeration using direct /proc access
|
||||
/// This avoids the expensive refresh_processes_specifics(All) call
|
||||
#[cfg(target_os = "linux")]
|
||||
fn enumerate_child_processes_lightweight(
|
||||
parent_pid: u32,
|
||||
system: &sysinfo::System,
|
||||
) -> Vec<DetailedProcessInfo> {
|
||||
let mut children = Vec::new();
|
||||
|
||||
// Read /proc to find all child processes
|
||||
// This is much faster than refresh_processes_specifics(All)
|
||||
if let Ok(entries) = fs::read_dir("/proc") {
|
||||
for entry in entries.flatten() {
|
||||
if let Ok(file_name) = entry.file_name().into_string() {
|
||||
if let Ok(pid) = file_name.parse::<u32>() {
|
||||
// Check if this process is a child of our target
|
||||
if let Some(child_parent_pid) = read_parent_pid_from_proc(pid) {
|
||||
if child_parent_pid == parent_pid {
|
||||
// Found a child! Collect its details from /proc
|
||||
if let Some(child_info) = collect_process_info_from_proc(pid, system) {
|
||||
children.push(child_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
children
|
||||
}
|
||||
|
||||
/// Read parent PID from /proc/{pid}/stat
|
||||
#[cfg(target_os = "linux")]
|
||||
fn read_parent_pid_from_proc(pid: u32) -> Option<u32> {
|
||||
let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
|
||||
// Format: pid (comm) state ppid ...
|
||||
// We need to handle process names with spaces/parentheses
|
||||
let ppid_start = stat.rfind(')')?;
|
||||
let fields: Vec<&str> = stat[ppid_start + 1..].split_whitespace().collect();
|
||||
// After the closing paren: state ppid ...
|
||||
// Field 1 (0-indexed) is ppid
|
||||
fields.get(1)?.parse::<u32>().ok()
|
||||
}
|
||||
|
||||
/// Collect process information from /proc files
|
||||
#[cfg(target_os = "linux")]
|
||||
fn collect_process_info_from_proc(
|
||||
pid: u32,
|
||||
system: &sysinfo::System,
|
||||
) -> Option<DetailedProcessInfo> {
|
||||
// Try to get basic info from sysinfo if it's already loaded (cheap lookup)
|
||||
// Otherwise read from /proc directly
|
||||
let (name, cpu_usage, mem_bytes, virtual_mem_bytes) =
|
||||
if let Some(proc) = system.process(sysinfo::Pid::from_u32(pid)) {
|
||||
(
|
||||
proc.name().to_string_lossy().to_string(),
|
||||
proc.cpu_usage(),
|
||||
proc.memory(),
|
||||
proc.virtual_memory(),
|
||||
)
|
||||
} else {
|
||||
// Process not in sysinfo cache, read minimal info from /proc
|
||||
let name = fs::read_to_string(format!("/proc/{pid}/comm"))
|
||||
.ok()?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
// Read memory from /proc/{pid}/status
|
||||
let status_content = fs::read_to_string(format!("/proc/{pid}/status")).ok()?;
|
||||
let mut mem_bytes = 0u64;
|
||||
let mut virtual_mem_bytes = 0u64;
|
||||
|
||||
for line in status_content.lines() {
|
||||
if let Some(value) = line.strip_prefix("VmRSS:") {
|
||||
if let Some(kb) = value.split_whitespace().next() {
|
||||
mem_bytes = kb.parse::<u64>().unwrap_or(0) * 1024;
|
||||
}
|
||||
} else if let Some(value) = line.strip_prefix("VmSize:") {
|
||||
if let Some(kb) = value.split_whitespace().next() {
|
||||
virtual_mem_bytes = kb.parse::<u64>().unwrap_or(0) * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(name, 0.0, mem_bytes, virtual_mem_bytes)
|
||||
};
|
||||
|
||||
// Read command line
|
||||
let command = fs::read_to_string(format!("/proc/{pid}/cmdline"))
|
||||
.ok()
|
||||
.map(|s| s.replace('\0', " ").trim().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Read status information
|
||||
let status_content = fs::read_to_string(format!("/proc/{pid}/status")).ok()?;
|
||||
let mut uid = 0u32;
|
||||
let mut gid = 0u32;
|
||||
let mut thread_count = 0u32;
|
||||
let mut status = "Unknown".to_string();
|
||||
|
||||
for line in status_content.lines() {
|
||||
if let Some(value) = line.strip_prefix("Uid:") {
|
||||
if let Some(uid_str) = value.split_whitespace().next() {
|
||||
uid = uid_str.parse().unwrap_or(0);
|
||||
}
|
||||
} else if let Some(value) = line.strip_prefix("Gid:") {
|
||||
if let Some(gid_str) = value.split_whitespace().next() {
|
||||
gid = gid_str.parse().unwrap_or(0);
|
||||
}
|
||||
} else if let Some(value) = line.strip_prefix("Threads:") {
|
||||
thread_count = value.trim().parse().unwrap_or(0);
|
||||
} else if let Some(value) = line.strip_prefix("State:") {
|
||||
status = value
|
||||
.trim()
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| match c {
|
||||
'R' => "Running",
|
||||
'S' => "Sleeping",
|
||||
'D' => "Disk Sleep",
|
||||
'Z' => "Zombie",
|
||||
'T' => "Stopped",
|
||||
't' => "Tracing Stop",
|
||||
'X' | 'x' => "Dead",
|
||||
'K' => "Wakekill",
|
||||
'W' => "Waking",
|
||||
'P' => "Parked",
|
||||
'I' => "Idle",
|
||||
_ => "Unknown",
|
||||
})
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Read start time from stat
|
||||
let start_time = if let Ok(stat) = fs::read_to_string(format!("/proc/{pid}/stat")) {
|
||||
let stat_end = stat.rfind(')')?;
|
||||
let fields: Vec<&str> = stat[stat_end + 1..].split_whitespace().collect();
|
||||
// Field 19 (0-indexed) is starttime in clock ticks since boot
|
||||
fields.get(19)?.parse::<u64>().ok()?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Read I/O stats if available
|
||||
let (read_bytes, write_bytes) =
|
||||
if let Ok(io_content) = fs::read_to_string(format!("/proc/{pid}/io")) {
|
||||
let mut read_bytes = None;
|
||||
let mut write_bytes = None;
|
||||
|
||||
for line in io_content.lines() {
|
||||
if let Some(value) = line.strip_prefix("read_bytes:") {
|
||||
read_bytes = value.trim().parse().ok();
|
||||
} else if let Some(value) = line.strip_prefix("write_bytes:") {
|
||||
write_bytes = value.trim().parse().ok();
|
||||
}
|
||||
}
|
||||
|
||||
(read_bytes, write_bytes)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// Read working directory
|
||||
let working_directory = fs::read_link(format!("/proc/{pid}/cwd"))
|
||||
.ok()
|
||||
.map(|p| p.to_string_lossy().to_string());
|
||||
|
||||
// Read executable path
|
||||
let executable_path = fs::read_link(format!("/proc/{pid}/exe"))
|
||||
.ok()
|
||||
.map(|p| p.to_string_lossy().to_string());
|
||||
|
||||
Some(DetailedProcessInfo {
|
||||
pid,
|
||||
name,
|
||||
command,
|
||||
cpu_usage,
|
||||
mem_bytes,
|
||||
virtual_mem_bytes,
|
||||
shared_mem_bytes: None, // Would need to parse /proc/{pid}/statm for this
|
||||
thread_count,
|
||||
fd_count: None, // Would need to count entries in /proc/{pid}/fd
|
||||
status,
|
||||
parent_pid: None, // We already know the parent
|
||||
user_id: uid,
|
||||
group_id: gid,
|
||||
start_time,
|
||||
cpu_time_user: get_cpu_time_user(pid),
|
||||
cpu_time_system: get_cpu_time_system(pid),
|
||||
read_bytes,
|
||||
write_bytes,
|
||||
working_directory,
|
||||
executable_path,
|
||||
child_processes: Vec::new(), // Don't recurse
|
||||
threads: Vec::new(), // Not collected for child processes
|
||||
})
|
||||
}
|
||||
|
||||
/// Fallback for non-Linux: use sysinfo (less efficient but functional)
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn enumerate_child_processes_lightweight(
|
||||
parent_pid: u32,
|
||||
system: &sysinfo::System,
|
||||
) -> Vec<DetailedProcessInfo> {
|
||||
let mut children = Vec::new();
|
||||
|
||||
// On non-Linux, we have to iterate through all processes in sysinfo
|
||||
// This is less efficient but maintains cross-platform compatibility
|
||||
for (child_pid, child_process) in system.processes() {
|
||||
if let Some(parent) = child_process.parent() {
|
||||
if parent.as_u32() == parent_pid {
|
||||
let child_info = DetailedProcessInfo {
|
||||
pid: child_pid.as_u32(),
|
||||
name: child_process.name().to_string_lossy().to_string(),
|
||||
command: child_process
|
||||
.cmd()
|
||||
.iter()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
cpu_usage: child_process.cpu_usage(),
|
||||
mem_bytes: child_process.memory(),
|
||||
virtual_mem_bytes: child_process.virtual_memory(),
|
||||
shared_mem_bytes: None,
|
||||
thread_count: child_process
|
||||
.tasks()
|
||||
.map(|tasks| tasks.len() as u32)
|
||||
.unwrap_or(0),
|
||||
fd_count: None,
|
||||
status: format!("{:?}", child_process.status()),
|
||||
parent_pid: Some(parent_pid),
|
||||
// On non-Linux platforms, sysinfo UID/GID might not be accurate
|
||||
// Just use 0 as placeholder since we can't read /proc
|
||||
user_id: 0,
|
||||
group_id: 0,
|
||||
start_time: child_process.start_time(),
|
||||
cpu_time_user: 0, // Not available on non-Linux in our implementation
|
||||
cpu_time_system: 0,
|
||||
read_bytes: Some(child_process.disk_usage().read_bytes),
|
||||
write_bytes: Some(child_process.disk_usage().written_bytes),
|
||||
working_directory: child_process.cwd().map(|p| p.to_string_lossy().to_string()),
|
||||
executable_path: child_process.exe().map(|p| p.to_string_lossy().to_string()),
|
||||
child_processes: Vec::new(),
|
||||
threads: Vec::new(), // Not collected for non-Linux
|
||||
};
|
||||
children.push(child_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
children
|
||||
}
|
||||
|
||||
/// Collect thread information for a specific process (Linux only)
|
||||
#[cfg(target_os = "linux")]
|
||||
fn collect_thread_info(pid: u32) -> Vec<crate::types::ThreadInfo> {
|
||||
let mut threads = Vec::new();
|
||||
|
||||
// Read /proc/{pid}/task directory
|
||||
let task_dir = format!("/proc/{pid}/task");
|
||||
let Ok(entries) = fs::read_dir(&task_dir) else {
|
||||
return threads;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let tid_str = file_name.to_string_lossy();
|
||||
let Ok(tid) = tid_str.parse::<u32>() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Read thread name from comm
|
||||
let name = fs::read_to_string(format!("/proc/{pid}/task/{tid}/comm"))
|
||||
.unwrap_or_else(|_| format!("Thread-{tid}"))
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
// Read thread stat for CPU times and status
|
||||
let stat_path = format!("/proc/{pid}/task/{tid}/stat");
|
||||
let Ok(stat_content) = fs::read_to_string(&stat_path) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Parse stat file (similar format to process stat)
|
||||
// Fields: pid comm state ... utime stime ...
|
||||
let fields: Vec<&str> = stat_content.split_whitespace().collect();
|
||||
if fields.len() < 15 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Field 2 is state (R, S, D, Z, T, etc.)
|
||||
let status = fields
|
||||
.get(2)
|
||||
.and_then(|s| s.chars().next())
|
||||
.map(|c| match c {
|
||||
'R' => "Running",
|
||||
'S' => "Sleeping",
|
||||
'D' => "Disk Sleep",
|
||||
'Z' => "Zombie",
|
||||
'T' => "Stopped",
|
||||
't' => "Tracing Stop",
|
||||
'X' | 'x' => "Dead",
|
||||
_ => "Unknown",
|
||||
})
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
|
||||
// Field 13 is utime (user CPU time in clock ticks)
|
||||
// Field 14 is stime (system CPU time in clock ticks)
|
||||
let utime = fields
|
||||
.get(13)
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
let stime = fields
|
||||
.get(14)
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Convert clock ticks to microseconds (assuming 100 Hz)
|
||||
// 1 tick = 10ms = 10,000 microseconds
|
||||
let cpu_time_user = utime * 10_000;
|
||||
let cpu_time_system = stime * 10_000;
|
||||
|
||||
threads.push(crate::types::ThreadInfo {
|
||||
tid,
|
||||
name,
|
||||
cpu_time_user,
|
||||
cpu_time_system,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
threads
|
||||
}
|
||||
|
||||
/// Fallback for non-Linux: return empty thread list
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn collect_thread_info(_pid: u32) -> Vec<crate::types::ThreadInfo> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Collect detailed metrics for a specific process
|
||||
pub async fn collect_process_metrics(
|
||||
pid: u32,
|
||||
state: &AppState,
|
||||
) -> Result<ProcessMetricsResponse, String> {
|
||||
let mut system = state.sys.lock().await;
|
||||
|
||||
// OPTIMIZED: Only refresh the specific process we care about
|
||||
// This avoids polluting the main process list with threads and prevents race conditions
|
||||
system.refresh_processes_specifics(
|
||||
ProcessesToUpdate::Some(&[sysinfo::Pid::from_u32(pid)]),
|
||||
false,
|
||||
ProcessRefreshKind::nothing()
|
||||
.with_memory()
|
||||
.with_cpu()
|
||||
.with_disk_usage(),
|
||||
);
|
||||
|
||||
let process = system
|
||||
.process(sysinfo::Pid::from_u32(pid))
|
||||
.ok_or_else(|| format!("Process {pid} not found"))?;
|
||||
|
||||
// Get current timestamp
|
||||
let cached_at = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|e| format!("Time error: {e}"))?
|
||||
.as_secs();
|
||||
|
||||
// Extract all needed data from process while we have the lock
|
||||
let name = process.name().to_string_lossy().to_string();
|
||||
let command = process
|
||||
.cmd()
|
||||
.iter()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let cpu_usage = process.cpu_usage();
|
||||
let mem_bytes = process.memory();
|
||||
let virtual_mem_bytes = process.virtual_memory();
|
||||
let thread_count = process.tasks().map(|tasks| tasks.len() as u32).unwrap_or(0);
|
||||
let status = format!("{:?}", process.status());
|
||||
let parent_pid = process.parent().map(|p| p.as_u32());
|
||||
let start_time = process.start_time();
|
||||
|
||||
// Read UID and GID directly from /proc/{pid}/status for accuracy
|
||||
let (user_id, group_id) =
|
||||
if let Ok(status_content) = fs::read_to_string(format!("/proc/{pid}/status")) {
|
||||
let mut uid = 0u32;
|
||||
let mut gid = 0u32;
|
||||
|
||||
for line in status_content.lines() {
|
||||
if let Some(value) = line.strip_prefix("Uid:") {
|
||||
// Uid line format: "Uid: 1000 1000 1000 1000" (real, effective, saved, filesystem)
|
||||
// We want the real UID (first value)
|
||||
if let Some(uid_str) = value.split_whitespace().next() {
|
||||
uid = uid_str.parse().unwrap_or(0);
|
||||
}
|
||||
} else if let Some(value) = line.strip_prefix("Gid:") {
|
||||
// Gid line format: "Gid: 1000 1000 1000 1000" (real, effective, saved, filesystem)
|
||||
// We want the real GID (first value)
|
||||
if let Some(gid_str) = value.split_whitespace().next() {
|
||||
gid = gid_str.parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(uid, gid)
|
||||
} else {
|
||||
// Fallback if /proc read fails (non-Linux or permission issue)
|
||||
(0, 0)
|
||||
};
|
||||
|
||||
// Read I/O stats directly from /proc/{pid}/io
|
||||
// Use rchar/wchar to capture ALL I/O including cached reads (like htop/btop do)
|
||||
// sysinfo's total_read_bytes/total_written_bytes only count actual disk I/O
|
||||
let (read_bytes, write_bytes) =
|
||||
if let Ok(io_content) = fs::read_to_string(format!("/proc/{pid}/io")) {
|
||||
let mut rchar = 0u64;
|
||||
let mut wchar = 0u64;
|
||||
|
||||
for line in io_content.lines() {
|
||||
if let Some(value) = line.strip_prefix("rchar: ") {
|
||||
rchar = value.trim().parse().unwrap_or(0);
|
||||
} else if let Some(value) = line.strip_prefix("wchar: ") {
|
||||
wchar = value.trim().parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
|
||||
(Some(rchar), Some(wchar))
|
||||
} else {
|
||||
// Fallback to sysinfo if we can't read /proc (permissions, non-Linux, etc.)
|
||||
let disk_usage = process.disk_usage();
|
||||
(
|
||||
Some(disk_usage.total_read_bytes),
|
||||
Some(disk_usage.total_written_bytes),
|
||||
)
|
||||
};
|
||||
let working_directory = process.cwd().map(|p| p.to_string_lossy().to_string());
|
||||
let executable_path = process.exe().map(|p| p.to_string_lossy().to_string());
|
||||
|
||||
// Collect child processes using lightweight /proc access
|
||||
// This avoids the expensive system.refresh_processes_specifics(All) call
|
||||
let child_processes = enumerate_child_processes_lightweight(pid, &system);
|
||||
|
||||
// Release the system lock early (automatic when system goes out of scope)
|
||||
drop(system);
|
||||
|
||||
// Collect thread information (Linux only)
|
||||
let threads = collect_thread_info(pid);
|
||||
|
||||
// Now construct the detailed info without holding the lock
|
||||
let detailed_info = DetailedProcessInfo {
|
||||
pid,
|
||||
name,
|
||||
command,
|
||||
cpu_usage,
|
||||
mem_bytes,
|
||||
virtual_mem_bytes,
|
||||
shared_mem_bytes: None, // Not available from sysinfo
|
||||
thread_count,
|
||||
fd_count: None, // Not available from sysinfo on all platforms
|
||||
status,
|
||||
parent_pid,
|
||||
user_id,
|
||||
group_id,
|
||||
start_time,
|
||||
cpu_time_user: get_cpu_time_user(pid),
|
||||
cpu_time_system: get_cpu_time_system(pid),
|
||||
read_bytes,
|
||||
write_bytes,
|
||||
working_directory,
|
||||
executable_path,
|
||||
child_processes,
|
||||
threads,
|
||||
};
|
||||
|
||||
Ok(ProcessMetricsResponse {
|
||||
process: detailed_info,
|
||||
cached_at,
|
||||
})
|
||||
}
|
||||
|
||||
/// Collect journal entries for a specific process
|
||||
pub fn collect_journal_entries(pid: u32) -> Result<JournalResponse, String> {
|
||||
let output = Command::new("journalctl")
|
||||
.args([
|
||||
&format!("_PID={pid}"),
|
||||
"--output=json",
|
||||
"--lines=100",
|
||||
"--no-pager",
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute journalctl: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"journalctl failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut entries = Vec::new();
|
||||
|
||||
// Parse each line as JSON (journalctl outputs one JSON object per line)
|
||||
for line in stdout.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_str(line).map_err(|e| format!("Failed to parse journal JSON: {e}"))?;
|
||||
|
||||
// Extract relevant fields
|
||||
let timestamp_str = json
|
||||
.get("__REALTIME_TIMESTAMP")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0");
|
||||
|
||||
// Convert timestamp to ISO 8601 format
|
||||
let timestamp = if let Ok(ts_micros) = timestamp_str.parse::<u64>() {
|
||||
let ts_secs = ts_micros / 1_000_000;
|
||||
let ts_nanos = (ts_micros % 1_000_000) * 1000;
|
||||
let time = SystemTime::UNIX_EPOCH
|
||||
+ Duration::from_secs(ts_secs)
|
||||
+ Duration::from_nanos(ts_nanos);
|
||||
// Simple ISO 8601 format - we can improve this if needed
|
||||
format!("{time:?}")
|
||||
.replace("SystemTime { tv_sec: ", "")
|
||||
.replace(", tv_nsec: ", ".")
|
||||
.replace(" }", "")
|
||||
} else {
|
||||
timestamp_str.to_string()
|
||||
};
|
||||
|
||||
let priority = match json.get("PRIORITY").and_then(|v| v.as_str()) {
|
||||
Some("0") => LogLevel::Emergency,
|
||||
Some("1") => LogLevel::Alert,
|
||||
Some("2") => LogLevel::Critical,
|
||||
Some("3") => LogLevel::Error,
|
||||
Some("4") => LogLevel::Warning,
|
||||
Some("5") => LogLevel::Notice,
|
||||
Some("6") => LogLevel::Info,
|
||||
Some("7") => LogLevel::Debug,
|
||||
_ => LogLevel::Info,
|
||||
};
|
||||
|
||||
let message = json
|
||||
.get("MESSAGE")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
let unit = json
|
||||
.get("_SYSTEMD_UNIT")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let entry_pid = json
|
||||
.get("_PID")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<u32>().ok());
|
||||
|
||||
let comm = json
|
||||
.get("_COMM")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let uid = json
|
||||
.get("_UID")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<u32>().ok());
|
||||
|
||||
let gid = json
|
||||
.get("_GID")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<u32>().ok());
|
||||
|
||||
entries.push(JournalEntry {
|
||||
timestamp,
|
||||
priority,
|
||||
message,
|
||||
unit,
|
||||
pid: entry_pid,
|
||||
comm,
|
||||
uid,
|
||||
gid,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by timestamp (newest first)
|
||||
entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
||||
|
||||
let response_timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|e| format!("Time error: {e}"))?
|
||||
.as_secs();
|
||||
|
||||
let total_count = entries.len() as u32;
|
||||
let truncated = entries.len() >= 100; // We requested 100 lines, so if we got 100, there might be more
|
||||
|
||||
Ok(JournalResponse {
|
||||
entries,
|
||||
total_count,
|
||||
truncated,
|
||||
cached_at: response_timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user