From fa8140a124d083d4d936174d4e300b3b0d085ec4 Mon Sep 17 00:00:00 2001 From: main Date: Sat, 25 Apr 2026 15:53:24 -0400 Subject: Show process working directories --- crates/memview/src/linux/app/rows.rs | 4 +++ crates/memview/src/linux/app/tests.rs | 61 ++++++++++++++++++++++++++++++++++- crates/memview/src/linux/model.rs | 22 +++++++++++++ crates/memview/src/linux/probe.rs | 13 ++++++-- crates/memview/src/linux/ui.rs | 13 +++++--- 5 files changed, 105 insertions(+), 8 deletions(-) (limited to 'crates') diff --git a/crates/memview/src/linux/app/rows.rs b/crates/memview/src/linux/app/rows.rs index d6d904d..740c3e3 100644 --- a/crates/memview/src/linux/app/rows.rs +++ b/crates/memview/src/linux/app/rows.rs @@ -313,6 +313,10 @@ fn push_process_search_tree( fn process_matches(search: &Search, node: &ProcessNode) -> bool { search.matches(&node.name) || search.matches(&node.command) + || node + .cwd + .as_ref() + .is_some_and(|cwd| search.matches(cwd.as_str())) || search.matches(&node.username) || search.matches(&node.state) || search.matches(&node.pid.to_string()) diff --git a/crates/memview/src/linux/app/tests.rs b/crates/memview/src/linux/app/tests.rs index 583863a..62230e7 100644 --- a/crates/memview/src/linux/app/tests.rs +++ b/crates/memview/src/linux/app/tests.rs @@ -1,3 +1,4 @@ +use super::super::model::{MemoryRollup, ProcessCwd, ProcessTree, ProcessTreeStats}; use super::*; #[derive(Clone, Debug)] @@ -60,7 +61,7 @@ fn tmpfs_snapshot(mounts: Vec) -> Snapshot { elapsed: Duration::ZERO, meminfo: Meminfo::default(), overview: super::super::model::Overview::default(), - process_tree: super::super::model::ProcessTree::default(), + process_tree: ProcessTree::default(), shared_objects: Vec::new(), sysv_segments: Vec::new(), tmpfs_mounts: mounts, @@ -68,6 +69,48 @@ fn tmpfs_snapshot(mounts: Vec) -> Snapshot { } } +fn process_snapshot(nodes: Vec) -> Snapshot { + Snapshot { + captured_at: SystemTime::UNIX_EPOCH, + elapsed: Duration::ZERO, + meminfo: Meminfo::default(), + overview: super::super::model::Overview::default(), + process_tree: ProcessTree { + roots: (0..nodes.len()).collect(), + nodes, + stats: ProcessTreeStats::default(), + }, + shared_objects: Vec::new(), + sysv_segments: Vec::new(), + tmpfs_mounts: Vec::new(), + warnings: Vec::new(), + } +} + +fn process_node(pid: i32, command: &str, cwd: Option<&str>) -> ProcessNode { + let rollup = MemoryRollup { + pss: Bytes(1024), + rss: Bytes(1024), + ..MemoryRollup::default() + }; + ProcessNode { + pid: Pid(pid), + ppid: None, + name: format!("p{pid}"), + command: command.to_string(), + cwd: cwd.map(|path| ProcessCwd::new(PathBuf::from(path))), + username: "test".to_string(), + state: "S".to_string(), + threads: 1, + rollup, + subtree: rollup, + children: Vec::new(), + objects: Vec::new(), + rollup_state: LedgerState::Exact, + mappings_state: LedgerState::Deferred, + } +} + fn regex(pattern: &str) -> Search { Search::compile(pattern.to_string()) .expect("test regex compiles") @@ -133,6 +176,22 @@ fn fold_policy_respects_roots_leaves_and_manual_overrides() { assert_eq!(policy.row_fold(&7, 1, true, Bytes(99)), RowFold::Expanded); } +#[test] +fn process_search_matches_cwd() { + let mut app = App::new(); + app.snapshot = Some(process_snapshot(vec![process_node( + 42, + "rust-analyzer", + Some("/home/main/programming/projects/memview"), + )])); + app.search = Some(regex("memview")); + + app.rebuild_process_rows(); + + assert_eq!(app.process_rows().len(), 1); + assert_eq!(app.process_rows()[0].pid, Pid(42)); +} + #[test] fn pane_rows_can_select_deleted_row_successor_slot() { let mut rows = PaneRows::default(); diff --git a/crates/memview/src/linux/model.rs b/crates/memview/src/linux/model.rs index a680332..421b6f8 100644 --- a/crates/memview/src/linux/model.rs +++ b/crates/memview/src/linux/model.rs @@ -313,6 +313,7 @@ pub struct ProcessNode { pub ppid: Option, pub name: String, pub command: String, + pub cwd: Option, pub username: String, pub state: String, pub threads: u32, @@ -335,6 +336,27 @@ impl ProcessNode { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProcessCwd(String); + +impl ProcessCwd { + #[must_use] + pub fn new(path: PathBuf) -> Self { + Self(path.display().to_string()) + } + + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Display for ProcessCwd { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + #[derive(Clone, Debug, Default)] pub struct ProcessTree { pub roots: Vec, diff --git a/crates/memview/src/linux/probe.rs b/crates/memview/src/linux/probe.rs index b47a5fa..93d4d6a 100644 --- a/crates/memview/src/linux/probe.rs +++ b/crates/memview/src/linux/probe.rs @@ -1,7 +1,7 @@ use super::model::{ Bytes, LedgerState, Meminfo, MeminfoEntry, MemoryRollup, Metric, ObjectConsumer, ObjectKind, - ObjectUsage, Overview, Pid, ProcessNode, ProcessTree, ProcessTreeStats, SharedObject, Snapshot, - SysvSegment, TmpfsMount, TmpfsNode, TmpfsNodeKind, + ObjectUsage, Overview, Pid, ProcessCwd, ProcessNode, ProcessTree, ProcessTreeStats, + SharedObject, Snapshot, SysvSegment, TmpfsMount, TmpfsNode, TmpfsNodeKind, }; use color_eyre::eyre::{Context, Result, eyre}; use std::cmp::Reverse; @@ -452,6 +452,7 @@ struct ScannedProcess { ppid: Option, name: String, command: String, + cwd: Option, username: String, state: String, threads: u32, @@ -515,6 +516,7 @@ fn scan_process_shell( let status = parse_status(&status_text); let command = read_cmdline(&root).unwrap_or_else(|| status.name.clone()); + let cwd = read_cwd(&root); let username = lookup_username(status.uid, usernames); let fallback_rollup = MemoryRollup { rss: status.vm_rss, @@ -536,6 +538,7 @@ fn scan_process_shell( ppid: status.ppid, name: status.name, command, + cwd, username, state: status.state, threads: status.threads, @@ -677,6 +680,10 @@ fn read_cmdline(root: &Path) -> Option { } } +fn read_cwd(root: &Path) -> Option { + fs::read_link(root.join("cwd")).ok().map(ProcessCwd::new) +} + fn lookup_username(uid: u32, cache: &mut BTreeMap) -> String { cache .entry(uid) @@ -947,6 +954,7 @@ fn build_process_tree(processes: Vec, stats: ProcessTreeStats) - ppid: process.ppid, name: process.name, command: process.command, + cwd: process.cwd, username: process.username, state: process.state, threads: process.threads, @@ -1354,6 +1362,7 @@ mod tests { ppid: None, name: format!("p{pid}"), command: format!("p{pid} --serve"), + cwd: None, username: "test".to_string(), state: "S".to_string(), threads: 1, diff --git a/crates/memview/src/linux/ui.rs b/crates/memview/src/linux/ui.rs index 8d0b8a2..853d839 100644 --- a/crates/memview/src/linux/ui.rs +++ b/crates/memview/src/linux/ui.rs @@ -295,7 +295,7 @@ fn render_processes(frame: &mut Frame<'_>, app: &App, snapshot: &Snapshot, area: .split(area); let right = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(12), Constraint::Min(8)]) + .constraints([Constraint::Length(16), Constraint::Min(8)]) .split(columns[1]); if snapshot.process_tree.nodes.is_empty() && app.process_scan_started_at.is_some() { @@ -357,11 +357,14 @@ fn render_processes(frame: &mut Frame<'_>, app: &App, snapshot: &Snapshot, area: if let Some(process) = app.selected_process() { let mut details = search_summary_lines(app, capacity); + details.push(Line::from(vec![Span::styled( + process.title(), + Style::default().fg(ACCENT).add_modifier(Modifier::BOLD), + )])); + if let Some(cwd) = &process.cwd { + details.push(detail_line("CWD", cwd.as_str())); + } details.extend([ - Line::from(vec![Span::styled( - process.title(), - Style::default().fg(ACCENT).add_modifier(Modifier::BOLD), - )]), detail_line("State", &process.state), detail_line("Threads", &process.threads.to_string()), detail_line("PSS", &process.rollup.pss.human_exact()), -- cgit v1.2.3