diff --git a/src/pages/processes/action_worker.rs b/src/pages/processes/action_worker.rs new file mode 100644 index 0000000..be0e861 --- /dev/null +++ b/src/pages/processes/action_worker.rs @@ -0,0 +1,405 @@ +// +------------------------------------------------------------------------- +// +// taskmgr-rs - 进程结束与优先级 +// +// 文件: src/pages/processes/action_worker.rs +// +// 日期: 2026年11月03日 +// 环境: Ubuntu 24.04.5 LTS x86_64;Linux 内核 6.12.94+;Rust 1.99.1 +// 作者: OpenAI Codex +// -------------------------------------------------------------------------- + +//! End Process, End Process Tree, and Set Priority after the UI confirmation. +//! +//! One persistent [`SingleFlightWorker`] owns every confirmed action. The page +//! posts no extra window message: completion reuses `PWM_PROC_WORKER_COMPLETE` +//! and is drained beside the process sampler. Dropping the worker retires it +//! without joining on the UI thread, including while a tree walk is still +//! inside the collector. +//! +//! Each action re-opens its target with `open_process_for_identity` and requires +//! both the pid and `creation_time_100ns`. An unknown or mismatched identity +//! returns before `TerminateProcess`, the tree walk, or `SetPriorityClass`. + +#[cfg(test)] +use std::sync::mpsc::{Receiver, Sender}; + +use windows_sys::Win32::Foundation::{ERROR_INVALID_PARAMETER, GetLastError}; +use windows_sys::Win32::System::Threading::{ + ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS, + IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, PROCESS_QUERY_LIMITED_INFORMATION, + PROCESS_SET_INFORMATION, PROCESS_TERMINATE, REALTIME_PRIORITY_CLASS, SetPriorityClass, + TerminateProcess, +}; + +use super::ProcPriority; +use super::actions::{ + ProcessTreePrepareError, ProcessTreeTerminationOutcome, prepare_process_tree_termination, + terminate_prepared_process_tree, +}; +use crate::infrastructure::native::OwnedHandle; +use crate::infrastructure::worker::SingleFlightWorker; +use crate::system::process_identity::{ProcIdentity, open_process_for_identity}; +use crate::ui::resource_ids::PWM_PROC_WORKER_COMPLETE; + +pub(super) type ProcessActionWorker = + SingleFlightWorker, Vec>; + +pub(super) enum ProcessAction { + End(ProcIdentity), + EndTree(ProcIdentity), + SetPriority { + identity: ProcIdentity, + priority: ProcPriority, + }, + #[cfg(test)] + Block { + entered: Sender<()>, + release: Receiver<()>, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct ProcessActionReport { + /// Unpause and refresh. Set when a terminate or priority change landed, + /// including a tree that ended only in part. + pub(super) refresh: bool, + pub(super) feedback: ProcessActionFeedback, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum ProcessActionFeedback { + None, + CantKill(u32), + CantChangePriority(u32), + /// `show_failure_message` with the existing tree-failure body. + KillTreePrepare(u32), + /// Existing warning dialog. This is not a full success. + PartialTree, +} + +pub(super) fn spawn_process_action_worker() -> Result { + // Asynchronous retirement closes completion posts and does not join a worker + // that is still inside TerminateProcess, Toolhelp, or SetPriorityClass. + SingleFlightWorker::spawn_cancellable_initialized( + "taskmgr-rs-process-actions", + PWM_PROC_WORKER_COMPLETE, + append_process_actions, + |_stop| |actions| actions.into_iter().map(run_process_action).collect(), + ) +} + +pub(super) fn append_process_actions( + pending: &mut Vec, + incoming: Vec, +) { + pending.extend(incoming); +} + +pub(super) fn require_action_identity(identity: ProcIdentity) -> Result<(), u32> { + if identity.pid != 0 && identity.is_verified() { + Ok(()) + } else { + Err(ERROR_INVALID_PARAMETER) + } +} + +pub(super) fn report_end_tree_outcome( + outcome: ProcessTreeTerminationOutcome, +) -> ProcessActionReport { + let feedback = if outcome.root_error != 0 && !outcome.any_success() { + ProcessActionFeedback::CantKill(outcome.root_error) + } else if outcome.any_failure() { + ProcessActionFeedback::PartialTree + } else { + ProcessActionFeedback::None + }; + ProcessActionReport { + refresh: outcome.any_completed, + feedback, + } +} + +pub(super) fn report_end_tree_prepare_error(error: ProcessTreePrepareError) -> ProcessActionReport { + match error { + ProcessTreePrepareError::Root(error) => kill_failure(error), + ProcessTreePrepareError::Tree(error) => ProcessActionReport { + refresh: false, + feedback: ProcessActionFeedback::KillTreePrepare(error), + }, + } +} + +fn run_process_action(action: ProcessAction) -> ProcessActionReport { + match action { + ProcessAction::End(identity) => execute_end(identity), + ProcessAction::EndTree(identity) => execute_end_tree(identity), + ProcessAction::SetPriority { identity, priority } => { + execute_set_priority(identity, priority) + } + #[cfg(test)] + ProcessAction::Block { entered, release } => { + let _ = entered.send(()); + let _ = release.recv(); + ProcessActionReport { + refresh: false, + feedback: ProcessActionFeedback::None, + } + } + } +} + +fn execute_end(identity: ProcIdentity) -> ProcessActionReport { + let handle = match open_action_process( + identity, + PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, + ) { + Ok(handle) => handle, + Err(error) => return kill_failure(error), + }; + // SAFETY: `open_process_for_identity` returned a live handle with PROCESS_TERMINATE + // access for this pid and creation time. The handle is owned for this call. + if unsafe { TerminateProcess(handle.as_raw(), 1) } == 0 { + let error = unsafe { GetLastError() }; + kill_failure(error) + } else { + succeeded() + } +} + +fn execute_end_tree(identity: ProcIdentity) -> ProcessActionReport { + // Unknown identities never reach the tree walk. `prepare_process_tree_termination` + // re-opens the root before it enumerates children or calls TerminateProcess. + if let Err(error) = require_action_identity(identity) { + return kill_failure(error); + } + match prepare_process_tree_termination(identity) { + Ok(prepared) => report_end_tree_outcome(terminate_prepared_process_tree(prepared)), + Err(error) => report_end_tree_prepare_error(error), + } +} + +fn execute_set_priority(identity: ProcIdentity, priority: ProcPriority) -> ProcessActionReport { + let handle = match open_action_process( + identity, + PROCESS_SET_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION, + ) { + Ok(handle) => handle, + Err(error) => return priority_failure(error), + }; + let priority_class = priority_class(priority); + // SAFETY: the identity-validated handle has PROCESS_SET_INFORMATION access. + if unsafe { SetPriorityClass(handle.as_raw(), priority_class) } == 0 { + let error = unsafe { GetLastError() }; + priority_failure(error) + } else { + succeeded() + } +} + +fn open_action_process(identity: ProcIdentity, access: u32) -> Result { + require_action_identity(identity)?; + open_process_for_identity(identity, access) +} + +fn priority_class(priority: ProcPriority) -> u32 { + match priority { + ProcPriority::Low => IDLE_PRIORITY_CLASS, + ProcPriority::BelowNormal => BELOW_NORMAL_PRIORITY_CLASS, + ProcPriority::Normal => NORMAL_PRIORITY_CLASS, + ProcPriority::AboveNormal => ABOVE_NORMAL_PRIORITY_CLASS, + ProcPriority::High => HIGH_PRIORITY_CLASS, + ProcPriority::Realtime => REALTIME_PRIORITY_CLASS, + } +} + +const fn kill_failure(error: u32) -> ProcessActionReport { + ProcessActionReport { + refresh: false, + feedback: ProcessActionFeedback::CantKill(error), + } +} + +const fn priority_failure(error: u32) -> ProcessActionReport { + ProcessActionReport { + refresh: false, + feedback: ProcessActionFeedback::CantChangePriority(error), + } +} + +const fn succeeded() -> ProcessActionReport { + ProcessActionReport { + refresh: true, + feedback: ProcessActionFeedback::None, + } +} + +#[cfg(test)] +mod tests { + use super::super::actions::ProcessTreePrepareError; + use super::{ + ProcessAction, ProcessActionFeedback, ProcessTreeTerminationOutcome, + append_process_actions, report_end_tree_outcome, report_end_tree_prepare_error, + require_action_identity, run_process_action, spawn_process_action_worker, + }; + use crate::system::process_identity::ProcIdentity; + use std::ptr::null_mut; + use std::sync::mpsc; + use std::thread; + use std::time::{Duration, Instant}; + use windows_sys::Win32::Foundation::ERROR_INVALID_PARAMETER; + + #[test] + fn unknown_identity_is_rejected_before_an_action() { + assert_eq!( + require_action_identity(ProcIdentity::pid_only(4)), + Err(ERROR_INVALID_PARAMETER) + ); + assert_eq!( + require_action_identity(ProcIdentity::new(0, 100)), + Err(ERROR_INVALID_PARAMETER) + ); + assert!(require_action_identity(ProcIdentity::new(4, 100)).is_ok()); + } + + #[test] + fn unverified_actions_fail_without_a_refresh() { + for action in [ + ProcessAction::End(ProcIdentity::pid_only(4)), + ProcessAction::EndTree(ProcIdentity::pid_only(8)), + ProcessAction::SetPriority { + identity: ProcIdentity::new(0, 50), + priority: super::super::ProcPriority::High, + }, + ] { + let report = run_process_action(action); + assert!(!report.refresh); + assert_ne!(report.feedback, ProcessActionFeedback::None); + assert_ne!(report.feedback, ProcessActionFeedback::PartialTree); + } + } + + #[test] + fn partial_tree_is_not_full_success() { + let partial = report_end_tree_outcome(ProcessTreeTerminationOutcome { + any_success: true, + any_completed: true, + any_failure: true, + root_error: 5, + }); + assert!(partial.refresh); + assert_eq!(partial.feedback, ProcessActionFeedback::PartialTree); + + let root_only = report_end_tree_outcome(ProcessTreeTerminationOutcome { + any_success: false, + any_completed: true, + any_failure: true, + root_error: 5, + }); + assert!(root_only.refresh); + assert_eq!(root_only.feedback, ProcessActionFeedback::CantKill(5)); + + let succeeded = report_end_tree_outcome(ProcessTreeTerminationOutcome { + any_success: true, + any_completed: true, + any_failure: false, + root_error: 0, + }); + assert!(succeeded.refresh); + assert_eq!(succeeded.feedback, ProcessActionFeedback::None); + } + + #[test] + fn tree_prepare_errors_keep_their_dialogs() { + assert_eq!( + report_end_tree_prepare_error(ProcessTreePrepareError::Root(87)), + super::kill_failure(87) + ); + assert_eq!( + report_end_tree_prepare_error(ProcessTreePrepareError::Tree(5)).feedback, + ProcessActionFeedback::KillTreePrepare(5) + ); + } + + #[test] + fn confirmed_actions_stay_queued_while_one_is_in_flight() { + let first = ProcIdentity::new(1, 10); + let second = ProcIdentity::new(2, 20); + let mut pending = vec![ProcessAction::End(first)]; + append_process_actions( + &mut pending, + vec![ + ProcessAction::EndTree(second), + ProcessAction::SetPriority { + identity: first, + priority: super::super::ProcPriority::Normal, + }, + ], + ); + assert!(matches!(pending[0], ProcessAction::End(identity) if identity == first)); + assert!(matches!(pending[1], ProcessAction::EndTree(identity) if identity == second)); + assert!(matches!( + pending[2], + ProcessAction::SetPriority { identity, priority: super::super::ProcPriority::Normal } + if identity == first + )); + } + + #[test] + fn worker_completion_rejects_an_unverified_process() { + let mut worker = spawn_process_action_worker().expect("action worker should start"); + worker + .request( + vec![ProcessAction::End(ProcIdentity::pid_only(4))], + null_mut(), + ) + .expect("request should submit"); + let deadline = Instant::now() + Duration::from_secs(2); + let reports = loop { + let drained = worker.drain(null_mut()); + assert_eq!(drained.error, None); + if let Some(completion) = drained.completions.into_iter().next() { + break completion.value; + } + assert!(Instant::now() < deadline, "action completion timed out"); + thread::yield_now(); + }; + assert_eq!(reports.len(), 1); + assert!(!reports[0].refresh); + assert_eq!( + reports[0].feedback, + ProcessActionFeedback::CantKill(ERROR_INVALID_PARAMETER) + ); + } + + #[test] + fn dropping_a_blocked_action_worker_does_not_join() { + let mut worker = spawn_process_action_worker().expect("action worker should start"); + let (entered_tx, entered_rx) = mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); + worker + .request( + vec![ProcessAction::Block { + entered: entered_tx, + release: release_rx, + }], + null_mut(), + ) + .expect("block request should queue"); + entered_rx + .recv_timeout(Duration::from_secs(2)) + .expect("worker should enter the block"); + + let (done_tx, done_rx) = mpsc::channel(); + thread::spawn(move || { + drop(worker); + let _ = done_tx.send(()); + }); + let finished = done_rx.recv_timeout(Duration::from_secs(1)); + let _ = release_tx.send(()); + assert!( + finished.is_ok(), + "shutdown must not wait for the action thread" + ); + } +} diff --git a/src/pages/processes/actions.rs b/src/pages/processes/actions.rs index 5af7d64..2d27767 100644 --- a/src/pages/processes/actions.rs +++ b/src/pages/processes/actions.rs @@ -10,6 +10,10 @@ //! Centralizes destructive and identity-sensitive process operations. //! Every target process is reopened through `ProcIdentity` immediately before use. +//! +//! End Process, End Process Tree, and Set Priority confirm on the UI thread, then +//! run on one persistent worker. Debug, Open File Location, and Affinity stay on +//! this thread. use std::collections::{HashMap, HashSet}; use std::ffi::c_void; @@ -19,9 +23,9 @@ use std::ptr::{null, null_mut}; 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_MORE_DATA, - ERROR_NO_MORE_FILES, ERROR_NOT_SUPPORTED, ERROR_PATH_NOT_FOUND, FILETIME, GetLastError, HANDLE, - HWND, LPARAM, WAIT_OBJECT_0, WPARAM, + ERROR_INVALID_DATA, ERROR_INVALID_HANDLE, ERROR_INVALID_PARAMETER, ERROR_INVALID_WINDOW_HANDLE, + ERROR_MORE_DATA, ERROR_NO_MORE_FILES, ERROR_NOT_SUPPORTED, ERROR_PATH_NOT_FOUND, FILETIME, + GetLastError, HANDLE, HWND, LPARAM, WAIT_OBJECT_0, WPARAM, }; use windows_sys::Win32::Security::SECURITY_ATTRIBUTES; use windows_sys::Win32::Storage::FileSystem::SYNCHRONIZE; @@ -41,17 +45,15 @@ use windows_sys::Win32::System::SystemInformation::{ IMAGE_FILE_MACHINE_UNKNOWN, }; use windows_sys::Win32::System::Threading::{ - ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, CREATE_NEW_CONSOLE, CreateEventW, - CreateProcessW, DeleteProcThreadAttributeList, EXTENDED_STARTUPINFO_PRESENT, - GetProcessAffinityMask, GetProcessGroupAffinity, GetProcessIdOfThread, GetThreadGroupAffinity, - HIGH_PRIORITY_CLASS, IDLE_PRIORITY_CLASS, InitializeProcThreadAttributeList, IsWow64Process2, - LPPROC_THREAD_ATTRIBUTE_LIST, NORMAL_PRIORITY_CLASS, OpenThread, - PROC_THREAD_ATTRIBUTE_HANDLE_LIST, PROCESS_INFORMATION, PROCESS_QUERY_INFORMATION, - PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SET_INFORMATION, PROCESS_SET_LIMITED_INFORMATION, - PROCESS_TERMINATE, QueryFullProcessImageNameW, REALTIME_PRIORITY_CLASS, STARTUPINFOEXW, - STARTUPINFOW, SetPriorityClass, SetProcessAffinityMask, SetProcessDefaultCpuSets, - SetThreadGroupAffinity, THREAD_QUERY_LIMITED_INFORMATION, THREAD_SET_INFORMATION, - TerminateProcess, UpdateProcThreadAttribute, WaitForSingleObject, + CREATE_NEW_CONSOLE, CreateEventW, CreateProcessW, DeleteProcThreadAttributeList, + EXTENDED_STARTUPINFO_PRESENT, GetProcessAffinityMask, GetProcessGroupAffinity, + GetProcessIdOfThread, GetThreadGroupAffinity, InitializeProcThreadAttributeList, + IsWow64Process2, LPPROC_THREAD_ATTRIBUTE_LIST, OpenThread, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + PROCESS_INFORMATION, PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, + PROCESS_SET_INFORMATION, PROCESS_SET_LIMITED_INFORMATION, PROCESS_TERMINATE, + QueryFullProcessImageNameW, STARTUPINFOEXW, STARTUPINFOW, SetProcessAffinityMask, + SetProcessDefaultCpuSets, SetThreadGroupAffinity, THREAD_QUERY_LIMITED_INFORMATION, + THREAD_SET_INFORMATION, TerminateProcess, UpdateProcThreadAttribute, WaitForSingleObject, }; use windows_sys::Win32::UI::Controls::{ BST_CHECKED, BST_UNCHECKED, CheckDlgButton, IsDlgButtonChecked, @@ -63,9 +65,13 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ MessageBoxW, SendMessageW, SetWindowTextW, WM_COMMAND, WM_INITDIALOG, }; +use super::action_worker::{ + ProcessAction, ProcessActionFeedback, ProcessActionReport, spawn_process_action_worker, +}; use super::{ProcPriority, ProcessPageState}; use crate::infrastructure::native::{ - OwnedHandle, get_window_userdata, hiword, loword, set_window_userdata, to_wide_null, + OwnedHandle, get_window_userdata, hiword, loword, record_win32_error, set_window_userdata, + to_wide_null, }; use crate::system::cpu_sets::{CpuSetTopology, query_process_default_cpu_sets}; use crate::system::process_identity::{ @@ -149,10 +155,10 @@ pub(super) struct PreparedProcessTree { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(super) struct ProcessTreeTerminationOutcome { - any_success: bool, - any_completed: bool, - any_failure: bool, - root_error: u32, + pub(super) any_success: bool, + pub(super) any_completed: bool, + pub(super) any_failure: bool, + pub(super) root_error: u32, } impl ProcessTreeTerminationOutcome { @@ -163,10 +169,6 @@ impl ProcessTreeTerminationOutcome { 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 { @@ -211,82 +213,20 @@ impl ProcessPageState { } } - // 结束指定 PID 的进程。先弹确认框,再通过 TerminateProcess 终止。 + // 结束指定进程。确认留在 UI 线程;TerminateProcess 在持久 worker 上执行。 pub(super) fn kill_process(&mut self, identity: ProcIdentity) -> bool { if !self.quick_confirm(&self.strings.warning, &self.strings.kill) { return false; } - - let handle = match open_process_for_identity( - identity, - PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, - ) { - Ok(handle) => handle, - Err(error) => { - self.show_failure_message(&self.strings.cant_kill, error); - return false; - } - }; - - // SAFETY: `open_process_for_identity` returned a live handle with PROCESS_TERMINATE - // access for the exact verified identity. - if unsafe { TerminateProcess(handle.as_raw(), 1) } == 0 { - let error = unsafe { GetLastError() }; - self.show_failure_message(&self.strings.cant_kill, error); - false - } else { - self.paused = false; - self.refresh_processes(); - true - } + self.submit_process_action(ProcessAction::End(identity)) } - // 结束进程以及所有子进程。按叶子优先的顺序遍历进程树,逐进程 TerminateProcess。 + // 结束进程以及所有子进程。确认留在 UI 线程;树遍历和 TerminateProcess 在持久 worker 上执行。 pub(super) fn kill_process_tree(&mut self, identity: ProcIdentity) -> bool { if !self.quick_confirm(&self.strings.warning, &self.strings.kill_tree) { return false; } - - 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; - } - Err(ProcessTreePrepareError::Tree(error)) => { - self.show_failure_message(&self.strings.kill_tree_fail_body, error); - return false; - } - }; - let outcome = terminate_prepared_process_tree(prepared); - - if outcome.any_completed { - self.paused = false; - self.refresh_processes(); - } - - if outcome.root_error != 0 && !outcome.any_success() { - self.show_failure_message(&self.strings.cant_kill, outcome.root_error); - return false; - } - - 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); - // SAFETY: the page HWND is borrowed and both terminated buffers outlive this - // synchronous message box call. - unsafe { - MessageBoxW( - self.hwnd_page, - body_wide.as_ptr(), - title_wide.as_ptr(), - MB_OK | MB_ICONEXCLAMATION, - ); - } - return false; - } - - outcome.completed_without_failure() + self.submit_process_action(ProcessAction::EndTree(identity)) } // 使用目标进程位数对应的 AeDebug 命令模板启动调试器。完整模板中的第一个 @@ -482,41 +422,104 @@ impl ProcessPageState { } } - // 通过 SetPriorityClass 修改进程优先级类。先弹确认框,操作成功后刷新列表。 + // 修改进程优先级类。确认留在 UI 线程;SetPriorityClass 在持久 worker 上执行。 pub(super) fn set_priority(&mut self, identity: ProcIdentity, priority: ProcPriority) -> bool { - let priority_class = match priority { - ProcPriority::Low => IDLE_PRIORITY_CLASS, - ProcPriority::BelowNormal => BELOW_NORMAL_PRIORITY_CLASS, - ProcPriority::Normal => NORMAL_PRIORITY_CLASS, - ProcPriority::AboveNormal => ABOVE_NORMAL_PRIORITY_CLASS, - ProcPriority::High => HIGH_PRIORITY_CLASS, - ProcPriority::Realtime => REALTIME_PRIORITY_CLASS, - }; - if !self.quick_confirm(&self.strings.warning, &self.strings.prichange) { return false; } + self.submit_process_action(ProcessAction::SetPriority { identity, priority }) + } - let handle = match open_process_for_identity( - identity, - PROCESS_SET_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION, - ) { - Ok(handle) => handle, - Err(error) => { - self.show_failure_message(&self.strings.cant_change_priority, error); - return false; + fn submit_process_action(&mut self, action: ProcessAction) -> bool { + let failure_kind = action_failure_kind(&action); + if self.hwnd_page.is_null() { + self.present_process_action_report(failure_report( + failure_kind, + ERROR_INVALID_WINDOW_HANDLE, + )); + return false; + } + if self.action_worker.is_none() { + match spawn_process_action_worker() { + Ok(worker) => self.action_worker = Some(worker), + Err(error) => { + self.present_process_action_report(failure_report(failure_kind, error)); + return false; + } + } + } + let requested = self + .action_worker + .as_mut() + .map(|worker| worker.request(vec![action], self.hwnd_page)); + match requested { + Some(Ok(_)) => true, + Some(Err(error)) => { + record_win32_error("process action submit", error); + self.present_process_action_report(failure_report(failure_kind, error)); + false } + None => false, + } + } + + pub(super) fn drain_process_action_results(&mut self) { + let awaiting_result = self + .action_worker + .as_ref() + .is_some_and(|worker| worker.is_in_flight() || worker.has_pending()); + let drain = match self.action_worker.as_mut() { + Some(worker) => worker.drain(self.hwnd_page), + None => return, }; + let worker_error = drain.error; + for completion in drain.completions { + crate::infrastructure::diagnostics::with_operation_id(completion.operation_id, || { + for report in completion.value { + self.present_process_action_report(report); + } + }); + } + if let Some(error) = worker_error { + record_win32_error("process action", error); + if awaiting_result { + self.show_failure_message(&self.strings.cant_kill, error); + } + } + } - // SAFETY: the identity-validated handle has PROCESS_SET_INFORMATION access. - if unsafe { SetPriorityClass(handle.as_raw(), priority_class) } == 0 { - let error = unsafe { GetLastError() }; - self.show_failure_message(&self.strings.cant_change_priority, error); - false - } else { + fn present_process_action_report(&mut self, report: ProcessActionReport) { + if report.refresh { self.paused = false; self.refresh_processes(); - true + } + match report.feedback { + ProcessActionFeedback::None => {} + ProcessActionFeedback::CantKill(error) => { + self.show_failure_message(&self.strings.cant_kill, error); + } + ProcessActionFeedback::CantChangePriority(error) => { + self.show_failure_message(&self.strings.cant_change_priority, error); + } + ProcessActionFeedback::KillTreePrepare(error) => { + self.show_failure_message(&self.strings.kill_tree_fail_body, error); + } + ProcessActionFeedback::PartialTree => self.show_partial_tree_failure(), + } + } + + fn show_partial_tree_failure(&self) { + let body_wide = to_wide_null(&self.strings.kill_tree_fail_body); + let title_wide = to_wide_null(&self.strings.kill_tree_fail); + // SAFETY: the page HWND is borrowed and both terminated buffers outlive this + // synchronous message box call. + unsafe { + MessageBoxW( + self.hwnd_page, + body_wide.as_ptr(), + title_wide.as_ptr(), + MB_OK | MB_ICONEXCLAMATION, + ); } } @@ -610,6 +613,30 @@ impl ProcessPageState { } } +enum ActionFailureKind { + Kill, + Priority, +} + +fn action_failure_kind(action: &ProcessAction) -> ActionFailureKind { + match action { + ProcessAction::End(_) | ProcessAction::EndTree(_) => ActionFailureKind::Kill, + ProcessAction::SetPriority { .. } => ActionFailureKind::Priority, + #[cfg(test)] + ProcessAction::Block { .. } => ActionFailureKind::Kill, + } +} + +fn failure_report(kind: ActionFailureKind, error: u32) -> ProcessActionReport { + ProcessActionReport { + refresh: false, + feedback: match kind { + ActionFailureKind::Kill => ProcessActionFeedback::CantKill(error), + ActionFailureKind::Priority => ProcessActionFeedback::CantChangePriority(error), + }, + } +} + fn initial_affinity_masks( process: HANDLE, topology: &CpuSetTopology, diff --git a/src/pages/processes/mod.rs b/src/pages/processes/mod.rs index 7244f37..0e12ddc 100644 --- a/src/pages/processes/mod.rs +++ b/src/pages/processes/mod.rs @@ -8,6 +8,7 @@ // 作者: OpenAI Codex // -------------------------------------------------------------------------- +mod action_worker; mod actions; mod dump; mod filter; @@ -25,7 +26,12 @@ mod tree; // 线程模型: // SingleFlightWorker 工作线程在后台执行 collect_process_entries() 采样, // UI 线程只提交刷新意图并异步提交结果;CPU delta 基线由 worker 独占。 -// 创建转储文件使用另一个 SingleFlightWorker。保存对话框留在 UI 线程, +// 结束进程、结束进程树和设置优先级在确认后进入同一个持久 SingleFlightWorker。 +// 确认框留在 UI 线程。TerminateProcess、进程树遍历和 SetPriorityClass 在该 +// worker 上执行,执行前用 open_process_for_identity 核对 pid 与创建时间。 +// 完成通知复用 PWM_PROC_WORKER_COMPLETE,和采样结果一起排空。页面销毁时丢弃 +// 该 worker,不在 UI 线程上 join。 +// 创建转储文件使用另一个 worker。保存对话框留在 UI 线程, // MiniDumpWriteDump 在该 worker 上执行,不与采样请求合并。 // // 缓存失效策略: @@ -408,6 +414,7 @@ pub struct ProcessPageState { pass_count: u64, request_generation: u64, worker: Option>, + action_worker: Option, dump_worker: Option, search_worker: Option>>, @@ -443,6 +450,7 @@ impl Default for ProcessPageState { pass_count: 0, request_generation: 0, worker: None, + action_worker: None, dump_worker: None, search_worker: None, last_refresh_error: None, @@ -454,28 +462,31 @@ impl Default for ProcessPageState { } impl ProcessPageState { - // 创建持久采样线程。线程、通道和单在途语义由 SingleFlightWorker 统一负责。 + // 创建持久采样线程和进程操作线程。通道和单在途语义由 SingleFlightWorker 负责。 fn start_worker_thread(&mut self) -> Result<(), u32> { - if self.worker.is_some() { - return Ok(()); + if self.worker.is_none() { + self.worker = Some(SingleFlightWorker::spawn_initialized( + "taskmgr-rs-process-sampler", + PWM_PROC_WORKER_COMPLETE, + replace_pending, + || { + let mut state = ProcWorkerState::default(); + move |request: ProcessCollectionRequest| ProcessCollectionCompletion { + generation: request.generation, + result: state.collect(request.sample), + } + }, + )?); + } + if self.action_worker.is_none() { + self.action_worker = Some(action_worker::spawn_process_action_worker()?); } - self.worker = Some(SingleFlightWorker::spawn_initialized( - "taskmgr-rs-process-sampler", - PWM_PROC_WORKER_COMPLETE, - replace_pending, - || { - let mut state = ProcWorkerState::default(); - move |request: ProcessCollectionRequest| ProcessCollectionCompletion { - generation: request.generation, - result: state.collect(request.sample), - } - }, - )?); Ok(()) } fn stop_worker_thread(&mut self) { self.worker = None; + self.action_worker = None; self.search_worker = None; } @@ -1106,21 +1117,26 @@ impl ProcessPageState { } fn drain_worker_results(&mut self) { - let drain = match self.worker.as_mut() { - Some(worker) => worker.drain(self.hwnd_page), - None => return, - }; - for completion in drain.completions { - crate::infrastructure::diagnostics::with_operation_id(completion.operation_id, || { - self.apply_worker_completion(completion.value) - }); - } - if let Some(error) = drain.error { - // Worker-level failures (including a collector panic) may have no result to - // correlate. Cancel navigation instead of leaving a request that cannot finish. - self.pending_find = None; - self.set_refresh_error(error); + let drain = self + .worker + .as_mut() + .map(|worker| worker.drain(self.hwnd_page)); + if let Some(drain) = drain { + for completion in drain.completions { + crate::infrastructure::diagnostics::with_operation_id( + completion.operation_id, + || self.apply_worker_completion(completion.value), + ); + } + if let Some(error) = drain.error { + // Worker-level failures (including a collector panic) may have no result to + // correlate. Cancel navigation instead of leaving a request that cannot finish. + self.pending_find = None; + self.set_refresh_error(error); + } } + // The action worker posts the same completion message as the sampler. + self.drain_process_action_results(); } fn apply_worker_completion(&mut self, completion: ProcessCollectionCompletion) {