#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(dead_code)]
use core::ffi::{c_char, c_int};
use std::ffi::CString;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::os::unix::io::FromRawFd;
use crate::ported::functionbar::Ncurses;
use crate::ported::incset::IncSet_new;
use crate::ported::infoscreen::{
InfoScreen, InfoScreenClass, InfoScreen_addLine, InfoScreen_done, InfoScreen_drawTitled,
InfoScreen_init,
};
use crate::ported::listitem::ListItem_new;
use crate::ported::object::{Object, ObjectClass};
use crate::ported::panel::{
Panel_getSelectedIndex, Panel_new, Panel_prune, Panel_setHeader, Panel_setSelected,
};
use crate::ported::process::{
Process, Process_getCommand, Process_getPid, Process_getThreadGroup, Process_isThread,
};
use crate::ported::vector::{Vector_insertionSort, Vector_new};
const LSOF_DATACOL_COUNT: usize = 8;
const VECTOR_DEFAULT_SIZE: i32 = 10;
const INT16_MAX: usize = 32767;
pub struct OpenFiles_Data {
pub data: [Option<String>; LSOF_DATACOL_COUNT],
}
impl OpenFiles_Data {
fn empty() -> OpenFiles_Data {
OpenFiles_Data {
data: Default::default(),
}
}
}
pub struct OpenFiles_FileData {
pub data: OpenFiles_Data,
}
pub struct OpenFiles_ProcessData {
pub data: OpenFiles_Data,
pub error: i32,
pub cols: [i32; LSOF_DATACOL_COUNT],
pub files: Vec<OpenFiles_FileData>,
}
impl OpenFiles_ProcessData {
fn empty() -> OpenFiles_ProcessData {
let mut pdata = OpenFiles_ProcessData {
data: OpenFiles_Data::empty(),
error: 0,
cols: [0; LSOF_DATACOL_COUNT],
files: Vec::new(),
};
pdata.cols[getIndexForType(b's')] = 8;
pdata.cols[getIndexForType(b'o')] = 8;
pdata.cols[getIndexForType(b'i')] = 8;
pdata
}
fn parseLsofFields<R: BufRead>(&mut self, mut reader: R) -> bool {
let mut lsofIncludesFileSize = false;
let mut current: Option<usize> = None;
let mut buf: Vec<u8> = Vec::new();
loop {
buf.clear();
let n = reader.read_until(b'\n', &mut buf).unwrap_or_default();
if n == 0 {
break;
}
if buf.last() == Some(&b'\n') {
buf.pop();
}
if buf.is_empty() {
continue;
}
let cmd = buf[0];
if cmd == b'f' {
self.files.push(OpenFiles_FileData {
data: OpenFiles_Data::empty(),
});
current = Some(self.files.len() - 1);
}
match cmd {
b'f' | b'a' | b'D' | b'i' | b'n' | b's' | b't' => {
let index = getIndexForType(cmd);
let value = String::from_utf8_lossy(&buf[1..]).into_owned();
let dlen = value.len();
{
let item = match current {
None => &mut self.data,
Some(i) => &mut self.files[i].data,
};
item.data[index] = Some(value);
}
if dlen > self.cols[index] as usize {
self.cols[index] = dlen.min(INT16_MAX) as i32;
}
}
b'o' => {
let index = getIndexForType(cmd);
let rest = &buf[1..];
let value_bytes: &[u8] = if rest.starts_with(b"0t") {
&buf[3..]
} else {
rest
};
let value = String::from_utf8_lossy(value_bytes).into_owned();
let dlen = value.len();
{
let item = match current {
None => &mut self.data,
Some(i) => &mut self.files[i].data,
};
item.data[index] = Some(value);
}
if dlen > self.cols[index] as usize {
self.cols[index] = dlen.min(INT16_MAX) as i32;
}
}
_ => {}
}
if cmd == b's' {
lsofIncludesFileSize = true;
}
}
lsofIncludesFileSize
}
}
pub fn getIndexForType(type_: u8) -> usize {
match type_ {
b'f' => 0,
b'a' => 1,
b'D' => 2,
b'i' => 3,
b'n' => 4,
b's' => 5,
b't' => 6,
b'o' => 7,
_ => unreachable!("getIndexForType: invalid lsof -F type (C abort())"),
}
}
pub fn getDataForType(data: &OpenFiles_Data, type_: u8) -> &str {
let index = getIndexForType(type_);
match &data.data[index] {
Some(s) => s.as_str(),
None => "",
}
}
pub struct OpenFilesScreen {
pub super_: InfoScreen,
pub pid: i32,
}
impl InfoScreenClass for OpenFilesScreen {
fn super_InfoScreen(&mut self) -> &mut InfoScreen {
&mut self.super_
}
fn draw(&mut self) {
OpenFilesScreen_draw(&mut self.super_);
}
fn scan(&mut self) {
OpenFilesScreen_scan(self);
}
fn has_scan(&self) -> bool {
true
}
}
pub fn OpenFilesScreen_new(process: &Process) -> OpenFilesScreen {
let list_item_class: &'static ObjectClass = ListItem_new("", 0).klass();
let mut this = OpenFilesScreen {
super_: InfoScreen {
process: core::ptr::null(),
display: Panel_new(0, 0, 0, 0, None),
inc: IncSet_new(None),
lines: Vector_new(list_item_class, true, VECTOR_DEFAULT_SIZE),
},
pid: 0,
};
if Process_isThread(process) {
this.pid = Process_getThreadGroup(process);
} else {
this.pid = Process_getPid(process);
}
InfoScreen_init(
&mut this.super_,
process as *const Process,
None,
Ncurses::lines() - 2,
" FD TYPE MODE DEVICE SIZE OFFSET NODE NAME",
);
this
}
pub fn OpenFilesScreen_delete(this: OpenFilesScreen) {
let OpenFilesScreen { super_, pid } = this;
InfoScreen_done(super_);
let _ = pid;
}
pub fn OpenFilesScreen_draw(this: &mut InfoScreen) {
let p = unsafe { &*this.process };
let pid = if Process_isThread(p) {
Process_getThreadGroup(p)
} else {
Process_getPid(p)
};
let cmd = match Process_getCommand(p) {
Some(b) => String::from_utf8_lossy(b).into_owned(),
None => String::new(),
};
let title = format!("Snapshot of files open in process {} - {}", pid, cmd);
InfoScreen_drawTitled(this, &title);
}
pub fn OpenFilesScreen_getProcessData(pid: i32) -> OpenFiles_ProcessData {
let mut pdata = OpenFiles_ProcessData::empty();
let mut fdpair: [c_int; 2] = [-1, -1];
if unsafe { libc::pipe(fdpair.as_mut_ptr()) } < 0 {
pdata.error = 1;
return pdata;
}
let c_lsof = CString::new("lsof").expect("no interior NUL");
let c_dash_p_cap = CString::new("-P").expect("no interior NUL");
let c_dash_o = CString::new("-o").expect("no interior NUL");
let c_dash_p = CString::new("-p").expect("no interior NUL");
let c_pid = CString::new(pid.to_string()).expect("no interior NUL");
let c_dash_f = CString::new("-F").expect("no interior NUL");
let argv: [*const c_char; 7] = [
c_lsof.as_ptr(),
c_dash_p_cap.as_ptr(),
c_dash_o.as_ptr(),
c_dash_p.as_ptr(),
c_pid.as_ptr(),
c_dash_f.as_ptr(),
core::ptr::null(),
];
let child = unsafe { libc::fork() };
if child < 0 {
unsafe {
libc::close(fdpair[1]);
libc::close(fdpair[0]);
}
pdata.error = 1;
return pdata;
}
if child == 0 {
unsafe {
libc::close(fdpair[0]);
libc::dup2(fdpair[1], libc::STDOUT_FILENO);
libc::close(fdpair[1]);
let fdnull = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY);
if fdnull < 0 {
libc::_exit(1);
}
libc::dup2(fdnull, libc::STDERR_FILENO);
libc::close(fdnull);
libc::execvp(c_lsof.as_ptr(), argv.as_ptr());
libc::_exit(127);
}
}
unsafe {
libc::close(fdpair[1]);
}
let lsofIncludesFileSize = {
let file = unsafe { File::from_raw_fd(fdpair[0]) };
let reader = BufReader::new(file);
pdata.parseLsofFields(reader)
};
let mut wstatus: c_int = 0;
let ret = loop {
let r = unsafe { libc::waitpid(child, &mut wstatus, 0) };
if r == -1 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
continue;
}
break r;
};
if ret < 0 {
pdata.files.clear();
pdata.error = 1;
return pdata;
}
if !libc::WIFEXITED(wstatus) {
pdata.error = 1;
} else {
pdata.error = libc::WEXITSTATUS(wstatus);
}
if lsofIncludesFileSize {
return pdata;
}
let fileSizeIndex = getIndexForType(b's');
for fdata in pdata.files.iter_mut() {
let filename = getDataForType(&fdata.data, b'n').to_string();
let cfn = match CString::new(filename) {
Ok(c) => c,
Err(_) => continue,
};
let mut sb: libc::stat = unsafe { core::mem::zeroed() };
if unsafe { libc::stat(cfn.as_ptr(), &mut sb) } == 0 {
fdata.data.data[fileSizeIndex] = Some(format!("{}", sb.st_size as u64));
}
}
pdata
}
pub fn OpenFiles_Data_clear(data: &mut OpenFiles_Data) {
for cell in data.data.iter_mut() {
*cell = None;
}
}
pub fn OpenFilesScreen_scan(this: &mut OpenFilesScreen) {
let idx = Panel_getSelectedIndex(&this.super_.display);
Panel_prune(&mut this.super_.display);
let pdata = OpenFilesScreen_getProcessData(this.pid);
if pdata.error == 127 {
InfoScreen_addLine(
&mut this.super_,
"Could not execute 'lsof'. Please make sure it is available in your $PATH.",
);
} else if pdata.error == 1 {
InfoScreen_addLine(&mut this.super_, "Failed listing open files.");
} else {
let w_size = pdata.cols[getIndexForType(b's')] as usize;
let w_offset = pdata.cols[getIndexForType(b'o')] as usize;
let w_node = pdata.cols[getIndexForType(b'i')] as usize;
let hdrbuf = format!(
"{:>5.5} {:<7.7} {:<4.4} {:>6.6} {:>ws$} {:>wo$} {:>wn$} {}",
"FD",
"TYPE",
"MODE",
"DEVICE",
"SIZE",
"OFFSET",
"NODE",
"NAME",
ws = w_size,
wo = w_offset,
wn = w_node,
);
Panel_setHeader(&mut this.super_.display, &hdrbuf);
for fdata in &pdata.files {
let data = &fdata.data;
let entry = format!(
"{:>5.5} {:<7.7} {:<4.4} {:>6.6} {:>ws$} {:>wo$} {:>wn$} {}",
getDataForType(data, b'f'),
getDataForType(data, b't'),
getDataForType(data, b'a'),
getDataForType(data, b'D'),
getDataForType(data, b's'),
getDataForType(data, b'o'),
getDataForType(data, b'i'),
getDataForType(data, b'n'),
ws = w_size,
wo = w_offset,
wn = w_node,
);
InfoScreen_addLine(&mut this.super_, &entry);
}
}
Vector_insertionSort(&mut this.super_.lines);
this.super_
.display
.items
.sort_by(|a, b| a.object().compare(b.object()).cmp(&0));
Panel_setSelected(&mut this.super_.display, idx);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::incset::IncSet_filter;
use crate::ported::listitem::ListItem;
use crate::ported::panel::{Panel_headerHeight, Panel_size};
use crate::ported::process::{Process, Process_setPid, Process_setThreadGroup};
use crate::ported::vector::{Vector_get, Vector_size};
use std::io::Cursor;
#[test]
fn get_index_for_type_maps_every_letter() {
assert_eq!(getIndexForType(b'f'), 0);
assert_eq!(getIndexForType(b'a'), 1);
assert_eq!(getIndexForType(b'D'), 2);
assert_eq!(getIndexForType(b'i'), 3);
assert_eq!(getIndexForType(b'n'), 4);
assert_eq!(getIndexForType(b's'), 5);
assert_eq!(getIndexForType(b't'), 6);
assert_eq!(getIndexForType(b'o'), 7);
for &c in b"faDinsto" {
assert!(getIndexForType(c) < LSOF_DATACOL_COUNT);
}
}
#[test]
#[should_panic(expected = "getIndexForType")]
fn get_index_for_type_aborts_on_unknown() {
let _ = getIndexForType(b'z');
}
fn data_with(pairs: &[(u8, &str)]) -> OpenFiles_Data {
let mut d = OpenFiles_Data::empty();
for &(t, v) in pairs {
d.data[getIndexForType(t)] = Some(v.to_string());
}
d
}
#[test]
fn get_data_for_type_returns_cell_or_empty() {
let d = data_with(&[(b'n', "/etc/passwd"), (b'f', "3")]);
assert_eq!(getDataForType(&d, b'n'), "/etc/passwd");
assert_eq!(getDataForType(&d, b'f'), "3");
assert_eq!(getDataForType(&d, b't'), "");
assert_eq!(getDataForType(&d, b's'), "");
assert_eq!(getDataForType(&d, b'o'), "");
}
#[test]
fn get_data_for_type_all_empty_by_default() {
let d = OpenFiles_Data::empty();
for &c in b"faDinsto" {
assert_eq!(getDataForType(&d, c), "");
}
}
fn parse(sample: &str) -> OpenFiles_ProcessData {
let mut pdata = OpenFiles_ProcessData::empty();
pdata.parseLsofFields(Cursor::new(sample.as_bytes().to_vec()));
pdata
}
#[test]
fn parse_splits_one_file_into_columns() {
let sample = "f3\nar\ntREG\nD8,1\ns1024\no0t512\ni98765\nn/etc/passwd\n";
let pdata = parse(sample);
assert_eq!(pdata.files.len(), 1);
let d = &pdata.files[0].data;
assert_eq!(getDataForType(d, b'f'), "3");
assert_eq!(getDataForType(d, b'a'), "r");
assert_eq!(getDataForType(d, b't'), "REG");
assert_eq!(getDataForType(d, b'D'), "8,1");
assert_eq!(getDataForType(d, b's'), "1024");
assert_eq!(getDataForType(d, b'o'), "512");
assert_eq!(getDataForType(d, b'i'), "98765");
assert_eq!(getDataForType(d, b'n'), "/etc/passwd");
}
#[test]
fn parse_offset_without_0t_prefix_is_kept_verbatim() {
let sample = "f1\no0x1f\n";
let pdata = parse(sample);
assert_eq!(getDataForType(&pdata.files[0].data, b'o'), "0x1f");
}
#[test]
fn parse_new_f_record_starts_a_new_file() {
let sample = "f0\nn/dev/tty\nf1\nn/tmp/a\nf2\nn/tmp/b\n";
let pdata = parse(sample);
assert_eq!(pdata.files.len(), 3);
assert_eq!(getDataForType(&pdata.files[0].data, b'f'), "0");
assert_eq!(getDataForType(&pdata.files[0].data, b'n'), "/dev/tty");
assert_eq!(getDataForType(&pdata.files[1].data, b'f'), "1");
assert_eq!(getDataForType(&pdata.files[1].data, b'n'), "/tmp/a");
assert_eq!(getDataForType(&pdata.files[2].data, b'f'), "2");
assert_eq!(getDataForType(&pdata.files[2].data, b'n'), "/tmp/b");
}
#[test]
fn parse_fields_before_first_f_go_to_process_row() {
let sample = "n/process/level\nf7\nn/file/level\n";
let pdata = parse(sample);
assert_eq!(getDataForType(&pdata.data, b'n'), "/process/level");
assert_eq!(pdata.files.len(), 1);
assert_eq!(getDataForType(&pdata.files[0].data, b'n'), "/file/level");
}
#[test]
fn parse_ignores_unknown_and_process_only_fields() {
let sample = "p1234\ncbash\ng1000\nu501\nPTCP\nf3\nn/x\n";
let pdata = parse(sample);
assert_eq!(pdata.files.len(), 1);
assert_eq!(getDataForType(&pdata.files[0].data, b'n'), "/x");
}
#[test]
fn parse_tracks_column_widths_with_seed_of_8() {
let sample = "f1\ns123456789012\no0t99\ni7\n"; let pdata = parse(sample);
assert_eq!(pdata.cols[getIndexForType(b's')], 12); assert_eq!(pdata.cols[getIndexForType(b'o')], 8); assert_eq!(pdata.cols[getIndexForType(b'i')], 8); assert_eq!(pdata.cols[getIndexForType(b'n')], 0);
}
#[test]
fn parse_column_width_is_clamped_to_int16_max() {
let huge = "x".repeat(40000);
let sample = format!("f1\nn{huge}\n");
let pdata = parse(&sample);
assert_eq!(pdata.cols[getIndexForType(b'n')], INT16_MAX as i32);
}
#[test]
fn parse_reports_size_field_presence() {
let mut with_size = OpenFiles_ProcessData::empty();
assert!(with_size.parseLsofFields(Cursor::new(b"f1\ns42\n".to_vec())));
let mut without_size = OpenFiles_ProcessData::empty();
assert!(!without_size.parseLsofFields(Cursor::new(b"f1\nn/x\n".to_vec())));
}
#[test]
fn parse_handles_last_line_without_trailing_newline() {
let sample = "f9\nn/no/newline";
let pdata = parse(sample);
assert_eq!(pdata.files.len(), 1);
assert_eq!(getDataForType(&pdata.files[0].data, b'n'), "/no/newline");
}
#[test]
fn parse_empty_stream_yields_no_files() {
let pdata = parse("");
assert_eq!(pdata.files.len(), 0);
assert_eq!(pdata.cols[getIndexForType(b's')], 8);
}
#[test]
fn parse_later_field_overwrites_earlier_same_type() {
let sample = "f1\nn/first\nn/second\n";
let pdata = parse(sample);
assert_eq!(getDataForType(&pdata.files[0].data, b'n'), "/second");
}
const HEADER: &str = " FD TYPE MODE DEVICE SIZE OFFSET NODE NAME";
#[test]
fn new_uses_pid_for_a_non_thread() {
let mut p = Process::default();
Process_setPid(&mut p, 4321);
Process_setThreadGroup(&mut p, 4000);
assert!(!Process_isThread(&p));
let s = OpenFilesScreen_new(&p);
assert_eq!(s.pid, 4321);
}
#[test]
fn new_uses_thread_group_for_a_thread() {
let mut p = Process::default();
Process_setPid(&mut p, 4321);
Process_setThreadGroup(&mut p, 4000);
p.isUserlandThread = true;
assert!(Process_isThread(&p));
let s = OpenFilesScreen_new(&p);
assert_eq!(s.pid, 4000);
}
#[test]
fn new_initializes_the_embedded_infoscreen() {
let mut p = Process::default();
Process_setPid(&mut p, 7);
let s = OpenFilesScreen_new(&p);
assert_eq!(s.super_.process, &p as *const Process);
assert_eq!(Vector_size(&s.super_.lines), 0);
assert_eq!(Panel_size(&s.super_.display), 0);
assert_eq!(s.super_.display.x, 0);
assert_eq!(s.super_.display.y, 1);
assert_eq!(s.super_.display.w, Ncurses::cols());
assert_eq!(s.super_.display.h, Ncurses::lines() - 2);
assert_eq!(Panel_headerHeight(&s.super_.display), 1);
assert!(IncSet_filter(&s.super_.inc).is_none());
}
#[test]
fn new_builds_the_default_infoscreen_bar() {
let p = Process::default();
let s = OpenFilesScreen_new(&p);
let bar = s
.super_
.display
.defaultBar
.as_ref()
.expect("default bar built");
assert_eq!(
bar.functions,
vec!["Search ", "Filter ", "Refresh", "Done "]
);
assert_eq!(bar.keys, vec!["F3", "F4", "F5", "Esc"]);
}
#[test]
fn new_installs_the_lsof_column_header() {
let p = Process::default();
let s = OpenFilesScreen_new(&p);
assert_eq!(Panel_headerHeight(&s.super_.display), 1);
assert_eq!(
HEADER,
" FD TYPE MODE DEVICE SIZE OFFSET NODE NAME"
);
}
#[test]
fn get_process_data_runs_or_reports_missing_lsof() {
let pid = unsafe { libc::getpid() };
let pdata = OpenFilesScreen_getProcessData(pid);
assert!(
matches!(pdata.error, 0 | 1 | 127),
"unexpected error code {}",
pdata.error
);
if pdata.error == 0 {
assert!(!pdata.files.is_empty());
assert!(pdata.cols[getIndexForType(b's')] >= 8);
}
}
#[test]
fn scan_populates_lines_from_lsof() {
let mut p = Process::default();
Process_setPid(&mut p, unsafe { libc::getpid() });
let mut s = OpenFilesScreen_new(&p);
OpenFilesScreen_scan(&mut s);
assert!(Vector_size(&s.super_.lines) >= 1);
assert_eq!(Panel_size(&s.super_.display), Vector_size(&s.super_.lines));
}
#[test]
fn scan_sorts_lines_lexicographically() {
let mut p = Process::default();
Process_setPid(&mut p, unsafe { libc::getpid() });
let mut s = OpenFilesScreen_new(&p);
OpenFilesScreen_scan(&mut s);
let n = Vector_size(&s.super_.lines);
let mut prev: Option<String> = None;
for idx in 0..n {
let any: &dyn std::any::Any = Vector_get(&s.super_.lines, idx as usize);
let cur = any.downcast_ref::<ListItem>().unwrap().value.clone();
if let Some(p) = &prev {
assert!(*p <= cur, "lines not sorted: {p:?} > {cur:?}");
}
prev = Some(cur);
}
}
}