diff --git a/Cargo.lock b/Cargo.lock index d7c4c9ad..51c569a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "addr2line" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.0" @@ -120,21 +111,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object 0.36.4", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base64" version = "0.21.7" @@ -693,12 +669,6 @@ dependencies = [ name = "gettext-rs" version = "0.2.1" -[[package]] -name = "gimli" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" - [[package]] name = "glob" version = "0.3.1" @@ -720,12 +690,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hostname" version = "0.3.1" @@ -1035,18 +999,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mio" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi", - "libc", - "wasi", - "windows-sys 0.52.0", -] - [[package]] name = "nix" version = "0.28.0" @@ -1083,7 +1035,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio 0.8.11", + "mio", "walkdir", "windows-sys 0.48.0", ] @@ -1168,15 +1120,6 @@ dependencies = [ "ruzstd", ] -[[package]] -name = "object" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.19.0" @@ -1365,7 +1308,7 @@ dependencies = [ "chrono", "clap", "gettext-rs", - "object 0.35.0", + "object", "plib", ] @@ -1461,7 +1404,6 @@ dependencies = [ "libc", "plib", "sysinfo", - "tokio", ] [[package]] @@ -1719,12 +1661,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -1867,16 +1803,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -2052,32 +1978,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tokio" -version = "1.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" -dependencies = [ - "backtrace", - "libc", - "mio 1.0.2", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "topological-sort" version = "0.2.2" diff --git a/process/Cargo.toml b/process/Cargo.toml index 03c5c522..31d2ed2f 100644 --- a/process/Cargo.toml +++ b/process/Cargo.toml @@ -13,16 +13,15 @@ gettext-rs.workspace = true libc.workspace = true dirs = "5.0" -[dev-dependencies] -sysinfo = "0.31" -tokio = { version = "1.39", features = ["net", "macros", "rt"]} - [build-dependencies] bindgen = { version = "0.70.0", features = ["runtime"] } [lints] workspace = true +[dev-dependencies] +sysinfo = "0.31" + [[bin]] name = "fuser" path = "./fuser.rs" diff --git a/process/fuser.rs b/process/fuser.rs index 3e6d17d2..1541bd3e 100644 --- a/process/fuser.rs +++ b/process/fuser.rs @@ -10,14 +10,16 @@ use clap::{CommandFactory, Parser}; use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory}; use plib::PROJECT_NAME; +use std::fs::{metadata, Metadata}; use std::io::{self, Write}; +use std::sync::mpsc; +use std::thread; +use std::time::Duration; use std::{ collections::BTreeMap, - ffi::{CStr, CString}, + ffi::CStr, + os::unix::fs::MetadataExt, path::{Path, PathBuf}, - sync::mpsc, - thread, - time::Duration, }; const NAME_FIELD: usize = 20; @@ -436,8 +438,8 @@ mod linux { Err(_) => continue, }; - let st = timeout(&entry.path().to_string_lossy(), 5)?; - let uid = st.st_uid; + let st = fs::metadata(&entry.path())?; + let uid = st.uid(); check_root_access(names, pid, uid, &root_stat, device_list, inode_list)?; check_cwd_access(names, pid, uid, &cwd_stat, device_list, inode_list)?; @@ -486,20 +488,24 @@ mod linux { names: &mut Names, pid: i32, uid: u32, - root_stat: &libc::stat, + root_stat: &Metadata, device_list: &DeviceList, inode_list: &InodeList, ) -> Result<(), io::Error> { + let root_device_id = root_stat.dev(); + let root_inode_number = root_stat.ino(); + if device_list .iter() - .any(|device| device.device_id == root_stat.st_dev) + .any(|device| device.device_id == root_device_id) { add_process(names, pid, uid, Access::Root, ProcType::Normal); return Ok(()); } + if inode_list .iter() - .any(|inode| inode.device_id == root_stat.st_dev && inode.inode == root_stat.st_ino) + .any(|inode| inode.device_id == root_device_id && inode.inode == root_inode_number) { add_process(names, pid, uid, Access::Root, ProcType::Normal); return Ok(()); @@ -507,7 +513,6 @@ mod linux { Ok(()) } - /// Checks if a process has access to the current working directory and updates the `Names` object if it does. /// /// # Arguments @@ -530,20 +535,24 @@ mod linux { names: &mut Names, pid: i32, uid: u32, - cwd_stat: &libc::stat, + cwd_stat: &Metadata, device_list: &DeviceList, inode_list: &InodeList, - ) -> Result<(), std::io::Error> { + ) -> Result<(), io::Error> { + let cwd_device_id = cwd_stat.dev(); + let cwd_inode_number = cwd_stat.ino(); + if device_list .iter() - .any(|device| device.device_id == cwd_stat.st_dev) + .any(|device| device.device_id == cwd_device_id) { add_process(names, pid, uid, Access::Cwd, ProcType::Normal); return Ok(()); } + if inode_list .iter() - .any(|inode| inode.device_id == cwd_stat.st_dev && inode.inode == cwd_stat.st_ino) + .any(|inode| inode.device_id == cwd_device_id && inode.inode == cwd_inode_number) { add_process(names, pid, uid, Access::Cwd, ProcType::Normal); return Ok(()); @@ -551,7 +560,6 @@ mod linux { Ok(()) } - /// Checks if a process has access to the executable file and updates the `Names` object if it does. /// /// # Arguments @@ -574,20 +582,24 @@ mod linux { names: &mut Names, pid: i32, uid: u32, - exe_stat: &libc::stat, + exe_stat: &Metadata, device_list: &DeviceList, inode_list: &InodeList, ) -> Result<(), io::Error> { + let exe_device_id = exe_stat.dev(); + let exe_inode_number = exe_stat.ino(); + if device_list .iter() - .any(|device| device.device_id == exe_stat.st_dev) + .any(|device| device.device_id == exe_device_id) { add_process(names, pid, uid, Access::Exe, ProcType::Normal); return Ok(()); } + if inode_list .iter() - .any(|inode| inode.device_id == exe_stat.st_dev && inode.inode == exe_stat.st_ino) + .any(|inode| inode.device_id == exe_device_id && inode.inode == exe_inode_number) { add_process(names, pid, uid, Access::Exe, ProcType::Normal); return Ok(()); @@ -595,7 +607,6 @@ mod linux { Ok(()) } - /// Checks a directory within a process's `/proc` entry for matching devices and inodes, /// and updates the `Names` object with relevant process information. /// @@ -630,43 +641,60 @@ mod linux { net_dev: u64, ) -> Result<(), io::Error> { let dir_path = format!("/proc/{}/{}", pid, dirname); - let dir_entries = fs::read_dir(&dir_path)?; + let dir_entries = match fs::read_dir(&dir_path) { + Ok(entries) => entries, + Err(err) if err.kind() == ErrorKind::PermissionDenied => { + eprintln!("Permission denied for directory: {:?}", dir_path); + return Ok(()); + } + Err(err) => { + eprintln!("Failed to read directory {:?}: {:?}", dir_path, err); + return Err(err); + } + }; for entry in dir_entries { let entry = entry?; let path = entry.path(); let path_str = path.to_string_lossy(); - let mut stat = match timeout(&path_str, 5) { - Ok(stat) => stat, - Err(_) => continue, - }; + match timeout(&path_str, 5) { + Ok(metadata) => { + let st_dev = metadata.dev(); + let st_ino = metadata.ino(); + + let mut stat_dev = st_dev; + let mut stat_ino = st_ino; + + if stat_dev == net_dev { + if let Some(unix_socket) = unix_socket_list + .iter() + .find(|sock| sock.net_inode == stat_ino) + { + stat_dev = unix_socket.device_id; + stat_ino = unix_socket.inode; + } + } - if stat.st_dev == net_dev { - if let Some(unix_socket) = unix_socket_list - .iter() - .find(|sock| sock.net_inode == stat.st_ino) - { - stat.st_dev = unix_socket.device_id; - stat.st_ino = unix_socket.inode; - } - } + let new_access = match access { + Access::File => Access::Filewr, + _ => access.clone(), + }; - let new_access = match access { - Access::File => Access::Filewr, - _ => access.clone(), - }; - if device_list - .iter() - .any(|dev| dev.name.filename != PathBuf::from("") && stat.st_dev == dev.device_id) - || inode_list.iter().any(|inode| inode.inode == stat.st_ino) - { - add_process(names, pid, uid, new_access, ProcType::Normal); + if device_list.iter().any(|dev| { + dev.name.filename != PathBuf::from("") && stat_dev == dev.device_id + }) || inode_list.iter().any(|inode| inode.inode == stat_ino) + { + add_process(names, pid, uid, new_access, ProcType::Normal); + } + } + Err(_) => { + continue; + } } } Ok(()) } - /// Checks the memory map of a process for matching devices and updates the `Names` object. /// /// # Arguments @@ -735,9 +763,9 @@ mod linux { } /// get stat of current /proc/{pid}/{filename} - fn get_pid_stat(pid: i32, filename: &str) -> Result { + fn get_pid_stat(pid: i32, filename: &str) -> Result { let path = format!("{}/{}{}", PROC_PATH, pid, filename); - timeout(&path, 5) + fs::metadata(&path) } /// Fills the `unix_socket_list` with information from the `/proc/net/unix` file. @@ -766,19 +794,20 @@ mod linux { let path = normalize_path(scanned_path); match timeout(&path, 5) { - Ok(stat) => UnixSocketList::add_socket( - unix_socket_list, - stat.st_dev, - stat.st_ino, - net_inode, - ), + Ok(stat) => { + UnixSocketList::add_socket( + unix_socket_list, + stat.dev(), + stat.ino(), + net_inode, + ); + } Err(_) => continue, } } } Ok(()) } - /// Reads the `/proc/mounts` file and updates the `mount_list` with mount points. /// /// # Arguments @@ -1082,16 +1111,17 @@ mod linux { need_check_map: &mut bool, ) -> Result<(), std::io::Error> { names.filename = expand_path(&names.filename)?; + let st = timeout(&names.filename.to_string_lossy(), 5)?; read_proc_mounts(mount_list)?; if mount { - *device_list = DeviceList::new(names.clone(), st.st_dev); + *device_list = DeviceList::new(names.clone(), st.dev()); *need_check_map = true; } else { - let st = stat(&names.filename.to_string_lossy())?; - *inode_list = InodeList::new(st.st_dev, st.st_ino); + *inode_list = InodeList::new(st.dev(), st.ino()); } + Ok(()) } @@ -1194,7 +1224,7 @@ mod macos { include!(concat!(env!("OUT_DIR"), "/osx_libproc_bindings.rs")); } use libc::{c_char, c_int, c_void}; - use std::{os::unix::ffi::OsStrExt, ptr}; + use std::{ffi::CString, os::unix::ffi::OsStrExt, ptr}; // similar to list_pids_ret() below, there are two cases when 0 is returned, one when there are // no pids, and the other when there is an error @@ -1283,7 +1313,7 @@ mod macos { for name in names.iter_mut() { let st = timeout(&name.filename.to_string_lossy(), 5)?; - let uid = st.st_uid; + let uid = st.uid(); let pids = listpidspath( osx_libproc_bindings::PROC_ALL_PIDS, @@ -1442,21 +1472,37 @@ fn print_matches(name: &mut Names, user: bool) -> Result<(), io::Error> { Ok(()) } -/// Execute stat() system call with timeout to avoid deadlock -/// on network based file systems. -fn timeout(path: &str, seconds: u32) -> Result { +/// Adds a new process to the `Names` object with specified access and process type. +fn add_process(names: &mut Names, pid: i32, uid: u32, access: Access, proc_type: ProcType) { + let proc = Procs::new(pid, uid, access, proc_type); + names.add_procs(proc); +} + +/// Executes `metadata()` system call with timeout to avoid deadlock on network-based file systems. +/// +/// **Arguments:** +/// - `path`: The file path to retrieve metadata for. +/// - `seconds`: The number of seconds to wait before timing out. +/// +/// **Returns:** +/// - `Ok(fs::Metadata)` if the metadata is successfully retrieved within the timeout. +/// - `Err(io::Error)` if the operation fails or times out. +fn timeout(path: &str, seconds: u32) -> Result { let (tx, rx) = mpsc::channel(); + let path = path.to_string(); // Clone path into a `String` with `'static` lifetime - thread::scope(|s| { - s.spawn(|| { - if let Err(e) = tx.send(stat(path)) { - eprintln!("Failed to send result through channel: {}", e); - } - }); + // Spawn a thread to retrieve the metadata + thread::spawn(move || { + let metadata = metadata(&path); // Use the cloned `String` here + if let Err(e) = tx.send(metadata) { + eprintln!("Failed to send result through channel: {}", e); + } }); + // Wait for the result or timeout match rx.recv_timeout(Duration::from_secs(seconds.into())) { - Ok(stat) => stat, + Ok(Ok(metadata)) => Ok(metadata), // Successfully retrieved metadata + Ok(Err(e)) => Err(e), // Metadata retrieval failed with an error Err(mpsc::RecvTimeoutError::Timeout) => Err(io::Error::new( io::ErrorKind::TimedOut, "Operation timed out", @@ -1466,24 +1512,3 @@ fn timeout(path: &str, seconds: u32) -> Result { } } } - -/// Retrieves the status of a file given its filename. -fn stat(filename_str: &str) -> io::Result { - let filename = CString::new(filename_str)?; - - unsafe { - let mut st: libc::stat = std::mem::zeroed(); - let rc = libc::stat(filename.as_ptr(), &mut st); - if rc == 0 { - Ok(st) - } else { - Err(io::Error::last_os_error()) - } - } -} - -/// Adds a new process to the `Names` object with specified access and process type. -fn add_process(names: &mut Names, pid: i32, uid: u32, access: Access, proc_type: ProcType) { - let proc = Procs::new(pid, uid, access, proc_type); - names.add_procs(proc); -} diff --git a/process/tests/fuser/basic.rs b/process/tests/fuser/basic.rs new file mode 100644 index 00000000..507a112f --- /dev/null +++ b/process/tests/fuser/basic.rs @@ -0,0 +1,46 @@ +mod basic { + use crate::fuser::fuser_test; + use std::{fs::File, path::PathBuf, process::Command, str}; + + /// Tests the basic functionality of `fuser` by ensuring it can find the PID of a process. + /// + /// **Setup:** + /// - Starts a process running `tail -f` on a temporary file. + /// + /// **Assertions:** + /// - Verifies that the PID of the process is included in the output of `fuser`. + #[test] + fn test_fuser_basic() { + fn get_temp_file_path() -> PathBuf { + let mut path = std::env::temp_dir(); + + path.push("test_file"); + + path + } + let binding = get_temp_file_path(); + let temp_file_path = binding.to_str().unwrap(); + File::create(temp_file_path).expect("Failed to create temporary file"); + + let mut process = Command::new("tail") + .arg("-f") + .arg(temp_file_path) + .spawn() + .expect("Failed to start process"); + + let pid = process.id(); + + fuser_test(vec![temp_file_path.to_string()], "", 0, |_, output| { + let stdout_str = str::from_utf8(&output.stdout).expect("Invalid UTF-8 in stdout"); + let pid_str = pid.to_string(); + assert!( + stdout_str.contains(&pid_str), + "PID {} not found in the output.", + pid_str + ); + }); + + process.kill().expect("Failed to kill the process"); + std::fs::remove_file(temp_file_path).expect("Failed to remove temporary file"); + } +} diff --git a/process/tests/fuser/mod.rs b/process/tests/fuser/mod.rs index 5755e659..6c231bb2 100644 --- a/process/tests/fuser/mod.rs +++ b/process/tests/fuser/mod.rs @@ -1,19 +1,16 @@ -use libc::uid_t; use plib::{run_test_with_checker, TestPlan}; -use std::{ - ffi::CStr, - fs, io, - path::{Path, PathBuf}, - process::{Command, Output}, - str, -}; +use std::process::Output; +mod basic; #[cfg(target_os = "linux")] -use std::{fs::File, io::Read}; +mod tcp; #[cfg(target_os = "linux")] -use tokio::net::{TcpListener, UdpSocket, UnixListener}; +mod udp; +#[cfg(target_os = "linux")] +mod unix; +mod with_user; -fn fuser_test( +pub fn fuser_test( args: Vec, expected_err: &str, expected_exit_code: i32, @@ -31,300 +28,3 @@ fn fuser_test( checker, ); } - -/// Tests the basic functionality of `fuser` by ensuring it can find the PID of a process. -/// -/// **Setup:** -/// - Starts a process running `sleep 1`. -/// -/// **Assertions:** -/// - Verifies that the PID of the process is included in the output of `fuser`. -#[cfg(target_os = "linux")] -#[tokio::test] -async fn test_fuser_basic() { - let process = Command::new("sleep") - .arg("1") - .spawn() - .expect("Failed to start process"); - - let pid = process.id(); - - fuser_test(vec!["./".to_string()], "", 0, |_, output| { - let stdout_str = str::from_utf8(&output.stdout).expect("Invalid UTF-8 in stdout"); - let pid_str = pid.to_string(); - assert!( - stdout_str.contains(&pid_str), - "PID {} not found in the output.", - pid_str - ); - }); -} - -#[cfg(target_os = "linux")] -fn get_process_user(pid: u32) -> io::Result { - let status_path = format!("/proc/{}/status", pid); - let mut file = File::open(&status_path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - - let uid_line = contents - .lines() - .find(|line| line.starts_with("Uid:")) - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Uid line not found"))?; - - let uid_str = uid_line - .split_whitespace() - .nth(1) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "UID not found"))?; - let uid: uid_t = uid_str - .parse() - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UID"))?; - - get_username_by_uid(uid) -} - -#[cfg(target_os = "macos")] -fn get_process_user(_pid: u32) -> io::Result { - let uid = unsafe { libc::getuid() }; - get_username_by_uid(uid) -} - -fn get_username_by_uid(uid: uid_t) -> io::Result { - let pwd = unsafe { libc::getpwuid(uid) }; - if pwd.is_null() { - return Err(io::Error::new(io::ErrorKind::NotFound, "User not found")); - } - - let user_name = unsafe { - CStr::from_ptr((*pwd).pw_name) - .to_string_lossy() - .into_owned() - }; - - Ok(user_name) -} -/// Tests `fuser` with the `-u` flag to ensure it outputs the process owner. -/// -/// **Setup:** -/// - Starts a process running `sleep 1`. -/// -/// **Assertions:** -/// - Verifies that the owner printed in stderr. -#[test] -fn test_fuser_with_user() { - let process = Command::new("sleep") - .arg("1") - .spawn() - .expect("Failed to start process"); - - let pid = process.id(); - - fuser_test( - vec!["./".to_string(), "-u".to_string()], - "", - 0, - |_, output| { - let owner = get_process_user(pid).expect("Failed to get owner of process"); - let stderr_str = str::from_utf8(&output.stderr).expect("Invalid UTF-8 in stderr"); - assert!( - stderr_str.contains(&owner), - "owner {} not found in the output.", - owner - ); - }, - ); -} - -/// Tests `fuser` with multiple file paths. -/// -/// **Setup:** -/// - Starts two processes running `sleep 1` in different directories. -/// -/// **Assertions:** -/// - Verifies that the PIDs of both processes are included in the stdout. -#[ignore] -#[test] -fn test_fuser_with_many_files() { - let process1 = Command::new("sleep") - .current_dir("../") - .arg("1") - .spawn() - .expect("Failed to start process"); - - let process2 = Command::new("sleep") - .current_dir("/") - .arg("1") - .spawn() - .expect("Failed to start process"); - - let pid1 = process1.id(); - let pid2 = process2.id(); - - fuser_test( - vec!["/".to_string(), "../".to_string()], - "", - 0, - |_, output| { - let stdout_str = str::from_utf8(&output.stdout).expect("Invalid UTF-8 in stdout"); - let pid_str1 = pid1.to_string(); - let pid_str2 = pid2.to_string(); - assert!( - stdout_str.contains(&pid_str1), - "PID {} not found in the output.", - pid_str1 - ); - assert!( - stdout_str.contains(&pid_str2), - "PID {} not found in the output.", - pid_str2 - ); - }, - ); -} - -/// Starts a TCP server on port 8080. -#[cfg(target_os = "linux")] -async fn start_tcp_server() -> TcpListener { - TcpListener::bind(("127.0.0.1", 8080)) - .await - .expect("Failed to bind TCP server") -} - -/// Tests `fuser` with TCP socket. -/// -/// **Setup:** -/// - Starts a TCP server on port 8080. -/// -/// **Assertions:** -/// - Verifies that the output of `fuser` matches the manual execution for TCP sockets. -#[tokio::test] -#[ignore] -#[cfg(target_os = "linux")] -async fn test_fuser_tcp() { - let _server = start_tcp_server().await; - fuser_test(vec!["8080/tcp".to_string()], "", 0, |_, output| { - let manual_output = Command::new("fuser").arg("8080/tcp").output().unwrap(); - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, manual_output.stdout); - assert_eq!(output.stderr, manual_output.stderr); - }); -} - -/// Starts a UDP server on port 8081. -#[cfg(target_os = "linux")] -async fn start_udp_server() -> UdpSocket { - UdpSocket::bind(("127.0.0.1", 8081)) - .await - .expect("Failed to bind UDP server") -} - -/// Tests `fuser` with UDP socket. -/// -/// **Setup:** -/// - Starts a UDP server on port 8081. -/// -/// **Assertions:** -/// - Verifies that the output of `fuser` matches the manual execution for UDP sockets. -#[tokio::test] -#[ignore] -#[cfg(target_os = "linux")] -async fn test_fuser_udp() { - let _server = start_udp_server().await; - fuser_test(vec!["8081/udp".to_string()], "", 0, |_, output| { - let manual_output = Command::new("fuser").arg("8081/udp").output().unwrap(); - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, manual_output.stdout); - assert_eq!(output.stderr, manual_output.stderr); - }); -} -/// Starts a Unix socket server at the specified path. -#[cfg(target_os = "linux")] -async fn start_unix_socket(socket_path: &str) -> UnixListener { - if fs::metadata(socket_path).is_ok() { - println!("A socket is already present. Deleting..."); - fs::remove_file(socket_path).expect("Failed to delete existing socket"); - } - - UnixListener::bind(socket_path).expect("Failed to bind Unix socket") -} - -/// Tests `fuser` with Unix socket. -/// -/// **Setup:** -/// - Starts a Unix socket server at the specified path (`/tmp/test.sock`). -/// -/// **Assertions:** -/// - Verifies that the output of `fuser` matches the manual execution for the Unix socket at `/tmp/test.sock`. -/// -/// **Note:** -/// - Before binding to the socket, the function checks if a socket file already exists at the path and deletes it if present. -/// - This ensures that the test environment is clean and prevents issues with existing sockets. -#[tokio::test] -#[cfg(target_os = "linux")] -async fn test_fuser_unixsocket() { - let socket_path = "/tmp/test.sock"; - let _unix_socket = start_unix_socket(socket_path).await; - fuser_test(vec![socket_path.to_string()], "", 0, |_, output| { - let manual_output = Command::new("fuser").arg(socket_path).output().unwrap(); - assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stdout, manual_output.stdout); - assert_eq!(output.stderr, manual_output.stderr); - }); -} - -/// Creates a directory and populates it with a specified number of test files. -fn create_large_directory(dir_path: &Path, num_files: usize) -> std::io::Result<()> { - fs::create_dir_all(dir_path)?; - for i in 0..num_files { - let file_path = dir_path.join(format!("file_{:04}", i)); - fs::write(file_path, "This is a test file.")?; - } - Ok(()) -} - -/// Deletes a directory and all of its contents. -fn delete_directory(dir_path: &Path) -> io::Result<()> { - if dir_path.exists() { - fs::remove_dir_all(dir_path) - } else { - Ok(()) - } -} - -/// Tests `fuser` with a very large directory to ensure it can handle large numbers of files. -/// -/// **Setup:** -/// - Creates a directory with a large number of files at a fixed path. -/// -/// **Assertions:** -/// - Verifies that the `fuser` command completes successfully and the output is as expected. -/// - Executes an additional command to ensure it works after `fuser` and checks its output. -/// - Ensures that the test does not block indefinitely and completes within a reasonable time frame. -#[tokio::test] -async fn test_fuser_large_directory() { - let test_dir_path = PathBuf::from("large_test_dir"); - - let num_files = 10_000; - - create_large_directory(&test_dir_path, num_files).expect("Failed to create large directory"); - - fuser_test( - vec![test_dir_path.to_str().unwrap().to_string()], - "", - 0, - |_, output| { - let stdout_str = str::from_utf8(&output.stdout).expect("Invalid UTF-8 in stdout"); - assert!(stdout_str.contains("")); - }, - ); - - let additional_command = Command::new("ls") - .args(&[test_dir_path.to_str().unwrap()]) - .output() - .expect("Failed to execute command"); - - assert_eq!(additional_command.status.code(), Some(0)); - - // Clean up the directory after the test - delete_directory(&test_dir_path).expect("Failed to delete large directory"); -} diff --git a/process/tests/fuser/run_tests.sh b/process/tests/fuser/run_tests.sh new file mode 100644 index 00000000..9d1fe132 --- /dev/null +++ b/process/tests/fuser/run_tests.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +run_tests() { + for i in {1..1000} + do + echo "Running cargo test: Attempt $i" + cargo test + if [ $? -ne 0 ]; then + echo "Test failed on attempt $i" + exit 1 # Exit immediately if the tests fail + fi + # sleep 1 + done +} + +run_tests + +echo "All 1000 test runs completed successfully." diff --git a/process/tests/fuser/tcp.rs b/process/tests/fuser/tcp.rs new file mode 100644 index 00000000..722c23b4 --- /dev/null +++ b/process/tests/fuser/tcp.rs @@ -0,0 +1,62 @@ +#[cfg(test)] +mod tcp { + use crate::fuser::fuser_test; + use std::{ + io, + net::{TcpListener, TcpStream}, + process::Command, + }; + + /// Starts a TCP server on a predefined local address and port. + /// + /// **Returns:** + /// - An `io::Result` containing the bound `TcpListener` if successful. + fn start_tcp_server() -> io::Result { + TcpListener::bind("127.0.0.1:0") + } + + /// Waits for the TCP server to be ready by attempting to connect to it. + /// + /// **Arguments:** + /// - `port`: The port number where the TCP server is expected to be listening. + fn wait_for_tcp_server(port: u16) { + let address = format!("127.0.0.1:{}", port); + + loop { + if let Ok(stream) = TcpStream::connect(&address) { + stream + .shutdown(std::net::Shutdown::Both) + .expect("Failed to close the connection"); + + break; + } + + std::thread::yield_now(); + } + } + /// Tests `fuser` with the TCP server to ensure it can find the process associated with the server. + /// + /// **Setup:** + /// - Starts a TCP server and waits for it to become available. + /// + /// **Assertions:** + /// - Verifies that the `fuser` command can find the process associated with the TCP server. + #[test] + fn test_fuser_tcp() { + let server = start_tcp_server().expect("Failed to start TCP server"); + let port = server.local_addr().unwrap().port(); + wait_for_tcp_server(port); + + fuser_test(vec![format!("{}/tcp", port)], "", 0, |_, output| { + let manual_output = Command::new("fuser") + .arg(format!("{}/tcp", port)) + .output() + .unwrap(); + + assert_eq!(output.status.code(), Some(0)); + assert_eq!(output.stdout, manual_output.stdout); + }); + + drop(server); + } +} diff --git a/process/tests/fuser/udp.rs b/process/tests/fuser/udp.rs new file mode 100644 index 00000000..a600914f --- /dev/null +++ b/process/tests/fuser/udp.rs @@ -0,0 +1,54 @@ +#[cfg(test)] +mod udp { + use crate::fuser::fuser_test; + use std::{io, net::UdpSocket, process::Command}; + + /// Waits for a UDP server to become available by sending a dummy message to the specified port. + /// + /// **Arguments:** + /// - `port`: The port number where the UDP server is expected to be listening. + fn wait_for_udp_server(port: u16) { + let socket = UdpSocket::bind("127.0.0.1:0").expect("Failed to bind dummy UDP socket"); + let dummy_message = b"ping"; + + loop { + if let Ok(_) = socket.send_to(dummy_message, format!("127.0.0.1:{}", port)) { + break; + } + } + drop(socket); + } + /// Starts a UDP server listening on a specific port. + /// + /// **Returns:** + /// - An `io::Result` containing the bound `UdpSocket` if successful. + fn start_udp_server() -> io::Result { + UdpSocket::bind("127.0.0.1:0") + } + + /// Tests `fuser` with the `-u` flag to ensure it outputs the process owner for the UDP server. + /// + /// **Setup:** + /// - Starts a UDP server and waits for it to become available. + /// + /// **Assertions:** + /// - Verifies that the `fuser` command can find the process associated with the UDP server. + #[test] + fn test_fuser_udp() { + let server = start_udp_server().expect("Failed to start UDP server"); + let port = server.local_addr().unwrap().port(); + wait_for_udp_server(port); + + fuser_test(vec![format!("{}/udp", port)], "", 0, |_, output| { + let manual_output = Command::new("fuser") + .arg(format!("{}/udp", port)) + .output() + .unwrap(); + + assert_eq!(output.status.code(), Some(0)); + assert_eq!(output.stdout, manual_output.stdout); + }); + + drop(server); + } +} diff --git a/process/tests/fuser/unix.rs b/process/tests/fuser/unix.rs new file mode 100644 index 00000000..c871332b --- /dev/null +++ b/process/tests/fuser/unix.rs @@ -0,0 +1,87 @@ +mod unix { + use crate::fuser::fuser_test; + use std::{ + fs, + os::unix::net::{UnixListener, UnixStream}, + process::Command, + thread, + }; + + /// Starts a Unix socket server at the specified socket path. + /// + /// **Arguments:** + /// - `socket_path`: The path where the Unix socket will be created. + /// + /// **Returns:** + /// - A `Result` containing the bound `UnixListener` if successful, or an `io::Error`. + fn start_unix_socket(socket_path: &str) -> Result { + if fs::metadata(socket_path).is_ok() { + fs::remove_file(socket_path).map_err(|e| { + eprintln!("Failed to delete existing socket: {}", e); + e + })?; + } + UnixListener::bind(socket_path).map_err(|e| { + eprintln!("Failed to bind Unix socket: {}", e); + e + }) + } + + /// Waits for the Unix socket server to be ready by attempting to connect to it. + /// + /// **Arguments:** + /// - `socket_path`: The path of the Unix socket to connect to. + fn wait_for_unix_socket(socket_path: &str) { + loop { + match UnixStream::connect(socket_path) { + Ok(stream) => { + if let Err(e) = stream.shutdown(std::net::Shutdown::Both) { + eprintln!("Failed to close the connection: {}", e); + } + break; + } + Err(_) => { + thread::sleep(std::time::Duration::from_millis(10)); + } + } + } + } + + /// Tests `fuser` with a Unix socket to ensure it can find the process associated with the socket. + /// + /// **Setup:** + /// - Starts a Unix socket server. + /// + /// **Assertions:** + /// - Verifies that the `fuser` command can find the process associated with the Unix socket. + #[test] + fn test_fuser_unixsocket() { + let socket_path = "/tmp/test.sock"; + let _unix_socket = match start_unix_socket(socket_path) { + Ok(socket) => socket, + Err(e) => { + eprintln!("Failed to start Unix socket: {}", e); + return; + } + }; + + wait_for_unix_socket(socket_path); + + let handle = thread::spawn(move || { + fuser_test(vec![socket_path.to_string()], "", 0, |_, _output| { + let manual_output = Command::new("fuser").arg(socket_path).output(); + + match manual_output { + Ok(output) => { + assert_eq!(output.status.code(), Some(0)); + } + Err(e) => { + eprintln!("Failed to run fuser command: {}", e); + } + } + }); + }); + + handle.join().expect("Thread panicked"); + } +} diff --git a/process/tests/fuser/with_user.rs b/process/tests/fuser/with_user.rs new file mode 100644 index 00000000..1d4d0929 --- /dev/null +++ b/process/tests/fuser/with_user.rs @@ -0,0 +1,111 @@ +mod with_user { + use crate::fuser::fuser_test; + use libc::uid_t; + use std::{ffi::CStr, fs::File, io, process::Command, str}; + + /// Retrieves the user name of the process owner by process ID on Linux. + /// + /// **Arguments:** + /// - `pid`: The process ID of the target process. + /// + /// **Returns:** + /// - A `Result` containing the user name if successful, or an `io::Error`. + #[cfg(target_os = "linux")] + fn get_process_user(pid: u32) -> io::Result { + use std::io::Read; + let status_path = format!("/proc/{}/status", pid); + let mut file = File::open(&status_path).map_err(|e| { + eprintln!("Failed to open {}: {}", status_path, e); + e + })?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + let uid_line = contents + .lines() + .find(|line| line.starts_with("Uid:")) + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Uid line not found"))?; + + let uid_str = uid_line + .split_whitespace() + .nth(1) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "UID not found"))?; + let uid: uid_t = uid_str + .parse() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UID"))?; + + get_username_by_uid(uid) + } + + /// Retrieves the user name of the process owner by process ID on macOS. + /// + /// **Arguments:** + /// - `pid`: The process ID of the target process (not used here). + /// + /// **Returns:** + /// - A `Result` containing the user name if successful, or an `io::Error`. + #[cfg(target_os = "macos")] + fn get_process_user(_pid: u32) -> io::Result { + let uid = unsafe { libc::getuid() }; + get_username_by_uid(uid) + } + + fn get_username_by_uid(uid: uid_t) -> io::Result { + let pwd = unsafe { libc::getpwuid(uid) }; + if pwd.is_null() { + return Err(io::Error::new(io::ErrorKind::NotFound, "User not found")); + } + + let user_name = unsafe { + CStr::from_ptr((*pwd).pw_name) + .to_string_lossy() + .into_owned() + }; + + Ok(user_name) + } + /// Tests `fuser` with the `-u` flag to ensure it outputs the process owner. + /// + /// **Setup:** + /// - Starts a process running `sleep 1`. + /// + /// **Assertions:** + /// - Verifies that the owner printed in stderr. + #[test] + fn test_fuser_with_user() { + let temp_file_path = std::env::temp_dir().join("test_file_with_user"); + let temp_file_path_clone = temp_file_path.clone(); + + File::create(&temp_file_path_clone).expect("Failed to create temporary file"); + + let mut process = Command::new("tail") + .arg("-f") + .arg(&temp_file_path_clone) + .spawn() + .expect("Failed to start process"); + + let pid = process.id(); + let owner = get_process_user(pid).expect("Failed to get owner of process"); + + fuser_test( + vec![ + temp_file_path_clone.to_str().unwrap().to_string(), + "-u".to_string(), + ], + "", + 0, + |_, output| { + let stderr_str = str::from_utf8(&output.stderr).expect("Invalid UTF-8 in stderr"); + + assert!( + stderr_str.contains(&owner), + "Owner {} not found in the fuser output.", + owner + ); + }, + ); + + process.kill().expect("Failed to kill the process"); + std::fs::remove_file(temp_file_path).expect("Failed to remove temporary file"); + } +}