From 13d98d6cbf09dd8143114f15afa8c403cd50a693 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 03:03:26 +0000 Subject: [PATCH] feat: add command line, I/O, and GUI object process columns Grow the process column list through an options schema 3 migration that keeps the existing binary fields in place. Sample the new counters on the process worker, and leave a failed row unknown instead of zero. Accept command lines up to the UNICODE_STRING limit of 32,767 code units, and size the query buffer to include the UnicodeString header. Co-authored-by: jameslincn --- localization/de.toml | 14 + localization/en_us.toml | 14 + localization/es.toml | 14 + localization/fr.toml | 14 + localization/pt.toml | 14 + localization/ru.toml | 14 + localization/zh_cn.toml | 14 + localization/zh_tw.toml | 14 + src/config/options.rs | 415 +++++++++++++++++++++++--- src/pages/processes/mod.rs | 129 ++++++-- src/pages/processes/model.rs | 75 +++++ src/pages/processes/sampler.rs | 513 +++++++++++++++++++++++++++++++- src/ui/dialogs.rs | 85 +++++- src/ui/localization/text_key.rs | 14 + src/ui/localization/ui.rs | 7 + src/ui/resource_ids.rs | 9 +- 16 files changed, 1284 insertions(+), 75 deletions(-) diff --git a/localization/de.toml b/localization/de.toml index 7aab88e..09a7b0f 100644 --- a/localization/de.toml +++ b/localization/de.toml @@ -123,6 +123,13 @@ ProcessColumnNonPagedPool = "Nicht ausgelagerter Pool" ProcessColumnBasePriority = "Basisprioritaet" ProcessColumnHandleCount = "Handles" ProcessColumnThreadCount = "Threads" +ProcessColumnCommandLine = "Befehlszeile" +ProcessColumnIoReadBytes = "E/A-Lesebytes" +ProcessColumnIoWriteBytes = "E/A-Schreibbytes" +ProcessColumnIoReadCount = "E/A-Lesevorgänge" +ProcessColumnIoWriteCount = "E/A-Schreibvorgänge" +ProcessColumnGdiObjects = "GDI-Objekte" +ProcessColumnUserObjects = "USER-Objekte" ProcessColumnSessionId = "Sitzungs-ID" ProcessColumnUserName = "Benutzername" File = "&Datei" @@ -241,6 +248,13 @@ NonPagedPool = "Nicht ausgelagerter Pool" BasePriority = "Basisprioritaet" HandleCount = "Handleanzahl" ThreadCount = "Threadanzahl" +CommandLine = "Befehlszeile" +IoReadBytes = "E/A-Lesebytes" +IoWriteBytes = "E/A-Schreibbytes" +IoReadCount = "E/A-Lesevorgänge" +IoWriteCount = "E/A-Schreibvorgänge" +GdiObjects = "GDI-Objekte" +UserObjects = "USER-Objekte" ProcessorAffinity = "Prozessoraffinitaet" Processors = "Prozessoren" ProcessorAffinityDescription = "Legt fest, auf welchen CPUs der ausgewaehlten Prozessorgruppe der Prozess ausgefuehrt werden darf." diff --git a/localization/en_us.toml b/localization/en_us.toml index 5755c36..c1937a4 100644 --- a/localization/en_us.toml +++ b/localization/en_us.toml @@ -130,6 +130,13 @@ ProcessColumnNonPagedPool = "NP Pool" ProcessColumnBasePriority = "Base Pri" ProcessColumnHandleCount = "Handles" ProcessColumnThreadCount = "Threads" +ProcessColumnCommandLine = "Command Line" +ProcessColumnIoReadBytes = "I/O Read Bytes" +ProcessColumnIoWriteBytes = "I/O Write Bytes" +ProcessColumnIoReadCount = "I/O Reads" +ProcessColumnIoWriteCount = "I/O Writes" +ProcessColumnGdiObjects = "GDI Objects" +ProcessColumnUserObjects = "USER Objects" ProcessColumnSessionId = "Session ID" ProcessColumnUserName = "User Name" File = "&File" @@ -248,6 +255,13 @@ NonPagedPool = "Non-paged Pool" BasePriority = "Base Priority" HandleCount = "Handle Count" ThreadCount = "Thread Count" +CommandLine = "Command Line" +IoReadBytes = "I/O Read Bytes" +IoWriteBytes = "I/O Write Bytes" +IoReadCount = "I/O Reads" +IoWriteCount = "I/O Writes" +GdiObjects = "GDI Objects" +UserObjects = "USER Objects" ProcessorAffinity = "Processor Affinity" Processors = "Processors" ProcessorAffinityDescription = "Controls which CPUs in the selected processor group the process may execute on." diff --git a/localization/es.toml b/localization/es.toml index aece701..baf09bc 100644 --- a/localization/es.toml +++ b/localization/es.toml @@ -123,6 +123,13 @@ ProcessColumnNonPagedPool = "Bloque no paginado" ProcessColumnBasePriority = "Prioridad base" ProcessColumnHandleCount = "Handles" ProcessColumnThreadCount = "Hilos" +ProcessColumnCommandLine = "Línea de comandos" +ProcessColumnIoReadBytes = "Bytes de lectura de E/S" +ProcessColumnIoWriteBytes = "Bytes de escritura de E/S" +ProcessColumnIoReadCount = "Lecturas de E/S" +ProcessColumnIoWriteCount = "Escrituras de E/S" +ProcessColumnGdiObjects = "Objetos GDI" +ProcessColumnUserObjects = "Objetos USER" ProcessColumnSessionId = "Id. de sesión" ProcessColumnUserName = "Nombre de usuario" File = "&Archivo" @@ -241,6 +248,13 @@ NonPagedPool = "Bloque no paginado" BasePriority = "Prioridad base" HandleCount = "Número de handles" ThreadCount = "Número de hilos" +CommandLine = "Línea de comandos" +IoReadBytes = "Bytes de lectura de E/S" +IoWriteBytes = "Bytes de escritura de E/S" +IoReadCount = "Lecturas de E/S" +IoWriteCount = "Escrituras de E/S" +GdiObjects = "Objetos GDI" +UserObjects = "Objetos USER" ProcessorAffinity = "Afinidad del procesador" Processors = "Procesadores" ProcessorAffinityDescription = "Controla en que CPU del grupo de procesadores seleccionado puede ejecutarse el proceso." diff --git a/localization/fr.toml b/localization/fr.toml index a4077f1..873fc83 100644 --- a/localization/fr.toml +++ b/localization/fr.toml @@ -123,6 +123,13 @@ ProcessColumnNonPagedPool = "Pool non paginé" ProcessColumnBasePriority = "Priorité de base" ProcessColumnHandleCount = "Handles" ProcessColumnThreadCount = "Threads" +ProcessColumnCommandLine = "Ligne de commande" +ProcessColumnIoReadBytes = "Octets lus E/S" +ProcessColumnIoWriteBytes = "Octets écrits E/S" +ProcessColumnIoReadCount = "Lectures E/S" +ProcessColumnIoWriteCount = "Écritures E/S" +ProcessColumnGdiObjects = "Objets GDI" +ProcessColumnUserObjects = "Objets USER" ProcessColumnSessionId = "ID de session" ProcessColumnUserName = "Nom d'utilisateur" File = "&Fichier" @@ -241,6 +248,13 @@ NonPagedPool = "Pool non paginé" BasePriority = "Priorité de base" HandleCount = "Nombre de handles" ThreadCount = "Nombre de threads" +CommandLine = "Ligne de commande" +IoReadBytes = "Octets lus E/S" +IoWriteBytes = "Octets écrits E/S" +IoReadCount = "Lectures E/S" +IoWriteCount = "Écritures E/S" +GdiObjects = "Objets GDI" +UserObjects = "Objets USER" ProcessorAffinity = "Affinité du processeur" Processors = "Processeurs" ProcessorAffinityDescription = "Determine sur quels CPU du groupe de processeurs selectionne le processus peut s'executer." diff --git a/localization/pt.toml b/localization/pt.toml index 2ca1980..3f851dc 100644 --- a/localization/pt.toml +++ b/localization/pt.toml @@ -123,6 +123,13 @@ ProcessColumnNonPagedPool = "Pool não paginado" ProcessColumnBasePriority = "Prioridade básica" ProcessColumnHandleCount = "Handles" ProcessColumnThreadCount = "Threads" +ProcessColumnCommandLine = "Linha de comando" +ProcessColumnIoReadBytes = "Bytes de leitura de E/S" +ProcessColumnIoWriteBytes = "Bytes de gravação de E/S" +ProcessColumnIoReadCount = "Leituras de E/S" +ProcessColumnIoWriteCount = "Gravações de E/S" +ProcessColumnGdiObjects = "Objetos GDI" +ProcessColumnUserObjects = "Objetos USER" ProcessColumnSessionId = "ID da sessão" ProcessColumnUserName = "Nome de usuário" File = "&Arquivo" @@ -241,6 +248,13 @@ NonPagedPool = "Pool não paginado" BasePriority = "Prioridade básica" HandleCount = "Contagem de handles" ThreadCount = "Contagem de threads" +CommandLine = "Linha de comando" +IoReadBytes = "Bytes de leitura de E/S" +IoWriteBytes = "Bytes de gravação de E/S" +IoReadCount = "Leituras de E/S" +IoWriteCount = "Gravações de E/S" +GdiObjects = "Objetos GDI" +UserObjects = "Objetos USER" ProcessorAffinity = "Afinidade do processador" Processors = "Processadores" ProcessorAffinityDescription = "Controla em quais CPUs do grupo de processadores selecionado o processo pode ser executado." diff --git a/localization/ru.toml b/localization/ru.toml index ab7be1d..4289df1 100644 --- a/localization/ru.toml +++ b/localization/ru.toml @@ -123,6 +123,13 @@ ProcessColumnNonPagedPool = "Невыгружаемый пул" ProcessColumnBasePriority = "Базовый приоритет" ProcessColumnHandleCount = "Дескрипторы" ProcessColumnThreadCount = "Потоки" +ProcessColumnCommandLine = "Командная строка" +ProcessColumnIoReadBytes = "Байт чтения ввода-вывода" +ProcessColumnIoWriteBytes = "Байт записи ввода-вывода" +ProcessColumnIoReadCount = "Операций чтения ввода-вывода" +ProcessColumnIoWriteCount = "Операций записи ввода-вывода" +ProcessColumnGdiObjects = "Объекты GDI" +ProcessColumnUserObjects = "Объекты USER" ProcessColumnSessionId = "ID сеанса" ProcessColumnUserName = "Имя пользователя" File = "&Файл" @@ -241,6 +248,13 @@ NonPagedPool = "Невыгружаемый пул" BasePriority = "Базовый приоритет" HandleCount = "Число дескрипторов" ThreadCount = "Число потоков" +CommandLine = "Командная строка" +IoReadBytes = "Байт чтения ввода-вывода" +IoWriteBytes = "Байт записи ввода-вывода" +IoReadCount = "Операций чтения ввода-вывода" +IoWriteCount = "Операций записи ввода-вывода" +GdiObjects = "Объекты GDI" +UserObjects = "Объекты USER" ProcessorAffinity = "Привязка процессора" Processors = "Процессоры" ProcessorAffinityDescription = "Определяет, на каких CPU выбранной группы процессоров может выполняться процесс." diff --git a/localization/zh_cn.toml b/localization/zh_cn.toml index bfdb7c4..ba140d6 100644 --- a/localization/zh_cn.toml +++ b/localization/zh_cn.toml @@ -123,6 +123,13 @@ ProcessColumnNonPagedPool = "非分页池" ProcessColumnBasePriority = "基本优先级" ProcessColumnHandleCount = "句柄数" ProcessColumnThreadCount = "线程数" +ProcessColumnCommandLine = "命令行" +ProcessColumnIoReadBytes = "I/O 读取字节" +ProcessColumnIoWriteBytes = "I/O 写入字节" +ProcessColumnIoReadCount = "I/O 读取次数" +ProcessColumnIoWriteCount = "I/O 写入次数" +ProcessColumnGdiObjects = "GDI 对象" +ProcessColumnUserObjects = "USER 对象" ProcessColumnSessionId = "会话 ID" ProcessColumnUserName = "用户名" File = "文件(&F)" @@ -241,6 +248,13 @@ NonPagedPool = "非分页池" BasePriority = "基本优先级" HandleCount = "句柄数" ThreadCount = "线程数" +CommandLine = "命令行" +IoReadBytes = "I/O 读取字节" +IoWriteBytes = "I/O 写入字节" +IoReadCount = "I/O 读取次数" +IoWriteCount = "I/O 写入次数" +GdiObjects = "GDI 对象" +UserObjects = "USER 对象" ProcessorAffinity = "处理器相关性" Processors = "处理器" ProcessorAffinityDescription = "控制该进程可在所选处理器组中的哪些 CPU 上执行。" diff --git a/localization/zh_tw.toml b/localization/zh_tw.toml index f6a0d26..779cb4a 100644 --- a/localization/zh_tw.toml +++ b/localization/zh_tw.toml @@ -123,6 +123,13 @@ ProcessColumnNonPagedPool = "非分頁集區" ProcessColumnBasePriority = "基本優先順序" ProcessColumnHandleCount = "控制代碼數" ProcessColumnThreadCount = "執行緒數" +ProcessColumnCommandLine = "命令列" +ProcessColumnIoReadBytes = "I/O 讀取位元組" +ProcessColumnIoWriteBytes = "I/O 寫入位元組" +ProcessColumnIoReadCount = "I/O 讀取次數" +ProcessColumnIoWriteCount = "I/O 寫入次數" +ProcessColumnGdiObjects = "GDI 物件" +ProcessColumnUserObjects = "USER 物件" ProcessColumnSessionId = "工作階段 ID" ProcessColumnUserName = "使用者名稱" File = "檔案(&F)" @@ -241,6 +248,13 @@ NonPagedPool = "非分頁集區" BasePriority = "基本優先順序" HandleCount = "控制代碼數" ThreadCount = "執行緒數" +CommandLine = "命令列" +IoReadBytes = "I/O 讀取位元組" +IoWriteBytes = "I/O 寫入位元組" +IoReadCount = "I/O 讀取次數" +IoWriteCount = "I/O 寫入次數" +GdiObjects = "GDI 物件" +UserObjects = "USER 物件" ProcessorAffinity = "處理器相依性" Processors = "處理器" ProcessorAffinityDescription = "控制此處理程序可在所選處理器群組中的哪些 CPU 上執行。" diff --git a/src/config/options.rs b/src/config/options.rs index 871d1af..f67e3ce 100644 --- a/src/config/options.rs +++ b/src/config/options.rs @@ -39,7 +39,12 @@ const LEGACY_TASKMAN_KEY: &str = "Software\\Microsoft\\Windows NT\\CurrentVersio const LEGACY_OPTIONS_VALUE: &str = "Preferences"; const OPTIONS_STORAGE_MAGIC: [u8; 8] = *b"TMGRRS01"; const OPTIONS_STORAGE_VERSION: u32 = 1; -const OPTIONS_SCHEMA_VERSION: i32 = 2; +const OPTIONS_SCHEMA_VERSION: i32 = 3; +const SCHEMA_2_VERSION: i32 = 2; +// Schema 2 stored 16 process columns plus the terminating sentinel. Those two arrays stay at +// this length so later fields keep their offsets. Schema 3 appends the extra slots below. +const SCHEMA2_COLUMN_SLOTS: usize = 17; +const EXTENDED_COLUMN_SLOTS: usize = (NUM_COLUMN + 1) - SCHEMA2_COLUMN_SLOTS; const SCHEMA_0_NETWORK_PAGE: i32 = 3; const SCHEMA_0_USERS_PAGE: i32 = 4; const SCHEMA_1_GPU_PAGE: i32 = 3; @@ -130,6 +135,13 @@ pub enum ColumnId { BasePriority = 13, HandleCount = 14, ThreadCount = 15, + CommandLine = 16, + IoReadBytes = 17, + IoWriteBytes = 18, + IoReadCount = 19, + IoWriteCount = 20, + GdiObjects = 21, + UserObjects = 22, } #[repr(C)] @@ -143,11 +155,30 @@ pub struct Options { pub update_speed: i32, pub window_rect: RECT, pub current_page: i32, - pub active_process_columns: [i32; NUM_COLUMN + 1], - pub column_widths: [i32; NUM_COLUMN + 1], + pub active_process_columns: [i32; SCHEMA2_COLUMN_SLOTS], + pub column_widths: [i32; SCHEMA2_COLUMN_SLOTS], flags: u32, pub unused: i32, pub unused2: i32, + pub extended_process_columns: [i32; EXTENDED_COLUMN_SLOTS], + pub extended_column_widths: [i32; EXTENDED_COLUMN_SLOTS], +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct OptionsV2 { + cb_size: u32, + timer_interval: u32, + view_mode: i32, + cpu_history_mode: i32, + update_speed: i32, + window_rect: RECT, + current_page: i32, + active_process_columns: [i32; SCHEMA2_COLUMN_SLOTS], + column_widths: [i32; SCHEMA2_COLUMN_SLOTS], + flags: u32, + unused: i32, + unused2: i32, } #[repr(C)] @@ -190,8 +221,17 @@ impl StoredOptions { unsafe trait RegistryPod: Copy {} unsafe impl RegistryPod for Options {} +unsafe impl RegistryPod for OptionsV2 {} unsafe impl RegistryPod for StoredOptions {} +const _: () = { + assert!(SCHEMA2_COLUMN_SLOTS == 17); + assert!(EXTENDED_COLUMN_SLOTS == 7); + assert!(ColumnId::ThreadCount as i32 == 15); + assert!(ColumnId::UserObjects as i32 == 22); + assert!(size_of::() == size_of::() + EXTENDED_COLUMN_SLOTS * 2 * 4); +}; + impl Default for Options { fn default() -> Self { // 默认值尽量贴近经典任务管理器的首次启动体验。 @@ -208,11 +248,13 @@ impl Default for Options { bottom: 0, }, current_page: -1, - active_process_columns: [-1; NUM_COLUMN + 1], - column_widths: [-1; NUM_COLUMN + 1], + active_process_columns: [-1; SCHEMA2_COLUMN_SLOTS], + column_widths: [-1; SCHEMA2_COLUMN_SLOTS], flags: 0, unused: OPTIONS_SCHEMA_VERSION, unused2: 0, + extended_process_columns: [-1; EXTENDED_COLUMN_SLOTS], + extended_column_widths: [-1; EXTENDED_COLUMN_SLOTS], }; options.set_minimize_on_use(true); @@ -256,9 +298,9 @@ impl Options { return false; } - match read_registry_binary::(OPTIONS_KEY, OPTIONS_VALUE) { - Ok(Some(stored)) => { - let loaded = match stored.into_options() { + match read_registry_bytes(OPTIONS_KEY, OPTIONS_VALUE) { + Ok(Some(bytes)) => { + let loaded = match decode_stored_options(&bytes) { Ok(loaded) => loaded, Err(error) => { record_win32_error("taskmgr-rs options envelope", error); @@ -276,11 +318,15 @@ impl Options { } } - match read_registry_binary::(LEGACY_TASKMAN_KEY, LEGACY_OPTIONS_VALUE) { - Ok(Some(legacy)) if legacy_options_is_taskmgr_rs(&legacy) => { - self.apply_loaded_options(legacy, min_width, min_height, true) - } - Ok(Some(_)) | Ok(None) => { + match read_registry_bytes(LEGACY_TASKMAN_KEY, LEGACY_OPTIONS_VALUE) { + Ok(Some(bytes)) => match decode_legacy_taskmgr_rs_options(&bytes) { + Some(legacy) => self.apply_loaded_options(legacy, min_width, min_height, true), + None => { + self.set_default_values(min_width, min_height); + false + } + }, + Ok(None) => { self.set_default_values(min_width, min_height); false } @@ -398,7 +444,7 @@ impl Options { && self.flags & !ALL_VALID_FLAGS == 0 && self.unused == OPTIONS_SCHEMA_VERSION && self.unused2 == 0 - && process_columns_are_valid(&self.active_process_columns, &self.column_widths) + && self.process_columns_are_valid() } fn migrate_schema(&mut self) -> Result { @@ -409,8 +455,7 @@ impl Options { SCHEMA_0_USERS_PAGE => 6, page => page, }; - self.unused = OPTIONS_SCHEMA_VERSION; - self.unused2 = 0; + self.finish_column_schema_migration(); Ok(true) } 1 => { @@ -420,8 +465,11 @@ impl Options { ) { self.current_page += 1; } - self.unused = OPTIONS_SCHEMA_VERSION; - self.unused2 = 0; + self.finish_column_schema_migration(); + Ok(true) + } + SCHEMA_2_VERSION => { + self.finish_column_schema_migration(); Ok(true) } OPTIONS_SCHEMA_VERSION => Ok(false), @@ -429,6 +477,14 @@ impl Options { } } + fn finish_column_schema_migration(&mut self) { + self.cb_size = size_of::() as u32; + self.unused = OPTIONS_SCHEMA_VERSION; + self.unused2 = 0; + self.extended_process_columns = [-1; EXTENDED_COLUMN_SLOTS]; + self.extended_column_widths = [-1; EXTENDED_COLUMN_SLOTS]; + } + fn normalize(&mut self, min_width: i32, min_height: i32) { let mut defaults = Self::default(); defaults.set_default_values(min_width, min_height); @@ -475,7 +531,103 @@ impl Options { self.window_rect = defaults.window_rect; } - normalize_process_columns(&mut self.active_process_columns, &mut self.column_widths); + let (mut columns, mut widths) = self.logical_process_columns(); + normalize_process_columns(&mut columns, &mut widths); + self.store_logical_process_columns(&columns, &widths); + } + + pub(crate) fn clear_process_column_slots(&mut self) { + self.active_process_columns = [-1; SCHEMA2_COLUMN_SLOTS]; + self.column_widths = [-1; SCHEMA2_COLUMN_SLOTS]; + self.extended_process_columns = [-1; EXTENDED_COLUMN_SLOTS]; + self.extended_column_widths = [-1; EXTENDED_COLUMN_SLOTS]; + } + + pub(crate) fn process_column_id(&self, index: usize) -> i32 { + self.column_slot(index) + .map(|(column, _)| column) + .unwrap_or(-1) + } + + pub(crate) fn process_column_width(&self, index: usize) -> i32 { + self.column_slot(index) + .map(|(_, width)| width) + .unwrap_or(-1) + } + + pub(crate) fn set_process_column_slot(&mut self, index: usize, column: i32, width: i32) { + if index < SCHEMA2_COLUMN_SLOTS { + self.active_process_columns[index] = column; + self.column_widths[index] = width; + } else if index - SCHEMA2_COLUMN_SLOTS < EXTENDED_COLUMN_SLOTS { + let extended = index - SCHEMA2_COLUMN_SLOTS; + self.extended_process_columns[extended] = column; + self.extended_column_widths[extended] = width; + } + } + + fn column_slot(&self, index: usize) -> Option<(i32, i32)> { + if index < SCHEMA2_COLUMN_SLOTS { + Some(( + self.active_process_columns[index], + self.column_widths[index], + )) + } else { + let extended = index - SCHEMA2_COLUMN_SLOTS; + (extended < EXTENDED_COLUMN_SLOTS).then_some(( + self.extended_process_columns[extended], + self.extended_column_widths[extended], + )) + } + } + + fn process_columns_are_valid(&self) -> bool { + let (columns, widths) = self.logical_process_columns(); + process_columns_are_valid(&columns, &widths) + } + + fn logical_process_columns(&self) -> ([i32; NUM_COLUMN + 1], [i32; NUM_COLUMN + 1]) { + let mut columns = [-1; NUM_COLUMN + 1]; + let mut widths = [-1; NUM_COLUMN + 1]; + columns[..SCHEMA2_COLUMN_SLOTS].copy_from_slice(&self.active_process_columns); + widths[..SCHEMA2_COLUMN_SLOTS].copy_from_slice(&self.column_widths); + columns[SCHEMA2_COLUMN_SLOTS..].copy_from_slice(&self.extended_process_columns); + widths[SCHEMA2_COLUMN_SLOTS..].copy_from_slice(&self.extended_column_widths); + (columns, widths) + } + + fn store_logical_process_columns( + &mut self, + columns: &[i32; NUM_COLUMN + 1], + widths: &[i32; NUM_COLUMN + 1], + ) { + self.active_process_columns + .copy_from_slice(&columns[..SCHEMA2_COLUMN_SLOTS]); + self.column_widths + .copy_from_slice(&widths[..SCHEMA2_COLUMN_SLOTS]); + self.extended_process_columns + .copy_from_slice(&columns[SCHEMA2_COLUMN_SLOTS..]); + self.extended_column_widths + .copy_from_slice(&widths[SCHEMA2_COLUMN_SLOTS..]); + } + + fn from_v2(old: OptionsV2) -> Self { + Self { + cb_size: size_of::() as u32, + timer_interval: old.timer_interval, + view_mode: old.view_mode, + cpu_history_mode: old.cpu_history_mode, + update_speed: old.update_speed, + window_rect: old.window_rect, + current_page: old.current_page, + active_process_columns: old.active_process_columns, + column_widths: old.column_widths, + flags: old.flags, + unused: old.unused, + unused2: old.unused2, + extended_process_columns: [-1; EXTENDED_COLUMN_SLOTS], + extended_column_widths: [-1; EXTENDED_COLUMN_SLOTS], + } } fn set_flag(&mut self, mask: u32, value: bool) { @@ -497,13 +649,81 @@ fn legacy_options_is_taskmgr_rs(options: &Options) -> bool { && is_valid_cpu_history_mode(options.cpu_history_mode) && is_valid_update_speed(options.update_speed) && options.flags & !ALL_VALID_FLAGS == 0 - && process_columns_are_valid(&options.active_process_columns, &options.column_widths) + && options.process_columns_are_valid() } -fn read_registry_binary( - key_path: &str, - value_name: &str, -) -> Result, u32> { +fn legacy_v2_options_are_taskmgr_rs(options: &OptionsV2) -> bool { + let migrated = Options::from_v2(*options); + options.cb_size == size_of::() as u32 + && options.unused == SCHEMA_2_VERSION + && options.unused2 == 0 + && migrated.current_page >= -1 + && migrated.current_page < PageId::COUNT as i32 + && is_valid_view_mode(migrated.view_mode) + && is_valid_cpu_history_mode(migrated.cpu_history_mode) + && is_valid_update_speed(migrated.update_speed) + && migrated.flags & !ALL_VALID_FLAGS == 0 + && migrated.process_columns_are_valid() +} + +fn decode_stored_options(bytes: &[u8]) -> Result { + if bytes.len() < 16 { + return Err(ERROR_INVALID_DATA); + } + let mut magic = [0u8; 8]; + magic.copy_from_slice(&bytes[..8]); + if magic != OPTIONS_STORAGE_MAGIC { + return Err(ERROR_INVALID_DATA); + } + let storage_version = u32::from_le_bytes(bytes[8..12].try_into().unwrap()); + if storage_version != OPTIONS_STORAGE_VERSION { + return Err(ERROR_REVISION_MISMATCH); + } + let payload_size = u32::from_le_bytes(bytes[12..16].try_into().unwrap()); + let payload_len = usize::try_from(payload_size).map_err(|_| ERROR_INVALID_DATA)?; + if bytes.len() != 16 + payload_len { + return Err(ERROR_INVALID_DATA); + } + let payload = &bytes[16..]; + if payload_len == size_of::() { + Ok(Options::from_v2(read_pod::(payload)?)) + } else if payload_len == size_of::() && bytes.len() == size_of::() { + read_pod::(bytes)?.into_options() + } else { + Err(ERROR_INVALID_DATA) + } +} + +fn decode_legacy_taskmgr_rs_options(bytes: &[u8]) -> Option { + if bytes.len() == size_of::() { + let options = read_pod::(bytes).ok()?; + legacy_options_is_taskmgr_rs(&options).then_some(options) + } else if bytes.len() == size_of::() { + let old = read_pod::(bytes).ok()?; + legacy_v2_options_are_taskmgr_rs(&old).then_some(Options::from_v2(old)) + } else { + None + } +} + +fn read_pod(bytes: &[u8]) -> Result { + if bytes.len() != size_of::() { + return Err(ERROR_INVALID_DATA); + } + // SAFETY: RegistryPod permits every bit pattern. `bytes` has the exact size of `T`, and the + // copy lands in aligned owned storage before any field is read. + unsafe { + let mut value = zeroed::(); + std::ptr::copy_nonoverlapping( + bytes.as_ptr(), + (&mut value as *mut T).cast::(), + bytes.len(), + ); + Ok(value) + } +} + +fn read_registry_bytes(key_path: &str, value_name: &str) -> Result>, u32> { unsafe { let key_path = to_wide_null(key_path); let value_name = to_wide_null(value_name); @@ -539,21 +759,19 @@ fn read_registry_binary( RegCloseKey(key); return Err(size_status); } - if value_type != REG_BINARY || value_size != size_of::() as u32 { + if value_type != REG_BINARY || value_size == 0 || value_size > 1_048_576 { RegCloseKey(key); return Err(ERROR_INVALID_DATA); } - // SAFETY: RegistryPod requires every bit pattern to be valid. The exact-size check above - // and the second query's unchanged byte count guarantee that Windows initializes all bytes. - let mut value = zeroed::(); + let mut value = vec![0u8; value_size as usize]; let mut actual_size = value_size; let read_status = RegQueryValueExW( key, value_name.as_ptr(), null_mut(), &mut value_type, - (&mut value as *mut T).cast::(), + value.as_mut_ptr(), &mut actual_size, ); let close_status = RegCloseKey(key); @@ -563,9 +781,10 @@ fn read_registry_binary( if close_status != ERROR_SUCCESS { return Err(close_status); } - if value_type != REG_BINARY || actual_size != size_of::() as u32 { + if value_type != REG_BINARY || actual_size != value_size { return Err(ERROR_INVALID_DATA); } + value.truncate(actual_size as usize); Ok(Some(value)) } } @@ -774,6 +993,38 @@ fn screen_reader_enabled() -> bool { } } +#[cfg(test)] +fn schema2_stored_bytes(schema: i32, page: i32) -> Vec { + let current = Options::default(); + let old = OptionsV2 { + cb_size: size_of::() as u32, + timer_interval: current.timer_interval, + view_mode: current.view_mode, + cpu_history_mode: current.cpu_history_mode, + update_speed: current.update_speed, + window_rect: current.window_rect, + current_page: page, + active_process_columns: current.active_process_columns, + column_widths: current.column_widths, + flags: current.flags, + unused: schema, + unused2: 0, + }; + let mut bytes = Vec::with_capacity(16 + size_of::()); + bytes.extend_from_slice(&OPTIONS_STORAGE_MAGIC); + bytes.extend_from_slice(&OPTIONS_STORAGE_VERSION.to_le_bytes()); + bytes.extend_from_slice(&(size_of::() as u32).to_le_bytes()); + // SAFETY: OptionsV2 is RegistryPod. This copies the initialized value and does not retain the pointer. + let raw = unsafe { + std::slice::from_raw_parts( + (&old as *const OptionsV2).cast::(), + size_of::(), + ) + }; + bytes.extend_from_slice(raw); + bytes +} + #[cfg(test)] mod tests { use super::{ @@ -781,10 +1032,11 @@ mod tests { LEGACY_TASKMAN_KEY, NUM_COLUMN, OPTIONS_KEY, OPTIONS_SCHEMA_VERSION, OPTIONS_STORAGE_MAGIC, OPTIONS_STORAGE_VERSION, OPTIONS_VALUE, Options, SCHEMA_0_NETWORK_PAGE, SCHEMA_0_USERS_PAGE, SCHEMA_1_GPU_PAGE, SCHEMA_1_NETWORK_PAGE, SCHEMA_1_USERS_PAGE, - StoredOptions, UpdateSpeed, legacy_options_is_taskmgr_rs, normalize_process_columns, - process_columns_are_valid, update_speed_timer_interval, window_rect_dimensions_are_valid, - window_rect_is_valid, + StoredOptions, UpdateSpeed, decode_stored_options, legacy_options_is_taskmgr_rs, + normalize_process_columns, process_columns_are_valid, schema2_stored_bytes, + update_speed_timer_interval, window_rect_dimensions_are_valid, window_rect_is_valid, }; + use std::mem::size_of; use windows_sys::Win32::Foundation::RECT; #[test] @@ -881,10 +1133,99 @@ mod tests { options.timer_interval, update_speed_timer_interval(UpdateSpeed::Normal as i32).unwrap() ); - assert!(process_columns_are_valid( - &options.active_process_columns, - &options.column_widths - )); + assert!(options.process_columns_are_valid()); + assert!( + options + .extended_process_columns + .iter() + .all(|column| *column == -1) + ); + } + + #[test] + fn schema2_options_grow_into_schema3_without_reordering_saved_columns() { + let mut options = decode_stored_options(&schema2_stored_bytes(2, 2)).unwrap(); + assert_eq!(options.migrate_schema(), Ok(true)); + assert_eq!(options.unused, OPTIONS_SCHEMA_VERSION); + assert_eq!(options.cb_size, size_of::() as u32); + assert_eq!(options.current_page, 2); + assert_eq!( + options.active_process_columns[0], + ColumnId::ImageName as i32 + ); + assert_eq!(options.active_process_columns[1], ColumnId::Username as i32); + assert_eq!(options.active_process_columns[4], ColumnId::MemUsage as i32); + assert!( + options + .extended_process_columns + .iter() + .all(|column| *column == -1) + ); + assert!( + options + .extended_column_widths + .iter() + .all(|width| *width == -1) + ); + assert!(options.process_columns_are_valid()); + + let mut network = + decode_stored_options(&schema2_stored_bytes(0, SCHEMA_0_NETWORK_PAGE)).unwrap(); + assert_eq!(network.migrate_schema(), Ok(true)); + assert_eq!(network.current_page, 5); + assert_eq!(network.unused, OPTIONS_SCHEMA_VERSION); + assert_eq!( + network.active_process_columns[0], + ColumnId::ImageName as i32 + ); + } + + #[test] + fn stored_options_reject_a_payload_size_from_neither_schema() { + let mut bytes = schema2_stored_bytes(2, 0); + bytes.push(0); + let payload_size = (bytes.len() - 16) as u32; + bytes[12..16].copy_from_slice(&payload_size.to_le_bytes()); + assert_eq!( + decode_stored_options(&bytes).err(), + Some(ERROR_INVALID_DATA) + ); + } + + #[test] + fn added_process_columns_keep_primary_duplicate_and_sentinel_rules() { + let mut columns = [-1; NUM_COLUMN + 1]; + let mut widths = [-1; NUM_COLUMN + 1]; + columns[0] = ColumnId::ImageName as i32; + columns[1] = ColumnId::CommandLine as i32; + widths[1] = 180; + columns[2] = ColumnId::IoReadBytes as i32; + columns[3] = ColumnId::GdiObjects as i32; + columns[4] = ColumnId::UserObjects as i32; + assert!(process_columns_are_valid(&columns, &widths)); + + columns[5] = ColumnId::CommandLine as i32; + assert!(!process_columns_are_valid(&columns, &widths)); + + columns[5] = NUM_COLUMN as i32; + assert!(!process_columns_are_valid(&columns, &widths)); + + columns[5] = -1; + widths[5] = 8; + assert!(!process_columns_are_valid(&columns, &widths)); + + widths[5] = -1; + widths[2] = 0; + normalize_process_columns(&mut columns, &mut widths); + assert_eq!(columns[0], ColumnId::ImageName as i32); + assert_eq!(columns[1], ColumnId::CommandLine as i32); + assert_eq!(columns[2], ColumnId::IoReadBytes as i32); + assert_eq!(columns[3], ColumnId::GdiObjects as i32); + assert_eq!(columns[4], ColumnId::UserObjects as i32); + assert_eq!(columns[5], -1); + assert_eq!(widths[1], 180); + assert_eq!(widths[2], -1); + assert!(process_columns_are_valid(&columns, &widths)); } #[test] diff --git a/src/pages/processes/mod.rs b/src/pages/processes/mod.rs index c722f31..f767ec3 100644 --- a/src/pages/processes/mod.rs +++ b/src/pages/processes/mod.rs @@ -99,6 +99,13 @@ const PROCESS_COLUMNS: [ProcessColumn; NUM_COLUMN] = [ ProcessColumn::new(TextKey::ProcessColumnBasePriority, LVCFMT_RIGHT, 60), ProcessColumn::new(TextKey::ProcessColumnHandleCount, LVCFMT_RIGHT, 60), ProcessColumn::new(TextKey::ProcessColumnThreadCount, LVCFMT_RIGHT, 60), + ProcessColumn::new(TextKey::ProcessColumnCommandLine, LVCFMT_LEFT, 240), + ProcessColumn::new(TextKey::ProcessColumnIoReadBytes, LVCFMT_RIGHT, 90), + ProcessColumn::new(TextKey::ProcessColumnIoWriteBytes, LVCFMT_RIGHT, 90), + ProcessColumn::new(TextKey::ProcessColumnIoReadCount, LVCFMT_RIGHT, 70), + ProcessColumn::new(TextKey::ProcessColumnIoWriteCount, LVCFMT_RIGHT, 70), + ProcessColumn::new(TextKey::ProcessColumnGdiObjects, LVCFMT_RIGHT, 70), + ProcessColumn::new(TextKey::ProcessColumnUserObjects, LVCFMT_RIGHT, 70), ]; const COLUMN_DIALOG_IDS: [i32; NUM_COLUMN] = [ @@ -119,6 +126,13 @@ const COLUMN_DIALOG_IDS: [i32; NUM_COLUMN] = [ IDC_BASEPRIORITY, IDC_HANDLECOUNT, IDC_THREADCOUNT, + IDC_COMMANDLINE, + IDC_IOREADBYTES, + IDC_IOWRITEBYTES, + IDC_IOREADCOUNT, + IDC_IOWRITECOUNT, + IDC_GDIOBJECTS, + IDC_USEROBJECTS, ]; const DEFAULT_MARGIN: i32 = 8; @@ -912,6 +926,11 @@ impl ProcessPageState { }; let request = ProcWorkerRequest { processor_count: self.processor_count, + query_command_line: self.active_columns.contains(&ColumnId::CommandLine), + query_gui_objects: self + .active_columns + .iter() + .any(|column| matches!(column, ColumnId::GdiObjects | ColumnId::UserObjects)), }; if let Err(error) = worker.request(request, self.hwnd_page) { self.set_refresh_error(error); @@ -1298,10 +1317,7 @@ impl ProcessPageState { for (index, column_id) in self.active_columns.iter().enumerate() { let column = PROCESS_COLUMNS[*column_id as usize]; - let width = options - .column_widths - .get(index) - .copied() + let width = Some(options.process_column_width(index)) .filter(|value| *value > 0) .unwrap_or(column.default_width); let title = text(column.title_key).to_string(); @@ -1662,15 +1678,14 @@ fn write_process_column_layout( columns: &[ColumnId], widths: &HashMap, ) { - options.active_process_columns.fill(-1); - options.column_widths.fill(-1); + options.clear_process_column_slots(); for (index, &column) in columns.iter().take(NUM_COLUMN).enumerate() { - options.active_process_columns[index] = column as i32; - options.column_widths[index] = widths + let width = widths .get(&(column as i32)) .copied() .filter(|width| *width > 0) .unwrap_or(PROCESS_COLUMNS[column as usize].default_width); + options.set_process_column_slot(index, column as i32, width); } } @@ -1679,11 +1694,11 @@ fn apply_selected_columns(hwnd: HWND, options: &mut Options) { unsafe { let existing_columns = columns_from_options(options); let mut existing_widths = HashMap::with_capacity(NUM_COLUMN); - for (index, value) in options.active_process_columns.iter().copied().enumerate() { - let Some(column) = column_id_from_i32(value) else { + for index in 0..NUM_COLUMN + 1 { + let Some(column) = column_id_from_i32(options.process_column_id(index)) else { break; }; - existing_widths.insert(column as i32, options.column_widths[index]); + existing_widths.insert(column as i32, options.process_column_width(index)); } let mut selected = [false; NUM_COLUMN]; @@ -1717,10 +1732,9 @@ fn apply_selected_columns(hwnd: HWND, options: &mut Options) { } fn columns_from_options(options: &Options) -> Vec { - options - .active_process_columns - .iter() - .copied() + (0..NUM_COLUMN + 1) + .map(|index| options.process_column_id(index)) + .take_while(|column| *column != -1) .filter_map(column_id_from_i32) .collect() } @@ -1744,6 +1758,13 @@ fn column_id_from_i32(value: i32) -> Option { x if x == ColumnId::BasePriority as i32 => Some(ColumnId::BasePriority), x if x == ColumnId::HandleCount as i32 => Some(ColumnId::HandleCount), x if x == ColumnId::ThreadCount as i32 => Some(ColumnId::ThreadCount), + x if x == ColumnId::CommandLine as i32 => Some(ColumnId::CommandLine), + x if x == ColumnId::IoReadBytes as i32 => Some(ColumnId::IoReadBytes), + x if x == ColumnId::IoWriteBytes as i32 => Some(ColumnId::IoWriteBytes), + x if x == ColumnId::IoReadCount as i32 => Some(ColumnId::IoReadCount), + x if x == ColumnId::IoWriteCount as i32 => Some(ColumnId::IoWriteCount), + x if x == ColumnId::GdiObjects as i32 => Some(ColumnId::GdiObjects), + x if x == ColumnId::UserObjects as i32 => Some(ColumnId::UserObjects), _ => None, } } @@ -1765,11 +1786,14 @@ mod tests { merge_wts_process_identity, signed_kb_delta, system_time_delta, wts_identity_matches, }; use super::{ - ProcIdentity, ProcessPageState, SelectionScrollPolicy, refresh_selection_scroll_policy, - reorder_process_columns, write_process_column_layout, + ProcIdentity, ProcessPageState, SelectionScrollPolicy, column_id_from_i32, + columns_from_options, refresh_selection_scroll_policy, reorder_process_columns, + write_process_column_layout, }; use crate::config::options::{ColumnId, Options}; use crate::system::process_identity::query_process_identity_for_pid; + use crate::ui::localization::{TextKey, text}; + use crate::ui::resource_ids::NUM_COLUMN; use std::collections::{HashMap, HashSet}; use std::env; use std::fs; @@ -1829,6 +1853,13 @@ mod tests { priority_class: NORMAL_PRIORITY_CLASS, handle_count: 0, thread_count: 0, + command_line: None, + io_read_bytes: None, + io_write_bytes: None, + io_read_count: None, + io_write_count: None, + gdi_objects: None, + user_objects: None, display_text: std::array::from_fn(|_| String::new()), pass_count: 0, dirty_columns: DirtyColumns::default(), @@ -1925,7 +1956,11 @@ mod tests { let mut worker = ProcWorkerState::default(); for pass in 0..2 { let snapshot = worker - .collect(ProcWorkerRequest { processor_count: 1 }) + .collect(ProcWorkerRequest { + processor_count: 1, + query_command_line: false, + query_gui_objects: false, + }) .unwrap(); for (_, identity, mode) in &fixtures { let entry = snapshot @@ -1943,7 +1978,11 @@ mod tests { let identities: Vec<_> = fixtures.iter().map(|(_, identity, _)| *identity).collect(); drop(fixtures); let snapshot = worker - .collect(ProcWorkerRequest { processor_count: 1 }) + .collect(ProcWorkerRequest { + processor_count: 1, + query_command_line: false, + query_gui_objects: false, + }) .unwrap(); for identity in identities { assert!( @@ -2016,6 +2055,58 @@ mod tests { ] ); assert_eq!(&options.column_widths[..3], &[120, 44, 160]); + assert!( + options + .extended_process_columns + .iter() + .all(|column| *column == -1) + ); + } + + #[test] + fn extended_column_slots_keep_ids_past_the_schema2_array() { + let mut options = Options::default(); + let columns = (0..NUM_COLUMN) + .map(|index| column_id_from_i32(index as i32).unwrap()) + .collect::>(); + write_process_column_layout(&mut options, &columns, &HashMap::new()); + assert_eq!(columns_from_options(&options), columns); + assert_eq!( + options.process_column_id(ColumnId::CommandLine as usize), + ColumnId::CommandLine as i32 + ); + assert_eq!( + options.extended_process_columns[0], + ColumnId::IoReadBytes as i32 + ); + assert_eq!(options.process_column_id(NUM_COLUMN), -1); + } + + #[test] + fn unknown_detail_values_do_not_format_as_zero() { + let mut entry = empty_process_entry("app.exe"); + entry.rebuild_display_columns(&[ + ColumnId::CommandLine, + ColumnId::IoReadBytes, + ColumnId::GdiObjects, + ]); + let unknown = text(TextKey::NotAvailable); + assert_eq!(entry.display_text[ColumnId::CommandLine as usize], unknown); + assert_eq!(entry.display_text[ColumnId::IoReadBytes as usize], unknown); + assert_ne!(entry.display_text[ColumnId::IoReadBytes as usize], "0"); + assert_ne!(entry.display_text[ColumnId::GdiObjects as usize], "0"); + + entry.command_line = Some(String::new()); + entry.io_read_bytes = Some(0); + entry.gdi_objects = Some(0); + entry.rebuild_display_columns(&[ + ColumnId::CommandLine, + ColumnId::IoReadBytes, + ColumnId::GdiObjects, + ]); + assert_eq!(entry.display_text[ColumnId::CommandLine as usize], ""); + assert_eq!(entry.display_text[ColumnId::IoReadBytes as usize], "0"); + assert_eq!(entry.display_text[ColumnId::GdiObjects as usize], "0"); } #[test] diff --git a/src/pages/processes/model.rs b/src/pages/processes/model.rs index b7b0779..b7bbda9 100644 --- a/src/pages/processes/model.rs +++ b/src/pages/processes/model.rs @@ -22,6 +22,7 @@ use super::ProcessStrings; use crate::config::options::ColumnId; use crate::infrastructure::native::{ProcessArchitecture, append_architecture_suffix}; use crate::system::process_identity::ProcIdentity; +use crate::ui::localization::{TextKey, text}; use crate::ui::resource_ids::NUM_COLUMN; #[derive(Clone, Copy, Default)] @@ -82,6 +83,13 @@ pub struct ProcEntry { pub(super) priority_class: u32, pub(super) handle_count: u32, pub(super) thread_count: u32, + pub(super) command_line: Option, + pub(super) io_read_bytes: Option, + pub(super) io_write_bytes: Option, + pub(super) io_read_count: Option, + pub(super) io_write_count: Option, + pub(super) gdi_objects: Option, + pub(super) user_objects: Option, pub(super) display_text: [String; NUM_COLUMN], pub(super) pass_count: u64, pub(super) dirty_columns: DirtyColumns, @@ -124,6 +132,15 @@ pub(super) fn compare_entries( } ColumnId::HandleCount => left.handle_count.cmp(&right.handle_count), ColumnId::ThreadCount => left.thread_count.cmp(&right.thread_count), + ColumnId::CommandLine => { + cmp_option(left.command_line.as_deref(), right.command_line.as_deref()) + } + ColumnId::IoReadBytes => cmp_option(left.io_read_bytes, right.io_read_bytes), + ColumnId::IoWriteBytes => cmp_option(left.io_write_bytes, right.io_write_bytes), + ColumnId::IoReadCount => cmp_option(left.io_read_count, right.io_read_count), + ColumnId::IoWriteCount => cmp_option(left.io_write_count, right.io_write_count), + ColumnId::GdiObjects => cmp_option(left.gdi_objects, right.gdi_objects), + ColumnId::UserObjects => cmp_option(left.user_objects, right.user_objects), }; if ordering == Ordering::Equal { @@ -189,6 +206,29 @@ fn format_signed_kilobytes(value: i64) -> String { format!("{value} K") } +fn cmp_option(left: Option, right: Option) -> Ordering { + match (left, right) { + (Some(left), Some(right)) => left.cmp(&right), + (None, None) => Ordering::Equal, + (None, Some(_)) => Ordering::Less, + (Some(_), None) => Ordering::Greater, + } +} + +fn format_known_or_unknown(value: Option) -> String { + match value { + Some(value) => value.to_string(), + None => text(TextKey::NotAvailable).to_string(), + } +} + +fn format_command_line(value: &Option) -> String { + match value { + Some(value) => value.replace(['\r', '\n'], " "), + None => text(TextKey::NotAvailable).to_string(), + } +} + // 遍历进程树,按“子进程先于父进程”的顺序返回终止列表(后序遍历)。 // 使用 Toolhelp 快照建立父子关系表,递归收集子进程。 impl ProcEntry { @@ -221,6 +261,13 @@ impl ProcEntry { ColumnId::BasePriority => return, ColumnId::HandleCount => self.handle_count.to_string(), ColumnId::ThreadCount => self.thread_count.to_string(), + ColumnId::CommandLine => format_command_line(&self.command_line), + ColumnId::IoReadBytes => format_known_or_unknown(self.io_read_bytes), + ColumnId::IoWriteBytes => format_known_or_unknown(self.io_write_bytes), + ColumnId::IoReadCount => format_known_or_unknown(self.io_read_count), + ColumnId::IoWriteCount => format_known_or_unknown(self.io_write_count), + ColumnId::GdiObjects => format_known_or_unknown(self.gdi_objects), + ColumnId::UserObjects => format_known_or_unknown(self.user_objects), }; self.display_text[column_id as usize] = text; } @@ -339,6 +386,34 @@ pub(super) fn update_process_entry( entry.thread_count = snapshot.thread_count; mark_process_column_changed(entry, &mut changed, ColumnId::ThreadCount, visible_columns); } + if entry.command_line != snapshot.command_line { + entry.command_line.clone_from(&snapshot.command_line); + mark_process_column_changed(entry, &mut changed, ColumnId::CommandLine, visible_columns); + } + if entry.io_read_bytes != snapshot.io_read_bytes { + entry.io_read_bytes = snapshot.io_read_bytes; + mark_process_column_changed(entry, &mut changed, ColumnId::IoReadBytes, visible_columns); + } + if entry.io_write_bytes != snapshot.io_write_bytes { + entry.io_write_bytes = snapshot.io_write_bytes; + mark_process_column_changed(entry, &mut changed, ColumnId::IoWriteBytes, visible_columns); + } + if entry.io_read_count != snapshot.io_read_count { + entry.io_read_count = snapshot.io_read_count; + mark_process_column_changed(entry, &mut changed, ColumnId::IoReadCount, visible_columns); + } + if entry.io_write_count != snapshot.io_write_count { + entry.io_write_count = snapshot.io_write_count; + mark_process_column_changed(entry, &mut changed, ColumnId::IoWriteCount, visible_columns); + } + if entry.gdi_objects != snapshot.gdi_objects { + entry.gdi_objects = snapshot.gdi_objects; + mark_process_column_changed(entry, &mut changed, ColumnId::GdiObjects, visible_columns); + } + if entry.user_objects != snapshot.user_objects { + entry.user_objects = snapshot.user_objects; + mark_process_column_changed(entry, &mut changed, ColumnId::UserObjects, visible_columns); + } changed } diff --git a/src/pages/processes/sampler.rs b/src/pages/processes/sampler.rs index 711550b..03ab28e 100644 --- a/src/pages/processes/sampler.rs +++ b/src/pages/processes/sampler.rs @@ -11,10 +11,12 @@ //! Owns process enumeration, account/session enrichment, and successful-snapshot delta baselines. //! Failed collections leave CPU and per-process baselines untouched. //! -//! CPU, creation time, and memory counters come from one `SystemProcessInformation` query on -//! the worker thread. That query covers the Idle process and processes that refuse `OpenProcess`. -//! A limited, creation-time-checked open is used only for architecture and the primary token, -//! and only for a verified identity that is not already cached or access-denied. +//! CPU, creation time, memory, and I/O counters come from one `SystemProcessInformation` query +//! on the worker thread. That query covers the Idle process and processes that refuse `OpenProcess`. +//! A limited, creation-time-checked open is used only for architecture, the primary token, and, +//! when those columns are visible, the command line and GDI/USER counts. A process that exits +//! during one of those queries keeps its snapshot row; the missing value stays unknown and +//! `row_error` is set. Unknown counters are not stored as zero. use std::collections::{HashMap, HashSet}; use std::ffi::c_void; @@ -25,7 +27,7 @@ use std::slice; use windows_sys::Win32::Foundation::{ ERROR_ACCESS_DENIED, ERROR_GEN_FAILURE, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_DATA, ERROR_INVALID_PARAMETER, ERROR_NONE_MAPPED, GetLastError, HANDLE, LocalFree, - RtlNtStatusToDosError, + RtlNtStatusToDosError, SetLastError, }; use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW; use windows_sys::Win32::Security::{ @@ -36,9 +38,10 @@ use windows_sys::Win32::System::RemoteDesktop::{ WTS_CURRENT_SERVER_HANDLE, WTS_PROCESS_INFOW, WTSEnumerateProcessesW, }; use windows_sys::Win32::System::Threading::{ - ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS, - IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, OpenProcessToken, - PROCESS_QUERY_LIMITED_INFORMATION, REALTIME_PRIORITY_CLASS, + ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, GR_GDIOBJECTS, GR_USEROBJECTS, + GetGuiResources, HIGH_PRIORITY_CLASS, IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, + OpenProcessToken, PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, + REALTIME_PRIORITY_CLASS, }; use super::model::{DirtyColumns, ProcEntry, ProcStaticMetadata}; @@ -146,6 +149,14 @@ pub(super) type ProcWorkerResult = Result; pub(super) struct ProcWorkerRequest { pub(super) processor_count: usize, + pub(super) query_command_line: bool, + pub(super) query_gui_objects: bool, +} + +#[derive(Clone, Copy)] +struct ProcessDetailQuery { + command_line: bool, + gui_objects: bool, } #[derive(Default)] @@ -160,8 +171,15 @@ impl ProcWorkerState { let system_time = current_system_time(request.processor_count, &mut self.cache.processor_info)?; let total_delta = system_time_delta(system_time, self.previous_system_time); - let collected = - collect_process_entries(&self.previous_samples, total_delta, &mut self.cache)?; + let collected = collect_process_entries( + &self.previous_samples, + total_delta, + &mut self.cache, + ProcessDetailQuery { + command_line: request.query_command_line, + gui_objects: request.query_gui_objects, + }, + )?; // CPU and per-process deltas must share the same successful snapshot boundary. A failed // process walk leaves both baselines untouched so the next sample remains comparable. @@ -510,6 +528,14 @@ unsafe extern "system" { system_information_length: u32, return_length: *mut u32, ) -> i32; + + fn NtQueryInformationProcess( + process_handle: HANDLE, + process_information_class: i32, + process_information: *mut c_void, + process_information_length: u32, + return_length: *mut u32, + ) -> i32; } #[repr(C)] @@ -597,6 +623,10 @@ struct ParsedProcess { commit_bytes: usize, paged_pool_bytes: usize, nonpaged_pool_bytes: usize, + io_read_bytes: Option, + io_write_bytes: Option, + io_read_count: Option, + io_write_count: Option, } fn query_system_process_information(buffer: &mut Vec) -> Result { @@ -740,9 +770,22 @@ fn parsed_process( commit_bytes: header.pagefile_usage, paged_pool_bytes: header.quota_paged_pool_usage, nonpaged_pool_bytes: header.quota_non_paged_pool_usage, + io_read_bytes: counted_u64(header.read_transfer_count, row_error), + io_write_bytes: counted_u64(header.write_transfer_count, row_error), + io_read_count: counted_u64(header.read_operation_count, row_error), + io_write_count: counted_u64(header.write_operation_count, row_error), }) } +fn counted_u64(value: i64, row_error: &mut Option) -> Option { + if value < 0 { + row_error.get_or_insert(ERROR_INVALID_DATA); + None + } else { + Some(value as u64) + } +} + fn cpu_time_from_counters(kernel: i64, user: i64, row_error: &mut Option) -> Option { if kernel < 0 || user < 0 { row_error.get_or_insert(ERROR_INVALID_DATA); @@ -865,6 +908,13 @@ fn build_process_entries( priority_class: priority_class_from_base_priority(row.base_priority), handle_count: row.handle_count, thread_count: row.thread_count, + command_line: None, + io_read_bytes: row.io_read_bytes, + io_write_bytes: row.io_write_bytes, + io_read_count: row.io_read_count, + io_write_count: row.io_write_count, + gdi_objects: None, + user_objects: None, display_text: std::array::from_fn(|_| String::new()), pass_count: 0, dirty_columns: DirtyColumns::default(), @@ -872,6 +922,13 @@ fn build_process_entries( if !identity_usable || row.cpu_time_100ns.is_none() { row_error.get_or_insert(ERROR_INVALID_DATA); } + if row.io_read_bytes.is_none() + || row.io_write_bytes.is_none() + || row.io_read_count.is_none() + || row.io_write_count.is_none() + { + row_error.get_or_insert(ERROR_INVALID_DATA); + } if identity_usable && let Some(previous) = previous_samples.get(&identity) { entry.mem_diff_kb = signed_kb_delta(entry.mem_usage_kb, previous.mem_usage_kb); entry.page_faults_diff = i64::from(entry.page_faults) - i64::from(previous.page_faults); @@ -920,6 +977,7 @@ fn collect_process_entries( previous_samples: &HashMap, total_delta: Option, cache: &mut ProcWorkerCache, + details: ProcessDetailQuery, ) -> Result { let returned = query_system_process_information(&mut cache.process_info)?; let (rows, mut row_error) = parse_system_process_information(&cache.process_info[..returned])?; @@ -1064,10 +1122,224 @@ fn collect_process_entries( } } + query_process_details(&mut collected.entries, details, &mut row_error); collected.row_error = row_error; Ok(collected) } +fn query_process_details( + entries: &mut [ProcEntry], + query: ProcessDetailQuery, + row_error: &mut Option, +) { + if !query.command_line && !query.gui_objects { + return; + } + for entry in entries.iter_mut() { + if entry.pid == 0 || !entry.identity.is_verified() { + continue; + } + // GDI/USER counts need PROCESS_QUERY_INFORMATION. Command line is available with + // PROCESS_QUERY_LIMITED_INFORMATION, so an access-denied full query still tries that. + let mut command_line_done = false; + if query.gui_objects { + match open_process_for_identity( + entry.identity, + PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION, + ) { + Ok(handle) => { + if query.command_line { + apply_command_line(entry, handle.as_raw(), row_error); + command_line_done = true; + } + apply_gui_objects(entry, handle.as_raw(), row_error); + } + Err(error) => remember_row_error(row_error, error), + } + } + if query.command_line && !command_line_done { + match open_process_for_identity(entry.identity, PROCESS_QUERY_LIMITED_INFORMATION) { + Ok(handle) => apply_command_line(entry, handle.as_raw(), row_error), + Err(error) => remember_row_error(row_error, error), + } + } + } +} + +fn apply_command_line(entry: &mut ProcEntry, handle: HANDLE, row_error: &mut Option) { + match query_command_line(handle) { + Ok(command_line) => entry.command_line = Some(command_line), + Err(error) => { + entry.command_line = None; + remember_row_error(row_error, error); + } + } +} + +fn apply_gui_objects(entry: &mut ProcEntry, handle: HANDLE, row_error: &mut Option) { + match query_gui_objects(handle) { + Ok((gdi_objects, user_objects)) => { + entry.gdi_objects = Some(gdi_objects); + entry.user_objects = Some(user_objects); + } + Err(error) => { + entry.gdi_objects = None; + entry.user_objects = None; + remember_row_error(row_error, error); + } + } +} + +fn remember_row_error(row_error: &mut Option, error: u32) { + row_error.get_or_insert(if error == 0 { ERROR_GEN_FAILURE } else { error }); +} + +const PROCESS_COMMAND_LINE_INFORMATION: i32 = 60; +const COMMAND_LINE_INITIAL_BYTES: usize = 512; +/// `UNICODE_STRING::Length` counts bytes in a `u16`. A UTF-16 command line is even, so +/// 32,767 code units is the longest value that field can describe. +const MAX_COMMAND_LINE_UNITS: usize = 32_767; +/// Room for the `UnicodeString` header plus the longest legal command line. The header +/// size depends on pointer width, so it is not a fixed 64 KiB. +const COMMAND_LINE_MAX_BYTES: usize = + size_of::() + MAX_COMMAND_LINE_UNITS * size_of::(); +const STATUS_BUFFER_OVERFLOW: i32 = 0x8000_0005u32 as i32; + +fn query_command_line(handle: HANDLE) -> Result { + let mut buffer = vec![0u8; COMMAND_LINE_INITIAL_BYTES]; + loop { + let Ok(length) = u32::try_from(buffer.len()) else { + return Err(ERROR_INVALID_DATA); + }; + let mut returned = 0u32; + // SAFETY: `handle` is a creation-time-checked process handle owned by the caller. The + // worker owns `buffer` for this call and does not share it. The kernel writes at most + // `length` bytes and reports the used size in `returned`. + let status = unsafe { + NtQueryInformationProcess( + handle, + PROCESS_COMMAND_LINE_INFORMATION, + buffer.as_mut_ptr().cast(), + length, + &mut returned, + ) + }; + if status >= 0 { + let returned = returned as usize; + if returned == 0 || returned > buffer.len() { + return Err(ERROR_INVALID_DATA); + } + return parse_process_command_line(&buffer[..returned]); + } + if status != STATUS_INFO_LENGTH_MISMATCH + && status != STATUS_BUFFER_TOO_SMALL + && status != STATUS_BUFFER_OVERFLOW + { + return Err(ntstatus_to_win32(status)); + } + let Some(next) = next_command_line_capacity(buffer.len(), returned as usize) else { + return Err(ntstatus_to_win32(status)); + }; + buffer.resize(next, 0); + } +} + +fn parse_process_command_line(buffer: &[u8]) -> Result { + if buffer.len() < size_of::() { + return Err(ERROR_INVALID_DATA); + } + // SAFETY: `buffer` holds at least one `UnicodeString`. `read_unaligned` copies the header + // without requiring the struct alignment. The buffer is not mutated during the copy. + let header = unsafe { buffer.as_ptr().cast::().read_unaligned() }; + if header.length == 0 { + return Ok(String::new()); + } + if !header.length.is_multiple_of(2) { + return Err(ERROR_INVALID_DATA); + } + let bytes = usize::from(header.length); + if bytes > MAX_COMMAND_LINE_UNITS * size_of::() { + return Err(ERROR_INVALID_DATA); + } + let units = command_line_units(buffer, header.buffer, bytes)?; + let end = units + .iter() + .position(|unit| *unit == 0) + .unwrap_or(units.len()); + Ok(String::from_utf16_lossy(&units[..end])) +} + +fn next_command_line_capacity(current: usize, returned: usize) -> Option { + let next = if returned > current { + returned + } else { + // Doubling can jump past the header-inclusive cap. Try that cap once before failing, + // so a legal 32,767-unit command line still fits. + current.saturating_mul(2).min(COMMAND_LINE_MAX_BYTES) + }; + if next > current && next <= COMMAND_LINE_MAX_BYTES { + Some(next) + } else { + None + } +} + +fn command_line_units(buffer: &[u8], raw_buffer: *const u16, bytes: usize) -> Result<&[u16], u32> { + let buffer_start = buffer.as_ptr() as usize; + let Some(buffer_end) = buffer_start.checked_add(buffer.len()) else { + return Err(ERROR_INVALID_DATA); + }; + let pointer = raw_buffer as usize; + let absolute = pointer >= buffer_start + && pointer + .checked_add(bytes) + .is_some_and(|end| end <= buffer_end) + && pointer.is_multiple_of(align_of::()); + let start = if absolute { + pointer + } else if pointer != 0 + && pointer.is_multiple_of(align_of::()) + && pointer + .checked_add(bytes) + .is_some_and(|end| end <= buffer.len()) + { + buffer_start + pointer + } else { + return Err(ERROR_INVALID_DATA); + }; + // SAFETY: `start` is aligned for `u16` and `start..start + bytes` lies inside `buffer`, + // which outlives this slice. The kernel, or the test builder, placed the UTF-16 units there. + let units = unsafe { slice::from_raw_parts(start as *const u16, bytes / 2) }; + Ok(units) +} + +fn classify_gui_count(count: u32, last_error: u32) -> Result { + if count == 0 && last_error != 0 { + Err(last_error) + } else { + Ok(count) + } +} + +fn query_gui_objects(handle: HANDLE) -> Result<(u32, u32), u32> { + Ok(( + query_gui_count(handle, GR_GDIOBJECTS)?, + query_gui_count(handle, GR_USEROBJECTS)?, + )) +} + +fn query_gui_count(handle: HANDLE, flag: u32) -> Result { + // SAFETY: `handle` is a creation-time-checked process handle owned by the caller. + // GetGuiResources documents GetLastError when it returns zero, so the last-error slot is + // cleared first and read only for that zero result. A zero count with no error is a real zero. + unsafe { + SetLastError(0); + let count = GetGuiResources(handle, flag); + let error = if count == 0 { GetLastError() } else { 0 }; + classify_gui_count(count, error) + } +} + fn enrich_process_entries( entries: &mut [ProcEntry], cache: &mut ProcWorkerCache, @@ -1169,19 +1441,25 @@ mod tests { use std::collections::HashMap; use std::mem::size_of; - use windows_sys::Win32::Foundation::{ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER}; + use windows_sys::Win32::Foundation::{ + ERROR_ACCESS_DENIED, ERROR_INVALID_DATA, ERROR_INVALID_PARAMETER, + }; use windows_sys::Win32::System::Threading::{ GetCurrentProcess, GetCurrentProcessId, HIGH_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ, }; use super::{ - AccountNameCache, ParsedProcess, SystemProcessInformation, UnicodeString, - build_process_entries, parse_system_process_information, query_process_account_name, - query_system_process_information, wts_live_identity_allows_merge, + AccountNameCache, COMMAND_LINE_INITIAL_BYTES, COMMAND_LINE_MAX_BYTES, + MAX_COMMAND_LINE_UNITS, ParsedProcess, ProcessDetailQuery, SystemProcessInformation, + UnicodeString, build_process_entries, classify_gui_count, next_command_line_capacity, + parse_process_command_line, parse_system_process_information, query_process_account_name, + query_process_details, query_system_process_information, wts_live_identity_allows_merge, }; + use crate::config::options::ColumnId; use crate::infrastructure::native::OwnedHandle; use crate::system::process_identity::{ProcIdentity, query_process_identity_for_pid}; + use crate::ui::localization::{TextKey, text}; #[test] fn current_process_primary_token_resolves_to_a_non_empty_account_label() { @@ -1376,6 +1654,7 @@ mod tests { paged_pool: 1_024, nonpaged_pool: 3_072, parent_pid: 0, + ..BufferSpec::default() }, BufferSpec { pid: 55, @@ -1416,6 +1695,196 @@ mod tests { assert!(built.entries[2].identity.is_verified()); } + #[test] + fn negative_io_counter_stays_unknown_and_keeps_the_sibling_row() { + let buffer = system_process_buffer(&[ + BufferSpec { + pid: 10, + creation_time: 1, + name: Some("bad.exe".to_string()), + read_bytes: -1, + write_bytes: 4, + read_count: 2, + write_count: 3, + ..BufferSpec::default() + }, + BufferSpec { + pid: 11, + creation_time: 2, + name: Some("ok.exe".to_string()), + read_bytes: 8, + write_bytes: 9, + read_count: 1, + write_count: 1, + ..BufferSpec::default() + }, + ]); + let (rows, row_error) = + parse_system_process_information(&buffer).expect("synthetic buffer should parse"); + assert!(row_error.is_some()); + assert!(rows[0].io_read_bytes.is_none()); + assert_eq!(rows[0].io_write_bytes, Some(4)); + assert_eq!(rows[0].io_read_count, Some(2)); + assert_eq!(rows[1].io_read_bytes, Some(8)); + assert_ne!(rows[0].io_read_bytes, Some(0)); + + let built = build_process_entries(&rows, &HashMap::new(), None); + assert_eq!(built.entries.len(), 2); + assert!(built.row_error.is_some()); + assert!(built.entries[0].io_read_bytes.is_none()); + assert_eq!(built.entries[0].io_write_bytes, Some(4)); + assert_eq!(built.entries[1].io_read_bytes, Some(8)); + assert_eq!(built.entries[1].image_name, "ok.exe"); + } + + #[test] + fn command_line_bytes_outside_the_returned_buffer_are_not_an_empty_success() { + assert_eq!( + parse_process_command_line(&[]).err(), + Some(ERROR_INVALID_DATA) + ); + assert_eq!( + parse_process_command_line(&command_line_buffer(0x1000, "nope")).err(), + Some(ERROR_INVALID_DATA) + ); + assert_eq!( + parse_process_command_line(&command_line_buffer_inside("taskmgr.exe")).unwrap(), + "taskmgr.exe" + ); + assert_eq!( + parse_process_command_line(&command_line_buffer_inside("")).unwrap(), + "" + ); + } + + #[test] + fn command_line_accepts_the_unicode_string_length_limit() { + assert_eq!( + COMMAND_LINE_MAX_BYTES, + size_of::() + MAX_COMMAND_LINE_UNITS * 2 + ); + const { + assert!(COMMAND_LINE_MAX_BYTES > 64 * 1024); + } + + let full = next_command_line_capacity(64 * 1024, 0).expect("cap is reachable"); + assert_eq!(full, COMMAND_LINE_MAX_BYTES); + assert!(next_command_line_capacity(COMMAND_LINE_MAX_BYTES, 0).is_none()); + assert!( + next_command_line_capacity(COMMAND_LINE_INITIAL_BYTES, COMMAND_LINE_MAX_BYTES + 1) + .is_none() + ); + + let mut built = build_process_entries( + &[sample_row(40, 10, Some(10), "long.exe", 8)], + &HashMap::new(), + None, + ); + let unknown = text(TextKey::NotAvailable); + for units in [16_385_usize, MAX_COMMAND_LINE_UNITS] { + let invocation = "A".repeat(units); + let parsed = + parse_process_command_line(&command_line_buffer_inside(&invocation)).unwrap(); + assert_eq!(parsed.chars().count(), units); + built.entries[0].command_line = Some(parsed); + built.entries[0].rebuild_display_columns(&[ColumnId::CommandLine]); + let shown = &built.entries[0].display_text[ColumnId::CommandLine as usize]; + assert_eq!(shown.chars().count(), units); + assert_ne!(shown, unknown); + } + } + + #[test] + fn gui_object_zero_is_success_only_when_the_call_reports_no_error() { + assert_eq!(classify_gui_count(0, 0), Ok(0)); + assert_eq!(classify_gui_count(0, 5), Err(5)); + assert_eq!(classify_gui_count(4, 5), Ok(4)); + } + + #[test] + fn departed_process_keeps_sibling_rows_and_records_row_error() { + // SAFETY: GetCurrentProcessId has no preconditions. + let self_pid = unsafe { GetCurrentProcessId() }; + let self_identity = query_process_identity_for_pid(self_pid).expect("current identity"); + let mut collected = build_process_entries( + &[ + sample_row(u32::MAX, 1, Some(10), "gone.exe", 8), + sample_row( + self_identity.pid, + self_identity.creation_time_100ns, + Some(10), + "self.exe", + 8, + ), + ], + &HashMap::new(), + None, + ); + let mut row_error = None; + query_process_details( + &mut collected.entries, + ProcessDetailQuery { + command_line: true, + gui_objects: true, + }, + &mut row_error, + ); + assert_eq!(collected.entries.len(), 2); + assert!(row_error.is_some()); + assert!(collected.entries[0].command_line.is_none()); + assert!(collected.entries[0].gdi_objects.is_none()); + assert!(collected.entries[0].user_objects.is_none()); + assert_eq!(collected.entries[0].io_read_bytes, Some(0)); + assert!( + collected.entries[1] + .command_line + .as_ref() + .is_some_and(|line| !line.is_empty()) + ); + assert!(collected.entries[1].gdi_objects.is_some()); + assert!(collected.entries[1].user_objects.is_some()); + assert_eq!(collected.entries[1].identity, self_identity); + } + + fn command_line_buffer_inside(text: &str) -> Vec { + let units: Vec = text.encode_utf16().collect(); + let mut buffer = vec![0u8; size_of::() + units.len() * 2]; + let string_offset = size_of::(); + for (index, unit) in units.iter().enumerate() { + let offset = string_offset + index * 2; + buffer[offset..offset + 2].copy_from_slice(&unit.to_le_bytes()); + } + let header = UnicodeString { + length: u16::try_from(units.len() * 2).unwrap(), + maximum_length: u16::try_from(units.len() * 2).unwrap(), + buffer: unsafe { buffer.as_ptr().add(string_offset).cast() }, + }; + // SAFETY: the header occupies the reserved prefix of `buffer` and does not alias a live + // reference to that range. + unsafe { + buffer + .as_mut_ptr() + .cast::() + .write_unaligned(header); + } + buffer + } + + fn command_line_buffer(pointer: usize, text: &str) -> Vec { + let mut buffer = command_line_buffer_inside(text); + // SAFETY: the prefix of `buffer` is a UnicodeString written above. This only replaces the + // pointer field with an address that is outside the allocation. + unsafe { + let mut header = buffer.as_ptr().cast::().read_unaligned(); + header.buffer = pointer as *const u16; + buffer + .as_mut_ptr() + .cast::() + .write_unaligned(header); + } + buffer + } + #[test] fn live_system_process_information_includes_idle_without_a_process_handle() { let mut buffer = Vec::new(); @@ -1497,6 +1966,10 @@ mod tests { commit_bytes: 0, paged_pool_bytes: 0, nonpaged_pool_bytes: 0, + io_read_bytes: Some(0), + io_write_bytes: Some(0), + io_read_count: Some(0), + io_write_count: Some(0), } } @@ -1526,6 +1999,10 @@ mod tests { paged_pool: usize, nonpaged_pool: usize, parent_pid: u32, + read_bytes: i64, + write_bytes: i64, + read_count: i64, + write_count: i64, } impl Default for BufferSpec { @@ -1546,6 +2023,10 @@ mod tests { paged_pool: 0, nonpaged_pool: 0, parent_pid: 0, + read_bytes: 0, + write_bytes: 0, + read_count: 0, + write_count: 0, } } } @@ -1596,6 +2077,10 @@ mod tests { quota_paged_pool_usage: spec.paged_pool, quota_non_paged_pool_usage: spec.nonpaged_pool, pagefile_usage: spec.commit, + read_operation_count: spec.read_count, + write_operation_count: spec.write_count, + read_transfer_count: spec.read_bytes, + write_transfer_count: spec.write_bytes, ..SystemProcessInformation::default() }; // SAFETY: `entry_offset` addresses `header_size` bytes reserved above, and this write diff --git a/src/ui/dialogs.rs b/src/ui/dialogs.rs index 397a290..f0e0fb3 100644 --- a/src/ui/dialogs.rs +++ b/src/ui/dialogs.rs @@ -753,15 +753,15 @@ fn build_task_dialog() -> DialogSpec<'static> { fn build_select_columns_dialog() -> DialogSpec<'static> { let controls = vec![ - button("OK", 1, BS_DEFPUSHBUTTON_STYLE, 83, 122, 50, 14), - button("Cancel", 2, 0, 137, 122, 50, 14), + button("OK", 1, BS_DEFPUSHBUTTON_STYLE, 208, 122, 50, 14), + button("Cancel", 2, 0, 262, 122, 50, 14), static_text( "Select the columns that will appear on the Process page of the Task Manager.", IDC_SELECTPROCCOLS_DESC, 0, 7, 4, - 177, + 304, 19, ), ControlSpec { @@ -940,13 +940,90 @@ fn build_select_columns_dialog() -> DialogSpec<'static> { cx: 87, cy: 10, }, + ControlSpec { + class_name: "Button", + text: "Command Line", + id: IDC_COMMANDLINE as u16, + style: WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX_STYLE, + ex_style: 0, + x: 199, + y: 26, + cx: 112, + cy: 10, + }, + ControlSpec { + class_name: "Button", + text: "I/O Read Bytes", + id: IDC_IOREADBYTES as u16, + style: WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX_STYLE, + ex_style: 0, + x: 199, + y: 37, + cx: 112, + cy: 10, + }, + ControlSpec { + class_name: "Button", + text: "I/O Write Bytes", + id: IDC_IOWRITEBYTES as u16, + style: WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX_STYLE, + ex_style: 0, + x: 199, + y: 48, + cx: 112, + cy: 10, + }, + ControlSpec { + class_name: "Button", + text: "I/O Reads", + id: IDC_IOREADCOUNT as u16, + style: WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX_STYLE, + ex_style: 0, + x: 199, + y: 59, + cx: 112, + cy: 10, + }, + ControlSpec { + class_name: "Button", + text: "I/O Writes", + id: IDC_IOWRITECOUNT as u16, + style: WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX_STYLE, + ex_style: 0, + x: 199, + y: 70, + cx: 112, + cy: 10, + }, + ControlSpec { + class_name: "Button", + text: "GDI Objects", + id: IDC_GDIOBJECTS as u16, + style: WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX_STYLE, + ex_style: 0, + x: 199, + y: 81, + cx: 112, + cy: 10, + }, + ControlSpec { + class_name: "Button", + text: "USER Objects", + id: IDC_USEROBJECTS as u16, + style: WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX_STYLE, + ex_style: 0, + x: 199, + y: 92, + cx: 112, + cy: 10, + }, ]; DialogSpec { style: DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU, ex_style: 0, x: 20, y: 20, - cx: 191, + cx: 318, cy: 141, title: "Select Columns", font_name: DIALOG_FONT_NAME, diff --git a/src/ui/localization/text_key.rs b/src/ui/localization/text_key.rs index c6ef46f..c77acaa 100644 --- a/src/ui/localization/text_key.rs +++ b/src/ui/localization/text_key.rs @@ -138,6 +138,13 @@ pub enum TextKey { ProcessColumnBasePriority, ProcessColumnHandleCount, ProcessColumnThreadCount, + ProcessColumnCommandLine, + ProcessColumnIoReadBytes, + ProcessColumnIoWriteBytes, + ProcessColumnIoReadCount, + ProcessColumnIoWriteCount, + ProcessColumnGdiObjects, + ProcessColumnUserObjects, ProcessColumnSessionId, ProcessColumnUserName, File, @@ -256,6 +263,13 @@ pub enum TextKey { BasePriority, HandleCount, ThreadCount, + CommandLine, + IoReadBytes, + IoWriteBytes, + IoReadCount, + IoWriteCount, + GdiObjects, + UserObjects, ProcessorAffinity, Processors, ProcessorAffinityDescription, diff --git a/src/ui/localization/ui.rs b/src/ui/localization/ui.rs index aaa762c..008522f 100644 --- a/src/ui/localization/ui.rs +++ b/src/ui/localization/ui.rs @@ -148,6 +148,13 @@ pub fn localize_dialog(hwnd: HWND, dialog_id: u16) { set_dialog_item_text(hwnd, IDC_BASEPRIORITY, TextKey::BasePriority); set_dialog_item_text(hwnd, IDC_HANDLECOUNT, TextKey::HandleCount); set_dialog_item_text(hwnd, IDC_THREADCOUNT, TextKey::ThreadCount); + set_dialog_item_text(hwnd, IDC_COMMANDLINE, TextKey::CommandLine); + set_dialog_item_text(hwnd, IDC_IOREADBYTES, TextKey::IoReadBytes); + set_dialog_item_text(hwnd, IDC_IOWRITEBYTES, TextKey::IoWriteBytes); + set_dialog_item_text(hwnd, IDC_IOREADCOUNT, TextKey::IoReadCount); + set_dialog_item_text(hwnd, IDC_IOWRITECOUNT, TextKey::IoWriteCount); + set_dialog_item_text(hwnd, IDC_GDIOBJECTS, TextKey::GdiObjects); + set_dialog_item_text(hwnd, IDC_USEROBJECTS, TextKey::UserObjects); } IDD_AFFINITY => { set_window_text(hwnd, TextKey::ProcessorAffinity); diff --git a/src/ui/resource_ids.rs b/src/ui/resource_ids.rs index 148cc25..cf18a9c 100644 --- a/src/ui/resource_ids.rs +++ b/src/ui/resource_ids.rs @@ -14,7 +14,7 @@ pub const TEMPLATE_CPU_GRAPH_COUNT: usize = 32; -pub const MAX_COLUMN: usize = 15; +pub const MAX_COLUMN: usize = 22; pub const NUM_COLUMN: usize = MAX_COLUMN + 1; pub const WM_FINDPROC: u32 = 0x0400 + 1; @@ -143,6 +143,13 @@ pub const IDC_NONPAGEDPOOL: i32 = 1071; pub const IDC_BASEPRIORITY: i32 = 1072; pub const IDC_HANDLECOUNT: i32 = 1073; pub const IDC_THREADCOUNT: i32 = 1074; +pub const IDC_COMMANDLINE: i32 = 2615; +pub const IDC_IOREADBYTES: i32 = 2616; +pub const IDC_IOWRITEBYTES: i32 = 2617; +pub const IDC_IOREADCOUNT: i32 = 2618; +pub const IDC_IOWRITECOUNT: i32 = 2619; +pub const IDC_GDIOBJECTS: i32 = 2620; +pub const IDC_USEROBJECTS: i32 = 2621; pub const IDC_SESSIONID: i32 = 2032; pub const IDC_USERNAME: i32 = 2033; pub const IDC_MEMBARFRAME: i32 = 1064;