1 /*
2 * Copyright 2020-2022 The OSHI Project Contributors
3 * SPDX-License-Identifier: MIT
4 */
5 package oshi.driver.windows.registry;
6
7 import java.util.ArrayList;
8 import java.util.Collections;
9 import java.util.EnumMap;
10 import java.util.HashMap;
11 import java.util.List;
12 import java.util.Map;
13
14 import org.slf4j.Logger;
15 import org.slf4j.LoggerFactory;
16
17 import com.sun.jna.Memory;
18 import com.sun.jna.platform.win32.Advapi32;
19 import com.sun.jna.platform.win32.Advapi32Util;
20 import com.sun.jna.platform.win32.Win32Exception;
21 import com.sun.jna.platform.win32.WinBase.FILETIME;
22 import com.sun.jna.platform.win32.WinError;
23 import com.sun.jna.platform.win32.WinPerf.PERF_COUNTER_BLOCK;
24 import com.sun.jna.platform.win32.WinPerf.PERF_COUNTER_DEFINITION;
25 import com.sun.jna.platform.win32.WinPerf.PERF_DATA_BLOCK;
26 import com.sun.jna.platform.win32.WinPerf.PERF_INSTANCE_DEFINITION;
27 import com.sun.jna.platform.win32.WinPerf.PERF_OBJECT_TYPE;
28 import com.sun.jna.platform.win32.WinReg;
29
30 import oshi.annotation.SuppressForbidden;
31 import oshi.annotation.concurrent.ThreadSafe;
32 import oshi.jna.ByRef.CloseableIntByReference;
33 import oshi.util.platform.windows.PerfCounterWildcardQuery.PdhCounterWildcardProperty;
34 import oshi.util.tuples.Pair;
35 import oshi.util.tuples.Triplet;
36
37 /**
38 * Utility to read HKEY_PERFORMANCE_DATA information.
39 */
40 @ThreadSafe
41 public final class HkeyPerformanceDataUtil {
42
43 private static final Logger LOG = LoggerFactory.getLogger(HkeyPerformanceDataUtil.class);
44
45 /*
46 * Do a one-time lookup of the HKEY_PERFORMANCE_TEXT counter indices and store in a map for efficient lookups
47 * on-demand.
48 */
49 private static final String HKEY_PERFORMANCE_TEXT = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Perflib\\009";
50 private static final String COUNTER = "Counter";
51 private static final Map<String, Integer> COUNTER_INDEX_MAP = mapCounterIndicesFromRegistry();
52
53 private static int maxPerfBufferSize = 16384;
54
55 private HkeyPerformanceDataUtil() {
56 }
57
58 /**
59 * Reads and parses a block of performance data from the registry.
60 *
61 * @param <T> PDH Counters use an Enum to identify the fields to query in either the counter or WMI backup,
62 * and use the enum values as keys to retrieve the results.
63 * @param objectName The counter object for which to fetch data
64 * @param counterEnum Which counters to return data for
65 * @return A triplet containing the results. The first element maps the input enum to the counter values where the
66 * first enum will contain the instance name as a {@link String}, and the remaining values will either be
67 * {@link Long}, {@link Integer}, or {@code null} depending on whether the specified enum counter was
68 * present and the size of the counter value. The second element is a timestamp in 100nSec increments
69 * (Windows 1601 Epoch) while the third element is a timestamp in milliseconds since the 1970 Epoch.
70 */
71 public static <T extends Enum<T> & PdhCounterWildcardProperty> Triplet<List<Map<T, Object>>, Long, Long> readPerfDataFromRegistry(
72 String objectName, Class<T> counterEnum) {
73 // Load indices
74 // e.g., call with "Process" and ProcessPerformanceProperty.class
75 Pair<Integer, EnumMap<T, Integer>> indices = getCounterIndices(objectName, counterEnum);
76 if (indices == null) {
77 return null;
78 }
79 // The above test checks validity of objectName as an index but it could still
80 // fail to read
81 try (Memory pPerfData = readPerfDataBuffer(objectName)) {
82 if (pPerfData == null) {
83 return null;
84 }
85 // Buffer is now successfully populated.
86 // See format at
87 // https://msdn.microsoft.com/en-us/library/windows/desktop/aa373105(v=vs.85).aspx
88
89 // Start with a data header (PERF_DATA_BLOCK)
90 // Then iterate one or more objects
91 // Each object contains
92 // [ ] Object Type header (PERF_OBJECT_TYPE)
93 // [ ][ ][ ] Multiple counter definitions (PERF_COUNTER_DEFINITION)
94 // Then after object(s), multiple:
95 // [ ] Instance Definition
96 // [ ] Instance name
97 // [ ] Counter Block
98 // [ ][ ][ ] Counter data for each definition above
99
100 // Store timestamp
101 PERF_DATA_BLOCK perfData = new PERF_DATA_BLOCK(pPerfData.share(0));
102 long perfTime100nSec = perfData.PerfTime100nSec.getValue(); // 1601
103 long now = FILETIME.filetimeToDate((int) (perfTime100nSec >> 32), (int) (perfTime100nSec & 0xffffffffL))
104 .getTime(); // 1970
105
106 // Iterate object types.
107 long perfObjectOffset = perfData.HeaderLength;
108 for (int obj = 0; obj < perfData.NumObjectTypes; obj++) {
109 PERF_OBJECT_TYPE perfObject = new PERF_OBJECT_TYPE(pPerfData.share(perfObjectOffset));
110 // Some counters will require multiple objects so we iterate until we find the
111 // right one. e.g. Process (230) is by itself but Thread (232) has Process
112 // object first
113 if (perfObject.ObjectNameTitleIndex == COUNTER_INDEX_MAP.get(objectName).intValue()) {
114 // We found a matching object.
115
116 // Counter definitions start after the object header
117 long perfCounterOffset = perfObjectOffset + perfObject.HeaderLength;
118 // Iterate counter definitions and fill maps with counter offsets and sizes
119 Map<Integer, Integer> counterOffsetMap = new HashMap<>();
120 Map<Integer, Integer> counterSizeMap = new HashMap<>();
121 for (int counter = 0; counter < perfObject.NumCounters; counter++) {
122 PERF_COUNTER_DEFINITION perfCounter = new PERF_COUNTER_DEFINITION(
123 pPerfData.share(perfCounterOffset));
124 counterOffsetMap.put(perfCounter.CounterNameTitleIndex, perfCounter.CounterOffset);
125 counterSizeMap.put(perfCounter.CounterNameTitleIndex, perfCounter.CounterSize);
126 // Increment for next Counter
127 perfCounterOffset += perfCounter.ByteLength;
128 }
129
130 // Instances start after all the object definitions. The DefinitionLength
131 // includes both the header and all the definitions.
132 long perfInstanceOffset = perfObjectOffset + perfObject.DefinitionLength;
133
134 // Iterate instances and fill map
135 List<Map<T, Object>> counterMaps = new ArrayList<>(perfObject.NumInstances);
136 for (int inst = 0; inst < perfObject.NumInstances; inst++) {
137 PERF_INSTANCE_DEFINITION perfInstance = new PERF_INSTANCE_DEFINITION(
138 pPerfData.share(perfInstanceOffset));
139 long perfCounterBlockOffset = perfInstanceOffset + perfInstance.ByteLength;
140 // Populate the enumMap
141 Map<T, Object> counterMap = new EnumMap<>(counterEnum);
142 T[] counterKeys = counterEnum.getEnumConstants();
143 // First enum index is the name, ignore the counter text which is used for other
144 // purposes
145 counterMap.put(counterKeys[0],
146 pPerfData.getWideString(perfInstanceOffset + perfInstance.NameOffset));
147 for (int i = 1; i < counterKeys.length; i++) {
148 T key = counterKeys[i];
149 int keyIndex = COUNTER_INDEX_MAP.get(key.getCounter());
150 // All entries in size map have corresponding entry in offset map
151 int size = counterSizeMap.getOrDefault(keyIndex, 0);
152 // Currently, only DWORDs (4 bytes) and ULONGLONGs (8 bytes) are used to provide
153 // counter values.
154 if (size == 4) {
155 counterMap.put(key,
156 pPerfData.getInt(perfCounterBlockOffset + counterOffsetMap.get(keyIndex)));
157 } else if (size == 8) {
158 counterMap.put(key,
159 pPerfData.getLong(perfCounterBlockOffset + counterOffsetMap.get(keyIndex)));
160 } else {
161 // If counter defined in enum isn't in registry, fail
162 return null;
163 }
164 }
165 counterMaps.add(counterMap);
166
167 // counters at perfCounterBlockOffset + appropriate offset per enum
168 // use pPerfData.getInt or getLong as determined by counter size
169 // Currently, only DWORDs (4 bytes) and ULONGLONGs (8 bytes) are used to provide
170 // counter values.
171
172 // Increment to next instance
173 perfInstanceOffset = perfCounterBlockOffset
174 + new PERF_COUNTER_BLOCK(pPerfData.share(perfCounterBlockOffset)).ByteLength;
175 }
176 // We've found the necessary object and are done, no need to look at any other
177 // objects (shouldn't be any). Return results
178 return new Triplet<>(counterMaps, perfTime100nSec, now);
179 }
180 // Increment for next object
181 perfObjectOffset += perfObject.TotalByteLength;
182 }
183 }
184 // Failed, return null
185 return null;
186 }
187
188 /**
189 * Looks up the counter index values for the given counter object and the enum of counter names.
190 *
191 * @param <T> An enum containing the counters, whose class is passed as {@code counterEnum}
192 * @param objectName The counter object to look up the index for
193 * @param counterEnum The {@link Enum} containing counters to look up the indices for. The first Enum value will be
194 * ignored.
195 * @return A {@link Pair} containing the index of the counter object as the first element, and an {@link EnumMap}
196 * mapping counter enum values to their index as the second element, if the lookup is successful; null
197 * otherwise.
198 */
199 private static <T extends Enum<T> & PdhCounterWildcardProperty> Pair<Integer, EnumMap<T, Integer>> getCounterIndices(
200 String objectName, Class<T> counterEnum) {
201 if (!COUNTER_INDEX_MAP.containsKey(objectName)) {
202 LOG.debug("Couldn't find counter index of {}.", objectName);
203 return null;
204 }
205 int counterIndex = COUNTER_INDEX_MAP.get(objectName);
206 T[] enumConstants = counterEnum.getEnumConstants();
207 EnumMap<T, Integer> indexMap = new EnumMap<>(counterEnum);
208 // Start iterating at 1 because first Enum value defines the name/instance and
209 // is not a counter name
210 for (int i = 1; i < enumConstants.length; i++) {
211 T key = enumConstants[i];
212 String counterName = key.getCounter();
213 if (!COUNTER_INDEX_MAP.containsKey(counterName)) {
214 LOG.debug("Couldn't find counter index of {}.", counterName);
215 return null;
216 }
217 indexMap.put(key, COUNTER_INDEX_MAP.get(counterName));
218 }
219 // We have all the pieces! Return them.
220 return new Pair<>(counterIndex, indexMap);
221 }
222
223 /**
224 * Read the performance data for a counter object from the registry.
225 *
226 * @param objectName The counter object for which to fetch data. It is the user's responsibility to ensure this key
227 * exists in {@link #COUNTER_INDEX_MAP}.
228 * @return A buffer containing the data if successful, null otherwise.
229 */
230 private static synchronized Memory readPerfDataBuffer(String objectName) {
231 // Need this index as a string
232 String objectIndexStr = Integer.toString(COUNTER_INDEX_MAP.get(objectName));
233
234 // Now load the data from the regsitry.
235
236 try (CloseableIntByReference lpcbData = new CloseableIntByReference(maxPerfBufferSize)) {
237 Memory pPerfData = new Memory(maxPerfBufferSize);
238 int ret = Advapi32.INSTANCE.RegQueryValueEx(WinReg.HKEY_PERFORMANCE_DATA, objectIndexStr, 0, null,
239 pPerfData, lpcbData);
240 if (ret != WinError.ERROR_SUCCESS && ret != WinError.ERROR_MORE_DATA) {
241 LOG.error("Error reading performance data from registry for {}.", objectName);
242 pPerfData.close();
243 return null;
244 }
245 // Grow buffer as needed to fit the data
246 while (ret == WinError.ERROR_MORE_DATA) {
247 maxPerfBufferSize += 8192;
248 lpcbData.setValue(maxPerfBufferSize);
249 pPerfData.close();
250 pPerfData = new Memory(maxPerfBufferSize);
251 ret = Advapi32.INSTANCE.RegQueryValueEx(WinReg.HKEY_PERFORMANCE_DATA, objectIndexStr, 0, null,
252 pPerfData, lpcbData);
253 }
254 return pPerfData;
255 }
256 }
257
258 /*
259 * Registry entries subordinate to HKEY_PERFORMANCE_TEXT key reference the text strings that describe counters in US
260 * English. Not supported in Windows 2000.
261 *
262 * With the "Counter" value, the resulting array contains alternating index/name pairs "1", "1847", "2", "System",
263 * "4", "Memory", ...
264 *
265 * These pairs are translated to a map for later lookup.
266 *
267 * @return An unmodifiable map containing counter name strings as keys and indices as integer values if the key is
268 * read successfully; an empty map otherwise.
269 */
270 @SuppressForbidden(reason = "Catching the error here")
271 private static Map<String, Integer> mapCounterIndicesFromRegistry() {
272 HashMap<String, Integer> indexMap = new HashMap<>();
273 try {
274 String[] counterText = Advapi32Util.registryGetStringArray(WinReg.HKEY_LOCAL_MACHINE, HKEY_PERFORMANCE_TEXT,
275 COUNTER);
276 for (int i = 1; i < counterText.length; i += 2) {
277 indexMap.putIfAbsent(counterText[i], Integer.parseInt(counterText[i - 1]));
278 }
279 } catch (Win32Exception we) {
280 LOG.error(
281 "Unable to locate English counter names in registry Perflib 009. Counters may need to be rebuilt: ",
282 we);
283 } catch (NumberFormatException nfe) {
284 // Unexpected to ever get this, but handling it anyway
285 LOG.error("Unable to parse English counter names in registry Perflib 009.");
286 }
287 return Collections.unmodifiableMap(indexMap);
288 }
289 }