├── .cargo └── config.toml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.toml ├── README.md └── src ├── bin └── pslist.rs ├── core.rs ├── enums.rs ├── lib.rs └── process.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = ["-C", "target-feature=+crt-static"] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Cargo launch", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | ] 15 | }, 16 | "program": "${cargo:program}", 17 | "args": [ "-p 104" ] 18 | }, 19 | { 20 | "type": "lldb", 21 | "request": "launch", 22 | "name": "Debug unit tests in library 'winplat'", 23 | "cargo": { 24 | "args": [ 25 | "test", 26 | "--no-run", 27 | "--lib", 28 | "--package=winplat" 29 | ], 30 | "filter": { 31 | "name": "winplat", 32 | "kind": "lib" 33 | } 34 | }, 35 | "args": [], 36 | "cwd": "${workspaceFolder}" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "winplat" 3 | version = "0.1.0" 4 | authors = ["Pavel Yosifovich "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | winapi = "0.3.9" 11 | ntapi = "0.3.6" 12 | clap = "2.33.3" 13 | num-format = "0.4.0" 14 | 15 | [target.'cfg(windows)'.dependencies] 16 | winapi = { version = "0.3.9", features = ["winuser", "psapi", "winnt", "processthreadsapi", "errhandlingapi", "handleapi", "tlhelp32", "impl-default", "winbase"] } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pstools (Rust style) 2 | 3 | Inspired by the set of ps* tools from Sysinternals, similar tools will be implemented in Rust, for my own practice. 4 | -------------------------------------------------------------------------------- /src/bin/pslist.rs: -------------------------------------------------------------------------------- 1 | use winplat::process::{self, ProcessInfoEx }; 2 | use winplat::core; 3 | use std::collections::HashMap; 4 | use clap::{Arg, App}; 5 | use num_format::{Locale, ToFormattedString}; 6 | 7 | fn main() -> Result<(), core::Win32Error> { 8 | let matches = App::new("pslist (Rust version)").version("0.1").author("Pavel Yosifovich").about("List process information") 9 | .arg(Arg::with_name("tree") 10 | .short("t").long("tree").help("Show process tree")) 11 | .arg(Arg::with_name("memory") 12 | .short("m").long("mem").help("Show memory information")) 13 | .arg(Arg::with_name("processid") 14 | .short("p").long("pid").help("Show specific process information") 15 | .value_name("PID").takes_value(true)) 16 | .arg(Arg::with_name("processname") 17 | .short("n").long("pname").help("Show specific process(es) information by name") 18 | .value_name("NAME").takes_value(true)) 19 | .get_matches(); 20 | 21 | if matches.is_present("tree") { 22 | display_tree()?; 23 | return Ok(()); 24 | } 25 | 26 | if let Some(pid) = matches.value_of("processid") { 27 | display_process( 28 | match pid.trim().parse::() { 29 | Ok(pid) => pid, 30 | Err(e) => { 31 | println!("{}", e); 32 | return Ok(()); 33 | } 34 | }); 35 | return Ok(()); 36 | } 37 | 38 | if let Some(pname) = matches.value_of("processname") { 39 | return display_processes(pname, matches.is_present("threadinfo")); 40 | } 41 | 42 | let processes = process::enum_processes_native(false)?; 43 | display_default(&processes); 44 | 45 | Ok(()) 46 | } 47 | 48 | fn display_processes(pname: &str, include_threads: bool) -> Result<(), core::Win32Error> { 49 | todo!(); 50 | } 51 | 52 | fn display_process(pid: u32) { 53 | if let Some(pi) = process::enum_processes_native_pid(pid, false) { 54 | // display process info 55 | display_process_info(&pi); 56 | } 57 | else { 58 | println!("Process ID {} not found", pid); 59 | } 60 | } 61 | 62 | fn display_default(processes: &Vec) { 63 | println!("{:6} {:3} {:5} {:6} {:6} {:3} {:>10} {:>10} Name", " PID", "SID", " Thr", " PPID", " Han", "Pri", "Commit(K)", "WS(K)"); 64 | 65 | for pi in processes.iter() { 66 | println!("{:6} {:3} {:5} {:6} {:6} {:3} {:>10} {:>10} {}", 67 | pi.id, pi.session_id, pi.thread_count, pi.parent_id, pi.handle_count, pi.priority, 68 | (pi.commit_size >> 10).to_formatted_string(&Locale::en), 69 | (pi.working_set >> 10).to_formatted_string(&Locale::en), 70 | pi.name); 71 | } 72 | } 73 | 74 | fn display_tree() -> Result<(), core::Win32Error> { 75 | let processes = process::enum_processes_native(false)?; 76 | // build process tree 77 | let mut tree = Vec::with_capacity(32); 78 | let mut map = HashMap::with_capacity(256); 79 | 80 | for p in &processes { 81 | map.insert(p.id, p); 82 | } 83 | let map2 = map.clone(); 84 | 85 | for p in &processes { 86 | let contains = map2.contains_key(&p.parent_id); 87 | if p.parent_id == 0 || !contains || map2[&p.parent_id].create_time > p.create_time { 88 | tree.push((p, 0)); 89 | map.remove(&p.id); 90 | if p.id == 0 { 91 | continue; 92 | } 93 | let children = find_children(&map, &processes, &p, 1); 94 | children.iter().for_each(|p| { 95 | map.remove(&p.0); 96 | tree.push((&map2[&p.0], p.1)); 97 | }); 98 | } 99 | } 100 | for (p, indent) in tree.iter() { 101 | println!("{} {} ({})", String::from(" ").repeat(*indent as usize * 2), p.name, p.id); 102 | } 103 | Ok(()) 104 | } 105 | 106 | fn find_children(map: &HashMap, processes: &Vec, parent: &ProcessInfoEx, indent: u32) -> Vec<(u32, u32)> { 107 | let mut children = Vec::new(); 108 | for p in processes.iter() { 109 | if p.parent_id == parent.id && parent.create_time < p.create_time { 110 | children.push((p.id, indent)); 111 | let mut children2 = find_children(&map, &processes, &p, indent + 1); 112 | children.append(&mut children2); 113 | } 114 | } 115 | children 116 | } 117 | 118 | fn display_process_info(pi: &ProcessInfoEx) { 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | use winapi::shared::ntdef::HANDLE; 2 | use winapi::um::handleapi::*; 3 | use winapi::um::errhandlingapi::*; 4 | use std::ptr::null_mut; 5 | 6 | #[repr(C)] 7 | #[derive(Debug)] 8 | pub struct KernelHandle(HANDLE); 9 | 10 | #[derive(Debug, Clone, Copy)] 11 | pub struct Win32Error(u32); 12 | 13 | impl Drop for KernelHandle { 14 | fn drop(&mut self) { 15 | if self.0 as isize > 0 { 16 | unsafe { CloseHandle(self.0); } 17 | } 18 | } 19 | } 20 | 21 | impl KernelHandle { 22 | pub fn new(handle: HANDLE) -> Self { 23 | KernelHandle(handle) 24 | } 25 | 26 | pub fn close(&mut self) -> Result<(), Win32Error> { 27 | let success = unsafe { CloseHandle(self.0) }; 28 | if success != 0 { 29 | Ok(()) 30 | } 31 | else { 32 | Err(Win32Error::new()) 33 | } 34 | } 35 | 36 | pub fn is_valid(&self) -> bool { 37 | self.0 != null_mut() 38 | } 39 | 40 | pub fn get(&self) -> HANDLE { 41 | self.0 42 | } 43 | } 44 | 45 | impl Win32Error { 46 | pub fn new() -> Self { 47 | unsafe { Win32Error(GetLastError()) } 48 | } 49 | 50 | pub fn from_error(error: u32) -> Self { 51 | Win32Error(error) 52 | } 53 | 54 | pub fn from_ntstatus(status: i32) -> Self { 55 | unsafe { 56 | Win32Error(ntapi::ntrtl::RtlNtStatusToDosError(status)) 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/enums.rs: -------------------------------------------------------------------------------- 1 | use winapi::um::winnt::*; 2 | 3 | #[repr(u32)] 4 | pub enum ProcessAccessMask { 5 | Terminate = PROCESS_TERMINATE, 6 | CreateThread = PROCESS_CREATE_THREAD, 7 | SetSessionId = PROCESS_SET_SESSIONID, 8 | VmOperation = PROCESS_VM_OPERATION, 9 | VmRead = PROCESS_VM_READ, 10 | VmWrite = PROCESS_VM_WRITE, 11 | DupHandle = PROCESS_DUP_HANDLE, 12 | CreateProcess = PROCESS_CREATE_PROCESS, 13 | SetQuota = PROCESS_SET_QUOTA, 14 | SetInformation = PROCESS_SET_INFORMATION, 15 | QueryInformation = PROCESS_QUERY_INFORMATION, 16 | SuspendResume = PROCESS_SUSPEND_RESUME, 17 | QueryLimitedInformation = PROCESS_QUERY_LIMITED_INFORMATION, 18 | SetLimitedInformation = PROCESS_SET_LIMITED_INFORMATION, 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod enums; 2 | pub mod process; 3 | pub mod core; 4 | 5 | -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | use crate::enums; 2 | use crate::core; 3 | use std::time; 4 | use std::ptr::null_mut; 5 | use winapi::um::processthreadsapi::*; 6 | use winapi::um::tlhelp32::*; 7 | use winapi::um::handleapi::*; 8 | use winapi::um::winbase::*; 9 | use winapi::ctypes::c_void; 10 | use ntapi::ntexapi; 11 | use winapi::shared::minwindef::MAX_PATH; 12 | 13 | pub struct Process { 14 | handle: core::KernelHandle, 15 | } 16 | 17 | impl Process { 18 | pub fn current() -> Self { 19 | Self { 20 | handle: core::KernelHandle::new( unsafe { GetCurrentProcess() }) 21 | } 22 | } 23 | pub fn open(access_mask: enums::ProcessAccessMask, pid: u32) -> Result { 24 | let handle = unsafe { OpenProcess(access_mask as u32, 0, pid) }; 25 | if handle != null_mut() { 26 | Ok(Process { 27 | handle: core::KernelHandle::new(handle) 28 | }) 29 | } 30 | else { 31 | Err(core::Win32Error::new()) 32 | } 33 | } 34 | 35 | pub fn id(&self) -> u32 { 36 | unsafe { GetProcessId(self.handle.get()) } 37 | } 38 | 39 | pub fn full_path(&self) -> Result { 40 | unsafe { 41 | let mut path = Vec::new(); 42 | path.resize(MAX_PATH, 0u16); 43 | let mut size = MAX_PATH as u32; 44 | if QueryFullProcessImageNameW(self.handle.get(), 0, path.as_mut_ptr(), &mut size) == 0 { 45 | Err(core::Win32Error::new()) 46 | } 47 | else { 48 | Ok(String::from_utf16(&path[..size as usize]).unwrap()) 49 | } 50 | } 51 | } 52 | 53 | 54 | } 55 | 56 | #[derive(Debug, Clone)] 57 | pub struct ProcessInfo { 58 | pub id: u32, 59 | pub parent_id: u32, 60 | pub thread_count: u32, 61 | pub name: String, 62 | } 63 | 64 | #[derive(Debug, Clone)] 65 | pub struct ThreadInfoEx { 66 | 67 | } 68 | 69 | #[derive(Debug, Clone)] 70 | pub struct ProcessInfoEx { 71 | pub id: u32, 72 | pub parent_id: u32, 73 | pub thread_count: u32, 74 | pub name: String, 75 | pub handle_count: u32, 76 | pub priority: i32, 77 | pub create_time: i64, 78 | pub user_time: std::time::Duration, 79 | pub kernel_time: std::time::Duration, 80 | pub session_id: u32, 81 | pub virtual_size: usize, 82 | pub peak_virtual_size: usize, 83 | pub working_set: usize, 84 | pub peak_working_set: usize, 85 | pub page_fault_count: u32, 86 | pub commit_size: usize, 87 | pub peak_commit_size: usize, 88 | pub paged_pool: usize, 89 | pub non_paged_pool: usize, 90 | pub peak_paged_pool: usize, 91 | pub peak_non_paged_pool: usize, 92 | pub threads: Vec, 93 | } 94 | 95 | impl ProcessInfoEx { 96 | unsafe fn from_native(p: ntexapi::PSYSTEM_PROCESS_INFORMATION, name: String, include_threads: bool) -> Self { 97 | let pp = &*p; 98 | ProcessInfoEx { 99 | id: pp.UniqueProcessId as u32, 100 | parent_id: pp.InheritedFromUniqueProcessId as u32, 101 | thread_count: pp.NumberOfThreads, 102 | name: if pp.UniqueProcessId as u32 == 0 { "(Idle)".to_string() } else { String::from_utf16(std::slice::from_raw_parts(pp.ImageName.Buffer, (pp.ImageName.Length / 2) as usize)).unwrap() }, 103 | create_time: *pp.CreateTime.QuadPart(), 104 | user_time: time::Duration::from_nanos((*pp.UserTime.QuadPart() * 100) as u64), 105 | kernel_time: time::Duration::from_nanos((*pp.KernelTime.QuadPart() * 100) as u64), 106 | handle_count: pp.HandleCount, 107 | priority: pp.BasePriority, 108 | session_id: pp.SessionId, 109 | working_set: pp.WorkingSetSize, 110 | peak_working_set: pp.PeakWorkingSetSize, 111 | commit_size: pp.PagefileUsage, 112 | peak_commit_size: pp.PeakPagefileUsage, 113 | paged_pool: pp.QuotaPagedPoolUsage, 114 | non_paged_pool: pp.QuotaNonPagedPoolUsage, 115 | peak_paged_pool: pp.QuotaPeakPagedPoolUsage, 116 | peak_non_paged_pool: pp.QuotaPeakNonPagedPoolUsage, 117 | virtual_size: pp.VirtualSize, 118 | peak_virtual_size: pp.PeakVirtualSize, 119 | page_fault_count: pp.PageFaultCount, 120 | threads: if include_threads { enum_threads(pp) } else { Vec::new() }, 121 | } 122 | } 123 | } 124 | 125 | pub fn enum_processes_native_pid(pid: u32, include_threads: bool) -> Option { 126 | let mut result = enum_processes_native_generic(include_threads, pid, "").unwrap(); 127 | if result.is_empty() { 128 | None 129 | } 130 | else { 131 | Some(result.pop().unwrap()) 132 | } 133 | } 134 | 135 | pub fn enum_processes_native_name(name: &str, include_threads: bool) -> Result, core::Win32Error> { 136 | enum_processes_native_generic(include_threads, std::u32::MAX, &name) 137 | } 138 | 139 | pub fn enum_processes_native(include_threads: bool) -> Result, core::Win32Error> { 140 | enum_processes_native_generic(include_threads, std::u32::MAX, "") 141 | } 142 | 143 | fn enum_processes_native_generic(include_threads: bool, pid: u32, pname: &str) -> Result, core::Win32Error> { 144 | unsafe { 145 | let mut buffer = Vec::new(); 146 | let size: u32 = 1 << 22; 147 | buffer.resize(size as usize, 0u8); 148 | let status = ntexapi::NtQuerySystemInformation(ntexapi::SystemExtendedProcessInformation, buffer.as_mut_ptr() as *mut c_void, size, null_mut()); 149 | if status != 0 { 150 | return Err(core::Win32Error::from_ntstatus(status)); 151 | } 152 | 153 | let mut p = buffer.as_mut_ptr() as ntexapi::PSYSTEM_PROCESS_INFORMATION; 154 | let mut processes = Vec::with_capacity(512); 155 | 156 | loop { 157 | let pp = &*p; 158 | let name = if pp.UniqueProcessId as u32 == 0 { "(Idle)".to_string() } else { String::from_utf16(std::slice::from_raw_parts(pp.ImageName.Buffer, (pp.ImageName.Length / 2) as usize)).unwrap() }; 159 | if (pid == std::u32::MAX || pid == pp.UniqueProcessId as u32) && (pname.len() == 0 || pname == name) { 160 | let pi = ProcessInfoEx::from_native(p, name, include_threads); 161 | processes.push(pi); 162 | } 163 | if pp.NextEntryOffset == 0 { 164 | break; 165 | } 166 | p = (p as *mut u8).offset((*p).NextEntryOffset as isize) as ntexapi::PSYSTEM_PROCESS_INFORMATION; 167 | } 168 | 169 | processes.shrink_to_fit(); 170 | Ok(processes) 171 | } 172 | } 173 | 174 | fn enum_threads(pi: &ntexapi::SYSTEM_PROCESS_INFORMATION) -> Vec { 175 | todo!(); 176 | } 177 | 178 | pub fn enum_processes_toolhelp() -> Result, core::Win32Error> { 179 | unsafe { 180 | let handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 181 | // create a KernelHandle that is automatically closed 182 | let _khandle = core::KernelHandle::new(handle); 183 | 184 | if handle == INVALID_HANDLE_VALUE { 185 | return Err(core::Win32Error::new()) 186 | } 187 | let mut processes = Vec::with_capacity(512); 188 | 189 | let mut pe = PROCESSENTRY32W::default(); 190 | pe.dwSize = std::mem::size_of::() as u32; 191 | 192 | if Process32FirstW(handle, &mut pe) != 0 { 193 | loop { 194 | let pi = ProcessInfo { 195 | id: pe.th32ProcessID, 196 | parent_id: pe.th32ParentProcessID, 197 | thread_count: pe.cntThreads, 198 | name: String::from_utf16(&pe.szExeFile[..lstrlenW(pe.szExeFile.as_ptr()) as usize]).unwrap() 199 | }; 200 | processes.push(pi); 201 | if Process32NextW(handle, &mut pe) == 0 { 202 | break; 203 | } 204 | } 205 | } 206 | else { 207 | return Err(core::Win32Error::new()); 208 | } 209 | processes.shrink_to_fit(); 210 | Ok(processes) 211 | } 212 | } 213 | 214 | #[cfg(test)] 215 | mod tests { 216 | use super::*; 217 | 218 | fn locate_process(name: &str) -> u32 { 219 | for pi in &enum_processes_toolhelp().unwrap() { 220 | if pi.name == name && pi.thread_count > 0 { 221 | return pi.id; 222 | } 223 | } 224 | 0 225 | } 226 | 227 | #[test] 228 | fn open_process() { 229 | let explorer = locate_process("explorer.exe"); 230 | assert!(explorer != 0); 231 | let process = Process::open(enums::ProcessAccessMask::QueryLimitedInformation, explorer).unwrap(); 232 | assert_eq!(process.id(), explorer); 233 | let path = process.full_path().unwrap(); 234 | assert!(path.eq_ignore_ascii_case("c:\\Windows\\explorer.exe")); 235 | } 236 | 237 | #[test] 238 | fn enum_processes1() { 239 | let processes = enum_processes_toolhelp().unwrap(); 240 | for pi in &processes { 241 | println!("{:?}", pi); 242 | } 243 | } 244 | 245 | #[test] 246 | fn enum_processes2() { 247 | let processes = enum_processes_native(false).unwrap(); 248 | for pi in &processes { 249 | println!("{:?}", pi); 250 | } 251 | } 252 | 253 | #[test] 254 | fn current_process() { 255 | let process = Process::current(); 256 | unsafe { 257 | assert_eq!(process.id(), GetCurrentProcessId()); 258 | } 259 | println!("{}", process.full_path().unwrap()); 260 | } 261 | } 262 | 263 | --------------------------------------------------------------------------------