mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-09-22 17:28:19 +02:00
bug: fix colon at end of process name for now on Linux (#1800)
* driveby use rustix * refactor some code aeround * bug: fix colon at end of process name for now * clippy * comments * changelog * some other changes + test * extra test
This commit is contained in:
parent
6409f67dbc
commit
47cc0b346a
@ -26,6 +26,10 @@ That said, these are more guidelines rather than hardset rules, though the proje
|
|||||||
|
|
||||||
- [#1793](https://github.com/ClementTsang/bottom/pull/1793): Add support for threads in Linux.
|
- [#1793](https://github.com/ClementTsang/bottom/pull/1793): Add support for threads in Linux.
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- [#1800](https://github.com/ClementTsang/bottom/pull/1800): Fix colon at end of process name in Linux.
|
||||||
|
|
||||||
## [0.11.1] - 2025-08-15
|
## [0.11.1] - 2025-08-15
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
@ -137,7 +137,7 @@ fn read_proc(
|
|||||||
thread_parent: Option<Pid>,
|
thread_parent: Option<Pid>,
|
||||||
) -> CollectionResult<(ProcessHarvest, u64)> {
|
) -> CollectionResult<(ProcessHarvest, u64)> {
|
||||||
let Process {
|
let Process {
|
||||||
pid: _,
|
pid: _pid,
|
||||||
uid,
|
uid,
|
||||||
stat,
|
stat,
|
||||||
io,
|
io,
|
||||||
@ -221,39 +221,56 @@ fn read_proc(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (command, name) = {
|
let (command, name) = {
|
||||||
let truncated_name = stat.comm;
|
let comm = stat.comm;
|
||||||
if let Some(cmdline) = cmdline {
|
if let Some(cmdline) = cmdline {
|
||||||
if cmdline.is_empty() {
|
if cmdline.is_empty() {
|
||||||
(concat_string!("[", truncated_name, "]"), truncated_name)
|
(concat_string!("[", comm, "]"), comm)
|
||||||
} else {
|
} else {
|
||||||
let name = if truncated_name.len() >= MAX_STAT_NAME_LEN {
|
// If the comm fits then we'll default to whatever is set.
|
||||||
let first_part = match cmdline.split_once(' ') {
|
// If it doesn't, we need to do some magic to determine what it's
|
||||||
Some((first, _)) => first,
|
// supposed to be.
|
||||||
None => &cmdline,
|
//
|
||||||
};
|
// We follow something similar to how htop does it to identify a valid name based on the cmdline.
|
||||||
|
// - https://github.com/htop-dev/htop/blob/bcb18ef82269c68d54a160290e5f8b2e939674ec/Process.c#L268 (kinda)
|
||||||
|
// - https://github.com/htop-dev/htop/blob/bcb18ef82269c68d54a160290e5f8b2e939674ec/Process.c#L573
|
||||||
|
//
|
||||||
|
// Also note that cmdline is (for us) separated by \0.
|
||||||
|
|
||||||
// We're only interested in the executable part, not the file path (part of command),
|
// TODO: We might want to re-evaluate if we want to do it like this,
|
||||||
// so strip everything but the command name if needed.
|
// as it turns out I was dumb and sometimes comm != process name...
|
||||||
let command = match first_part.rsplit_once('/') {
|
//
|
||||||
Some((_, last)) => last,
|
// What we should do is store:
|
||||||
None => first_part,
|
// - basename (what we're kinda doing now, except we're gating on comm length)
|
||||||
};
|
// - command (full thing)
|
||||||
|
// - comm (as a separate thing)
|
||||||
// TODO: Needed as some processes have stuff like "systemd-userwork: waiting..."
|
//
|
||||||
// command.trim_end_matches(':').to_string()
|
// Stuff like htop also offers the option to "highlight" basename and comm in command. Might be neat?
|
||||||
|
let name = if comm.len() >= MAX_STAT_NAME_LEN {
|
||||||
command.to_string()
|
name_from_cmdline(&cmdline)
|
||||||
} else {
|
} else {
|
||||||
truncated_name
|
comm
|
||||||
};
|
};
|
||||||
|
|
||||||
(cmdline, name)
|
(cmdline, name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(truncated_name.clone(), truncated_name)
|
(comm.clone(), comm)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We have moved command processing here.
|
||||||
|
// SAFETY: We are only replacing a single char (NUL) with another single char (space).
|
||||||
|
|
||||||
|
let mut command = command;
|
||||||
|
let buf_mut = unsafe { command.as_mut_vec() };
|
||||||
|
|
||||||
|
for byte in buf_mut {
|
||||||
|
if *byte == 0 {
|
||||||
|
const SPACE: u8 = ' '.to_ascii_lowercase() as u8;
|
||||||
|
*byte = SPACE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
ProcessHarvest {
|
ProcessHarvest {
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
@ -284,6 +301,22 @@ fn read_proc(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn name_from_cmdline(cmdline: &str) -> String {
|
||||||
|
let mut start = 0;
|
||||||
|
let mut end = cmdline.len();
|
||||||
|
|
||||||
|
for (i, c) in cmdline.chars().enumerate() {
|
||||||
|
if c == '/' {
|
||||||
|
start = i + 1;
|
||||||
|
} else if c == '\0' || c == ':' {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdline[start..end].to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct PrevProc<'a> {
|
pub(crate) struct PrevProc<'a> {
|
||||||
pub prev_idle: &'a mut f64,
|
pub prev_idle: &'a mut f64,
|
||||||
pub prev_non_idle: &'a mut f64,
|
pub prev_non_idle: &'a mut f64,
|
||||||
@ -502,4 +535,19 @@ mod tests {
|
|||||||
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 10 values"
|
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 10 values"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_name_from_cmdline() {
|
||||||
|
assert_eq!(name_from_cmdline("/usr/bin/btm"), "btm");
|
||||||
|
assert_eq!(name_from_cmdline("/usr/bin/btm\0--asdf\0--asdf/gkj"), "btm");
|
||||||
|
assert_eq!(name_from_cmdline("/usr/bin/btm:"), "btm");
|
||||||
|
assert_eq!(name_from_cmdline("/usr/bin/b tm"), "b tm");
|
||||||
|
assert_eq!(name_from_cmdline("/usr/bin/b tm:"), "b tm");
|
||||||
|
assert_eq!(name_from_cmdline("/usr/bin/b tm\0--test"), "b tm");
|
||||||
|
assert_eq!(name_from_cmdline("/usr/bin/b tm:\0--test"), "b tm");
|
||||||
|
assert_eq!(
|
||||||
|
name_from_cmdline("/usr/bin/b t m:\0--\"test thing\""),
|
||||||
|
"b t m"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,7 +237,7 @@ impl Process {
|
|||||||
) -> anyhow::Result<(Process, Vec<PathBuf>)> {
|
) -> anyhow::Result<(Process, Vec<PathBuf>)> {
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
|
|
||||||
let fd = rustix::fs::openat(
|
let pid_dir = rustix::fs::openat(
|
||||||
rustix::fs::CWD,
|
rustix::fs::CWD,
|
||||||
pid_path.as_path(),
|
pid_path.as_path(),
|
||||||
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||||
@ -257,7 +257,7 @@ impl Process {
|
|||||||
.ok_or_else(|| anyhow!("PID for {pid_path:?} was not found"))?;
|
.ok_or_else(|| anyhow!("PID for {pid_path:?} was not found"))?;
|
||||||
|
|
||||||
let uid = {
|
let uid = {
|
||||||
let metadata = rustix::fs::fstat(&fd);
|
let metadata = rustix::fs::fstat(&pid_dir);
|
||||||
match metadata {
|
match metadata {
|
||||||
Ok(md) => Some(md.st_uid),
|
Ok(md) => Some(md.st_uid),
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@ -271,10 +271,10 @@ impl Process {
|
|||||||
|
|
||||||
// Stat is pretty long, do this first to pre-allocate up-front.
|
// Stat is pretty long, do this first to pre-allocate up-front.
|
||||||
let stat =
|
let stat =
|
||||||
open_at(&mut root, "stat", &fd).and_then(|file| Stat::from_file(file, buffer))?;
|
open_at(&mut root, "stat", &pid_dir).and_then(|file| Stat::from_file(file, buffer))?;
|
||||||
reset(&mut root, buffer);
|
reset(&mut root, buffer);
|
||||||
|
|
||||||
let cmdline = if cmdline(&mut root, &fd, buffer).is_ok() {
|
let cmdline = if cmdline(&mut root, &pid_dir, buffer).is_ok() {
|
||||||
// The clone will give a string with the capacity of the length of buffer, don't worry.
|
// The clone will give a string with the capacity of the length of buffer, don't worry.
|
||||||
Some(buffer.clone())
|
Some(buffer.clone())
|
||||||
} else {
|
} else {
|
||||||
@ -282,37 +282,13 @@ impl Process {
|
|||||||
};
|
};
|
||||||
reset(&mut root, buffer);
|
reset(&mut root, buffer);
|
||||||
|
|
||||||
let io = open_at(&mut root, "io", &fd)
|
let io = open_at(&mut root, "io", &pid_dir)
|
||||||
.and_then(|file| Io::from_file(file, buffer))
|
.and_then(|file| Io::from_file(file, buffer))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
reset(&mut root, buffer);
|
reset(&mut root, buffer);
|
||||||
|
|
||||||
let threads = if get_threads {
|
let threads = threads(&mut root, pid, get_threads);
|
||||||
root.push("task");
|
|
||||||
|
|
||||||
if let Ok(task) = std::fs::read_dir(root) {
|
|
||||||
let pid_str = pid.to_string();
|
|
||||||
|
|
||||||
task.flatten()
|
|
||||||
.filter_map(|thread_dir| {
|
|
||||||
let file_name = thread_dir.file_name();
|
|
||||||
let file_name = file_name.to_string_lossy();
|
|
||||||
let file_name = file_name.trim();
|
|
||||||
|
|
||||||
if is_str_numeric(file_name) && file_name != pid_str {
|
|
||||||
Some(thread_dir.path())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Process {
|
Process {
|
||||||
@ -329,19 +305,7 @@ impl Process {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cmdline(root: &mut PathBuf, fd: &OwnedFd, buffer: &mut String) -> anyhow::Result<()> {
|
fn cmdline(root: &mut PathBuf, fd: &OwnedFd, buffer: &mut String) -> anyhow::Result<()> {
|
||||||
let _ = open_at(root, "cmdline", fd)
|
let _ = open_at(root, "cmdline", fd).map(|mut file| file.read_to_string(buffer))?;
|
||||||
.map(|mut file| file.read_to_string(buffer))
|
|
||||||
.inspect(|_| {
|
|
||||||
// SAFETY: We are only replacing a single char (NUL) with another single char (space).
|
|
||||||
let buf_mut = unsafe { buffer.as_mut_vec() };
|
|
||||||
|
|
||||||
for byte in buf_mut {
|
|
||||||
if *byte == 0 {
|
|
||||||
const SPACE: u8 = ' '.to_ascii_lowercase() as u8;
|
|
||||||
*byte = SPACE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -356,3 +320,40 @@ fn open_at(root: &mut PathBuf, child: &str, fd: &OwnedFd) -> anyhow::Result<File
|
|||||||
|
|
||||||
Ok(File::from(new_fd))
|
Ok(File::from(new_fd))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn threads(root: &mut PathBuf, pid: Pid, get_threads: bool) -> Vec<PathBuf> {
|
||||||
|
if get_threads {
|
||||||
|
root.push("task");
|
||||||
|
|
||||||
|
let Ok(task_dir) = rustix::fs::openat(
|
||||||
|
rustix::fs::CWD,
|
||||||
|
root.as_path(),
|
||||||
|
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||||
|
Mode::empty(),
|
||||||
|
) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(task) = rustix::fs::Dir::read_from(task_dir) {
|
||||||
|
let pid_str = pid.to_string();
|
||||||
|
|
||||||
|
return task
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|thread_dir| {
|
||||||
|
let file_name = thread_dir.file_name();
|
||||||
|
let file_name = file_name.to_string_lossy();
|
||||||
|
let file_name = file_name.trim();
|
||||||
|
|
||||||
|
if is_str_numeric(file_name) && file_name != pid_str {
|
||||||
|
Some(root.join(file_name).to_path_buf())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user