From 90bae80bb28c163db59d9f4a4ab2312a2c89a199 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 2 Sep 2022 19:48:34 -0700 Subject: [PATCH] WIP fixing arguments in title bug, need to validate fix, add to wezterm, push to our copy, refresh our cargo, and make a PR for wezterm. TODO: Learn how to do c-style buffer munging. --- Cargo.lock | 1 - crates/procinfo/Cargo.toml | 23 ++ crates/procinfo/src/lib.rs | 93 +++++++ crates/procinfo/src/linux.rs | 139 +++++++++++ crates/procinfo/src/macos.rs | 231 ++++++++++++++++++ crates/procinfo/src/windows.rs | 419 ++++++++++++++++++++++++++++++++ crates/terminal/Cargo.toml | 2 +- crates/terminal/src/terminal.rs | 144 ++++------- 8 files changed, 953 insertions(+), 99 deletions(-) create mode 100644 crates/procinfo/Cargo.toml create mode 100644 crates/procinfo/src/lib.rs create mode 100644 crates/procinfo/src/linux.rs create mode 100644 crates/procinfo/src/macos.rs create mode 100644 crates/procinfo/src/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 0a5d85d3b3..377d845f81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3824,7 +3824,6 @@ dependencies = [ [[package]] name = "procinfo" version = "0.1.0" -source = "git+https://github.com/zed-industries/wezterm?rev=40a7dbf93542fbe4178c2e4b4bd438126a6432b9#40a7dbf93542fbe4178c2e4b4bd438126a6432b9" dependencies = [ "libc", "log", diff --git a/crates/procinfo/Cargo.toml b/crates/procinfo/Cargo.toml new file mode 100644 index 0000000000..567b390bf4 --- /dev/null +++ b/crates/procinfo/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "procinfo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] + +[dependencies] +libc = "0.2" +log = "0.4" + +[target."cfg(windows)".dependencies] +ntapi = "0.3" +winapi = { version = "0.3", features = [ + "handleapi", + "memoryapi", + "psapi", + "processthreadsapi", + "shellapi", + "tlhelp32", +]} + diff --git a/crates/procinfo/src/lib.rs b/crates/procinfo/src/lib.rs new file mode 100644 index 0000000000..cfbec762b8 --- /dev/null +++ b/crates/procinfo/src/lib.rs @@ -0,0 +1,93 @@ +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +mod linux; +mod macos; +mod windows; + +#[derive(Debug, Copy, Clone)] +pub enum LocalProcessStatus { + Idle, + Run, + Sleep, + Stop, + Zombie, + Tracing, + Dead, + Wakekill, + Waking, + Parked, + LockBlocked, + Unknown, +} + +#[derive(Debug, Clone)] +pub struct LocalProcessInfo { + /// The process identifier + pub pid: u32, + /// The parent process identifier + pub ppid: u32, + /// The COMM name of the process. May not bear any relation to + /// the executable image name. May be changed at runtime by + /// the process. + /// Many systems truncate this + /// field to 15-16 characters. + pub name: String, + /// Path to the executable image + pub executable: PathBuf, + /// The argument vector. + /// Some systems allow changing the argv block at runtime + /// eg: setproctitle(). + pub argv: Vec, + /// The current working directory for the process, or an empty + /// path if it was not accessible for some reason. + pub cwd: PathBuf, + /// The status of the process. Not all possible values are + /// portably supported on all systems. + pub status: LocalProcessStatus, + /// A clock value in unspecified system dependent units that + /// indicates the relative age of the process. + pub start_time: u64, + /// The console handle associated with the process, if any. + #[cfg(windows)] + pub console: u64, + /// Child processes, keyed by pid + pub children: HashMap, +} +#[cfg(feature = "lua")] +luahelper::impl_lua_conversion_dynamic!(LocalProcessInfo); + +impl LocalProcessInfo { + /// Walk this sub-tree of processes and return a unique set + /// of executable base names. eg: `foo/bar` and `woot/bar` + /// produce a set containing just `bar`. + pub fn flatten_to_exe_names(&self) -> HashSet { + let mut names = HashSet::new(); + + fn flatten(item: &LocalProcessInfo, names: &mut HashSet) { + if let Some(exe) = item.executable.file_name() { + names.insert(exe.to_string_lossy().into_owned()); + } + for proc in item.children.values() { + flatten(proc, names); + } + } + + flatten(self, &mut names); + names + } + + #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] + pub fn with_root_pid(_pid: u32) -> Option { + None + } + + #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] + pub fn current_working_dir(_pid: u32) -> Option { + None + } + + #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] + pub fn executable_path(_pid: u32) -> Option { + None + } +} diff --git a/crates/procinfo/src/linux.rs b/crates/procinfo/src/linux.rs new file mode 100644 index 0000000000..9211f61551 --- /dev/null +++ b/crates/procinfo/src/linux.rs @@ -0,0 +1,139 @@ +#![cfg(target_os = "linux")] +use super::*; + +impl From<&str> for LocalProcessStatus { + fn from(s: &str) -> Self { + match s { + "R" => Self::Run, + "S" => Self::Sleep, + "D" => Self::Idle, + "Z" => Self::Zombie, + "T" => Self::Stop, + "t" => Self::Tracing, + "X" | "x" => Self::Dead, + "K" => Self::Wakekill, + "W" => Self::Waking, + "P" => Self::Parked, + _ => Self::Unknown, + } + } +} + +impl LocalProcessInfo { + pub fn current_working_dir(pid: u32) -> Option { + std::fs::read_link(format!("/proc/{}/cwd", pid)).ok() + } + + pub fn executable_path(pid: u32) -> Option { + std::fs::read_link(format!("/proc/{}/exe", pid)).ok() + } + + pub fn with_root_pid(pid: u32) -> Option { + use libc::pid_t; + + let pid = pid as pid_t; + + fn all_pids() -> Vec { + let mut pids = vec![]; + if let Ok(dir) = std::fs::read_dir("/proc") { + for entry in dir { + if let Ok(entry) = entry { + if let Ok(file_type) = entry.file_type() { + if file_type.is_dir() { + if let Some(name) = entry.file_name().to_str() { + if let Ok(pid) = name.parse::() { + pids.push(pid); + } + } + } + } + } + } + } + pids + } + + struct LinuxStat { + pid: pid_t, + name: String, + status: String, + ppid: pid_t, + // Time process started after boot, measured in ticks + starttime: u64, + } + + fn info_for_pid(pid: pid_t) -> Option { + let data = std::fs::read_to_string(format!("/proc/{}/stat", pid)).ok()?; + let (_pid_space, name) = data.split_once('(')?; + let (name, fields) = name.rsplit_once(')')?; + let fields = fields.split_whitespace().collect::>(); + + Some(LinuxStat { + pid, + name: name.to_string(), + status: fields.get(0)?.to_string(), + ppid: fields.get(1)?.parse().ok()?, + starttime: fields.get(20)?.parse().ok()?, + }) + } + + fn exe_for_pid(pid: pid_t) -> PathBuf { + std::fs::read_link(format!("/proc/{}/exe", pid)).unwrap_or_else(|_| PathBuf::new()) + } + + fn cwd_for_pid(pid: pid_t) -> PathBuf { + LocalProcessInfo::current_working_dir(pid as u32).unwrap_or_else(|| PathBuf::new()) + } + + fn parse_cmdline(pid: pid_t) -> Vec { + let data = match std::fs::read(format!("/proc/{}/cmdline", pid)) { + Ok(data) => data, + Err(_) => return vec![], + }; + + let mut args = vec![]; + + let data = data.strip_suffix(&[0]).unwrap_or(&data); + + for arg in data.split(|&c| c == 0) { + args.push(String::from_utf8_lossy(arg).to_owned().to_string()); + } + + args + } + + let procs: Vec<_> = all_pids().into_iter().filter_map(info_for_pid).collect(); + + fn build_proc(info: &LinuxStat, procs: &[LinuxStat]) -> LocalProcessInfo { + let mut children = HashMap::new(); + + for kid in procs { + if kid.ppid == info.pid { + children.insert(kid.pid as u32, build_proc(kid, procs)); + } + } + + let executable = exe_for_pid(info.pid); + let name = info.name.clone(); + let argv = parse_cmdline(info.pid); + + LocalProcessInfo { + pid: info.pid as _, + ppid: info.ppid as _, + name, + executable, + cwd: cwd_for_pid(info.pid), + argv, + start_time: info.starttime, + status: info.status.as_str().into(), + children, + } + } + + if let Some(info) = procs.iter().find(|info| info.pid == pid) { + Some(build_proc(info, &procs)) + } else { + None + } + } +} diff --git a/crates/procinfo/src/macos.rs b/crates/procinfo/src/macos.rs new file mode 100644 index 0000000000..5d2de1c399 --- /dev/null +++ b/crates/procinfo/src/macos.rs @@ -0,0 +1,231 @@ +#![cfg(target_os = "macos")] +use super::*; +use std::ffi::{OsStr, OsString}; +use std::os::unix::ffi::{OsStrExt, OsStringExt}; + +impl From for LocalProcessStatus { + fn from(s: u32) -> Self { + match s { + 1 => Self::Idle, + 2 => Self::Run, + 3 => Self::Sleep, + 4 => Self::Stop, + 5 => Self::Zombie, + _ => Self::Unknown, + } + } +} + +impl LocalProcessInfo { + pub fn current_working_dir(pid: u32) -> Option { + let mut pathinfo: libc::proc_vnodepathinfo = unsafe { std::mem::zeroed() }; + let size = std::mem::size_of_val(&pathinfo) as libc::c_int; + let ret = unsafe { + libc::proc_pidinfo( + pid as _, + libc::PROC_PIDVNODEPATHINFO, + 0, + &mut pathinfo as *mut _ as *mut _, + size, + ) + }; + if ret != size { + return None; + } + + // Workaround a workaround for an old rustc version supported by libc; + // the type of vip_path should just be [c_char; MAXPATHLEN] but it + // is defined as a horrible nested array by the libc crate: + // `[[c_char; 32]; 32]`. + // Urgh. Let's re-cast it as the correct kind of slice. + let vip_path = unsafe { + std::slice::from_raw_parts( + pathinfo.pvi_cdir.vip_path.as_ptr() as *const u8, + libc::MAXPATHLEN as usize, + ) + }; + let nul = vip_path.iter().position(|&c| c == 0)?; + Some(OsStr::from_bytes(&vip_path[0..nul]).into()) + } + + pub fn executable_path(pid: u32) -> Option { + let mut buffer: Vec = Vec::with_capacity(libc::PROC_PIDPATHINFO_MAXSIZE as _); + let x = unsafe { + libc::proc_pidpath( + pid as _, + buffer.as_mut_ptr() as *mut _, + libc::PROC_PIDPATHINFO_MAXSIZE as _, + ) + }; + if x <= 0 { + return None; + } + + unsafe { buffer.set_len(x as usize) }; + Some(OsString::from_vec(buffer).into()) + } + + pub fn with_root_pid(pid: u32) -> Option { + /// Enumerate all current process identifiers + fn all_pids() -> Vec { + let num_pids = unsafe { libc::proc_listallpids(std::ptr::null_mut(), 0) }; + if num_pids < 1 { + return vec![]; + } + + // Give a bit of padding to avoid looping if processes are spawning + // rapidly while we're trying to collect this info + const PADDING: usize = 32; + let mut pids: Vec = Vec::with_capacity(num_pids as usize + PADDING); + loop { + let n = unsafe { + libc::proc_listallpids( + pids.as_mut_ptr() as *mut _, + (pids.capacity() * std::mem::size_of::()) as _, + ) + }; + + if n < 1 { + return vec![]; + } + + let n = n as usize; + + if n > pids.capacity() { + pids.reserve(n + PADDING); + continue; + } + + unsafe { pids.set_len(n) }; + return pids; + } + } + + /// Obtain info block for a pid. + /// Note that the process could have gone away since we first + /// observed the pid and the time we call this, so we must + /// be able to tolerate this failing. + fn info_for_pid(pid: libc::pid_t) -> Option { + let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() }; + let wanted_size = std::mem::size_of::() as _; + let res = unsafe { + libc::proc_pidinfo( + pid, + libc::PROC_PIDTBSDINFO, + 0, + &mut info as *mut _ as *mut _, + wanted_size, + ) + }; + + if res == wanted_size { + Some(info) + } else { + None + } + } + + fn cwd_for_pid(pid: libc::pid_t) -> PathBuf { + LocalProcessInfo::current_working_dir(pid as _).unwrap_or_else(PathBuf::new) + } + + fn exe_and_args_for_pid_sysctl(pid: libc::pid_t) -> Option<(PathBuf, Vec)> { + use libc::c_int; + let mut size = 64 * 1024; + let mut buf: Vec = Vec::with_capacity(size); + let mut mib = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as c_int]; + + let res = unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as _, + buf.as_mut_ptr() as *mut _, + &mut size, + std::ptr::null_mut(), + 0, + ) + }; + if res == -1 { + return None; + } + if size < (std::mem::size_of::() * 2) { + // Not big enough + return None; + } + unsafe { buf.set_len(size) }; + + // The data in our buffer is laid out like this: + // argc - c_int + // exe_path - NUL terminated string + // argv[0] - NUL terminated string + // argv[1] - NUL terminated string + // ... + // argv[n] - NUL terminated string + // envp[0] - NUL terminated string + // ... + + let mut ptr = &buf[0..size]; + + let argc: c_int = unsafe { std::ptr::read(ptr.as_ptr() as *const c_int) }; + ptr = &ptr[std::mem::size_of::()..]; + + fn consume_cstr(ptr: &mut &[u8]) -> Option { + let nul = ptr.iter().position(|&c| c == 0)?; + let s = String::from_utf8_lossy(&ptr[0..nul]).to_owned().to_string(); + *ptr = ptr.get(nul + 1..)?; + Some(s) + } + + let exe_path = consume_cstr(&mut ptr)?.into(); + + let mut args = vec![]; + for _ in 0..argc { + args.push(consume_cstr(&mut ptr)?); + } + + dbg!(&exe_path); + dbg!(&args); + Some((exe_path, args)) + } + + fn exe_for_pid(pid: libc::pid_t) -> PathBuf { + LocalProcessInfo::executable_path(pid as _).unwrap_or_else(PathBuf::new) + } + + let procs: Vec<_> = all_pids().into_iter().filter_map(info_for_pid).collect(); + + fn build_proc(info: &libc::proc_bsdinfo, procs: &[libc::proc_bsdinfo]) -> LocalProcessInfo { + let mut children = HashMap::new(); + + for kid in procs { + if kid.pbi_ppid == info.pbi_pid { + children.insert(kid.pbi_pid, build_proc(kid, procs)); + } + } + + let (executable, argv) = exe_and_args_for_pid_sysctl(info.pbi_pid as _) + .unwrap_or_else(|| (exe_for_pid(info.pbi_pid as _), vec![])); + + let name = unsafe { std::ffi::CStr::from_ptr(info.pbi_comm.as_ptr() as _) }; + let name = name.to_str().unwrap_or("").to_string(); + + LocalProcessInfo { + pid: info.pbi_pid, + ppid: info.pbi_ppid, + name, + executable, + cwd: cwd_for_pid(info.pbi_pid as _), + argv, + start_time: info.pbi_start_tvsec, + status: LocalProcessStatus::from(info.pbi_status), + children, + } + } + + if let Some(info) = procs.iter().find(|info| info.pbi_pid == pid) { + Some(build_proc(info, &procs)) + } else { + None + } + } +} diff --git a/crates/procinfo/src/windows.rs b/crates/procinfo/src/windows.rs new file mode 100644 index 0000000000..bebcd0157a --- /dev/null +++ b/crates/procinfo/src/windows.rs @@ -0,0 +1,419 @@ +#![cfg(windows)] +use super::*; +use ntapi::ntpebteb::PEB; +use ntapi::ntpsapi::{ + NtQueryInformationProcess, ProcessBasicInformation, ProcessWow64Information, + PROCESS_BASIC_INFORMATION, +}; +use ntapi::ntrtl::RTL_USER_PROCESS_PARAMETERS; +use ntapi::ntwow64::RTL_USER_PROCESS_PARAMETERS32; +use std::ffi::OsString; +use std::mem::MaybeUninit; +use std::os::windows::ffi::OsStringExt; +use winapi::shared::minwindef::{DWORD, FILETIME, LPVOID, MAX_PATH}; +use winapi::shared::ntdef::{FALSE, NT_SUCCESS}; +use winapi::um::handleapi::CloseHandle; +use winapi::um::memoryapi::ReadProcessMemory; +use winapi::um::processthreadsapi::{GetCurrentProcessId, GetProcessTimes, OpenProcess}; +use winapi::um::shellapi::CommandLineToArgvW; +use winapi::um::tlhelp32::*; +use winapi::um::winbase::{LocalFree, QueryFullProcessImageNameW}; +use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}; + +/// Manages a Toolhelp32 snapshot handle +struct Snapshot(HANDLE); + +impl Snapshot { + pub fn new() -> Option { + let handle = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }; + if handle.is_null() { + None + } else { + Some(Self(handle)) + } + } + + pub fn iter(&self) -> ProcIter { + ProcIter { + snapshot: &self, + first: true, + } + } + + pub fn entries() -> Vec { + match Self::new() { + Some(snapshot) => snapshot.iter().collect(), + None => vec![], + } + } +} + +impl Drop for Snapshot { + fn drop(&mut self) { + unsafe { CloseHandle(self.0) }; + } +} + +struct ProcIter<'a> { + snapshot: &'a Snapshot, + first: bool, +} + +impl<'a> Iterator for ProcIter<'a> { + type Item = PROCESSENTRY32W; + + fn next(&mut self) -> Option { + let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() }; + entry.dwSize = std::mem::size_of::() as _; + let res = if self.first { + self.first = false; + unsafe { Process32FirstW(self.snapshot.0, &mut entry) } + } else { + unsafe { Process32NextW(self.snapshot.0, &mut entry) } + }; + if res == 0 { + None + } else { + Some(entry) + } + } +} + +fn wstr_to_path(slice: &[u16]) -> PathBuf { + match slice.iter().position(|&c| c == 0) { + Some(nul) => OsString::from_wide(&slice[..nul]), + None => OsString::from_wide(slice), + } + .into() +} + +fn wstr_to_string(slice: &[u16]) -> String { + wstr_to_path(slice).to_string_lossy().into_owned() +} + +struct ProcParams { + argv: Vec, + cwd: PathBuf, + console: HANDLE, +} + +/// A handle to an opened process +struct ProcHandle { + pid: u32, + proc: HANDLE, +} + +impl ProcHandle { + pub fn new(pid: u32) -> Option { + if pid == unsafe { GetCurrentProcessId() } { + // Avoid the potential for deadlock if we're examining ourselves + log::trace!("ProcHandle::new({}): skip because it is my own pid", pid); + return None; + } + let options = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; + log::trace!("ProcHandle::new({}): OpenProcess", pid); + let handle = unsafe { OpenProcess(options, FALSE as _, pid) }; + log::trace!("ProcHandle::new({}): OpenProcess -> {:?}", pid, handle); + if handle.is_null() { + return None; + } + Some(Self { pid, proc: handle }) + } + + /// Returns the executable image for the process + pub fn executable(&self) -> Option { + let mut buf = [0u16; MAX_PATH + 1]; + let mut len = buf.len() as DWORD; + let res = unsafe { QueryFullProcessImageNameW(self.proc, 0, buf.as_mut_ptr(), &mut len) }; + if res == 0 { + None + } else { + Some(wstr_to_path(&buf)) + } + } + + /// Wrapper around NtQueryInformationProcess that fetches `what` as `T` + fn query_proc(&self, what: u32) -> Option { + let mut data = MaybeUninit::::uninit(); + let res = unsafe { + NtQueryInformationProcess( + self.proc, + what, + data.as_mut_ptr() as _, + std::mem::size_of::() as _, + std::ptr::null_mut(), + ) + }; + if !NT_SUCCESS(res) { + return None; + } + let data = unsafe { data.assume_init() }; + Some(data) + } + + /// Read a `T` from the target process at the specified address + fn read_struct(&self, addr: LPVOID) -> Option { + let mut data = MaybeUninit::::uninit(); + let res = unsafe { + ReadProcessMemory( + self.proc, + addr as _, + data.as_mut_ptr() as _, + std::mem::size_of::() as _, + std::ptr::null_mut(), + ) + }; + if res == 0 { + return None; + } + let data = unsafe { data.assume_init() }; + Some(data) + } + + /// If the process is a 32-bit process running on Win64, return the address + /// of its process parameters. + /// Otherwise, return None to indicate a native win64 process. + fn get_peb32_addr(&self) -> Option { + let peb32_addr: LPVOID = self.query_proc(ProcessWow64Information)?; + if peb32_addr.is_null() { + None + } else { + Some(peb32_addr) + } + } + + /// Returns the cwd and args for the process + pub fn get_params(&self) -> Option { + match self.get_peb32_addr() { + Some(peb32) => self.get_params_32(peb32), + None => self.get_params_64(), + } + } + + fn get_basic_info(&self) -> Option { + self.query_proc(ProcessBasicInformation) + } + + fn get_peb(&self, info: &PROCESS_BASIC_INFORMATION) -> Option { + self.read_struct(info.PebBaseAddress as _) + } + + fn get_proc_params(&self, peb: &PEB) -> Option { + self.read_struct(peb.ProcessParameters as _) + } + + /// Returns the cwd and args for a 64 bit process + fn get_params_64(&self) -> Option { + let info = self.get_basic_info()?; + let peb = self.get_peb(&info)?; + let params = self.get_proc_params(&peb)?; + + let cmdline = self.read_process_wchar( + params.CommandLine.Buffer as _, + params.CommandLine.Length as _, + )?; + let cwd = self.read_process_wchar( + params.CurrentDirectory.DosPath.Buffer as _, + params.CurrentDirectory.DosPath.Length as _, + )?; + + Some(ProcParams { + argv: cmd_line_to_argv(&cmdline), + cwd: wstr_to_path(&cwd), + console: params.ConsoleHandle, + }) + } + + fn get_proc_params_32(&self, peb32: LPVOID) -> Option { + self.read_struct(peb32) + } + + /// Returns the cwd and args for a 32 bit process + fn get_params_32(&self, peb32: LPVOID) -> Option { + let params = self.get_proc_params_32(peb32)?; + + let cmdline = self.read_process_wchar( + params.CommandLine.Buffer as _, + params.CommandLine.Length as _, + )?; + let cwd = self.read_process_wchar( + params.CurrentDirectory.DosPath.Buffer as _, + params.CurrentDirectory.DosPath.Length as _, + )?; + + Some(ProcParams { + argv: cmd_line_to_argv(&cmdline), + cwd: wstr_to_path(&cwd), + console: params.ConsoleHandle as _, + }) + } + + /// Copies a sized WSTR from the address in the process + fn read_process_wchar(&self, ptr: LPVOID, byte_size: usize) -> Option> { + if byte_size > MAX_PATH * 4 { + // Defend against implausibly large paths, just in + // case we're reading the wrong offset into a kernel struct + return None; + } + + let mut buf = vec![0u16; byte_size / 2]; + let mut bytes_read = 0; + + let res = unsafe { + ReadProcessMemory( + self.proc, + ptr as _, + buf.as_mut_ptr() as _, + byte_size, + &mut bytes_read, + ) + }; + if res == 0 { + return None; + } + + // In the unlikely event that we have a short read, + // truncate the buffer to fit. + let wide_chars_read = bytes_read / 2; + buf.resize(wide_chars_read, 0); + + // Ensure that it is NUL terminated + match buf.iter().position(|&c| c == 0) { + Some(n) => { + // Truncate to include existing NUL but no later chars + buf.resize(n + 1, 0); + } + None => { + // Add a NUL + buf.push(0); + } + } + + Some(buf) + } + + /// Retrieves the start time of the process + fn start_time(&self) -> Option { + const fn empty() -> FILETIME { + FILETIME { + dwLowDateTime: 0, + dwHighDateTime: 0, + } + } + + let mut start = empty(); + let mut exit = empty(); + let mut kernel = empty(); + let mut user = empty(); + + let res = + unsafe { GetProcessTimes(self.proc, &mut start, &mut exit, &mut kernel, &mut user) }; + if res == 0 { + return None; + } + + Some((start.dwHighDateTime as u64) << 32 | start.dwLowDateTime as u64) + } +} + +/// Parse a command line string into an argv array +fn cmd_line_to_argv(buf: &[u16]) -> Vec { + let mut argc = 0; + let argvp = unsafe { CommandLineToArgvW(buf.as_ptr(), &mut argc) }; + if argvp.is_null() { + return vec![]; + } + + let argv = unsafe { std::slice::from_raw_parts(argvp, argc as usize) }; + let mut args = vec![]; + for &arg in argv { + let len = unsafe { libc::wcslen(arg) }; + let arg = unsafe { std::slice::from_raw_parts(arg, len) }; + args.push(wstr_to_string(arg)); + } + unsafe { LocalFree(argvp as _) }; + args +} + +impl Drop for ProcHandle { + fn drop(&mut self) { + log::trace!("ProcHandle::drop(pid={} proc={:?})", self.pid, self.proc); + unsafe { CloseHandle(self.proc) }; + } +} + +impl LocalProcessInfo { + pub fn current_working_dir(pid: u32) -> Option { + log::trace!("current_working_dir({})", pid); + let proc = ProcHandle::new(pid)?; + let params = proc.get_params()?; + Some(params.cwd) + } + + pub fn executable_path(pid: u32) -> Option { + log::trace!("executable_path({})", pid); + let proc = ProcHandle::new(pid)?; + proc.executable() + } + + pub fn with_root_pid(pid: u32) -> Option { + log::trace!("LocalProcessInfo::with_root_pid({}), getting snapshot", pid); + let procs = Snapshot::entries(); + log::trace!("Got snapshot"); + + fn build_proc(info: &PROCESSENTRY32W, procs: &[PROCESSENTRY32W]) -> LocalProcessInfo { + let mut children = HashMap::new(); + + for kid in procs { + if kid.th32ParentProcessID == info.th32ProcessID { + children.insert(kid.th32ProcessID, build_proc(kid, procs)); + } + } + + let mut executable = None; + let mut start_time = 0; + let mut cwd = PathBuf::new(); + let mut argv = vec![]; + let mut console = 0; + + if let Some(proc) = ProcHandle::new(info.th32ProcessID) { + if let Some(exe) = proc.executable() { + executable.replace(exe); + } + if let Some(params) = proc.get_params() { + cwd = params.cwd; + argv = params.argv; + console = params.console as _; + } + if let Some(start) = proc.start_time() { + start_time = start; + } + } + + let executable = executable.unwrap_or_else(|| wstr_to_path(&info.szExeFile)); + let name = match executable.file_name() { + Some(name) => name.to_string_lossy().into_owned(), + None => String::new(), + }; + + LocalProcessInfo { + pid: info.th32ProcessID, + ppid: info.th32ParentProcessID, + name, + executable, + cwd, + argv, + start_time, + status: LocalProcessStatus::Run, + children, + console, + } + } + + if let Some(info) = procs.iter().find(|info| info.th32ProcessID == pid) { + Some(build_proc(info, &procs)) + } else { + None + } + } +} diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 5910a2897c..6c49fb81bd 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -9,7 +9,7 @@ doctest = false [dependencies] alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "4e1f0c6177975a040b37f942dfb0e723e46a9971" } -procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "40a7dbf93542fbe4178c2e4b4bd438126a6432b9", default-features = false } +procinfo = { path = "../procinfo" } editor = { path = "../editor" } util = { path = "../util" } gpui = { path = "../gpui" } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 7b504de4ae..a6c2e6aa9a 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -3,7 +3,7 @@ pub mod modal; pub mod terminal_container_view; pub mod terminal_element; pub mod terminal_view; - +// procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "40a7dbf93542fbe4178c2e4b4bd438126a6432b9", default-features = false } use alacritty_terminal::{ ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig, Scrolling}, @@ -237,28 +237,12 @@ impl TerminalError { self.shell .clone() .map(|shell| match shell { - Shell::System => { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + Shell::System => "".to_string(), - match pw { - Some(pw) => format!(" {}", pw.shell), - None => "".to_string(), - } - } Shell::Program(s) => s, Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), }) - .unwrap_or_else(|| { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); - match pw { - Some(pw) => { - format!(" {}", pw.shell) - } - None => " {}".to_string(), - } - }) + .unwrap_or_else(|| "".to_string()) } } @@ -538,6 +522,7 @@ impl Terminal { AlacTermEvent::Wakeup => { cx.emit(Event::Wakeup); + dbg!("*********"); if self.update_process_info() { cx.emit(Event::TitleChanged) } @@ -1039,84 +1024,49 @@ fn make_search_matches<'a, T>( #[cfg(test)] mod tests { + use libc::c_int; + pub mod terminal_test_context; + + #[test] + pub fn wez_test() { + fn test() -> Option> { + let size = 28; + + //Test data pulled from running the code + let buf = [ + 2, 0, 0, 0, 47, 98, 105, 110, 47, 115, 108, 101, 101, 112, 0, 0, 0, 0, 0, 0, 115, + 108, 101, 101, 112, 0, 53, 0, + ]; + + let mut ptr = &buf[0..size]; + + let argc: c_int = unsafe { std::ptr::read(ptr.as_ptr() as *const c_int) }; + ptr = &ptr[std::mem::size_of::()..]; + + fn consume_cstr(ptr: &mut &[u8]) -> Option { + let nul = ptr.iter().position(|&c| c == 0)?; + let s = String::from_utf8_lossy(&ptr[0..nul]).to_owned().to_string(); + *ptr = ptr.get(nul + 1..)?; + Some(s) + } + + let _exe_path: Option = consume_cstr(&mut ptr)?.into(); + + //Clear out the trailing null pointers + while ptr[0] == 0 { + ptr = ptr.get(1..)?; + } + + let mut args = vec![]; + for _ in 0..argc { + args.push(consume_cstr(&mut ptr)?); + } + Some(args) + } + + assert_eq!(test(), Some(vec!["sleep".to_string(), "5".to_string()])); + } } -//TODO Move this around and clean up the code -mod alacritty_unix { - use alacritty_terminal::config::Program; - use gpui::anyhow::{bail, Result}; - - use std::ffi::CStr; - use std::mem::MaybeUninit; - use std::ptr; - - #[derive(Debug)] - pub struct Passwd<'a> { - _name: &'a str, - _dir: &'a str, - pub shell: &'a str, - } - - /// Return a Passwd struct with pointers into the provided buf. - /// - /// # Unsafety - /// - /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. - pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result> { - // Create zeroed passwd struct. - let mut entry: MaybeUninit = MaybeUninit::uninit(); - - let mut res: *mut libc::passwd = ptr::null_mut(); - - // Try and read the pw file. - let uid = unsafe { libc::getuid() }; - let status = unsafe { - libc::getpwuid_r( - uid, - entry.as_mut_ptr(), - buf.as_mut_ptr() as *mut _, - buf.len(), - &mut res, - ) - }; - let entry = unsafe { entry.assume_init() }; - - if status < 0 { - bail!("getpwuid_r failed"); - } - - if res.is_null() { - bail!("pw not found"); - } - - // Sanity check. - assert_eq!(entry.pw_uid, uid); - - // Build a borrowed Passwd struct. - Ok(Passwd { - _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, - _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, - shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, - }) - } - - #[cfg(target_os = "macos")] - pub fn _default_shell(pw: &Passwd<'_>) -> Program { - let shell_name = pw.shell.rsplit('/').next().unwrap(); - let argv = vec![ - String::from("-c"), - format!("exec -a -{} {}", shell_name, pw.shell), - ]; - - Program::WithArgs { - program: "/bin/bash".to_owned(), - args: argv, - } - } - - #[cfg(not(target_os = "macos"))] - pub fn default_shell(pw: &Passwd<'_>) -> Program { - Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned())) - } -} +mod wez_proc_info {}