From c06e790c88f7c1a90f6c9f64e3aefd45d0876ea9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 06:54:37 +0000 Subject: [PATCH] move process affinity off the UI thread The affinity dialog stays on the UI thread. After confirmation, the process identity and selected group masks are queued on the existing process-action worker. That worker re-opens the target with open_process_for_identity before SetProcessAffinityMask or SetProcessDefaultCpuSets. An unknown identity, a mask that does not fit the native KAFFINITY, or a group that cannot be expressed fails without writing a truncated mask. A later failure is not reported as success. Co-authored-by: jameslincn --- src/pages/processes/action_worker.rs | 136 +++++++++- src/pages/processes/actions.rs | 381 ++++++++++++++++++++++----- src/pages/processes/mod.rs | 12 +- 3 files changed, 449 insertions(+), 80 deletions(-) diff --git a/src/pages/processes/action_worker.rs b/src/pages/processes/action_worker.rs index be0e861..f7da789 100644 --- a/src/pages/processes/action_worker.rs +++ b/src/pages/processes/action_worker.rs @@ -1,6 +1,6 @@ // +------------------------------------------------------------------------- // -// taskmgr-rs - 进程结束与优先级 +// taskmgr-rs - 进程结束、优先级与亲和性 // // 文件: src/pages/processes/action_worker.rs // @@ -9,17 +9,20 @@ // 作者: OpenAI Codex // -------------------------------------------------------------------------- -//! End Process, End Process Tree, and Set Priority after the UI confirmation. +//! End Process, End Process Tree, Set Priority, and Set Affinity 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. +//! without joining on the UI thread, including while a tree walk or an affinity +//! update 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`. +//! returns before `TerminateProcess`, the tree walk, `SetPriorityClass`, +//! `SetProcessAffinityMask`, or `SetProcessDefaultCpuSets`. A group or mask that +//! the native `KAFFINITY` cannot express fails before those calls. #[cfg(test)] use std::sync::mpsc::{Receiver, Sender}; @@ -34,8 +37,8 @@ use windows_sys::Win32::System::Threading::{ use super::ProcPriority; use super::actions::{ - ProcessTreePrepareError, ProcessTreeTerminationOutcome, prepare_process_tree_termination, - terminate_prepared_process_tree, + ProcessTreePrepareError, ProcessTreeTerminationOutcome, apply_confirmed_affinity, + prepare_process_tree_termination, terminate_prepared_process_tree, }; use crate::infrastructure::native::OwnedHandle; use crate::infrastructure::worker::SingleFlightWorker; @@ -45,6 +48,16 @@ use crate::ui::resource_ids::PWM_PROC_WORKER_COMPLETE; pub(super) type ProcessActionWorker = SingleFlightWorker, Vec>; +/// Group-qualified affinity bits chosen in the dialog. +/// +/// `mask` is the full group mask. A later conversion that would keep only the +/// low bits of `KAFFINITY` is rejected instead. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) struct AffinityGroupMask { + pub(super) group: u16, + pub(super) mask: u64, +} + pub(super) enum ProcessAction { End(ProcIdentity), EndTree(ProcIdentity), @@ -52,6 +65,10 @@ pub(super) enum ProcessAction { identity: ProcIdentity, priority: ProcPriority, }, + SetAffinity { + identity: ProcIdentity, + masks: Vec, + }, #[cfg(test)] Block { entered: Sender<()>, @@ -72,6 +89,7 @@ pub(super) enum ProcessActionFeedback { None, CantKill(u32), CantChangePriority(u32), + CantSetAffinity(u32), /// `show_failure_message` with the existing tree-failure body. KillTreePrepare(u32), /// Existing warning dialog. This is not a full success. @@ -80,7 +98,8 @@ pub(super) enum ProcessActionFeedback { 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. + // that is still inside TerminateProcess, Toolhelp, SetPriorityClass, or an + // affinity update. SingleFlightWorker::spawn_cancellable_initialized( "taskmgr-rs-process-actions", PWM_PROC_WORKER_COMPLETE, @@ -137,6 +156,7 @@ fn run_process_action(action: ProcessAction) -> ProcessActionReport { ProcessAction::SetPriority { identity, priority } => { execute_set_priority(identity, priority) } + ProcessAction::SetAffinity { identity, masks } => execute_set_affinity(identity, masks), #[cfg(test)] ProcessAction::Block { entered, release } => { let _ = entered.send(()); @@ -179,6 +199,21 @@ fn execute_end_tree(identity: ProcIdentity) -> ProcessActionReport { } } +fn execute_set_affinity( + identity: ProcIdentity, + masks: Vec, +) -> ProcessActionReport { + // Reject an unknown identity before `apply_confirmed_affinity` opens the + // process and reaches either affinity API. + if let Err(error) = require_action_identity(identity) { + return affinity_failure(error); + } + match apply_confirmed_affinity(identity, &masks) { + Ok(()) => succeeded(), + Err(error) => affinity_failure(error), + } +} + fn execute_set_priority(identity: ProcIdentity, priority: ProcPriority) -> ProcessActionReport { let handle = match open_action_process( identity, @@ -227,6 +262,13 @@ const fn priority_failure(error: u32) -> ProcessActionReport { } } +const fn affinity_failure(error: u32) -> ProcessActionReport { + ProcessActionReport { + refresh: false, + feedback: ProcessActionFeedback::CantSetAffinity(error), + } +} + const fn succeeded() -> ProcessActionReport { ProcessActionReport { refresh: true, @@ -238,9 +280,10 @@ const fn succeeded() -> ProcessActionReport { 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, + AffinityGroupMask, ProcessAction, ProcessActionFeedback, ProcessTreeTerminationOutcome, + affinity_failure, 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; @@ -271,6 +314,13 @@ mod tests { identity: ProcIdentity::new(0, 50), priority: super::super::ProcPriority::High, }, + ProcessAction::SetAffinity { + identity: ProcIdentity::pid_only(12), + masks: vec![AffinityGroupMask { + group: 0, + mask: 0b1111, + }], + }, ] { let report = run_process_action(action); assert!(!report.refresh); @@ -334,6 +384,19 @@ mod tests { identity: first, priority: super::super::ProcPriority::Normal, }, + ProcessAction::SetAffinity { + identity: second, + masks: vec![ + AffinityGroupMask { + group: 0, + mask: 0b0011, + }, + AffinityGroupMask { + group: 1, + mask: 1u64 << 63, + }, + ], + }, ], ); assert!(matches!(pending[0], ProcessAction::End(identity) if identity == first)); @@ -343,6 +406,24 @@ mod tests { ProcessAction::SetPriority { identity, priority: super::super::ProcPriority::Normal } if identity == first )); + assert!(matches!( + &pending[3], + ProcessAction::SetAffinity { identity, masks } + if *identity == second + && masks.len() == 2 + && masks[0] == AffinityGroupMask { group: 0, mask: 0b0011 } + && masks[1] == AffinityGroupMask { group: 1, mask: 1u64 << 63 } + )); + } + + #[test] + fn affinity_failure_does_not_refresh_or_count_as_success() { + let report = affinity_failure(ERROR_INVALID_PARAMETER); + assert!(!report.refresh); + assert_eq!( + report.feedback, + ProcessActionFeedback::CantSetAffinity(ERROR_INVALID_PARAMETER) + ); } #[test] @@ -372,6 +453,39 @@ mod tests { ); } + #[test] + fn worker_completion_does_not_apply_affinity_for_an_unverified_process() { + let mut worker = spawn_process_action_worker().expect("action worker should start"); + worker + .request( + vec![ProcessAction::SetAffinity { + identity: ProcIdentity::new(0, 80), + masks: vec![AffinityGroupMask { + group: 1, + mask: 1u64 << 63, + }], + }], + 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, "affinity completion timed out"); + thread::yield_now(); + }; + assert_eq!(reports.len(), 1); + assert!(!reports[0].refresh); + assert_eq!( + reports[0].feedback, + ProcessActionFeedback::CantSetAffinity(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"); diff --git a/src/pages/processes/actions.rs b/src/pages/processes/actions.rs index 2d27767..f2b7379 100644 --- a/src/pages/processes/actions.rs +++ b/src/pages/processes/actions.rs @@ -11,9 +11,9 @@ //! 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. +//! End Process, End Process Tree, Set Priority, and Set Affinity confirm on the UI +//! thread, then run on one persistent worker. The affinity dialog stays on this +//! thread. Debug and Open File Location stay on this thread. use std::collections::{HashMap, HashSet}; use std::ffi::c_void; @@ -66,7 +66,8 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ }; use super::action_worker::{ - ProcessAction, ProcessActionFeedback, ProcessActionReport, spawn_process_action_worker, + AffinityGroupMask, ProcessAction, ProcessActionFeedback, ProcessActionReport, + require_action_identity, spawn_process_action_worker, }; use super::{ProcPriority, ProcessPageState}; use crate::infrastructure::native::{ @@ -87,12 +88,16 @@ struct AffinityDialogContext { topology: CpuSetTopology, selected_masks: Vec, selected_group_index: usize, - original_default_ids: Vec, } const PROCESS_TREE_ACCESS: u32 = PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE; +const AFFINITY_QUERY_ACCESS: u32 = PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION; + +const AFFINITY_UPDATE_ACCESS: u32 = + AFFINITY_QUERY_ACCESS | PROCESS_SET_INFORMATION | PROCESS_SET_LIMITED_INFORMATION; + // `TerminateProcess` fails once a process has begun to exit, and the process object may not be // signaled in that same instant. This bound only applies after termination has already failed. // The wait returns as soon as the process exits. @@ -501,6 +506,9 @@ impl ProcessPageState { ProcessActionFeedback::CantChangePriority(error) => { self.show_failure_message(&self.strings.cant_change_priority, error); } + ProcessActionFeedback::CantSetAffinity(error) => { + self.show_failure_message(&self.strings.cant_set_affinity, error); + } ProcessActionFeedback::KillTreePrepare(error) => { self.show_failure_message(&self.strings.kill_tree_fail_body, error); } @@ -523,20 +531,26 @@ impl ProcessPageState { } } - // Single-group processes retain the classic hard-affinity API. Multi-group systems use CPU - // Set IDs, whose group-qualified identities do not collapse at the 64-processor boundary. + // 亲和性对话框留在 UI 线程。确认后把身份和各组掩码交给持久 worker。 + // SetProcessAffinityMask 与 SetProcessDefaultCpuSets 在 worker 上执行。 pub(super) fn set_affinity(&mut self, identity: ProcIdentity) -> bool { - let handle = match open_process_for_identity( - identity, - PROCESS_QUERY_INFORMATION - | PROCESS_QUERY_LIMITED_INFORMATION - | PROCESS_SET_INFORMATION - | PROCESS_SET_LIMITED_INFORMATION, - ) { + let Some(masks) = self.confirmed_affinity_masks(identity) else { + return false; + }; + self.submit_process_action(ProcessAction::SetAffinity { identity, masks }) + } + + // Query-only handle for the dialog. It is dropped before the action is queued, + // so the worker re-opens the process before it writes a mask. + fn confirmed_affinity_masks( + &mut self, + identity: ProcIdentity, + ) -> Option> { + let handle = match open_process_for_identity(identity, AFFINITY_QUERY_ACCESS) { Ok(handle) => handle, Err(error) => { self.show_failure_message(&self.strings.cant_set_affinity, error); - return false; + return None; } }; @@ -544,21 +558,21 @@ impl ProcessPageState { Ok(topology) => topology, Err(error) => { self.show_failure_message(&self.strings.cant_set_affinity, error.win32_code()); - return false; + return None; } }; let original_default_ids = match query_process_default_cpu_sets(handle.as_raw()) { Ok(ids) => ids, Err(error) => { self.show_failure_message(&self.strings.cant_set_affinity, error.win32_code()); - return false; + return None; } }; let original_process_groups = match query_process_groups(handle.as_raw()) { Ok(groups) => groups, Err(error) => { self.show_failure_message(&self.strings.cant_set_affinity, error); - return false; + return None; } }; let selected_masks = match initial_affinity_masks( @@ -570,7 +584,7 @@ impl ProcessPageState { Ok(masks) => masks, Err(error) => { self.show_failure_message(&self.strings.cant_set_affinity, error); - return false; + return None; } }; let selected_group_index = selected_masks @@ -582,7 +596,6 @@ impl ProcessPageState { topology, selected_masks, selected_group_index, - original_default_ids, }; match dialog_box( @@ -592,22 +605,17 @@ impl ProcessPageState { Some(affinity_dialog_proc), &mut context as *mut AffinityDialogContext as LPARAM, ) { - Ok(result) if result == IDOK as isize => { - match apply_affinity_selection(handle.as_raw(), identity.pid, &context) { - Ok(()) => { - self.refresh_processes(); - true - } - Err(error) => { - self.show_failure_message(&self.strings.cant_set_affinity, error); - false - } + Ok(result) if result == IDOK as isize => match confirmed_group_masks(&context) { + Ok(masks) => Some(masks), + Err(error) => { + self.show_failure_message(&self.strings.cant_set_affinity, error); + None } - } - Ok(_) => false, + }, + Ok(_) => None, Err(error) => { self.show_failure_message(&self.strings.cant_set_affinity, error); - false + None } } } @@ -616,12 +624,14 @@ impl ProcessPageState { enum ActionFailureKind { Kill, Priority, + Affinity, } fn action_failure_kind(action: &ProcessAction) -> ActionFailureKind { match action { ProcessAction::End(_) | ProcessAction::EndTree(_) => ActionFailureKind::Kill, ProcessAction::SetPriority { .. } => ActionFailureKind::Priority, + ProcessAction::SetAffinity { .. } => ActionFailureKind::Affinity, #[cfg(test)] ProcessAction::Block { .. } => ActionFailureKind::Kill, } @@ -633,10 +643,30 @@ fn failure_report(kind: ActionFailureKind, error: u32) -> ProcessActionReport { feedback: match kind { ActionFailureKind::Kill => ProcessActionFeedback::CantKill(error), ActionFailureKind::Priority => ProcessActionFeedback::CantChangePriority(error), + ActionFailureKind::Affinity => ProcessActionFeedback::CantSetAffinity(error), }, } } +fn confirmed_group_masks(context: &AffinityDialogContext) -> Result, u32> { + context + .topology + .groups() + .iter() + .zip(context.selected_masks.iter().copied()) + .map(|(group, mask)| { + // `u64` can hold every `usize` bit. A failed conversion is reported + // instead of submitting a shorter mask. + u64::try_from(mask) + .map(|mask| AffinityGroupMask { + group: group.number, + mask, + }) + .map_err(|_| ERROR_NOT_SUPPORTED) + }) + .collect() +} + fn initial_affinity_masks( process: HANDLE, topology: &CpuSetTopology, @@ -678,48 +708,167 @@ fn initial_affinity_masks( Ok(masks) } -fn apply_affinity_selection( - process: HANDLE, - process_id: u32, - context: &AffinityDialogContext, -) -> Result<(), u32> { - let selected_ids = context - .topology - .ids_for_masks(&context.selected_masks) - .map_err(|error| error.win32_code())?; - if selected_ids.is_empty() { +/// How a validated selection is written. A multi-group selection stays a list of +/// group masks and is never folded into `SetProcessAffinityMask`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum AffinityUpdatePlan { + ProcessMask(usize), + GroupCpuSets(Vec), +} + +pub(super) fn plan_affinity_update(masks: &[usize]) -> Result { + if masks.is_empty() || masks.iter().all(|mask| *mask == 0) { return Err(ERROR_INVALID_PARAMETER); } + if masks.len() == 1 { + Ok(AffinityUpdatePlan::ProcessMask(masks[0])) + } else { + Ok(AffinityUpdatePlan::GroupCpuSets(masks.to_vec())) + } +} - if context.topology.groups().len() == 1 { - set_process_default_cpu_sets(process, &[])?; - if unsafe { SetProcessAffinityMask(process, context.selected_masks[0]) } == 0 { - let error = nonzero_last_error(); - let _ = set_process_default_cpu_sets(process, &context.original_default_ids); - return Err(error); +/// Place each selected mask on the topology group with the same number. +/// +/// A missing group, a duplicated group, a bit outside the assignable mask, or a +/// mask wider than native `KAFFINITY` is an error. Bits are not clipped to the +/// low half of the mask or moved onto another group. +pub(super) fn align_affinity_selection( + topology_groups: &[(u16, usize)], + selected: &[(u16, u64)], +) -> Result, u32> { + if topology_groups.is_empty() || selected.len() != topology_groups.len() { + return Err(ERROR_NOT_SUPPORTED); + } + if duplicate_group_ids(topology_groups.iter().map(|(group, _)| *group)) + || duplicate_group_ids(selected.iter().map(|(group, _)| *group)) + { + return Err(ERROR_INVALID_PARAMETER); + } + + let mut masks = Vec::with_capacity(topology_groups.len()); + for &(group, assignable) in topology_groups { + let Some(&(_, wide_mask)) = selected.iter().find(|(number, _)| *number == group) else { + return Err(ERROR_NOT_SUPPORTED); + }; + let native = usize::try_from(wide_mask).map_err(|_| ERROR_NOT_SUPPORTED)?; + if native & !assignable != 0 { + return Err(ERROR_INVALID_PARAMETER); } - return Ok(()); + masks.push(native); } + if masks.iter().all(|mask| *mask == 0) { + return Err(ERROR_INVALID_PARAMETER); + } + Ok(masks) +} - set_process_default_cpu_sets(process, &selected_ids)?; - let selected_groups = context - .topology +fn duplicate_group_ids(mut groups: impl Iterator) -> bool { + let mut seen = HashSet::::new(); + groups.any(|group| !seen.insert(group)) +} + +pub(super) fn apply_confirmed_affinity( + identity: ProcIdentity, + selected: &[AffinityGroupMask], +) -> Result<(), u32> { + // Unknown and mismatched identities fail here, before either mask API. + require_action_identity(identity)?; + let handle = open_process_for_identity(identity, AFFINITY_UPDATE_ACCESS)?; + let topology = CpuSetTopology::query(handle.as_raw()).map_err(|error| error.win32_code())?; + let original_default_ids = + query_process_default_cpu_sets(handle.as_raw()).map_err(|error| error.win32_code())?; + let topology_groups = topology .groups() .iter() - .zip(context.selected_masks.iter().copied()) - .filter(|(_, mask)| *mask != 0) - .map(|(group, mask)| (group.number, mask)) + .map(|group| (group.number, group.assignable_mask)) .collect::>(); + let selected_pairs = selected + .iter() + .map(|group| (group.group, group.mask)) + .collect::>(); + let masks = align_affinity_selection(&topology_groups, &selected_pairs)?; + apply_affinity_selection( + handle.as_raw(), + identity, + &topology, + &masks, + &original_default_ids, + ) +} - // CPU Sets span groups, but a restrictive thread affinity mask takes precedence over a - // conflicting CPU Set assignment. Apply a group-qualified hard mask to every existing thread - // so an old per-thread affinity cannot keep using a CPU the user just deselected. The process - // default CPU Sets above cover threads created after the verified thread snapshot. - if let Err(error) = apply_thread_group_affinities(process_id, &selected_groups) { - let _ = set_process_default_cpu_sets(process, &context.original_default_ids); - return Err(error); +fn apply_affinity_selection( + process: HANDLE, + identity: ProcIdentity, + topology: &CpuSetTopology, + selected_masks: &[usize], + original_default_ids: &[u32], +) -> Result<(), u32> { + let selected_ids = topology + .ids_for_masks(selected_masks) + .map_err(|error| error.win32_code())?; + if selected_ids.is_empty() { + return Err(ERROR_INVALID_PARAMETER); + } + if selected_masks.len() != topology.groups().len() { + return Err(ERROR_NOT_SUPPORTED); + } + + // Validation is finished. The handle was opened for this identity above. + match plan_affinity_update(selected_masks)? { + AffinityUpdatePlan::ProcessMask(mask) => { + if topology.groups().len() != 1 { + return Err(ERROR_NOT_SUPPORTED); + } + set_process_default_cpu_sets(process, &[])?; + if unsafe { SetProcessAffinityMask(process, mask) } == 0 { + let error = nonzero_last_error(); + let _ = set_process_default_cpu_sets(process, original_default_ids); + // The CPU-set clear already landed. Restoring it does not make the + // failed mask a success. + return Err(error); + } + Ok(()) + } + AffinityUpdatePlan::GroupCpuSets(masks) => { + if masks.len() != topology.groups().len() || topology.groups().len() < 2 { + return Err(ERROR_NOT_SUPPORTED); + } + set_process_default_cpu_sets(process, &selected_ids)?; + let selected_groups = topology + .groups() + .iter() + .zip(masks.iter().copied()) + .filter(|(_, mask)| *mask != 0) + .map(|(group, mask)| (group.number, mask)) + .collect::>(); + // Thread masks are applied by PID. Re-check the identity first so a + // reused PID does not receive this process's group masks. A mismatch + // after the CPU-set update is still a failure. + match query_process_identity_for_pid(identity.pid) { + Ok(current) if current == identity => {} + Ok(_) => { + let _ = set_process_default_cpu_sets(process, original_default_ids); + return Err(ERROR_INVALID_PARAMETER); + } + Err(error) => { + let _ = set_process_default_cpu_sets(process, original_default_ids); + return Err(error); + } + } + + // CPU Sets span groups, but a restrictive thread affinity mask takes + // precedence over a conflicting CPU Set assignment. Apply a + // group-qualified hard mask to every existing thread so an old + // per-thread affinity cannot keep using a CPU the user just + // deselected. The process default CPU Sets above cover threads + // created after the verified thread snapshot. + if let Err(error) = apply_thread_group_affinities(identity.pid, &selected_groups) { + let _ = set_process_default_cpu_sets(process, original_default_ids); + return Err(error); + } + Ok(()) + } } - Ok(()) } fn set_process_default_cpu_sets(process: HANDLE, ids: &[u32]) -> Result<(), u32> { @@ -1943,3 +2092,107 @@ mod tree_identity_tests { )); } } + +#[cfg(test)] +mod affinity_selection_tests { + use super::{ + AffinityGroupMask, AffinityUpdatePlan, align_affinity_selection, apply_confirmed_affinity, + plan_affinity_update, + }; + use crate::system::process_identity::ProcIdentity; + use windows_sys::Win32::Foundation::{ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED}; + + #[test] + fn unverified_identity_does_not_apply_a_mask() { + let masks = [AffinityGroupMask { + group: 0, + mask: 0b1111, + }]; + assert_eq!( + apply_confirmed_affinity(ProcIdentity::pid_only(4), &masks), + Err(ERROR_INVALID_PARAMETER) + ); + assert_eq!( + apply_confirmed_affinity(ProcIdentity::new(0, 100), &masks), + Err(ERROR_INVALID_PARAMETER) + ); + } + + #[test] + fn multigroup_plan_does_not_collapse_to_the_low_group() { + let masks = [0b0011usize, 0b1100]; + assert_eq!( + plan_affinity_update(&masks), + Ok(AffinityUpdatePlan::GroupCpuSets(vec![0b0011, 0b1100])) + ); + assert_ne!( + plan_affinity_update(&masks), + Ok(AffinityUpdatePlan::ProcessMask(0b0011)) + ); + assert_eq!( + plan_affinity_update(&[0b1010]), + Ok(AffinityUpdatePlan::ProcessMask(0b1010)) + ); + } + + #[test] + fn inexpressible_group_or_mask_is_not_truncated() { + let topology = [(0u16, 0b0011usize), (1u16, 0b1100)]; + // Bit 0 of group 0 is expressible. Bit 3 is not, so the mask is not reduced to bit 0. + assert_eq!( + align_affinity_selection(&topology, &[(0, 0b1001), (1, 0b0100)]), + Err(ERROR_INVALID_PARAMETER) + ); + // Group 2 is absent. Its bits are not applied to group 0 or group 1. + assert_eq!( + align_affinity_selection(&topology, &[(0, 0b0001), (2, 0b0100)]), + Err(ERROR_NOT_SUPPORTED) + ); + assert_eq!( + align_affinity_selection(&topology, &[(1, 0b1000), (0, 0b0010)]).unwrap(), + vec![0b0010, 0b1000] + ); + } + + #[test] + fn mask_wider_than_native_kaffinity_is_not_truncated() { + let wide = 1u64 << 63; + let result = align_affinity_selection(&[(0, usize::MAX)], &[(0, wide)]); + if usize::BITS < 64 { + assert_eq!(result, Err(ERROR_NOT_SUPPORTED)); + } else { + assert_eq!(result.unwrap(), vec![usize::try_from(wide).unwrap()]); + } + + // Truncating to 32 bits would leave 0x1. That value must not be applied. + let widened = (1u64 << 32) | 1; + let narrowed = align_affinity_selection(&[(0, usize::MAX)], &[(0, widened)]); + if usize::BITS < 64 { + assert_eq!(narrowed, Err(ERROR_NOT_SUPPORTED)); + } else { + assert_eq!(narrowed.unwrap(), vec![usize::try_from(widened).unwrap()]); + } + } + + #[test] + fn highest_native_affinity_bit_survives_alignment() { + let high = 1usize << (usize::BITS - 1); + assert_eq!( + align_affinity_selection(&[(3, high)], &[(3, u64::try_from(high).unwrap())]).unwrap(), + vec![high] + ); + } + + #[test] + fn empty_or_duplicate_affinity_selection_is_rejected() { + assert_eq!( + align_affinity_selection(&[(0, 0b1)], &[(0, 0)]), + Err(ERROR_INVALID_PARAMETER) + ); + assert_eq!(plan_affinity_update(&[0, 0]), Err(ERROR_INVALID_PARAMETER)); + assert_eq!( + align_affinity_selection(&[(0, 1), (0, 1)], &[(0, 1), (1, 1)]), + Err(ERROR_INVALID_PARAMETER) + ); + } +} diff --git a/src/pages/processes/mod.rs b/src/pages/processes/mod.rs index 0e12ddc..9d27136 100644 --- a/src/pages/processes/mod.rs +++ b/src/pages/processes/mod.rs @@ -26,11 +26,13 @@ mod tree; // 线程模型: // SingleFlightWorker 工作线程在后台执行 collect_process_entries() 采样, // UI 线程只提交刷新意图并异步提交结果;CPU delta 基线由 worker 独占。 -// 结束进程、结束进程树和设置优先级在确认后进入同一个持久 SingleFlightWorker。 -// 确认框留在 UI 线程。TerminateProcess、进程树遍历和 SetPriorityClass 在该 -// worker 上执行,执行前用 open_process_for_identity 核对 pid 与创建时间。 -// 完成通知复用 PWM_PROC_WORKER_COMPLETE,和采样结果一起排空。页面销毁时丢弃 -// 该 worker,不在 UI 线程上 join。 +// 结束进程、结束进程树、设置优先级和设置亲和性在确认后进入同一个持久 +// SingleFlightWorker。确认框和亲和性对话框留在 UI 线程。TerminateProcess、 +// 进程树遍历、SetPriorityClass、SetProcessAffinityMask 和 +// SetProcessDefaultCpuSets 在该 worker 上执行,执行前用 +// open_process_for_identity 核对 pid 与创建时间。完成通知复用 +// PWM_PROC_WORKER_COMPLETE,和采样结果一起排空。页面销毁时丢弃该 worker, +// 不在 UI 线程上 join。 // 创建转储文件使用另一个 worker。保存对话框留在 UI 线程, // MiniDumpWriteDump 在该 worker 上执行,不与采样请求合并。 //