From 898d996061aff9e8ffc8012d475e007a8b24d167 Mon Sep 17 00:00:00 2001 From: wnstngs <64602598+wnstngs@users.noreply.github.com> Date: Mon, 26 May 2025 22:56:42 +0300 Subject: [PATCH 1/4] Initial support for CPU Sets Related issue: #2289 Changes made: - Process Default CPU Sets - Get/Set (API) - Get/Set (GUI) - System CPU Sets - Get (API) --- SystemInformer/SystemInformer.rc | 39 +- SystemInformer/SystemInformer.vcxproj | 1 + SystemInformer/SystemInformer.vcxproj.filters | 3 + SystemInformer/cpuset.c | 1256 +++++++++++++++++ SystemInformer/include/phapp.h | 7 + SystemInformer/include/sysinfop.h | 4 - SystemInformer/mainwnd.c | 12 + SystemInformer/mwpgproc.c | 3 + SystemInformer/resource.h | 59 +- SystemInformer/syssccpu.c | 4 +- phlib/include/cpuset.h | 55 + phlib/phlib.vcxproj | 1 + 12 files changed, 1408 insertions(+), 36 deletions(-) create mode 100644 SystemInformer/cpuset.c create mode 100644 phlib/include/cpuset.h diff --git a/SystemInformer/SystemInformer.rc b/SystemInformer/SystemInformer.rc index 25249c591d09..343c6dc5e378 100644 --- a/SystemInformer/SystemInformer.rc +++ b/SystemInformer/SystemInformer.rc @@ -26,18 +26,18 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // TEXTINCLUDE // -1 TEXTINCLUDE +1 TEXTINCLUDE BEGIN "resource.h\0" END -2 TEXTINCLUDE +2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "#include ""include/phappres.h""\0" END -3 TEXTINCLUDE +3 TEXTINCLUDE BEGIN "\0" END @@ -611,6 +611,21 @@ BEGIN COMBOBOX IDC_GROUPCPU,220,4,52,30,CBS_DROPDOWNLIST | CBS_SORT | NOT WS_VISIBLE | WS_VSCROLL | WS_TABSTOP END +IDD_CPUSETS DIALOGEX 0, 0, 368, 244 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +CAPTION "CPU Sets" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "CPU Sets provide a way to declare CPU affinity in a soft manner that is compatible with OS power management, and to reaffinitize all background threads in the process to a subset of processors to avoid interference from OS threads within the process.",IDC_STATIC,6,6,339,23 + CONTROL "",IDC_LIST,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_NOSORTHEADER | WS_BORDER | WS_TABSTOP,6,35,357,174 + PUSHBUTTON "Cancel",IDCANCEL,261,219,50,14 + DEFPUSHBUTTON "OK",IDOK,314,219,50,14 + PUSHBUTTON "Select all",IDC_SELECTALL,6,219,50,14 + PUSHBUTTON "Unselect all",IDC_DESELECTALL,59,219,50,14 + CONTROL "When all are unselected, all CPU Sets may be used",IDC_STATIC, + "Static",SS_SIMPLE | SS_WORDELLIPSIS | WS_GROUP,8,234,154,8 +END + IDD_SYSINFO DIALOGEX 0, 0, 423, 247 STYLE DS_SETFONT | DS_FIXEDSYS | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME EXSTYLE WS_EX_APPWINDOW @@ -1799,6 +1814,11 @@ BEGIN BOTTOMMARGIN, 220 END + IDD_CPUSETS, DIALOG + BEGIN + BOTTOMMARGIN, 4 + END + IDD_SYSINFO, DIALOG BEGIN END @@ -2187,7 +2207,6 @@ BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 253 TOPMARGIN, 7 - BOTTOMMARGIN, 253 END END #endif // APSTUDIO_INVOKED @@ -2234,6 +2253,7 @@ BEGIN VK_F11, IDC_MAXSCREEN, VIRTKEY, NOINVERT END + ///////////////////////////////////////////////////////////////////////////// // // Bitmap @@ -2243,6 +2263,17 @@ IDB_SEARCH_ACTIVE_BMP BITMAP "resources\\active_search.bmp" IDB_SEARCH_INACTIVE_BMP BITMAP "resources\\inactive_search.bmp" + +///////////////////////////////////////////////////////////////////////////// +// +// AFX_DIALOG_LAYOUT +// + +IDD_CPUSETS AFX_DIALOG_LAYOUT +BEGIN + 0 +END + #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// diff --git a/SystemInformer/SystemInformer.vcxproj b/SystemInformer/SystemInformer.vcxproj index 1c28b7dafd53..364858f42f65 100644 --- a/SystemInformer/SystemInformer.vcxproj +++ b/SystemInformer/SystemInformer.vcxproj @@ -230,6 +230,7 @@ + diff --git a/SystemInformer/SystemInformer.vcxproj.filters b/SystemInformer/SystemInformer.vcxproj.filters index 35d8d0002268..d8c8a556f7ac 100644 --- a/SystemInformer/SystemInformer.vcxproj.filters +++ b/SystemInformer/SystemInformer.vcxproj.filters @@ -345,6 +345,9 @@ System Informer + + System Informer + diff --git a/SystemInformer/cpuset.c b/SystemInformer/cpuset.c new file mode 100644 index 000000000000..517d3ab79b24 --- /dev/null +++ b/SystemInformer/cpuset.c @@ -0,0 +1,1256 @@ +/* + * Copyright (c) 2025 Winsider Seminars & Solutions, Inc. All rights reserved. + * + * This file is part of System Informer ("SI"). + * + * Description: + * + * This module contains code to support CPU Sets in SI. + * + * Authors: + * + * wnstngs 2025 + * + */ + +// +// The current version only supports modifying CPU Sets at the Process Default +// level. TODO: Support the Thread Selected level. +// + +// +// ------------------------------------------------------------------- Includes +// + +#include +#include + +#include + +// +// ---------------------------------------------------------------- Definitions +// + +// +// Each CPU Set ID is encoded as follows: +// Bits [31:16]: Processor group index +// Bits [15:8]: Processor index within the group +// Bits [7:0]: CPUSET_BIAS +// +#define CPUSET_BIAS 0x100 +#define CPUSET_GROUP_SHIFT 16 +#define CPUSET_ID_MASK 0xFF + +/** + * Encodes a CPU Set ID from a group index and a logical processor index. + * + * @param GroupIndex + * The index of the processor group. + * + * @param CpuIndex + * The index of the logical processor within the group. + * + * @return The encoded CPU Set ID. + */ +#define ENCODE_CPU_SET_ID(GroupIndex, CpuIndex) \ + (((GroupIndex) << CPUSET_GROUP_SHIFT) | \ + ((CpuIndex) & CPUSET_ID_MASK) | \ + CPUSET_BIAS) + +/** + * Decodes a CPU Set ID to extract the processor group index. + * + * @param CpuSetId + * The encoded CPU Set ID. + * + * @return The processor group index. + */ +#define DECODE_CPU_SET_GROUP(CpuSetId) \ + (((CpuSetId) - CPUSET_BIAS) >> CPUSET_GROUP_SHIFT) + +/** + * A helper which iterates through a list of CPU Set information structures + * and calls the specified callback for each CPU Set. + * + * @param InfoStart + * Pointer to the start of the CPU Set information buffer. + * + * @param Context + * Optional context pointer to pass to the callback function. + * + * @param Callback + * Callback function to invoke. If it fails, the enumeration stops. + * TBD: Add a parameter to control this behavior (break vs continue). + */ +#define FOREACH_CPU_SET_INFO(InfoStart, Context, Callback) \ + do { \ + PSYSTEM_CPU_SET_INFORMATION _info = (InfoStart); \ + while (_info->Size != 0 && \ + RTL_CONTAINS_FIELD(_info, _info->Size, CpuSet)) { \ + if (_info->Type == CpuSetInformation) { \ + if (!Callback(_info, (PVOID)(Context))) { \ + break; \ + } \ + } \ + _info = PTR_ADD_OFFSET(_info, _info->Size); \ + } \ + } while (0) + +typedef enum PH_CPU_SET_TYPE { + PhCpuSetTypeSystem, + PhCpuSetTypeProcessDefault, + PhCpuSetTypeThreadSelected +} PH_CPU_SET_TYPE; + +typedef struct PHP_CPU_SETS_DLG_CTX { + HWND WindowHandle; + HWND ListViewHandle; + PPH_PROCESS_ITEM ProcessItem; + PH_CPU_SET_TYPE CpuSetType; + PULONG CpuSetIds; + ULONG CpuSetIdCount; + ULONG Index; +} PHP_CPU_SETS_DLG_CTX, *PPHP_CPU_SETS_DLG_CTX; + +// +// ----------------------------------------------------------------- Prototypes +// + +INT_PTR +CALLBACK +PhpProcessCpuSetsDlgProc( + _In_ HWND DlgHandle, + _In_ UINT Message, + _In_ WPARAM WParam, + _In_ LPARAM LParam + ); + +// +// ------------------------------------------------------------------ Functions +// + +// +// CPU Set ID Helpers +// + +/** + * Converts an array of CPU Set IDs into processor group affinity masks. + * + * @param CpuSetIds + * An array of CPU Set IDs to convert. + * + * @param CpuSetIdCount + * The number of CPU Set IDs in the array. + * + * @param CpuSetMasks + * An output buffer for processor group affinity masks. + * + * @param GroupCount + * The number of processor groups. + * + * @return TRUE if successful, FALSE otherwise. + */ +_Success_(return != FALSE) +static +BOOLEAN +PhpCpuSetIdsToMasks( + _In_reads_opt_(CpuSetIdCount) CONST ULONG *CpuSetIds, + _In_ ULONG CpuSetIdCount, + _Out_writes_(GroupCount) PKAFFINITY CpuSetMasks, + _In_ USHORT GroupCount + ) +{ + ULONG i; + USHORT group; + UCHAR cpuIndex; + + assert(CpuSetMasks != NULL); + assert(GroupCount > 0); + + memset(CpuSetMasks, 0, GroupCount * sizeof(KAFFINITY)); + + for (i = 0; i < CpuSetIdCount; i += 1) { + + // + // Decode group and processor index + // + cpuIndex = (UCHAR)CpuSetIds[i]; + group = DECODE_CPU_SET_GROUP(CpuSetIds[i]); + + assert(group < GroupCount); + assert(cpuIndex < MAXIMUM_PROC_PER_GROUP); + + if (group >= GroupCount || cpuIndex >= MAXIMUM_PROC_PER_GROUP) { + return FALSE; + } + + // + // Set the corresponding bit + // + CpuSetMasks[group] |= ((KAFFINITY)1 << cpuIndex); + } + + return TRUE; +} + +/** + * Converts processor group affinity masks to CPU Set IDs. + * + * @param CpuSetMasks + * An array of processor group affinity masks to be converted. + * + * @param GroupCount + * The number of processor groups. + * + * @param IdList + * An optional output array where CPU Set IDs will be stored if provided. + * + * @param IdListSize + * The size of the IdList array. + * + * @param RequiredCount + * A pointer to a variable that will receive the number of IDs required to + * fully represent the input affinity masks. + * + * @return TRUE if successful, FALSE otherwise. + */ +_Success_(return != FALSE) +static +BOOLEAN +PhpCpuSetMasksToIdList( + _In_reads_(GroupCount) CONST KAFFINITY *CpuSetMasks, + _In_ USHORT GroupCount, + _Out_writes_to_opt_(IdListSize, *RequiredCount) PULONG IdList, + _In_ ULONG IdListSize, + _Out_ PULONG RequiredCount + ) +{ + ULONG total = 0; + ULONG bit; + USHORT group; + KAFFINITY mask; + + assert(CpuSetMasks != NULL); + assert(GroupCount > 0); + assert(RequiredCount != NULL); + + for (group = 0; group < GroupCount; group += 1) { + mask = CpuSetMasks[group]; + + while (mask) { + // + // Find the next set bit (logical processor) + // + BitScanForward64(&bit, mask); + + // + // Clear the bit + // + mask &= ~(1ULL << bit); + + // + // Encode the CPU Set ID if there's room + // + if (IdList && total < IdListSize) { + IdList[total] = ENCODE_CPU_SET_ID(group, (UCHAR)bit); + } + + total += 1; + } + } + + *RequiredCount = total; + + if (total <= IdListSize) { + return TRUE; + } + + return FALSE; +} + +// +// System CPU Sets +// + +/** + * Internal helper function to query the available CPU Sets on the system. + * + * @param Information + * A pointer to a SYSTEM_CPU_SET_INFORMATION structure that receives + * the CPU Set data. + * + * @return An NTSTATUS error code. + */ +static +NTSTATUS +PhpQueryCpuSetInformation( + _Out_ PVOID *Information + ) +{ + NTSTATUS status; + static ULONG initialBufferSize = 0; + HANDLE processHandle = NULL; + ULONG returnLength; + PSYSTEM_CPU_SET_INFORMATION cpuSetInfo; + + *Information = NULL; + + // + // MSDN: https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-system_cpu_set_information + // This is a variable-sized structure designed for future expansion. When iterating over + // this structure, use the size field to determine the offset to the next structure. + // + // N.B. The kernel returns the size even in the last element, we over-allocate by the + // Size offset and check it to minimize instructions (jxy-s). + // + + if (initialBufferSize) { + returnLength = initialBufferSize; + cpuSetInfo = PhAllocateZero(returnLength); + + if (!cpuSetInfo) { + return STATUS_INSUFFICIENT_RESOURCES; + } + } + else { + returnLength = 0; + cpuSetInfo = NULL; + } + + status = NtQuerySystemInformationEx(SystemCpuSetInformation, + &processHandle, + sizeof(HANDLE), + cpuSetInfo, + returnLength, + &returnLength); + + if (status == STATUS_BUFFER_TOO_SMALL) { + returnLength += RTL_SIZEOF_THROUGH_FIELD(SYSTEM_CPU_SET_INFORMATION, + Size); + + PhFree(cpuSetInfo); + cpuSetInfo = PhAllocateZero(returnLength); + + if (!cpuSetInfo) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + status = NtQuerySystemInformationEx(SystemCpuSetInformation, + &processHandle, + sizeof(HANDLE), + cpuSetInfo, + returnLength, + NULL); + + if (NT_SUCCESS(status) && initialBufferSize <= 0x100000) { + initialBufferSize = returnLength; + } + } + + if (!NT_SUCCESS(status)) { + if (cpuSetInfo) { + PhFree(cpuSetInfo); + cpuSetInfo = NULL; + } + } + + *Information = cpuSetInfo; + + return status; +} + +/** + * Queries the available CPU Sets on the system. + * + * @param Information + * A pointer to a SYSTEM_CPU_SET_INFORMATION structure that receives + * the CPU Set data. + * + * @return An NTSTATUS error code. + */ +NTSTATUS +PhQuerySystemCpuSetInformation( + _Out_ PVOID *Information + ) +{ + return PhpQueryCpuSetInformation(Information); +} + +/** + * Enumerates all available CPU Sets on the system and calls the callback + * function for each CPU Set found. + * + * @param Callback + * A pointer to a callback function that is called for each CPU Set. + * If the callback returns FALSE, enumeration stops. + * + * @param Context + * An optional context pointer that is passed to the callback. + * + * @return An NTSTATUS error code. + */ +NTSTATUS +PhEnumerateSystemCpuSets( + _In_ PPH_CPU_SETS_ENUMERATION_CALLBACK Callback, + _In_opt_ PVOID Context + ) +{ + NTSTATUS status; + PVOID cpuSetInfo = NULL; + + if (!Callback) { + return STATUS_INVALID_PARAMETER; + } + + status = PhpQueryCpuSetInformation(&cpuSetInfo); + + if (NT_SUCCESS(status)) { + FOREACH_CPU_SET_INFO(cpuSetInfo, Context, Callback); + PhFree(cpuSetInfo); + } + + return status; +} + +// +// Process Default CPU Sets +// + +/** + * Queries the Process Default CPU Sets assigned to the given process. + * + * @param ProcessHandle + * A handle to the target process. The handle must have + * QUERY_LIMITED_INFORMATION permissions. + * + * @param CpuSetIds + * A pointer to an array of CPU Set IDs. + * + * @param CpuSetIdCount + * The number of CPU Set IDs in the array. + * + * @return An NTSTATUS error code. + */ +NTSTATUS +PhQueryCpuSetsProcess( + _In_ HANDLE ProcessHandle, + _Out_ PULONG *CpuSetIds, + _Out_ PULONG CpuSetIdCount + ) +{ + NTSTATUS status; + PKAFFINITY masks; + ULONG bufferSize; + static USHORT groupCount; + ULONG requiredCount; + + groupCount = USER_SHARED_DATA->ActiveGroupCount; + + *CpuSetIds = NULL; + *CpuSetIdCount = 0; + + bufferSize = groupCount * sizeof(KAFFINITY); + masks = PhAllocateZero(bufferSize); + + if (!masks) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + status = NtQueryInformationProcess(ProcessHandle, + ProcessDefaultCpuSetsInformation, + masks, + bufferSize, + NULL); + + if (!NT_SUCCESS(status)) { + PhFree(masks); + return status; + } + + PhpCpuSetMasksToIdList(masks, + groupCount, + NULL, + 0, + &requiredCount); + + if (requiredCount == 0) { + *CpuSetIds = NULL; + *CpuSetIdCount = 0; + PhFree(masks); + return STATUS_SUCCESS; + } + + *CpuSetIds = PhAllocate(requiredCount * sizeof(**CpuSetIds)); + + if (!*CpuSetIds) { + PhFree(masks); + return STATUS_INSUFFICIENT_RESOURCES; + } + + status = PhpCpuSetMasksToIdList(masks, + groupCount, + *CpuSetIds, + requiredCount, + CpuSetIdCount) + ? STATUS_SUCCESS + : STATUS_BUFFER_TOO_SMALL; + + if (!NT_SUCCESS(status)) { + PhFree(*CpuSetIds); + *CpuSetIds = NULL; + } + + PhFree(masks); + + return status; +} + +/** + * Sets the Process Default CPU Sets for a process. + * + * @param ProcessHandle + * A handle to the process. The handle must have + * PROCESS_SET_LIMITED_INFORMATION access. + * + * @param CpuSetIds + * An optional array of CPU Set IDs to assign to the process. If NULL or + * CpuSetIdCount is 0, the process will be assigned to all available + * CPU Sets. + * + * @param CpuSetIdCount + * The number of CPU Set IDs in the array. + * + * @return An NTSTATUS error code. + */ +NTSTATUS +PhSetCpuSetsProcess( + _In_ HANDLE ProcessHandle, + _In_reads_opt_(CpuSetIdCount) const ULONG *CpuSetIds, + _In_ ULONG CpuSetIdCount + ) +{ + NTSTATUS status; + static USHORT groupCount; + PKAFFINITY masks; + + groupCount = USER_SHARED_DATA->ActiveGroupCount; + + masks = PhAllocateZero(groupCount * sizeof(KAFFINITY)); + if (!masks) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + if (!PhpCpuSetIdsToMasks(CpuSetIds, CpuSetIdCount, masks, groupCount)) { + PhFree(masks); + return STATUS_CPU_SET_INVALID; + } + + status = NtSetInformationProcess(ProcessHandle, + ProcessDefaultCpuSetsInformation, + masks, + groupCount * sizeof(KAFFINITY)); + + PhFree(masks); + + return status; +} + +// +// Thread Selected CPU Sets +// TODO: PhQueryCpuSetsThread() +// TODO: PhSetCpuSetsThread() +// + +// +// CPU Sets Dialog +// + +/** + * Shows the CPU Sets dialog for a process. + * + * @param ParentWindowHandle + * A handle to the parent window. + * + * @param ProcessItem + * A pointer to a process item. The dialog will display and allow + * modification of the CPU Sets for this process. + */ +VOID +PhShowCpuSetsDialogProcess( + _In_ HWND ParentWindowHandle, + _In_ PPH_PROCESS_ITEM ProcessItem + ) +{ + PPHP_CPU_SETS_DLG_CTX context; + + context = PhAllocateZero(sizeof(PHP_CPU_SETS_DLG_CTX)); + context->ProcessItem = ProcessItem; + context->CpuSetType = PhCpuSetTypeProcessDefault; + + // + // We'll query the CPU Sets in WM_INITDIALOG + // + context->CpuSetIds = NULL; + context->CpuSetIdCount = 0; + + PhDialogBox(PhInstanceHandle, + MAKEINTRESOURCE(IDD_CPUSETS), + ParentWindowHandle, + PhpProcessCpuSetsDlgProc, + context); + + // + // Context is freed in WM_DESTROY + // +} + +/** + * Callback function invoked for each System CPU Set from + * PhpPopulateListViewWithCpuSets(). + * Adds detailed information about CPU Sets to a list view. + * + * @param Information + * A pointer to a structure representing a CPU Set. + * + * @param Context + * An optional pointer to a structure that provides the dialog context, + * including selected CPU Sets. + * + * @return TRUE if the operation was successful, FALSE otherwise. + */ +_Success_(return != FALSE) +static +BOOLEAN +PhpPopulateCpuSetsListViewCallback( + _In_ PSYSTEM_CPU_SET_INFORMATION Information, + _In_opt_ PVOID Context + ) +{ + PPHP_CPU_SETS_DLG_CTX context = (PPHP_CPU_SETS_DLG_CTX)Context; + INT lvItemIndex; + WCHAR number[PH_INT32_STR_LEN_1]; + BOOLEAN isChecked = FALSE; + ULONG i; + ULONG low; + ULONG high; + ULONG mid; + + assert(Information != NULL); + + if (!context || !context->ListViewHandle) { + return FALSE; + } + + if (context->CpuSetIds && context->CpuSetIdCount > 0) { +#define BINARY_SEARCH_THRESHOLD 32 + if (context->CpuSetIdCount > BINARY_SEARCH_THRESHOLD) { + low = 0; + high = context->CpuSetIdCount - 1; + + while (low <= high) { + mid = low + (high - low) / 2; + + if (context->CpuSetIds[mid] == Information->CpuSet.Id) { + isChecked = TRUE; + break; + } + + if (context->CpuSetIds[mid] < Information->CpuSet.Id) { + low = mid + 1; + } + else { + high = mid - 1; + } + } + } + else { + for (i = 0; i < context->CpuSetIdCount; i += 1) { + if (context->CpuSetIds[i] == Information->CpuSet.Id) { + isChecked = TRUE; + break; + } + } + } + } + + PhPrintUInt32(number, Information->CpuSet.Id); + + // + // Index = MAXINT because: + // "If this value [Index] is greater than the number of items currently + // contained by the listview control, the new item will be appended to + // the end of the list and assigned the correct index" + // + lvItemIndex = PhAddListViewItem(context->ListViewHandle, + MAXINT, + number, + UlongToPtr(context->Index++)); + + if (isChecked) { + assert(lvItemIndex >= 0); + ListView_SetCheckState(context->ListViewHandle, lvItemIndex, TRUE) + } + + // + // Node + // + PhPrintUInt32(number, Information->CpuSet.NumaNodeIndex); + PhSetListViewSubItem(context->ListViewHandle, lvItemIndex, 1, number); + + // + // Group + // + PhPrintUInt32(number, Information->CpuSet.Group); + PhSetListViewSubItem(context->ListViewHandle, lvItemIndex, 2, number); + + // + // Core + // + PhPrintUInt32(number, Information->CpuSet.CoreIndex); + PhSetListViewSubItem(context->ListViewHandle, lvItemIndex, 3, number); + + // + // Processor + // + PhPrintUInt32(number, Information->CpuSet.LogicalProcessorIndex); + PhSetListViewSubItem(context->ListViewHandle, lvItemIndex, 4, number); + + // + // Allocated + // + PhSetListViewSubItem(context->ListViewHandle, + lvItemIndex, + 5, + Information->CpuSet.Allocated ? L"Yes" : L"No"); + + return TRUE; +} + +/** + * Adds CPU Set items to a list view control. + * + * @param ListViewHandle + * The handle to the list view control. + * + * @param DialogContext + * A pointer to a dialog context structure. + * + * @return STATUS_SUCCESS if no errors, or the specific error status code. + */ +static +NTSTATUS +PhpPopulateListViewWithCpuSets( + _In_ HWND ListViewHandle, + _In_ PPHP_CPU_SETS_DLG_CTX DialogContext + ) +{ + if (!DialogContext || !DialogContext->ProcessItem) { + return STATUS_INVALID_PARAMETER; + } + + NTSTATUS status; + HANDLE processHandle; + PULONG cpuSetIds = NULL; + ULONG cpuSetIdCount = 0; + PVOID cpuSetsInfo = NULL; + + status = PhOpenProcess(&processHandle, + PROCESS_QUERY_LIMITED_INFORMATION, + DialogContext->ProcessItem->ProcessId); + + if (!NT_SUCCESS(status)) { + PhShowStatus(ListViewHandle, + L"Unable to query CPU Sets information", + status, + 0); + return status; + } + + // + // Query Process Default CPU Sets + // + status = PhQueryCpuSetsProcess(processHandle, + &cpuSetIds, + &cpuSetIdCount); + + NtClose(processHandle); + + // + // Disable redraw while updating + // + ExtendedListView_SetRedraw(ListViewHandle, FALSE); + + if (NT_SUCCESS(status) && cpuSetIdCount > 0) { + // + // On success with count > 0, we keep cpuSetIds and check those IDs + // + if (DialogContext->CpuSetIds) { + PhFree(DialogContext->CpuSetIds); + } + DialogContext->CpuSetIds = cpuSetIds; + DialogContext->CpuSetIdCount = cpuSetIdCount; + + // + // Enumerate all system CPU Sets and check selected + // + DialogContext->Index = 0; + status = PhEnumerateSystemCpuSets(PhpPopulateCpuSetsListViewCallback, + DialogContext); + } + else { + // + // Otherwise, free the allocated buffer + // + if (cpuSetIds) { + PhFree(cpuSetIds); + } + DialogContext->CpuSetIds = NULL; + DialogContext->CpuSetIdCount = 0; + + // + // No Process Default CPU Set, show all System CPU Sets as unchecked + // + status = PhQuerySystemCpuSetInformation(&cpuSetsInfo); + if (NT_SUCCESS(status) && cpuSetsInfo) { + DialogContext->Index = 0; + FOREACH_CPU_SET_INFO(cpuSetsInfo, + DialogContext, + PhpPopulateCpuSetsListViewCallback); + PhFree(cpuSetsInfo); + } + + if (cpuSetIds) { + PhFree(cpuSetIds); + } + } + + // + // Bring back redrawing + // + ExtendedListView_SetRedraw(ListViewHandle, TRUE); + + return status; +} + +/** + * Attempts to parse a CPU Set ID from a null-terminated wide string. + * + * @param CpuSetIdText + * The input string to parse. + * + * @param AsUlong + * Receives the parsed unsigned long on success. + * + * @returns + * TRUE if the entire string is a valid non-negative integer; + * FALSE otherwise. + * + * @remark + * We know that every ListView item's text is exactly PhPrintUInt32(...): + * pure digits, no sign, no extra spaces. We use a specialized parser that + * simply walks the WCHAR* buffer once and stops on the first non-digit. + */ +static +BOOL +PhpTryParseCpuId( + _In_ LPCWSTR CpuSetIdText, + _Out_ PULONG AsUlong +) +{ + if (!CpuSetIdText || + *CpuSetIdText < L'0' || + *CpuSetIdText > L'9') { + return FALSE; + } + + ULONG v = 0; + WCHAR c; + for (; *CpuSetIdText; CpuSetIdText += 1) { + c = *CpuSetIdText; + if (c < L'0' || c > L'9') { + return FALSE; + } + v = v * 10 + (c - L'0'); + } + *AsUlong = v; + + return TRUE; +} + +/** + * Retrieves the selected CPU Set IDs from a ListView. + * + * @param ListViewHandle + * The handle to the ListView control. + * + * @param SelectedIds + * A pointer to store the dynamically allocated array of selected IDs. + * The caller must free this memory when it is no longer needed. + * + * @param SelectedCount + * Pointer to store the count of selected IDs. + */ +static +NTSTATUS +PhpGetSelectedCpuSetIds( + _In_ HWND ListViewHandle, + _Out_ PULONG *SelectedIds, + _Out_ PULONG SelectedCount +) +{ + INT i; + INT itemCount = ListView_GetItemCount(ListViewHandle); + ULONG count; + WCHAR buffer[PH_INT32_STR_LEN_1]; + LVITEMW lv = {0}; + PULONG shrunk; + + if (itemCount <= 0) { + *SelectedIds = NULL; + *SelectedCount = 0; + return STATUS_SUCCESS; + } + + ULONG *ids = PhAllocate(sizeof(*ids) * itemCount); + if (!ids) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + lv.mask = LVIF_TEXT; + lv.iSubItem = 0; + lv.pszText = buffer; + lv.cchTextMax = ARRAYSIZE(buffer); + + count = 0; + for (i = 0; i < itemCount; i += 1) { + if (!ListView_GetCheckState(ListViewHandle, i)) { + continue; + } + + // + // Retrieve the item text + // + lv.iItem = i; + if (SendMessageW(ListViewHandle, + LVM_GETITEMTEXTW, + (WPARAM)i, + (LPARAM)&lv) <= 0) { + continue; + } + + if (PhpTryParseCpuId(buffer, &ids[count])) { + count += 1; + } + } + + if (count == 0) { + PhFree(ids); + *SelectedIds = NULL; + *SelectedCount = 0; + return STATUS_SUCCESS; + } + + if (count < (ULONG)itemCount) { + shrunk = PhReAllocate(ids, sizeof(*ids) * count); + if (shrunk) { + ids = shrunk; + } + } + + *SelectedIds = ids; + *SelectedCount = count; + + return STATUS_SUCCESS; +} + +INT_PTR +CALLBACK +PhpProcessCpuSetsDlgProc( + _In_ HWND DlgHandle, + _In_ UINT Message, + _In_ WPARAM WParam, + _In_ LPARAM LParam +) +{ + PPHP_CPU_SETS_DLG_CTX context; + + if (Message == WM_INITDIALOG) { + context = (PPHP_CPU_SETS_DLG_CTX)LParam; + + if (!context) { + return FALSE; + } + + context->WindowHandle = DlgHandle; + + PhSetWindowContext(DlgHandle, + PH_WINDOW_CONTEXT_DEFAULT, + context); + } + else { + context = PhGetWindowContext(DlgHandle, PH_WINDOW_CONTEXT_DEFAULT); + + if (!context && Message != WM_DESTROY) { + return FALSE; + } + } + + switch (Message) { + case WM_INITDIALOG: { + NTSTATUS status; + PWSTR titleText; + + context->WindowHandle = DlgHandle; + context->ListViewHandle = GetDlgItem(DlgHandle, IDC_LIST); + context->Index = 0; + + PhSetApplicationWindowIcon(DlgHandle); + + // + // Set dialog title with process name if available + // + // @formatter:off + if (context->ProcessItem) { + titleText = PhaFormatString(L"CPU Sets - %s (%lu)", + context->ProcessItem->ProcessName->Buffer, + HandleToUlong(context->ProcessItem->ProcessId))->Buffer; + + SetWindowText(DlgHandle, titleText); + } + + PhAddListViewColumn(context->ListViewHandle, 0, 0, 0, LVCFMT_LEFT, 60, L"ID"); + PhAddListViewColumn(context->ListViewHandle, 1, 1, 1, LVCFMT_LEFT, 60, L"Node"); + PhAddListViewColumn(context->ListViewHandle, 2, 2, 2, LVCFMT_LEFT, 60, L"Group"); + PhAddListViewColumn(context->ListViewHandle, 3, 3, 3, LVCFMT_LEFT, 60, L"Core"); + PhAddListViewColumn(context->ListViewHandle, 4, 4, 4, LVCFMT_LEFT, 60, L"Processor"); + PhAddListViewColumn(context->ListViewHandle, 5, 5, 5, LVCFMT_LEFT, 60, L"Allocated"); + // @formatter:on + + PhSetListViewStyle(context->ListViewHandle, FALSE, TRUE); + + PhSetControlTheme(context->ListViewHandle, L"explorer"); + + PhSetExtendedListView(context->ListViewHandle); + + ListView_SetExtendedListViewStyleEx(context->ListViewHandle, + LVS_EX_CHECKBOXES, + LVS_EX_CHECKBOXES); + + ExtendedListView_SetRedraw(context->ListViewHandle, FALSE); + + ListView_DeleteAllItems(context->ListViewHandle); + + status = PhpPopulateListViewWithCpuSets(context->ListViewHandle, + context); + + ExtendedListView_SetRedraw(context->ListViewHandle, TRUE); + + if (!NT_SUCCESS(status)) { + // + // If we couldn't query the CPU Sets, close the dialog + // + EndDialog(DlgHandle, IDCANCEL); + break; + } + + // + // Enable/disable the OK button based on whether we have a process + // + EnableWindow(GetDlgItem(DlgHandle, IDOK), context->ProcessItem != NULL); + + PhSetDialogFocus(DlgHandle, GetDlgItem(DlgHandle, IDOK)); + + PhInitializeWindowTheme(DlgHandle, PhEnableThemeSupport); + } + break; + case WM_DESTROY: { + PPHP_CPU_SETS_DLG_CTX destroyContext = + PhGetWindowContext(DlgHandle, PH_WINDOW_CONTEXT_DEFAULT); + + if (destroyContext) { + if (destroyContext->CpuSetIds) { + PhFree(destroyContext->CpuSetIds); + destroyContext->CpuSetIds = NULL; + destroyContext->CpuSetIdCount = 0; + } + + PhRemoveWindowContext(DlgHandle, PH_WINDOW_CONTEXT_DEFAULT); + PhFree(destroyContext); + } + + return FALSE; + } + case WM_COMMAND: { + switch (GET_WM_COMMAND_ID(WParam, LParam)) { + case IDCANCEL: + EndDialog(DlgHandle, IDCANCEL); + break; + case IDC_SELECTALL: + case IDC_DESELECTALL: { + BOOL state = (GET_WM_COMMAND_ID(WParam, LParam) == IDC_SELECTALL); + HWND listView = context->ListViewHandle; + for (INT i = 0; i < ListView_GetItemCount(listView); i += 1) { + ListView_SetCheckState(listView, i, state) + } + break; + } + case IDOK: { + NTSTATUS status; + HANDLE processHandle = NULL; + PULONG selectedIds = NULL; + ULONG selectedCount = 0; + BOOLEAN succeeded = FALSE; + + if (!context || !context->ListViewHandle) { + break; + } + + // + // Get selected CPU Sets from the list view + // + status = PhpGetSelectedCpuSetIds(context->ListViewHandle, + &selectedIds, + &selectedCount); + + if (!NT_SUCCESS(status)) { + PhShowStatus(DlgHandle, + L"Failed to retrieve selected CPU Set IDs.", + status, + 0); + break; + } + + if (!selectedIds && selectedCount > 0) { + selectedCount = 0; + } + + // + // Apply the selected CPU Sets to the process if we have a process + // + if (context->ProcessItem && context->ProcessItem->ProcessId) { + status = PhOpenProcess(&processHandle, + PROCESS_SET_LIMITED_INFORMATION | + PROCESS_QUERY_LIMITED_INFORMATION, + context->ProcessItem->ProcessId); + if (NT_SUCCESS(status)) { + // + // Set the process CPU Sets + // + status = PhSetCpuSetsProcess(processHandle, + selectedIds, + selectedCount); + + if (NT_SUCCESS(status)) { + // + // Verify the changes were actually applied + // + PULONG verifyIds = NULL; + ULONG verifyCount = 0; + + // + // Query the process again to verify our changes + // + status = PhQueryCpuSetsProcess(processHandle, + &verifyIds, + &verifyCount); + + if (NT_SUCCESS(status)) { + // + // If we selected CPU Sets but none were found on + // verification, something might have gone wrong + // + if (selectedCount > 0 && verifyCount == 0) { + succeeded = FALSE; + status = STATUS_UNSUCCESSFUL; + } + else { + succeeded = TRUE; + } + + if (verifyIds) { + PhFree(verifyIds); + verifyIds = NULL; + } + } + else { + succeeded = FALSE; + } + } + else { + succeeded = FALSE; + } + + NtClose(processHandle); + } + + if (!succeeded) { + PhShowStatus(DlgHandle, + L"Unable to set CPU Sets for the process.", + status, + 0); + + if (selectedIds) { + PhFree(selectedIds); + selectedIds = NULL; + } + + break; + } + } + else { + // + // If we don't have a process to set CPU Set for, + // just consider it successful + // + succeeded = TRUE; + } + + if (succeeded) { + // + // Free the previously selected IDs if any + // + if (context->CpuSetIds) { + PhFree(context->CpuSetIds); + } + + // + // Update context with the newly selected IDs + // + context->CpuSetIds = selectedIds; + context->CpuSetIdCount = selectedCount; + + EndDialog(DlgHandle, IDOK); + } + else { + // + // Clean up on failure + // + if (selectedIds) { + PhFree(selectedIds); + selectedIds = NULL; + } + } + } + break; + } + break; + case WM_CTLCOLORBTN: + return HANDLE_WM_CTLCOLORBTN(DlgHandle, + WParam, + LParam, + PhWindowThemeControlColor); + case WM_CTLCOLORDLG: + return HANDLE_WM_CTLCOLORDLG(DlgHandle, + WParam, + LParam, + PhWindowThemeControlColor); + case WM_CTLCOLORSTATIC: + return HANDLE_WM_CTLCOLORSTATIC(DlgHandle, + WParam, + LParam, + PhWindowThemeControlColor); + } + } + + return FALSE; +} diff --git a/SystemInformer/include/phapp.h b/SystemInformer/include/phapp.h index 173210906140..8087a56b1d34 100644 --- a/SystemInformer/include/phapp.h +++ b/SystemInformer/include/phapp.h @@ -324,6 +324,13 @@ VOID PhShowProcessAffinityDialog( _In_opt_ PPH_THREAD_ITEM ThreadItem ); +// CPU Sets + +VOID PhShowCpuSetsDialogProcess( + _In_ HWND ParentWindowHandle, + _In_ PPH_PROCESS_ITEM ProcessItem + ); + // begin_phapppub _Success_(return) PHAPPAPI diff --git a/SystemInformer/include/sysinfop.h b/SystemInformer/include/sysinfop.h index 1f8c7073283e..8ad92825626e 100644 --- a/SystemInformer/include/sysinfop.h +++ b/SystemInformer/include/sysinfop.h @@ -346,10 +346,6 @@ NTSTATUS PhSipQueryProcessorPerformanceDistributionEx( _Out_ PVOID* Buffer ); -NTSTATUS PhSipQueryCpuSetInformation( - _Out_ PVOID* Buffer - ); - PCPH_STRINGREF PhGetHybridProcessorType( _In_ ULONG ProcessorIndex ); diff --git a/SystemInformer/mainwnd.c b/SystemInformer/mainwnd.c index 348e534bd025..2db7a009ea58 100644 --- a/SystemInformer/mainwnd.c +++ b/SystemInformer/mainwnd.c @@ -1664,6 +1664,18 @@ VOID PhMwpOnCommand( } } break; + case ID_PROCESS_CPU_SETS: + { + PPH_PROCESS_ITEM processItem = PhGetSelectedProcessItem(); + + if (processItem) + { + PhReferenceObject(processItem); + PhShowCpuSetsDialogProcess(WindowHandle, processItem); + PhDereferenceObject(processItem); + } + } + break; case ID_MISCELLANEOUS_ACTIVITY: { PPH_PROCESS_ITEM processItem = PhGetSelectedProcessItem(); diff --git a/SystemInformer/mwpgproc.c b/SystemInformer/mwpgproc.c index 016cb1ca2f7c..660a737ddd58 100644 --- a/SystemInformer/mwpgproc.c +++ b/SystemInformer/mwpgproc.c @@ -909,6 +909,9 @@ PPH_EMENU PhpCreateProcessMenu( PhInsertEMenuItem(menu, PhCreateEMenuItem(0, ID_PROCESS_DEBUG, L"De&bug", NULL, NULL), ULONG_MAX); PhInsertEMenuItem(menu, PhCreateEMenuSeparator(), ULONG_MAX); PhInsertEMenuItem(menu, PhCreateEMenuItem(0, ID_PROCESS_AFFINITY, L"&Affinity", NULL, NULL), ULONG_MAX); + if (WindowsVersion >= WINDOWS_10) { + PhInsertEMenuItem(menu, PhCreateEMenuItem(0, ID_PROCESS_CPU_SETS, L"&CPU Sets", NULL, NULL), ULONG_MAX); + } menuItem = PhCreateEMenuItem(0, ID_PROCESS_PRIORITY, L"&Priority", NULL, NULL); PhInsertEMenuItem(menuItem, PhCreateEMenuItem(0, ID_PRIORITY_REALTIME, L"&Real time", NULL, NULL), ULONG_MAX); diff --git a/SystemInformer/resource.h b/SystemInformer/resource.h index 03a2111b6d39..832356b600b2 100644 --- a/SystemInformer/resource.h +++ b/SystemInformer/resource.h @@ -130,6 +130,7 @@ #define IDD_CHOOSENEW 275 #define IDD_MEMSTRINGSMINLEN 276 #define IDD_MEMSTRINGS 277 +#define IDD_CPUSETS 278 #define IDC_TERMINATE 1003 #define IDC_FILEICON 1005 #define IDC_FILE 1006 @@ -571,33 +572,36 @@ #define IDC_FONTMONOSPACE 1416 #define IDC_ONLYKERNELTHREADSTACKS 1417 #define IDC_HYPERVISORNONESSENTIAL 1418 +#define IDC_LISTBOX1 1418 #define IDC_INPUT 1419 -#define IDC_MINIDUMP_NORMAL 1420 -#define IDC_MINIDUMP_WITH_DATA_SEGS 1421 -#define IDC_MINIDUMP_WITH_FULL_MEM 1422 -#define IDC_MINIDUMP_WITH_HANDLE_DATA 1423 -#define IDC_MINIDUMP_FILTER_MEMORY 1424 -#define IDC_MINIDUMP_SCAN_MEMORY 1425 -#define IDC_MINIDUMP_WITH_UNLOADED_MODULES 1426 -#define IDC_MINIDUMP_WITH_INDIRECT_REFERENCED_MEM 1427 -#define IDC_MINIDUMP_FILTER_MODULE_PATHS 1428 -#define IDC_MINIDUMP_WITH_PROC_THRD_DATA 1429 -#define IDC_MINIDUMP_WITH_PRIVATE_RW_MEM 1430 -#define IDC_MINIDUMP_WITHOUT_OPTIONAL_DATA 1431 -#define IDC_MINIDUMP_WITH_FULL_MEM_INFO 1432 -#define IDC_MINIDUMP_WITH_THRD_INFO 1433 -#define IDC_MINIDUMP_WITH_CODE_SEGS 1434 -#define IDC_MINIDUMP_WITHOUT_AUXILIARY_STATE 1435 -#define IDC_MINIDUMP_WITH_FULL_AUXILIARY_STATE 1436 -#define IDC_MINIDUMP_WITH_PRIVATE_WRITE_COPY_MEM 1437 -#define IDC_MINIDUMP_IGNORE_INACCESSIBLE_MEM 1438 -#define IDC_MINIDUMP_WITH_TOKEN_INFO 1439 -#define IDC_MINIDUMP_WITH_MODULE_HEADERS 1440 -#define IDC_MINIDUMP_FILTER_TRIAGE 1441 -#define IDC_MINIDUMP_WITH_AVXX_STATE_CONTEXT 1442 -#define IDC_MINIDUMP_WITH_IPT_TRACE 1443 +#define IDC_MINIDUMP_NORMAL 1420 +#define IDC_MINIDUMP_WITH_DATA_SEGS 1421 +#define IDC_CHECK1 1421 +#define IDC_MINIDUMP_WITH_FULL_MEM 1422 +#define IDC_MINIDUMP_WITH_HANDLE_DATA 1423 +#define IDC_MINIDUMP_FILTER_MEMORY 1424 +#define IDC_BUTTON3 1424 +#define IDC_MINIDUMP_SCAN_MEMORY 1425 +#define IDC_MINIDUMP_WITH_UNLOADED_MODULES 1426 +#define IDC_MINIDUMP_WITH_INDIRECT_REFERENCED_MEM 1427 +#define IDC_MINIDUMP_FILTER_MODULE_PATHS 1428 +#define IDC_MINIDUMP_WITH_PROC_THRD_DATA 1429 +#define IDC_MINIDUMP_WITH_PRIVATE_RW_MEM 1430 +#define IDC_MINIDUMP_WITHOUT_OPTIONAL_DATA 1431 +#define IDC_MINIDUMP_WITH_FULL_MEM_INFO 1432 +#define IDC_MINIDUMP_WITH_THRD_INFO 1433 +#define IDC_MINIDUMP_WITH_CODE_SEGS 1434 +#define IDC_MINIDUMP_WITHOUT_AUXILIARY_STATE 1435 +#define IDC_MINIDUMP_WITH_FULL_AUXILIARY_STATE 1436 +#define IDC_MINIDUMP_WITH_PRIVATE_WRITE_COPY_MEM 1437 +#define IDC_MINIDUMP_IGNORE_INACCESSIBLE_MEM 1438 +#define IDC_MINIDUMP_WITH_TOKEN_INFO 1439 +#define IDC_MINIDUMP_WITH_MODULE_HEADERS 1440 +#define IDC_MINIDUMP_FILTER_TRIAGE 1441 +#define IDC_MINIDUMP_WITH_AVXX_STATE_CONTEXT 1442 +#define IDC_MINIDUMP_WITH_IPT_TRACE 1443 #define IDC_MINIDUMP_SCAN_INACCESSIBLE_PARTIAL_PAGES 1444 -#define IDC_MINIDUMP_FILTER_WRITE_COMBINE_MEM 1445 +#define IDC_MINIDUMP_FILTER_WRITE_COMBINE_MEM 1445 #define IDC_ZMEMSLOTS_V 1446 #define IDC_ZMEMFORMFACTOR_V 1447 #define IDC_ZMEMTYPE_V 1448 @@ -858,16 +862,17 @@ #define ID_NOTIFICATIONS_ENABLESINGLECLICKICONS 40312 #define ID_MISCELLANEOUS_EXECUTIONREQUIRED 40313 #define IDD_OBJAFDSOCKET 40314 +#define ID_PROCESS_CPU_SETS 40315 #define IDDYNAMIC 50000 #define IDPLUGINS 55000 // Next default values for new objects -// +// #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 273 #define _APS_NEXT_COMMAND_VALUE 40298 -#define _APS_NEXT_CONTROL_VALUE 1418 +#define _APS_NEXT_CONTROL_VALUE 1425 #define _APS_NEXT_SYMED_VALUE 170 #endif #endif diff --git a/SystemInformer/syssccpu.c b/SystemInformer/syssccpu.c index 8b3bdfc16f6f..f02017f6c8cc 100644 --- a/SystemInformer/syssccpu.c +++ b/SystemInformer/syssccpu.c @@ -2032,12 +2032,14 @@ PCPH_STRINGREF PhGetHybridProcessorType( * * This function determines whether a specific logical processor in the system * is parked, meaning it is temporarily unavailable for the thread scheduler. - * It utilizes CPU sets functionality available starting with Windows 10. + * It uses CPU Sets functionality available starting with Windows 10. * * \param[in] ProcessorIndex The index of the logical processor to check. * * \return TRUE if the specified logical processor is parked, FALSE otherwise or * in case of failure. + * + * \todo Use PhEnumerateSystemCpuSets() */ BOOLEAN PhIsCoreParked( _In_ ULONG ProcessorIndex diff --git a/phlib/include/cpuset.h b/phlib/include/cpuset.h new file mode 100644 index 000000000000..0b5c600c00d8 --- /dev/null +++ b/phlib/include/cpuset.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025 Winsider Seminars & Solutions, Inc. All rights reserved. + * + * This file is part of System Informer ("SI"). + * + * Description: + * + * This module contains the public routine prototypes for CPU Sets in SI. + * + * Authors: + * + * wnstngs 2025 + * + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef BOOLEAN +(*PPH_CPU_SETS_ENUMERATION_CALLBACK)( + _In_ PSYSTEM_CPU_SET_INFORMATION Information, + _In_opt_ PVOID Context + ); + +NTSTATUS +PhQuerySystemCpuSetInformation( + _Out_ PVOID *Information + ); + +NTSTATUS +PhEnumerateSystemCpuSets( + _In_ PPH_CPU_SETS_ENUMERATION_CALLBACK Callback, + _In_opt_ PVOID Context + ); + +NTSTATUS +PhQueryCpuSetsProcess( + _In_ HANDLE ProcessHandle, + _Out_ PULONG *CpuSetIds, + _Out_ PULONG CpuSetIdCount + ); + +NTSTATUS +PhSetCpuSetsProcess( + _In_ HANDLE ProcessHandle, + _In_reads_opt_(CpuSetIdCount) const ULONG *CpuSetIds, + _In_ ULONG CpuSetIdCount + ); + +#ifdef __cplusplus +} +#endif diff --git a/phlib/phlib.vcxproj b/phlib/phlib.vcxproj index 8e888850a9cb..9145b52a6510 100644 --- a/phlib/phlib.vcxproj +++ b/phlib/phlib.vcxproj @@ -221,6 +221,7 @@ + From 72e26a8b0279fce78ad91051348af460c11060e1 Mon Sep 17 00:00:00 2001 From: wnstngs <64602598+wnstngs@users.noreply.github.com> Date: Thu, 5 Jun 2025 18:29:54 +0300 Subject: [PATCH 2/4] Address code review feedback - Remove AFX_DIALOG_LAYOUT - Update processor group retrieval to use PhSystemProcessorInformation.NumberOfProcessorGroups - Replace slow text parsing in listview with ID passed via listview item lParam - Add new helper PhGetSelectedListViewItemParams() - Use NTAPI in cpuset.h for 32-bit compatibility --- SystemInformer/SystemInformer.rc | 16 +--- SystemInformer/cpuset.c | 133 +++++++++---------------------- phlib/guisuplistview.cpp | 50 ++++++++++++ phlib/include/cpuset.h | 9 ++- phlib/include/guisup.h | 9 +++ phlib/phlib.vcxproj.filters | 3 + 6 files changed, 106 insertions(+), 114 deletions(-) diff --git a/SystemInformer/SystemInformer.rc b/SystemInformer/SystemInformer.rc index 343c6dc5e378..49641584b673 100644 --- a/SystemInformer/SystemInformer.rc +++ b/SystemInformer/SystemInformer.rc @@ -2253,7 +2253,6 @@ BEGIN VK_F11, IDC_MAXSCREEN, VIRTKEY, NOINVERT END - ///////////////////////////////////////////////////////////////////////////// // // Bitmap @@ -2263,18 +2262,5 @@ IDB_SEARCH_ACTIVE_BMP BITMAP "resources\\active_search.bmp" IDB_SEARCH_INACTIVE_BMP BITMAP "resources\\inactive_search.bmp" - -///////////////////////////////////////////////////////////////////////////// -// -// AFX_DIALOG_LAYOUT -// - -IDD_CPUSETS AFX_DIALOG_LAYOUT -BEGIN - 0 -END - #endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - +///////////////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/SystemInformer/cpuset.c b/SystemInformer/cpuset.c index 517d3ab79b24..2847ead529ca 100644 --- a/SystemInformer/cpuset.c +++ b/SystemInformer/cpuset.c @@ -109,7 +109,6 @@ typedef struct PHP_CPU_SETS_DLG_CTX { PH_CPU_SET_TYPE CpuSetType; PULONG CpuSetIds; ULONG CpuSetIdCount; - ULONG Index; } PHP_CPU_SETS_DLG_CTX, *PPHP_CPU_SETS_DLG_CTX; // @@ -444,7 +443,7 @@ PhQueryCpuSetsProcess( static USHORT groupCount; ULONG requiredCount; - groupCount = USER_SHARED_DATA->ActiveGroupCount; + groupCount = PhSystemProcessorInformation.NumberOfProcessorGroups; *CpuSetIds = NULL; *CpuSetIdCount = 0; @@ -533,7 +532,7 @@ PhSetCpuSetsProcess( static USHORT groupCount; PKAFFINITY masks; - groupCount = USER_SHARED_DATA->ActiveGroupCount; + groupCount = PhSystemProcessorInformation.NumberOfProcessorGroups; masks = PhAllocateZero(groupCount * sizeof(KAFFINITY)); if (!masks) { @@ -641,6 +640,10 @@ PhpPopulateCpuSetsListViewCallback( return FALSE; } + // + // Determine if this CPU Set should be marked as checked + // by searching for its ID in the provided ID array. + // if (context->CpuSetIds && context->CpuSetIdCount > 0) { #define BINARY_SEARCH_THRESHOLD 32 if (context->CpuSetIdCount > BINARY_SEARCH_THRESHOLD) { @@ -681,11 +684,17 @@ PhpPopulateCpuSetsListViewCallback( // contained by the listview control, the new item will be appended to // the end of the list and assigned the correct index" // + // Associate the CPU Set ID to the list item so that it can be retrieved + // in the selection handler + // lvItemIndex = PhAddListViewItem(context->ListViewHandle, MAXINT, number, - UlongToPtr(context->Index++)); + UlongToPtr(Information->CpuSet.Id)); + // + // Set the check state if this CPU Set is in the selection list + // if (isChecked) { assert(lvItemIndex >= 0); ListView_SetCheckState(context->ListViewHandle, lvItemIndex, TRUE) @@ -793,7 +802,6 @@ PhpPopulateListViewWithCpuSets( // // Enumerate all system CPU Sets and check selected // - DialogContext->Index = 0; status = PhEnumerateSystemCpuSets(PhpPopulateCpuSetsListViewCallback, DialogContext); } @@ -812,7 +820,6 @@ PhpPopulateListViewWithCpuSets( // status = PhQuerySystemCpuSetInformation(&cpuSetsInfo); if (NT_SUCCESS(status) && cpuSetsInfo) { - DialogContext->Index = 0; FOREACH_CPU_SET_INFO(cpuSetsInfo, DialogContext, PhpPopulateCpuSetsListViewCallback); @@ -832,51 +839,6 @@ PhpPopulateListViewWithCpuSets( return status; } -/** - * Attempts to parse a CPU Set ID from a null-terminated wide string. - * - * @param CpuSetIdText - * The input string to parse. - * - * @param AsUlong - * Receives the parsed unsigned long on success. - * - * @returns - * TRUE if the entire string is a valid non-negative integer; - * FALSE otherwise. - * - * @remark - * We know that every ListView item's text is exactly PhPrintUInt32(...): - * pure digits, no sign, no extra spaces. We use a specialized parser that - * simply walks the WCHAR* buffer once and stops on the first non-digit. - */ -static -BOOL -PhpTryParseCpuId( - _In_ LPCWSTR CpuSetIdText, - _Out_ PULONG AsUlong -) -{ - if (!CpuSetIdText || - *CpuSetIdText < L'0' || - *CpuSetIdText > L'9') { - return FALSE; - } - - ULONG v = 0; - WCHAR c; - for (; *CpuSetIdText; CpuSetIdText += 1) { - c = *CpuSetIdText; - if (c < L'0' || c > L'9') { - return FALSE; - } - v = v * 10 + (c - L'0'); - } - *AsUlong = v; - - return TRUE; -} - /** * Retrieves the selected CPU Set IDs from a ListView. * @@ -889,6 +851,8 @@ PhpTryParseCpuId( * * @param SelectedCount * Pointer to store the count of selected IDs. + * + * @return STATUS_SUCCESS if no errors, or the specific error status code. */ static NTSTATUS @@ -898,65 +862,43 @@ PhpGetSelectedCpuSetIds( _Out_ PULONG SelectedCount ) { - INT i; - INT itemCount = ListView_GetItemCount(ListViewHandle); + PVOID *paramArray; + PULONG ids; ULONG count; - WCHAR buffer[PH_INT32_STR_LEN_1]; - LVITEMW lv = {0}; - PULONG shrunk; + ULONG i; - if (itemCount <= 0) { + if (!PhGetCheckedListViewItemParams(ListViewHandle, + ¶mArray, + &count)) { *SelectedIds = NULL; *SelectedCount = 0; - return STATUS_SUCCESS; + return STATUS_NOT_FOUND; } - ULONG *ids = PhAllocate(sizeof(*ids) * itemCount); - if (!ids) { - return STATUS_INSUFFICIENT_RESOURCES; + // + // If no rows were checked, clean up and return empty. + // + if (count == 0) { + *SelectedIds = NULL; + *SelectedCount = 0; + return STATUS_SUCCESS; } - lv.mask = LVIF_TEXT; - lv.iSubItem = 0; - lv.pszText = buffer; - lv.cchTextMax = ARRAYSIZE(buffer); + ids = PhAllocate(sizeof(*ids) * count); - count = 0; - for (i = 0; i < itemCount; i += 1) { - if (!ListView_GetCheckState(ListViewHandle, i)) { - continue; - } - - // - // Retrieve the item text - // - lv.iItem = i; - if (SendMessageW(ListViewHandle, - LVM_GETITEMTEXTW, - (WPARAM)i, - (LPARAM)&lv) <= 0) { - continue; - } - - if (PhpTryParseCpuId(buffer, &ids[count])) { - count += 1; - } - } - - if (count == 0) { - PhFree(ids); + if (!ids) { + PhFree(paramArray); *SelectedIds = NULL; *SelectedCount = 0; - return STATUS_SUCCESS; + return STATUS_INSUFFICIENT_RESOURCES; } - if (count < (ULONG)itemCount) { - shrunk = PhReAllocate(ids, sizeof(*ids) * count); - if (shrunk) { - ids = shrunk; - } + for (i = 0; i < count; i += 1) { + ids[i] = (ULONG)(ULONG_PTR)paramArray[i]; } + PhFree(paramArray); + *SelectedIds = ids; *SelectedCount = count; @@ -1002,7 +944,6 @@ PhpProcessCpuSetsDlgProc( context->WindowHandle = DlgHandle; context->ListViewHandle = GetDlgItem(DlgHandle, IDC_LIST); - context->Index = 0; PhSetApplicationWindowIcon(DlgHandle); diff --git a/phlib/guisuplistview.cpp b/phlib/guisuplistview.cpp index c0b6ed0efd25..a354c9c63d01 100644 --- a/phlib/guisuplistview.cpp +++ b/phlib/guisuplistview.cpp @@ -697,6 +697,56 @@ VOID PhGetSelectedIListViewItemParams( *Items = static_cast(PhFinalArrayItems(&array)); } +/*! + * Retrieves the lParam of each checked item in a checkbox-enabled ListView. + * + * @param ListViewHandle + * A handle to the ListView control (must have checkboxes enabled). + * + * @param Items + * Receives a pointer to an array of PVOID values, each corresponding + * to the lParam of a checked item. + * + * @param NumberOfItems + * Receives the number of entries in @p Items. + * + * @return TRUE if the operation succeeded, FALSE otherwise. + */ +_Success_(return != FALSE) +BOOLEAN PhGetCheckedListViewItemParams( + _In_ HWND ListViewHandle, + _Out_ PVOID **Items, + _Out_ PULONG NumberOfItems + ) +{ + PH_ARRAY array; + INT totalItems, i; + PVOID param; + + if (!ListViewHandle || !Items || !NumberOfItems) + { + return FALSE; + } + + PhInitializeArray(&array, sizeof(PVOID), 4); + + totalItems = ListView_GetItemCount(ListViewHandle); + for (i = 0; i < totalItems; i += 1) + { + if (ListView_GetCheckState(ListViewHandle, i)) + { + if (PhGetListViewItemParam(ListViewHandle, i, ¶m)) + { + PhAddItemArray(&array, ¶m); + } + } + } + + *NumberOfItems = static_cast(PhFinalArrayCount(&array)); + *Items = static_cast(PhFinalArrayItems(&array)); + return TRUE; +} + BOOLEAN PhGetIListViewClientRect( _In_ IListView* ListView, _Inout_ PRECT ClientRect diff --git a/phlib/include/cpuset.h b/phlib/include/cpuset.h index 0b5c600c00d8..eb2330bb22ff 100644 --- a/phlib/include/cpuset.h +++ b/phlib/include/cpuset.h @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2025 Winsider Seminars & Solutions, Inc. All rights reserved. * * This file is part of System Informer ("SI"). @@ -19,24 +19,26 @@ extern "C" { #endif -typedef BOOLEAN -(*PPH_CPU_SETS_ENUMERATION_CALLBACK)( +typedef BOOLEAN (NTAPI *PPH_CPU_SETS_ENUMERATION_CALLBACK)( _In_ PSYSTEM_CPU_SET_INFORMATION Information, _In_opt_ PVOID Context ); NTSTATUS +NTAPI PhQuerySystemCpuSetInformation( _Out_ PVOID *Information ); NTSTATUS +NTAPI PhEnumerateSystemCpuSets( _In_ PPH_CPU_SETS_ENUMERATION_CALLBACK Callback, _In_opt_ PVOID Context ); NTSTATUS +NTAPI PhQueryCpuSetsProcess( _In_ HANDLE ProcessHandle, _Out_ PULONG *CpuSetIds, @@ -44,6 +46,7 @@ PhQueryCpuSetsProcess( ); NTSTATUS +NTAPI PhSetCpuSetsProcess( _In_ HANDLE ProcessHandle, _In_reads_opt_(CpuSetIdCount) const ULONG *CpuSetIds, diff --git a/phlib/include/guisup.h b/phlib/include/guisup.h index 040f032bf8b7..b2db87ee721d 100644 --- a/phlib/include/guisup.h +++ b/phlib/include/guisup.h @@ -959,6 +959,15 @@ PhGetSelectedIListViewItemParams( _Out_ PULONG NumberOfItems ); +PHLIBAPI +BOOLEAN +NTAPI +PhGetCheckedListViewItemParams( + _In_ HWND ListViewHandle, + _Out_ PVOID **Items, + _Out_ PULONG NumberOfItems + ); + PHLIBAPI BOOLEAN NTAPI diff --git a/phlib/phlib.vcxproj.filters b/phlib/phlib.vcxproj.filters index e5b141a9a20a..e4a6e1522bd8 100644 --- a/phlib/phlib.vcxproj.filters +++ b/phlib/phlib.vcxproj.filters @@ -439,5 +439,8 @@ Header Files + + Header Files + \ No newline at end of file From 83282552ecec741b755bbcb16982d0300da8a6e1 Mon Sep 17 00:00:00 2001 From: dmex Date: Fri, 10 Apr 2026 00:00:24 +1000 Subject: [PATCH 3/4] Update resource.h --- SystemInformer/resource.h | 1 - 1 file changed, 1 deletion(-) diff --git a/SystemInformer/resource.h b/SystemInformer/resource.h index d02a22b1ae7c..758be96fc604 100644 --- a/SystemInformer/resource.h +++ b/SystemInformer/resource.h @@ -578,7 +578,6 @@ #define IDC_FONTMONOSPACE 1416 #define IDC_ONLYKERNELTHREADSTACKS 1417 #define IDC_HYPERVISORNONESSENTIAL 1418 -#define IDC_LISTBOX1 1418 #define IDC_INPUT 1419 #define IDC_MINIDUMP_NORMAL 1420 #define IDC_MINIDUMP_WITH_DATA_SEGS 1421 From 2e958f80f8ef3e2fed8ea514c8cb08071e14559d Mon Sep 17 00:00:00 2001 From: dmex Date: Fri, 10 Apr 2026 00:58:45 +1000 Subject: [PATCH 4/4] Update resource.h --- SystemInformer/resource.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SystemInformer/resource.h b/SystemInformer/resource.h index 758be96fc604..cd02d46ba82b 100644 --- a/SystemInformer/resource.h +++ b/SystemInformer/resource.h @@ -874,11 +874,11 @@ #define ID_MISCELLANEOUS_EXECUTIONREQUIRED 40313 #define IDD_OBJAFDSOCKET 40314 #define ID_PROCESS_CPU_SETS 40315 -#define ID_TOOLS_COM_ACCESS_PERMISSIONS 40315 -#define ID_TOOLS_COM_ACCESS_RESTRICTIONS 40316 -#define ID_TOOLS_COM_LAUNCH_PERMISSIONS 40317 -#define ID_TOOLS_COM_LAUNCH_RESTRICTIONS 40318 -#define ID_TOOLS_INFORMER 40319 +#define ID_TOOLS_COM_ACCESS_PERMISSIONS 40316 +#define ID_TOOLS_COM_ACCESS_RESTRICTIONS 40317 +#define ID_TOOLS_COM_LAUNCH_PERMISSIONS 40318 +#define ID_TOOLS_COM_LAUNCH_RESTRICTIONS 40319 +#define ID_TOOLS_INFORMER 40320 #define IDDYNAMIC 50000 #define IDPLUGINS 55000