View Javadoc
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 }