nushell/crates/nu-system/src/windows.rs

1060 lines
31 KiB
Rust
Raw Normal View History

// Attribution: a lot of this came from procs https://github.com/dalance/procs
// and sysinfo https://github.com/GuillaumeGomez/sysinfo
use chrono::offset::TimeZone;
use chrono::{Local, NaiveDate};
use libc::c_void;
use ntapi::ntpebteb::PEB;
use ntapi::ntpsapi::{
NtQueryInformationProcess, ProcessBasicInformation, ProcessCommandLineInformation,
ProcessWow64Information, PROCESSINFOCLASS, PROCESS_BASIC_INFORMATION,
};
use ntapi::ntrtl::{RtlGetVersion, PRTL_USER_PROCESS_PARAMETERS, RTL_USER_PROCESS_PARAMETERS};
use ntapi::ntwow64::{PEB32, PRTL_USER_PROCESS_PARAMETERS32, RTL_USER_PROCESS_PARAMETERS32};
use once_cell::sync::Lazy;
use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::OsString;
use std::mem::{size_of, zeroed, MaybeUninit};
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use std::ptr;
use std::ptr::null_mut;
use std::thread;
use std::time::{Duration, Instant};
use winapi::shared::basetsd::SIZE_T;
use winapi::shared::minwindef::{DWORD, FALSE, FILETIME, LPVOID, MAX_PATH, TRUE, ULONG};
use winapi::shared::ntdef::{NT_SUCCESS, UNICODE_STRING};
use winapi::shared::ntstatus::{
STATUS_BUFFER_OVERFLOW, STATUS_BUFFER_TOO_SMALL, STATUS_INFO_LENGTH_MISMATCH,
};
use winapi::um::handleapi::CloseHandle;
use winapi::um::memoryapi::{ReadProcessMemory, VirtualQueryEx};
use winapi::um::processthreadsapi::{
GetCurrentProcess, GetPriorityClass, GetProcessTimes, OpenProcess, OpenProcessToken,
};
use winapi::um::psapi::{
GetModuleBaseNameW, GetProcessMemoryInfo, K32EnumProcesses, PROCESS_MEMORY_COUNTERS,
PROCESS_MEMORY_COUNTERS_EX,
};
use winapi::um::securitybaseapi::{AdjustTokenPrivileges, GetTokenInformation};
use winapi::um::tlhelp32::{
CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS,
};
use winapi::um::winbase::{GetProcessIoCounters, LookupAccountSidW, LookupPrivilegeValueW};
use winapi::um::winnt::{
TokenGroups, TokenUser, HANDLE, IO_COUNTERS, MEMORY_BASIC_INFORMATION,
PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, PSID, RTL_OSVERSIONINFOEXW, SE_DEBUG_NAME,
SE_PRIVILEGE_ENABLED, SID, TOKEN_ADJUST_PRIVILEGES, TOKEN_GROUPS, TOKEN_PRIVILEGES,
TOKEN_QUERY, TOKEN_USER,
};
pub struct ProcessInfo {
pub pid: i32,
pub command: String,
pub ppid: i32,
pub start_time: chrono::DateTime<chrono::Local>,
pub cpu_info: CpuInfo,
pub memory_info: MemoryInfo,
pub disk_info: DiskInfo,
pub user: SidName,
pub groups: Vec<SidName>,
pub priority: u32,
pub thread: i32,
pub interval: Duration,
pub cmd: Vec<String>,
pub environ: Vec<String>,
pub cwd: PathBuf,
}
#[derive(Default)]
pub struct MemoryInfo {
pub page_fault_count: u64,
pub peak_working_set_size: u64,
pub working_set_size: u64,
pub quota_peak_paged_pool_usage: u64,
pub quota_paged_pool_usage: u64,
pub quota_peak_non_paged_pool_usage: u64,
pub quota_non_paged_pool_usage: u64,
pub page_file_usage: u64,
pub peak_page_file_usage: u64,
pub private_usage: u64,
}
#[derive(Default)]
pub struct DiskInfo {
pub prev_read: u64,
pub prev_write: u64,
pub curr_read: u64,
pub curr_write: u64,
}
#[derive(Default)]
pub struct CpuInfo {
pub prev_sys: u64,
pub prev_user: u64,
pub curr_sys: u64,
pub curr_user: u64,
}
#[cfg_attr(tarpaulin, skip)]
pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec<ProcessInfo> {
let mut base_procs = Vec::new();
let mut ret = Vec::new();
let _ = set_privilege();
for pid in get_pids() {
let handle = get_handle(pid);
if let Some(handle) = handle {
let times = get_times(handle);
let io = get_io(handle);
let time = Instant::now();
if let (Some((_, _, sys, user)), Some((read, write))) = (times, io) {
base_procs.push((pid, sys, user, read, write, time));
}
}
}
thread::sleep(interval);
let (mut ppids, mut threads) = get_ppid_threads();
for (pid, prev_sys, prev_user, prev_read, prev_write, prev_time) in base_procs {
let ppid = ppids.remove(&pid);
let thread = threads.remove(&pid);
let handle = get_handle(pid);
if let Some(handle) = handle {
let command = get_command(handle);
let memory_info = get_memory_info(handle);
let times = get_times(handle);
let io = get_io(handle);
let start_time = if let Some((start, _, _, _)) = times {
add more `ps` columns in Windows (#10275) # Description This PR adds a few more columns to `ps -l` on Windows. It would be good to add these changes cross-platform in separate PRs. This PR also fixes a bug where start time was calculated wrong. I've added: start_time user user_sid priority ![image](https://github.com/nushell/nushell/assets/343840/cba16a17-ee70-46b5-9e6d-ef06641b264e) # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
2023-09-08 19:24:29 +02:00
// 11_644_473_600 is the number of seconds between the Windows epoch (1601-01-01) and
// the Linux epoch (1970-01-01).
let time = chrono::Duration::seconds(start as i64 / 10_000_000);
chore: chrono_update (#7132) chrono version update # Description upgrade chrono to 0.4.23 # Major Changes If you're considering making any major change to nushell, before starting work on it, seek feedback from regular contributors and get approval for the idea from the core team either on [Discord](https://discordapp.com/invite/NtAbbGn) or [GitHub issue](https://github.com/nushell/nushell/issues/new/choose). Making sure we're all on board with the change saves everybody's time. Thanks! # Tests + Formatting Make sure you've done the following, if applicable: - Add tests that cover your changes (either in the command examples, the crate/tests folder, or in the /tests folder) - Try to think about corner cases and various ways how your changes could break. Cover those in the tests Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace --features=extra -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace --features=extra` to check that all tests pass # After Submitting * Help us keep the docs up to date: If your PR affects the user experience of Nushell (adding/removing a command, changing an input/output type, etc.), make sure the changes are reflected in the documentation (https://github.com/nushell/nushell.github.io) after the PR is merged. Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
2022-11-26 19:19:02 +01:00
let base =
add more `ps` columns in Windows (#10275) # Description This PR adds a few more columns to `ps -l` on Windows. It would be good to add these changes cross-platform in separate PRs. This PR also fixes a bug where start time was calculated wrong. I've added: start_time user user_sid priority ![image](https://github.com/nushell/nushell/assets/343840/cba16a17-ee70-46b5-9e6d-ef06641b264e) # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
2023-09-08 19:24:29 +02:00
NaiveDate::from_ymd_opt(1601, 1, 1).and_then(|nd| nd.and_hms_opt(0, 0, 0));
chore: chrono_update (#7132) chrono version update # Description upgrade chrono to 0.4.23 # Major Changes If you're considering making any major change to nushell, before starting work on it, seek feedback from regular contributors and get approval for the idea from the core team either on [Discord](https://discordapp.com/invite/NtAbbGn) or [GitHub issue](https://github.com/nushell/nushell/issues/new/choose). Making sure we're all on board with the change saves everybody's time. Thanks! # Tests + Formatting Make sure you've done the following, if applicable: - Add tests that cover your changes (either in the command examples, the crate/tests folder, or in the /tests folder) - Try to think about corner cases and various ways how your changes could break. Cover those in the tests Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace --features=extra -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace --features=extra` to check that all tests pass # After Submitting * Help us keep the docs up to date: If your PR affects the user experience of Nushell (adding/removing a command, changing an input/output type, etc.), make sure the changes are reflected in the documentation (https://github.com/nushell/nushell.github.io) after the PR is merged. Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
2022-11-26 19:19:02 +01:00
if let Some(base) = base {
let time = base + time;
Local.from_utc_datetime(&time)
} else {
continue;
}
} else {
chore: chrono_update (#7132) chrono version update # Description upgrade chrono to 0.4.23 # Major Changes If you're considering making any major change to nushell, before starting work on it, seek feedback from regular contributors and get approval for the idea from the core team either on [Discord](https://discordapp.com/invite/NtAbbGn) or [GitHub issue](https://github.com/nushell/nushell/issues/new/choose). Making sure we're all on board with the change saves everybody's time. Thanks! # Tests + Formatting Make sure you've done the following, if applicable: - Add tests that cover your changes (either in the command examples, the crate/tests folder, or in the /tests folder) - Try to think about corner cases and various ways how your changes could break. Cover those in the tests Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace --features=extra -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace --features=extra` to check that all tests pass # After Submitting * Help us keep the docs up to date: If your PR affects the user experience of Nushell (adding/removing a command, changing an input/output type, etc.), make sure the changes are reflected in the documentation (https://github.com/nushell/nushell.github.io) after the PR is merged. Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
2022-11-26 19:19:02 +01:00
let time =
add more `ps` columns in Windows (#10275) # Description This PR adds a few more columns to `ps -l` on Windows. It would be good to add these changes cross-platform in separate PRs. This PR also fixes a bug where start time was calculated wrong. I've added: start_time user user_sid priority ![image](https://github.com/nushell/nushell/assets/343840/cba16a17-ee70-46b5-9e6d-ef06641b264e) # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
2023-09-08 19:24:29 +02:00
NaiveDate::from_ymd_opt(1601, 1, 1).and_then(|nt| nt.and_hms_opt(0, 0, 0));
chore: chrono_update (#7132) chrono version update # Description upgrade chrono to 0.4.23 # Major Changes If you're considering making any major change to nushell, before starting work on it, seek feedback from regular contributors and get approval for the idea from the core team either on [Discord](https://discordapp.com/invite/NtAbbGn) or [GitHub issue](https://github.com/nushell/nushell/issues/new/choose). Making sure we're all on board with the change saves everybody's time. Thanks! # Tests + Formatting Make sure you've done the following, if applicable: - Add tests that cover your changes (either in the command examples, the crate/tests folder, or in the /tests folder) - Try to think about corner cases and various ways how your changes could break. Cover those in the tests Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace --features=extra -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace --features=extra` to check that all tests pass # After Submitting * Help us keep the docs up to date: If your PR affects the user experience of Nushell (adding/removing a command, changing an input/output type, etc.), make sure the changes are reflected in the documentation (https://github.com/nushell/nushell.github.io) after the PR is merged. Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
2022-11-26 19:19:02 +01:00
if let Some(time) = time {
Local.from_utc_datetime(&time)
} else {
continue;
}
};
let cpu_info = if let Some((_, _, curr_sys, curr_user)) = times {
Some(CpuInfo {
prev_sys,
prev_user,
curr_sys,
curr_user,
})
} else {
None
};
let disk_info = if let Some((curr_read, curr_write)) = io {
Some(DiskInfo {
prev_read,
prev_write,
curr_read,
curr_write,
})
} else {
None
};
let user = get_user(handle);
let groups = get_groups(handle);
let priority = get_priority(handle);
let curr_time = Instant::now();
let interval = curr_time - prev_time;
let mut all_ok = true;
all_ok &= command.is_some();
all_ok &= cpu_info.is_some();
all_ok &= memory_info.is_some();
all_ok &= disk_info.is_some();
all_ok &= user.is_some();
all_ok &= groups.is_some();
all_ok &= thread.is_some();
if all_ok {
let (proc_cmd, proc_env, proc_cwd) = match unsafe { get_process_params(handle) } {
Ok(pp) => (pp.0, pp.1, pp.2),
Err(_) => (vec![], vec![], PathBuf::new()),
};
let command = command.unwrap_or_default();
let ppid = ppid.unwrap_or(0);
let cpu_info = cpu_info.unwrap_or_default();
let memory_info = memory_info.unwrap_or_default();
let disk_info = disk_info.unwrap_or_default();
let user = user.unwrap_or_else(|| SidName {
sid: vec![],
name: None,
domainname: None,
});
let groups = groups.unwrap_or_default();
let thread = thread.unwrap_or_default();
let proc = ProcessInfo {
pid,
command,
ppid,
start_time,
cpu_info,
memory_info,
disk_info,
user,
groups,
priority,
thread,
interval,
cmd: proc_cmd,
environ: proc_env,
cwd: proc_cwd,
};
ret.push(proc);
}
unsafe {
CloseHandle(handle);
}
}
}
ret
}
#[cfg_attr(tarpaulin, skip)]
fn set_privilege() -> bool {
unsafe {
let handle = GetCurrentProcess();
let mut token: HANDLE = zeroed();
let ret = OpenProcessToken(handle, TOKEN_ADJUST_PRIVILEGES, &mut token);
if ret == 0 {
return false;
}
let mut tps: TOKEN_PRIVILEGES = zeroed();
let se_debug_name: Vec<u16> = format!("{}\0", SE_DEBUG_NAME).encode_utf16().collect();
tps.PrivilegeCount = 1;
let ret = LookupPrivilegeValueW(
ptr::null(),
se_debug_name.as_ptr(),
&mut tps.Privileges[0].Luid,
);
if ret == 0 {
return false;
}
tps.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
let ret = AdjustTokenPrivileges(
token,
FALSE,
&mut tps,
0,
ptr::null::<TOKEN_PRIVILEGES>() as *mut TOKEN_PRIVILEGES,
ptr::null::<u32>() as *mut u32,
);
if ret == 0 {
return false;
}
true
}
}
#[cfg_attr(tarpaulin, skip)]
fn get_pids() -> Vec<i32> {
let dword_size = size_of::<DWORD>();
let mut pids: Vec<DWORD> = Vec::with_capacity(10192);
let mut cb_needed = 0;
unsafe {
pids.set_len(10192);
let result = K32EnumProcesses(
pids.as_mut_ptr(),
(dword_size * pids.len()) as DWORD,
&mut cb_needed,
);
if result == 0 {
return Vec::new();
}
let pids_len = cb_needed / dword_size as DWORD;
pids.set_len(pids_len as usize);
}
pids.iter().map(|x| *x as i32).collect()
}
#[cfg_attr(tarpaulin, skip)]
fn get_ppid_threads() -> (HashMap<i32, i32>, HashMap<i32, i32>) {
let mut ppids = HashMap::new();
let mut threads = HashMap::new();
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
let mut entry: PROCESSENTRY32 = zeroed();
entry.dwSize = size_of::<PROCESSENTRY32>() as u32;
let mut not_the_end = Process32First(snapshot, &mut entry);
while not_the_end != 0 {
ppids.insert(entry.th32ProcessID as i32, entry.th32ParentProcessID as i32);
threads.insert(entry.th32ProcessID as i32, entry.cntThreads as i32);
not_the_end = Process32Next(snapshot, &mut entry);
}
CloseHandle(snapshot);
}
(ppids, threads)
}
#[cfg_attr(tarpaulin, skip)]
fn get_handle(pid: i32) -> Option<HANDLE> {
if pid == 0 {
return None;
}
let handle = unsafe {
OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE,
pid as DWORD,
)
};
if handle.is_null() {
None
} else {
Some(handle)
}
}
#[cfg_attr(tarpaulin, skip)]
fn get_times(handle: HANDLE) -> Option<(u64, u64, u64, u64)> {
unsafe {
let mut start: FILETIME = zeroed();
let mut exit: FILETIME = zeroed();
let mut sys: FILETIME = zeroed();
let mut user: FILETIME = zeroed();
let ret = GetProcessTimes(
handle,
&mut start as *mut FILETIME,
&mut exit as *mut FILETIME,
&mut sys as *mut FILETIME,
&mut user as *mut FILETIME,
);
let start = u64::from(start.dwHighDateTime) << 32 | u64::from(start.dwLowDateTime);
let exit = u64::from(exit.dwHighDateTime) << 32 | u64::from(exit.dwLowDateTime);
let sys = u64::from(sys.dwHighDateTime) << 32 | u64::from(sys.dwLowDateTime);
let user = u64::from(user.dwHighDateTime) << 32 | u64::from(user.dwLowDateTime);
if ret != 0 {
Some((start, exit, sys, user))
} else {
None
}
}
}
#[cfg_attr(tarpaulin, skip)]
fn get_memory_info(handle: HANDLE) -> Option<MemoryInfo> {
unsafe {
let mut pmc: PROCESS_MEMORY_COUNTERS_EX = zeroed();
let ret = GetProcessMemoryInfo(
handle,
&mut pmc as *mut PROCESS_MEMORY_COUNTERS_EX as *mut c_void
as *mut PROCESS_MEMORY_COUNTERS,
size_of::<PROCESS_MEMORY_COUNTERS_EX>() as DWORD,
);
if ret != 0 {
let info = MemoryInfo {
page_fault_count: u64::from(pmc.PageFaultCount),
peak_working_set_size: pmc.PeakWorkingSetSize as u64,
working_set_size: pmc.WorkingSetSize as u64,
quota_peak_paged_pool_usage: pmc.QuotaPeakPagedPoolUsage as u64,
quota_paged_pool_usage: pmc.QuotaPagedPoolUsage as u64,
quota_peak_non_paged_pool_usage: pmc.QuotaPeakNonPagedPoolUsage as u64,
quota_non_paged_pool_usage: pmc.QuotaNonPagedPoolUsage as u64,
page_file_usage: pmc.PagefileUsage as u64,
peak_page_file_usage: pmc.PeakPagefileUsage as u64,
private_usage: pmc.PrivateUsage as u64,
};
Some(info)
} else {
None
}
}
}
#[cfg_attr(tarpaulin, skip)]
fn get_command(handle: HANDLE) -> Option<String> {
unsafe {
let mut exe_buf = [0u16; MAX_PATH + 1];
let h_mod = std::ptr::null_mut();
let ret = GetModuleBaseNameW(
handle,
h_mod as _,
exe_buf.as_mut_ptr(),
MAX_PATH as DWORD + 1,
);
let mut pos = 0;
for x in exe_buf.iter() {
if *x == 0 {
break;
}
pos += 1;
}
if ret != 0 {
Some(String::from_utf16_lossy(&exe_buf[..pos]))
} else {
None
}
}
}
trait RtlUserProcessParameters {
fn get_cmdline(&self, handle: HANDLE) -> Result<Vec<u16>, &'static str>;
fn get_cwd(&self, handle: HANDLE) -> Result<Vec<u16>, &'static str>;
fn get_environ(&self, handle: HANDLE) -> Result<Vec<u16>, &'static str>;
}
macro_rules! impl_RtlUserProcessParameters {
($t:ty) => {
impl RtlUserProcessParameters for $t {
fn get_cmdline(&self, handle: HANDLE) -> Result<Vec<u16>, &'static str> {
let ptr = self.CommandLine.Buffer;
let size = self.CommandLine.Length;
unsafe { get_process_data(handle, ptr as _, size as _) }
}
fn get_cwd(&self, handle: HANDLE) -> Result<Vec<u16>, &'static str> {
let ptr = self.CurrentDirectory.DosPath.Buffer;
let size = self.CurrentDirectory.DosPath.Length;
unsafe { get_process_data(handle, ptr as _, size as _) }
}
fn get_environ(&self, handle: HANDLE) -> Result<Vec<u16>, &'static str> {
let ptr = self.Environment;
unsafe {
let size = get_region_size(handle, ptr as LPVOID)?;
get_process_data(handle, ptr as _, size as _)
}
}
}
};
}
impl_RtlUserProcessParameters!(RTL_USER_PROCESS_PARAMETERS32);
impl_RtlUserProcessParameters!(RTL_USER_PROCESS_PARAMETERS);
unsafe fn null_terminated_wchar_to_string(slice: &[u16]) -> String {
match slice.iter().position(|&x| x == 0) {
Some(pos) => OsString::from_wide(&slice[..pos])
.to_string_lossy()
.into_owned(),
None => OsString::from_wide(slice).to_string_lossy().into_owned(),
}
}
#[allow(clippy::uninit_vec)]
unsafe fn get_process_data(
handle: HANDLE,
ptr: LPVOID,
size: usize,
) -> Result<Vec<u16>, &'static str> {
let mut buffer: Vec<u16> = Vec::with_capacity(size / 2 + 1);
buffer.set_len(size / 2);
if ReadProcessMemory(
handle,
ptr as *mut _,
buffer.as_mut_ptr() as *mut _,
size,
std::ptr::null_mut(),
) != TRUE
{
return Err("Unable to read process data");
}
Ok(buffer)
}
unsafe fn get_region_size(handle: HANDLE, ptr: LPVOID) -> Result<usize, &'static str> {
let mut meminfo = MaybeUninit::<MEMORY_BASIC_INFORMATION>::uninit();
if VirtualQueryEx(
handle,
ptr,
meminfo.as_mut_ptr() as *mut _,
size_of::<MEMORY_BASIC_INFORMATION>(),
) == 0
{
return Err("Unable to read process memory information");
}
let meminfo = meminfo.assume_init();
Ok((meminfo.RegionSize as isize - ptr.offset_from(meminfo.BaseAddress)) as usize)
}
#[allow(clippy::uninit_vec)]
unsafe fn ph_query_process_variable_size(
process_handle: HANDLE,
process_information_class: PROCESSINFOCLASS,
) -> Option<Vec<u16>> {
let mut return_length = MaybeUninit::<ULONG>::uninit();
let mut status = NtQueryInformationProcess(
process_handle,
process_information_class,
std::ptr::null_mut(),
0,
return_length.as_mut_ptr() as *mut _,
);
if status != STATUS_BUFFER_OVERFLOW
&& status != STATUS_BUFFER_TOO_SMALL
&& status != STATUS_INFO_LENGTH_MISMATCH
{
return None;
}
let mut return_length = return_length.assume_init();
let buf_len = (return_length as usize) / 2;
let mut buffer: Vec<u16> = Vec::with_capacity(buf_len + 1);
buffer.set_len(buf_len);
status = NtQueryInformationProcess(
process_handle,
process_information_class,
buffer.as_mut_ptr() as *mut _,
return_length,
&mut return_length as *mut _,
);
if !NT_SUCCESS(status) {
return None;
}
buffer.push(0);
Some(buffer)
}
unsafe fn get_cmdline_from_buffer(buffer: *const u16) -> Vec<String> {
// Get argc and argv from the command line
let mut argc = MaybeUninit::<i32>::uninit();
let argv_p = winapi::um::shellapi::CommandLineToArgvW(buffer, argc.as_mut_ptr());
if argv_p.is_null() {
return Vec::new();
}
let argc = argc.assume_init();
let argv = std::slice::from_raw_parts(argv_p, argc as usize);
let mut res = Vec::new();
for arg in argv {
let len = libc::wcslen(*arg);
let str_slice = std::slice::from_raw_parts(*arg, len);
res.push(String::from_utf16_lossy(str_slice));
}
winapi::um::winbase::LocalFree(argv_p as *mut _);
res
}
unsafe fn get_process_params(
handle: HANDLE,
) -> Result<(Vec<String>, Vec<String>, PathBuf), &'static str> {
if !cfg!(target_pointer_width = "64") {
return Err("Non 64 bit targets are not supported");
}
// First check if target process is running in wow64 compatibility emulator
let mut pwow32info = MaybeUninit::<LPVOID>::uninit();
let result = NtQueryInformationProcess(
handle,
ProcessWow64Information,
pwow32info.as_mut_ptr() as *mut _,
size_of::<LPVOID>() as u32,
null_mut(),
);
if !NT_SUCCESS(result) {
return Err("Unable to check WOW64 information about the process");
}
let pwow32info = pwow32info.assume_init();
if pwow32info.is_null() {
// target is a 64 bit process
let mut pbasicinfo = MaybeUninit::<PROCESS_BASIC_INFORMATION>::uninit();
let result = NtQueryInformationProcess(
handle,
ProcessBasicInformation,
pbasicinfo.as_mut_ptr() as *mut _,
size_of::<PROCESS_BASIC_INFORMATION>() as u32,
null_mut(),
);
if !NT_SUCCESS(result) {
return Err("Unable to get basic process information");
}
let pinfo = pbasicinfo.assume_init();
let mut peb = MaybeUninit::<PEB>::uninit();
if ReadProcessMemory(
handle,
pinfo.PebBaseAddress as *mut _,
peb.as_mut_ptr() as *mut _,
size_of::<PEB>() as SIZE_T,
std::ptr::null_mut(),
) != TRUE
{
return Err("Unable to read process PEB");
}
let peb = peb.assume_init();
let mut proc_params = MaybeUninit::<RTL_USER_PROCESS_PARAMETERS>::uninit();
if ReadProcessMemory(
handle,
peb.ProcessParameters as *mut PRTL_USER_PROCESS_PARAMETERS as *mut _,
proc_params.as_mut_ptr() as *mut _,
size_of::<RTL_USER_PROCESS_PARAMETERS>() as SIZE_T,
std::ptr::null_mut(),
) != TRUE
{
return Err("Unable to read process parameters");
}
let proc_params = proc_params.assume_init();
return Ok((
get_cmd_line(&proc_params, handle),
get_proc_env(&proc_params, handle),
get_cwd(&proc_params, handle),
));
}
// target is a 32 bit process in wow64 mode
let mut peb32 = MaybeUninit::<PEB32>::uninit();
if ReadProcessMemory(
handle,
pwow32info,
peb32.as_mut_ptr() as *mut _,
size_of::<PEB32>() as SIZE_T,
std::ptr::null_mut(),
) != TRUE
{
return Err("Unable to read PEB32");
}
let peb32 = peb32.assume_init();
let mut proc_params = MaybeUninit::<RTL_USER_PROCESS_PARAMETERS32>::uninit();
if ReadProcessMemory(
handle,
peb32.ProcessParameters as *mut PRTL_USER_PROCESS_PARAMETERS32 as *mut _,
proc_params.as_mut_ptr() as *mut _,
size_of::<RTL_USER_PROCESS_PARAMETERS32>() as SIZE_T,
std::ptr::null_mut(),
) != TRUE
{
return Err("Unable to read 32 bit process parameters");
}
let proc_params = proc_params.assume_init();
Ok((
get_cmd_line(&proc_params, handle),
get_proc_env(&proc_params, handle),
get_cwd(&proc_params, handle),
))
}
static WINDOWS_8_1_OR_NEWER: Lazy<bool> = Lazy::new(|| {
let mut version_info: RTL_OSVERSIONINFOEXW = unsafe { MaybeUninit::zeroed().assume_init() };
version_info.dwOSVersionInfoSize = std::mem::size_of::<RTL_OSVERSIONINFOEXW>() as u32;
if !NT_SUCCESS(unsafe {
RtlGetVersion(&mut version_info as *mut RTL_OSVERSIONINFOEXW as *mut _)
}) {
return true;
}
// Windows 8.1 is 6.3
version_info.dwMajorVersion > 6
|| version_info.dwMajorVersion == 6 && version_info.dwMinorVersion >= 3
});
fn get_cmd_line<T: RtlUserProcessParameters>(params: &T, handle: HANDLE) -> Vec<String> {
if *WINDOWS_8_1_OR_NEWER {
get_cmd_line_new(handle)
} else {
get_cmd_line_old(params, handle)
}
}
#[allow(clippy::cast_ptr_alignment)]
fn get_cmd_line_new(handle: HANDLE) -> Vec<String> {
unsafe {
if let Some(buffer) = ph_query_process_variable_size(handle, ProcessCommandLineInformation)
{
let buffer = (*(buffer.as_ptr() as *const UNICODE_STRING)).Buffer;
get_cmdline_from_buffer(buffer)
} else {
vec![]
}
}
}
fn get_cmd_line_old<T: RtlUserProcessParameters>(params: &T, handle: HANDLE) -> Vec<String> {
match params.get_cmdline(handle) {
Ok(buffer) => unsafe { get_cmdline_from_buffer(buffer.as_ptr()) },
Err(_e) => Vec::new(),
}
}
fn get_proc_env<T: RtlUserProcessParameters>(params: &T, handle: HANDLE) -> Vec<String> {
match params.get_environ(handle) {
Ok(buffer) => {
let equals = "="
.encode_utf16()
.next()
.expect("unable to get next utf16 value");
let raw_env = buffer;
let mut result = Vec::new();
let mut begin = 0;
while let Some(offset) = raw_env[begin..].iter().position(|&c| c == 0) {
let end = begin + offset;
if raw_env[begin..end].iter().any(|&c| c == equals) {
result.push(
OsString::from_wide(&raw_env[begin..end])
.to_string_lossy()
.into_owned(),
);
begin = end + 1;
} else {
break;
}
}
result
}
Err(_e) => Vec::new(),
}
}
fn get_cwd<T: RtlUserProcessParameters>(params: &T, handle: HANDLE) -> PathBuf {
match params.get_cwd(handle) {
Ok(buffer) => unsafe { PathBuf::from(null_terminated_wchar_to_string(buffer.as_slice())) },
Err(_e) => PathBuf::new(),
}
}
#[cfg_attr(tarpaulin, skip)]
fn get_io(handle: HANDLE) -> Option<(u64, u64)> {
unsafe {
let mut io: IO_COUNTERS = zeroed();
let ret = GetProcessIoCounters(handle, &mut io);
if ret != 0 {
Some((io.ReadTransferCount, io.WriteTransferCount))
} else {
None
}
}
}
add more `ps` columns in Windows (#10275) # Description This PR adds a few more columns to `ps -l` on Windows. It would be good to add these changes cross-platform in separate PRs. This PR also fixes a bug where start time was calculated wrong. I've added: start_time user user_sid priority ![image](https://github.com/nushell/nushell/assets/343840/cba16a17-ee70-46b5-9e6d-ef06641b264e) # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
2023-09-08 19:24:29 +02:00
#[derive(Clone)]
pub struct SidName {
pub sid: Vec<u64>,
pub name: Option<String>,
pub domainname: Option<String>,
}
#[cfg_attr(tarpaulin, skip)]
fn get_user(handle: HANDLE) -> Option<SidName> {
unsafe {
let mut token: HANDLE = zeroed();
let ret = OpenProcessToken(handle, TOKEN_QUERY, &mut token);
if ret == 0 {
return None;
}
let mut cb_needed = 0;
let _ = GetTokenInformation(
token,
TokenUser,
ptr::null::<c_void>() as *mut c_void,
0,
&mut cb_needed,
);
let mut buf: Vec<u8> = Vec::with_capacity(cb_needed as usize);
let ret = GetTokenInformation(
token,
TokenUser,
buf.as_mut_ptr() as *mut c_void,
cb_needed,
&mut cb_needed,
);
buf.set_len(cb_needed as usize);
if ret == 0 {
return None;
}
#[allow(clippy::cast_ptr_alignment)]
let token_user = buf.as_ptr() as *const TOKEN_USER;
let psid = (*token_user).User.Sid;
let sid = get_sid(psid);
let (name, domainname) = if let Some((x, y)) = get_name_cached(psid) {
(Some(x), Some(y))
} else {
(None, None)
};
Some(SidName {
sid,
name,
domainname,
})
}
}
#[cfg_attr(tarpaulin, skip)]
fn get_groups(handle: HANDLE) -> Option<Vec<SidName>> {
unsafe {
let mut token: HANDLE = zeroed();
let ret = OpenProcessToken(handle, TOKEN_QUERY, &mut token);
if ret == 0 {
return None;
}
let mut cb_needed = 0;
let _ = GetTokenInformation(
token,
TokenGroups,
ptr::null::<c_void>() as *mut c_void,
0,
&mut cb_needed,
);
let mut buf: Vec<u8> = Vec::with_capacity(cb_needed as usize);
let ret = GetTokenInformation(
token,
TokenGroups,
buf.as_mut_ptr() as *mut c_void,
cb_needed,
&mut cb_needed,
);
buf.set_len(cb_needed as usize);
if ret == 0 {
return None;
}
#[allow(clippy::cast_ptr_alignment)]
let token_groups = buf.as_ptr() as *const TOKEN_GROUPS;
let mut ret = Vec::new();
let sa = (*token_groups).Groups.as_ptr();
for i in 0..(*token_groups).GroupCount {
let psid = (*sa.offset(i as isize)).Sid;
let sid = get_sid(psid);
let (name, domainname) = if let Some((x, y)) = get_name_cached(psid) {
(Some(x), Some(y))
} else {
(None, None)
};
let sid_name = SidName {
sid,
name,
domainname,
};
ret.push(sid_name);
}
Some(ret)
}
}
#[cfg_attr(tarpaulin, skip)]
fn get_sid(psid: PSID) -> Vec<u64> {
unsafe {
let mut ret = Vec::new();
let psid = psid as *const SID;
let mut ia = 0;
ia |= u64::from((*psid).IdentifierAuthority.Value[0]) << 40;
ia |= u64::from((*psid).IdentifierAuthority.Value[1]) << 32;
ia |= u64::from((*psid).IdentifierAuthority.Value[2]) << 24;
ia |= u64::from((*psid).IdentifierAuthority.Value[3]) << 16;
ia |= u64::from((*psid).IdentifierAuthority.Value[4]) << 8;
ia |= u64::from((*psid).IdentifierAuthority.Value[5]);
ret.push(u64::from((*psid).Revision));
ret.push(ia);
let cnt = (*psid).SubAuthorityCount;
let sa = (*psid).SubAuthority.as_ptr();
for i in 0..cnt {
ret.push(u64::from(*sa.offset(i as isize)));
}
ret
}
}
thread_local!(
pub static NAME_CACHE: RefCell<HashMap<PSID, Option<(String, String)>>> =
RefCell::new(HashMap::new());
);
#[cfg_attr(tarpaulin, skip)]
fn get_name_cached(psid: PSID) -> Option<(String, String)> {
NAME_CACHE.with(|c| {
let mut c = c.borrow_mut();
if let Some(x) = c.get(&psid) {
x.clone()
} else {
let x = get_name(psid);
c.insert(psid, x.clone());
x
}
})
}
#[cfg_attr(tarpaulin, skip)]
fn get_name(psid: PSID) -> Option<(String, String)> {
unsafe {
let mut cc_name = 0;
let mut cc_domainname = 0;
let mut pe_use = 0;
let _ = LookupAccountSidW(
ptr::null::<u16>() as *mut u16,
psid,
ptr::null::<u16>() as *mut u16,
&mut cc_name,
ptr::null::<u16>() as *mut u16,
&mut cc_domainname,
&mut pe_use,
);
if cc_name == 0 || cc_domainname == 0 {
return None;
}
let mut name: Vec<u16> = Vec::with_capacity(cc_name as usize);
let mut domainname: Vec<u16> = Vec::with_capacity(cc_domainname as usize);
name.set_len(cc_name as usize);
domainname.set_len(cc_domainname as usize);
let ret = LookupAccountSidW(
ptr::null::<u16>() as *mut u16,
psid,
name.as_mut_ptr(),
&mut cc_name,
domainname.as_mut_ptr(),
&mut cc_domainname,
&mut pe_use,
);
if ret == 0 {
return None;
}
let name = from_wide_ptr(name.as_ptr());
let domainname = from_wide_ptr(domainname.as_ptr());
Some((name, domainname))
}
}
#[cfg_attr(tarpaulin, skip)]
fn from_wide_ptr(ptr: *const u16) -> String {
unsafe {
assert!(!ptr.is_null());
let len = (0..std::isize::MAX)
.position(|i| *ptr.offset(i) == 0)
.unwrap_or_default();
let slice = std::slice::from_raw_parts(ptr, len);
OsString::from_wide(slice).to_string_lossy().into_owned()
}
}
#[cfg_attr(tarpaulin, skip)]
fn get_priority(handle: HANDLE) -> u32 {
unsafe { GetPriorityClass(handle) }
}
impl ProcessInfo {
/// PID of process
pub fn pid(&self) -> i32 {
self.pid
}
/// Parent PID of process
pub fn ppid(&self) -> i32 {
self.ppid
}
/// Name of command
pub fn name(&self) -> String {
self.command.clone()
}
/// Full name of command, with arguments
pub fn command(&self) -> String {
self.cmd.join(" ")
}
pub fn environ(&self) -> Vec<String> {
self.environ.clone()
}
pub fn cwd(&self) -> String {
self.cwd.display().to_string()
}
/// Get the status of the process
pub fn status(&self) -> String {
"unknown".to_string()
}
/// CPU usage as a percent of total
pub fn cpu_usage(&self) -> f64 {
let curr_time = self.cpu_info.curr_sys + self.cpu_info.curr_user;
let prev_time = self.cpu_info.prev_sys + self.cpu_info.prev_user;
let usage_ms = (curr_time - prev_time) / 10000u64;
let interval_ms = self.interval.as_secs() * 1000 + u64::from(self.interval.subsec_millis());
usage_ms as f64 * 100.0 / interval_ms as f64
}
/// Memory size in number of bytes
pub fn mem_size(&self) -> u64 {
self.memory_info.working_set_size
}
/// Virtual memory size in bytes
pub fn virtual_size(&self) -> u64 {
self.memory_info.private_usage
}
}