diff --git a/SystemInformer/SystemInformer.rc b/SystemInformer/SystemInformer.rc index 08a52beb6e47..0f1b510f1c7f 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 @@ -622,6 +622,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 @@ -1873,6 +1888,11 @@ BEGIN BOTTOMMARGIN, 220 END + IDD_CPUSETS, DIALOG + BEGIN + BOTTOMMARGIN, 4 + END + IDD_SYSINFO, DIALOG BEGIN END @@ -2326,6 +2346,4 @@ IDB_SEARCH_ACTIVE_BMP BITMAP "resources\\active_search.bmp" IDB_SEARCH_INACTIVE_BMP BITMAP "resources\\inactive_search.bmp" #endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - +///////////////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/SystemInformer/SystemInformer.vcxproj b/SystemInformer/SystemInformer.vcxproj index 6caab1c3b69e..c1e774274949 100644 --- a/SystemInformer/SystemInformer.vcxproj +++ b/SystemInformer/SystemInformer.vcxproj @@ -231,6 +231,7 @@ + diff --git a/SystemInformer/SystemInformer.vcxproj.filters b/SystemInformer/SystemInformer.vcxproj.filters index b2d852463fcc..989d9895c75a 100644 --- a/SystemInformer/SystemInformer.vcxproj.filters +++ b/SystemInformer/SystemInformer.vcxproj.filters @@ -342,6 +342,9 @@ System Informer + + System Informer + System Informer diff --git a/SystemInformer/cpuset.c b/SystemInformer/cpuset.c new file mode 100644 index 000000000000..2847ead529ca --- /dev/null +++ b/SystemInformer/cpuset.c @@ -0,0 +1,1197 @@ +/* + * 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; +} 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 = PhSystemProcessorInformation.NumberOfProcessorGroups; + + *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 = PhSystemProcessorInformation.NumberOfProcessorGroups; + + 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; + } + + // + // 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) { + 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" + // + // 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(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) + } + + // + // 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 + // + 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) { + FOREACH_CPU_SET_INFO(cpuSetsInfo, + DialogContext, + PhpPopulateCpuSetsListViewCallback); + PhFree(cpuSetsInfo); + } + + if (cpuSetIds) { + PhFree(cpuSetIds); + } + } + + // + // Bring back redrawing + // + ExtendedListView_SetRedraw(ListViewHandle, TRUE); + + return status; +} + +/** + * 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. + * + * @return STATUS_SUCCESS if no errors, or the specific error status code. + */ +static +NTSTATUS +PhpGetSelectedCpuSetIds( + _In_ HWND ListViewHandle, + _Out_ PULONG *SelectedIds, + _Out_ PULONG SelectedCount +) +{ + PVOID *paramArray; + PULONG ids; + ULONG count; + ULONG i; + + if (!PhGetCheckedListViewItemParams(ListViewHandle, + ¶mArray, + &count)) { + *SelectedIds = NULL; + *SelectedCount = 0; + return STATUS_NOT_FOUND; + } + + // + // If no rows were checked, clean up and return empty. + // + if (count == 0) { + *SelectedIds = NULL; + *SelectedCount = 0; + return STATUS_SUCCESS; + } + + ids = PhAllocate(sizeof(*ids) * count); + + if (!ids) { + PhFree(paramArray); + *SelectedIds = NULL; + *SelectedCount = 0; + return STATUS_INSUFFICIENT_RESOURCES; + } + + for (i = 0; i < count; i += 1) { + ids[i] = (ULONG)(ULONG_PTR)paramArray[i]; + } + + PhFree(paramArray); + + *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); + + 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 03ced885d8f6..3baeff38a565 100644 --- a/SystemInformer/include/phapp.h +++ b/SystemInformer/include/phapp.h @@ -336,6 +336,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 301f82cbfeb8..f372aac3657f 100644 --- a/SystemInformer/include/sysinfop.h +++ b/SystemInformer/include/sysinfop.h @@ -358,6 +358,15 @@ BOOLEAN PhSipGetCpuFrequencyFromDistribution( _Out_ DOUBLE *Frequency ); +NTSTATUS PhSipQueryProcessorPerformanceDistribution( + _Out_ PVOID *Buffer + ); + +NTSTATUS PhSipQueryProcessorPerformanceDistributionEx( + _In_ USHORT ProcessorGroup, + _Out_ PVOID* Buffer + ); + PCPH_STRINGREF PhGetHybridProcessorType( _In_ ULONG ProcessorIndex ); diff --git a/SystemInformer/mainwnd.c b/SystemInformer/mainwnd.c index 749df517d04c..e07e7742921d 100644 --- a/SystemInformer/mainwnd.c +++ b/SystemInformer/mainwnd.c @@ -2049,6 +2049,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 44c1e3ef15e1..9a92ef7706c1 100644 --- a/SystemInformer/mwpgproc.c +++ b/SystemInformer/mwpgproc.c @@ -924,6 +924,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_PRIORITYCLASS, 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 7aaa9b22b6b6..cd02d46ba82b 100644 --- a/SystemInformer/resource.h +++ b/SystemInformer/resource.h @@ -132,6 +132,7 @@ #define IDD_CHOOSENEW 275 #define IDD_MEMSTRINGSMINLEN 276 #define IDD_MEMSTRINGS 277 +#define IDD_CPUSETS 278 #define IDD_HNDLAUDITING 278 #define IDD_HNDLSECAUDIT 278 #define IDD_INFORMER 279 @@ -872,11 +873,12 @@ #define ID_NOTIFICATIONS_ENABLESINGLECLICKICONS 40312 #define ID_MISCELLANEOUS_EXECUTIONREQUIRED 40313 #define IDD_OBJAFDSOCKET 40314 -#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_PROCESS_CPU_SETS 40315 +#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 diff --git a/SystemInformer/syssccpu.c b/SystemInformer/syssccpu.c index febecf06ec1c..957e3e19d416 100644 --- a/SystemInformer/syssccpu.c +++ b/SystemInformer/syssccpu.c @@ -2084,12 +2084,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/guisuplistview.cpp b/phlib/guisuplistview.cpp index 9868d656047f..0309fc38f84e 100644 --- a/phlib/guisuplistview.cpp +++ b/phlib/guisuplistview.cpp @@ -654,6 +654,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 new file mode 100644 index 000000000000..eb2330bb22ff --- /dev/null +++ b/phlib/include/cpuset.h @@ -0,0 +1,58 @@ +/* + * 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 (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, + _Out_ PULONG CpuSetIdCount + ); + +NTSTATUS +NTAPI +PhSetCpuSetsProcess( + _In_ HANDLE ProcessHandle, + _In_reads_opt_(CpuSetIdCount) const ULONG *CpuSetIds, + _In_ ULONG CpuSetIdCount + ); + +#ifdef __cplusplus +} +#endif diff --git a/phlib/include/guisup.h b/phlib/include/guisup.h index 8420098b3573..dc7026b35d6b 100644 --- a/phlib/include/guisup.h +++ b/phlib/include/guisup.h @@ -1281,6 +1281,42 @@ PhGetSelectedListViewItemParams( _Out_ PULONG NumberOfItems ); +PHLIBAPI +VOID +NTAPI +PhGetSelectedIListViewItemParams( + _In_ IListView* ListView, + _Out_ PVOID** Items, + _Out_ PULONG NumberOfItems + ); + +PHLIBAPI +BOOLEAN +NTAPI +PhGetCheckedListViewItemParams( + _In_ HWND ListViewHandle, + _Out_ PVOID **Items, + _Out_ PULONG NumberOfItems + ); + +PHLIBAPI +BOOLEAN +NTAPI +PhGetIListViewClientRect( + _In_ IListView* ListView, + _Inout_ PRECT ClientRect + ); + +PHLIBAPI +BOOLEAN +NTAPI +PhGetIListViewItemRect( + _In_ IListView* ListView, + _In_ LONG StartIndex, + _In_ ULONG Flags, + _Inout_ PRECT ItemRect + ); + FORCEINLINE IListView* NTAPI diff --git a/phlib/phlib.vcxproj b/phlib/phlib.vcxproj index ad815701d38b..f55a947f8fb4 100644 --- a/phlib/phlib.vcxproj +++ b/phlib/phlib.vcxproj @@ -230,6 +230,7 @@ + diff --git a/phlib/phlib.vcxproj.filters b/phlib/phlib.vcxproj.filters index 070d9e6f4343..159a23e0db48 100644 --- a/phlib/phlib.vcxproj.filters +++ b/phlib/phlib.vcxproj.filters @@ -466,5 +466,8 @@ Header Files + + Header Files + \ No newline at end of file