diff --git a/src/pages/processes/actions.rs b/src/pages/processes/actions.rs index 39d8cc9..52c882b 100644 --- a/src/pages/processes/actions.rs +++ b/src/pages/processes/actions.rs @@ -20,8 +20,9 @@ use windows_sys::Win32::Foundation::{ ERROR_BUSY, ERROR_FILE_NOT_FOUND, ERROR_GEN_FAILURE, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_DATA, ERROR_INVALID_HANDLE, ERROR_INVALID_PARAMETER, ERROR_NO_MORE_FILES, ERROR_NOT_SUPPORTED, ERROR_PATH_NOT_FOUND, FILETIME, GetLastError, HANDLE, HWND, LPARAM, - WPARAM, + WAIT_OBJECT_0, WPARAM, }; +use windows_sys::Win32::Storage::FileSystem::SYNCHRONIZE; use windows_sys::Win32::System::Diagnostics::ToolHelp::{ CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next, @@ -43,6 +44,7 @@ use windows_sys::Win32::System::Threading::{ QueryFullProcessImageNameW, REALTIME_PRIORITY_CLASS, STARTUPINFOW, SetPriorityClass, SetProcessAffinityMask, SetProcessDefaultCpuSets, SetThreadGroupAffinity, THREAD_QUERY_LIMITED_INFORMATION, THREAD_SET_INFORMATION, TerminateProcess, + WaitForSingleObject, }; use windows_sys::Win32::UI::Controls::{ BST_CHECKED, BST_UNCHECKED, CheckDlgButton, IsDlgButtonChecked, @@ -75,6 +77,86 @@ struct AffinityDialogContext { original_default_ids: Vec, } +const PROCESS_TREE_ACCESS: u32 = + PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE; + +#[derive(Debug, PartialEq, Eq)] +pub(super) enum DescendantProcessOutcome { + Verified(T), + GoneOrReused, + Fatal(u32), +} + +pub(super) fn classify_descendant_process_result( + result: Result, + is_verified: F, +) -> DescendantProcessOutcome +where + F: FnOnce(&T) -> bool, +{ + match result { + Ok(value) => { + if is_verified(&value) { + DescendantProcessOutcome::Verified(value) + } else { + DescendantProcessOutcome::GoneOrReused + } + } + Err(ERROR_INVALID_PARAMETER) => DescendantProcessOutcome::GoneOrReused, + Err(error) => DescendantProcessOutcome::Fatal(error), + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum FailedTerminationOutcome { + AlreadyTerminated, + Failed(u32), +} + +pub(super) const fn classify_failed_termination( + error: u32, + wait_result: u32, +) -> FailedTerminationOutcome { + if wait_result == WAIT_OBJECT_0 { + FailedTerminationOutcome::AlreadyTerminated + } else { + FailedTerminationOutcome::Failed(error) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub(super) enum ProcessTreePrepareError { + Root(u32), + Tree(u32), +} + +pub(super) struct PreparedProcessTree { + root_pid: u32, + targets: Vec<(ProcIdentity, OwnedHandle)>, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(super) struct ProcessTreeTerminationOutcome { + any_success: bool, + any_completed: bool, + any_failure: bool, + root_error: u32, +} + +impl ProcessTreeTerminationOutcome { + pub(super) const fn any_success(self) -> bool { + self.any_success + } + + pub(super) const fn any_failure(self) -> bool { + self.any_failure + } + + const fn completed_without_failure(self) -> bool { + self.any_completed && !self.any_failure + } +} + impl ProcessPageState { unsafe fn quick_confirm(&self, title: &str, body: &str) -> bool { unsafe { @@ -152,95 +234,30 @@ impl ProcessPageState { return false; } - let mut root_handle = match open_process_for_identity( - identity, - PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, - ) { - Ok(handle) => Some(handle), - Err(error) => { + let prepared = match prepare_process_tree_termination(identity) { + Ok(prepared) => prepared, + Err(ProcessTreePrepareError::Root(error)) => { self.show_failure_message(&self.strings.cant_kill, error); return false; } - }; - - let pid = identity.pid; - let termination_order = match collect_process_tree_termination_order(identity) { - Ok(order) if !order.is_empty() => order, - Ok(_) => { - self.show_failure_message( - &self.strings.kill_tree_fail_body, - windows_sys::Win32::Foundation::ERROR_GEN_FAILURE, - ); - return false; - } - Err(error) => { + Err(ProcessTreePrepareError::Tree(error)) => { self.show_failure_message(&self.strings.kill_tree_fail_body, error); return false; } }; + let outcome = terminate_prepared_process_tree(prepared); - // 先验证并打开整棵树,再开始终止,避免权限/身份错误造成可预见的半完成状态。 - let mut targets = Vec::with_capacity(termination_order.len()); - for target_identity in termination_order { - if target_identity == identity { - let Some(handle) = root_handle.take() else { - self.show_failure_message( - &self.strings.kill_tree_fail_body, - ERROR_INVALID_DATA, - ); - return false; - }; - targets.push((target_identity, handle)); - continue; - } - - match open_process_for_identity( - target_identity, - PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, - ) { - Ok(handle) => targets.push((target_identity, handle)), - Err(error) => { - self.show_failure_message(&self.strings.kill_tree_fail_body, error); - return false; - } - } - } - - if root_handle.is_some() { - self.show_failure_message( - &self.strings.kill_tree_fail_body, - windows_sys::Win32::Foundation::ERROR_GEN_FAILURE, - ); - return false; - } - - let mut any_success = false; - let mut any_failure = false; - let mut root_error = 0u32; - - for (target_identity, handle) in targets { - let target_pid = target_identity.pid; - if TerminateProcess(handle.as_raw(), 1) == 0 { - any_failure = true; - if target_pid == pid { - root_error = windows_sys::Win32::Foundation::GetLastError(); - } - } else { - any_success = true; - } - } - - if any_success { + if outcome.any_completed { self.paused = false; self.refresh_processes(); } - if root_error != 0 && !any_success { - self.show_failure_message(&self.strings.cant_kill, root_error); + if outcome.root_error != 0 && !outcome.any_success() { + self.show_failure_message(&self.strings.cant_kill, outcome.root_error); return false; } - if any_failure { + if outcome.any_failure() { let body_wide = to_wide_null(&self.strings.kill_tree_fail_body); let title_wide = to_wide_null(&self.strings.kill_tree_fail); MessageBoxW( @@ -252,7 +269,7 @@ impl ProcessPageState { return false; } - any_success + outcome.completed_without_failure() } } @@ -1176,7 +1193,86 @@ fn expand_environment_variables(command_line: &str) -> Result { Ok(String::from_utf16_lossy(&buffer[..len])) } -// 检查 SID 是否为已知服务帐户(SYSTEM、LOCAL SERVICE、NETWORK SERVICE),返回对应名称。 +// 先构建完整的已验证句柄集合,再把不可逆的终止阶段与 UI 结果呈现分离。 +pub(super) fn prepare_process_tree_termination( + root_identity: ProcIdentity, +) -> Result { + let mut root_handle = Some( + open_process_for_identity(root_identity, PROCESS_TREE_ACCESS) + .map_err(ProcessTreePrepareError::Root)?, + ); + let termination_order = collect_process_tree_termination_order(root_identity) + .map_err(ProcessTreePrepareError::Tree)?; + if termination_order.is_empty() { + return Err(ProcessTreePrepareError::Tree(ERROR_GEN_FAILURE)); + } + + // Validate and own every available handle before terminating anything. A descendant that + // disappeared or changed identity is no longer an actionable target, while permission and + // system errors still abort preparation before the root can be partially terminated. + let mut targets = Vec::with_capacity(termination_order.len()); + for target_identity in termination_order { + if target_identity == root_identity { + let handle = root_handle + .take() + .ok_or(ProcessTreePrepareError::Tree(ERROR_INVALID_DATA))?; + targets.push((target_identity, handle)); + continue; + } + + match classify_descendant_process_result( + open_process_for_identity(target_identity, PROCESS_TREE_ACCESS), + |_| true, + ) { + DescendantProcessOutcome::Verified(handle) => { + targets.push((target_identity, handle)); + } + DescendantProcessOutcome::GoneOrReused => {} + DescendantProcessOutcome::Fatal(error) => { + return Err(ProcessTreePrepareError::Tree(error)); + } + } + } + + if root_handle.is_some() { + return Err(ProcessTreePrepareError::Tree(ERROR_GEN_FAILURE)); + } + + Ok(PreparedProcessTree { + root_pid: root_identity.pid, + targets, + }) +} + +pub(super) fn terminate_prepared_process_tree( + prepared: PreparedProcessTree, +) -> ProcessTreeTerminationOutcome { + let mut outcome = ProcessTreeTerminationOutcome::default(); + for (target_identity, handle) in prepared.targets { + if unsafe { TerminateProcess(handle.as_raw(), 1) } != 0 { + outcome.any_success = true; + outcome.any_completed = true; + continue; + } + + // Capture TerminateProcess's error before WaitForSingleObject can change last-error. + let error = nonzero_last_error(); + let wait_result = unsafe { WaitForSingleObject(handle.as_raw(), 0) }; + match classify_failed_termination(error, wait_result) { + FailedTerminationOutcome::AlreadyTerminated => { + outcome.any_completed = true; + } + FailedTerminationOutcome::Failed(error) => { + outcome.any_failure = true; + if target_identity.pid == prepared.root_pid { + outcome.root_error = error; + } + } + } + } + outcome +} + fn collect_process_tree_termination_order( root_identity: ProcIdentity, ) -> Result, u32> { @@ -1265,10 +1361,14 @@ unsafe fn collect_verified_process_tree_children( if visited.contains(&child_pid) { continue; } - let child = query_process_identity_for_pid(child_pid)?; - if !is_valid_process_tree_edge(parent, child, snapshot_time_100ns) { - continue; - } + let child = match classify_descendant_process_result( + query_process_identity_for_pid(child_pid), + |child| is_valid_process_tree_edge(parent, *child, snapshot_time_100ns), + ) { + DescendantProcessOutcome::Verified(child) => child, + DescendantProcessOutcome::GoneOrReused => continue, + DescendantProcessOutcome::Fatal(error) => return Err(error), + }; collect_verified_process_tree_children( child, snapshot_time_100ns, diff --git a/src/pages/processes/mod.rs b/src/pages/processes/mod.rs index 0465956..c9d6ed7 100644 --- a/src/pages/processes/mod.rs +++ b/src/pages/processes/mod.rs @@ -1353,8 +1353,11 @@ fn column_id_from_i32(value: i32) -> Option { #[cfg(test)] mod tests { use super::actions::{ - affinity_cpu_mask, affinity_is_within_selection, affinity_target_for_thread, + DescendantProcessOutcome, FailedTerminationOutcome, affinity_cpu_mask, + affinity_is_within_selection, affinity_target_for_thread, + classify_descendant_process_result, classify_failed_termination, extract_first_command_token, is_valid_process_tree_edge, normalize_debugger_command_with, + prepare_process_tree_termination, terminate_prepared_process_tree, validate_snapshot_root_identity, }; use super::model::{DirtyColumns, ProcEntry}; @@ -1367,9 +1370,41 @@ mod tests { reorder_process_columns, write_process_column_layout, }; use crate::config::options::{ColumnId, Options}; + use crate::system::process_identity::query_process_identity_for_pid; use std::collections::HashMap; + use std::env; + use std::fs; + use std::os::windows::io::AsRawHandle; + use std::os::windows::process::CommandExt; + use std::path::PathBuf; + use std::process::{Child, Command, Stdio}; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::thread; + use std::time::{Duration, Instant}; + use windows_sys::Win32::Foundation::{ + ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER, WAIT_OBJECT_0, WAIT_TIMEOUT, + }; use windows_sys::Win32::System::Registry::{REG_EXPAND_SZ, REG_SZ}; - use windows_sys::Win32::System::Threading::NORMAL_PRIORITY_CLASS; + use windows_sys::Win32::System::Threading::{ + CREATE_NO_WINDOW, NORMAL_PRIORITY_CLASS, WaitForSingleObject, + }; + + const PROCESS_TREE_CHURN_WORKER_ENV: &str = "TASKMGR_RS_PROCESS_TREE_CHURN_WORKER"; + const PROCESS_TREE_CHURN_READY_ENV: &str = "TASKMGR_RS_PROCESS_TREE_CHURN_READY"; + + struct ChurnWorkerGuard { + child: Child, + ready_path: PathBuf, + } + + impl Drop for ChurnWorkerGuard { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + let _ = fs::remove_file(&self.ready_path); + } + } fn empty_process_entry(image_name: &str) -> ProcEntry { ProcEntry { @@ -1573,6 +1608,137 @@ mod tests { assert!(validate_snapshot_root_identity(expected, ProcIdentity::new(10, 200)).is_err()); } + #[test] + fn descendant_process_outcomes_preserve_fatal_errors() { + assert_eq!( + classify_descendant_process_result(Ok(7), |_| true), + DescendantProcessOutcome::Verified(7) + ); + assert_eq!( + classify_descendant_process_result(Ok(7), |_| false), + DescendantProcessOutcome::GoneOrReused + ); + assert_eq!( + classify_descendant_process_result::<(), _>(Err(ERROR_INVALID_PARAMETER), |_| true), + DescendantProcessOutcome::GoneOrReused + ); + assert_eq!( + classify_descendant_process_result::<(), _>(Err(ERROR_ACCESS_DENIED), |_| true), + DescendantProcessOutcome::Fatal(ERROR_ACCESS_DENIED) + ); + } + + #[test] + fn failed_termination_of_an_exited_process_is_benign() { + assert_eq!( + classify_failed_termination(ERROR_ACCESS_DENIED, WAIT_OBJECT_0), + FailedTerminationOutcome::AlreadyTerminated + ); + assert_eq!( + classify_failed_termination(ERROR_ACCESS_DENIED, WAIT_TIMEOUT), + FailedTerminationOutcome::Failed(ERROR_ACCESS_DENIED) + ); + } + + #[test] + fn process_tree_churn_worker() { + if env::var_os(PROCESS_TREE_CHURN_WORKER_ENV).is_none() { + return; + } + + let ready_path = PathBuf::from( + env::var_os(PROCESS_TREE_CHURN_READY_ENV) + .expect("the churn worker requires a readiness path"), + ); + let completed_children = Arc::new(AtomicUsize::new(0)); + for _ in 0..4 { + let completed_children = Arc::clone(&completed_children); + thread::spawn(move || { + loop { + if Command::new("cmd.exe") + .args(["/D", "/Q", "/C", "exit"]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .creation_flags(CREATE_NO_WINDOW) + .status() + .is_ok() + { + completed_children.fetch_add(1, Ordering::Release); + } + } + }); + } + + let deadline = Instant::now() + Duration::from_secs(5); + while completed_children.load(Ordering::Acquire) < 16 { + assert!( + Instant::now() < deadline, + "the churn worker could not launch short-lived children" + ); + thread::yield_now(); + } + fs::write(&ready_path, b"ready").expect("the churn worker should signal readiness"); + + loop { + thread::park(); + } + } + + #[test] + fn process_tree_termination_survives_rapidly_exiting_descendants() { + let ready_path = env::temp_dir().join(format!( + "taskmgr-rs-process-tree-churn-{}.ready", + std::process::id() + )); + let _ = fs::remove_file(&ready_path); + let worker = Command::new(env::current_exe().expect("the test executable should exist")) + .args([ + "--exact", + "pages::processes::tests::process_tree_churn_worker", + ]) + .env(PROCESS_TREE_CHURN_WORKER_ENV, "1") + .env(PROCESS_TREE_CHURN_READY_ENV, &ready_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .creation_flags(CREATE_NO_WINDOW) + .spawn() + .expect("the churn worker should start"); + let mut worker = ChurnWorkerGuard { + child: worker, + ready_path, + }; + + let deadline = Instant::now() + Duration::from_secs(10); + while !worker.ready_path.exists() { + assert!( + worker + .child + .try_wait() + .expect("the churn worker state should be readable") + .is_none(), + "the churn worker exited before becoming ready" + ); + assert!( + Instant::now() < deadline, + "the churn worker did not become ready" + ); + thread::sleep(Duration::from_millis(5)); + } + + let identity = query_process_identity_for_pid(worker.child.id()) + .expect("the churn worker identity should be queryable"); + let prepared = prepare_process_tree_termination(identity) + .expect("short-lived descendants must not abort tree preparation"); + let outcome = terminate_prepared_process_tree(prepared); + + assert!(outcome.any_success()); + assert!(!outcome.any_failure()); + let wait = unsafe { WaitForSingleObject(worker.child.as_raw_handle(), 5_000) }; + assert_eq!(wait, WAIT_OBJECT_0, "the verified root must terminate"); + } + #[test] fn wts_identity_source_rejects_stale_image_or_session() { let identity = WtsProcessIdentity {