View Javadoc
1   /*
2    * Copyright 2020-2023 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.hardware.platform.windows;
6   
7   import java.util.ArrayList;
8   import java.util.Collections;
9   import java.util.Comparator;
10  import java.util.HashMap;
11  import java.util.List;
12  import java.util.Locale;
13  import java.util.Map;
14  import java.util.Objects;
15  import java.util.regex.Matcher;
16  import java.util.regex.Pattern;
17  import java.util.stream.Collectors;
18  
19  import org.slf4j.Logger;
20  import org.slf4j.LoggerFactory;
21  
22  import com.sun.jna.platform.win32.Kernel32;
23  import com.sun.jna.platform.win32.COM.COMException;
24  import com.sun.jna.platform.win32.COM.WbemcliUtil.WmiResult;
25  
26  import oshi.annotation.concurrent.ThreadSafe;
27  import oshi.driver.windows.perfmon.PhysicalDisk;
28  import oshi.driver.windows.perfmon.PhysicalDisk.PhysicalDiskProperty;
29  import oshi.driver.windows.wmi.Win32DiskDrive;
30  import oshi.driver.windows.wmi.Win32DiskDrive.DiskDriveProperty;
31  import oshi.driver.windows.wmi.Win32DiskDriveToDiskPartition;
32  import oshi.driver.windows.wmi.Win32DiskDriveToDiskPartition.DriveToPartitionProperty;
33  import oshi.driver.windows.wmi.Win32DiskPartition;
34  import oshi.driver.windows.wmi.Win32DiskPartition.DiskPartitionProperty;
35  import oshi.driver.windows.wmi.Win32LogicalDiskToPartition;
36  import oshi.driver.windows.wmi.Win32LogicalDiskToPartition.DiskToPartitionProperty;
37  import oshi.hardware.HWDiskStore;
38  import oshi.hardware.HWPartition;
39  import oshi.hardware.common.AbstractHWDiskStore;
40  import oshi.util.ParseUtil;
41  import oshi.util.platform.windows.WmiQueryHandler;
42  import oshi.util.platform.windows.WmiUtil;
43  import oshi.util.tuples.Pair;
44  
45  /**
46   * Windows hard disk implementation.
47   */
48  @ThreadSafe
49  public final class WindowsHWDiskStore extends AbstractHWDiskStore {
50  
51      private static final Logger LOG = LoggerFactory.getLogger(WindowsHWDiskStore.class);
52  
53      private static final String PHYSICALDRIVE_PREFIX = "\\\\.\\PHYSICALDRIVE";
54      private static final Pattern DEVICE_ID = Pattern.compile(".*\\.DeviceID=\"(.*)\"");
55  
56      // A reasonable size for the buffer to accommodate the largest possible volume
57      // GUID path is 50 characters.
58      private static final int GUID_BUFSIZE = 100;
59  
60      private long reads = 0L;
61      private long readBytes = 0L;
62      private long writes = 0L;
63      private long writeBytes = 0L;
64      private long currentQueueLength = 0L;
65      private long transferTime = 0L;
66      private long timeStamp = 0L;
67      private List<HWPartition> partitionList;
68  
69      private WindowsHWDiskStore(String name, String model, String serial, long size) {
70          super(name, model, serial, size);
71      }
72  
73      @Override
74      public long getReads() {
75          return reads;
76      }
77  
78      @Override
79      public long getReadBytes() {
80          return readBytes;
81      }
82  
83      @Override
84      public long getWrites() {
85          return writes;
86      }
87  
88      @Override
89      public long getWriteBytes() {
90          return writeBytes;
91      }
92  
93      @Override
94      public long getCurrentQueueLength() {
95          return currentQueueLength;
96      }
97  
98      @Override
99      public long getTransferTime() {
100         return transferTime;
101     }
102 
103     @Override
104     public long getTimeStamp() {
105         return timeStamp;
106     }
107 
108     @Override
109     public List<HWPartition> getPartitions() {
110         return this.partitionList;
111     }
112 
113     @Override
114     public boolean updateAttributes() {
115         String index = null;
116         List<HWPartition> partitions = getPartitions();
117         if (!partitions.isEmpty()) {
118             // If a partition exists on this drive, the major property
119             // corresponds to the disk index, so use it.
120             index = Integer.toString(partitions.get(0).getMajor());
121         } else if (getName().startsWith(PHYSICALDRIVE_PREFIX)) {
122             // If no partition exists, Windows reliably uses a name to match the
123             // disk index. That said, the skeptical person might wonder why a
124             // disk has read/write statistics without a partition, and wonder
125             // why this branch is even relevant as an option. The author of this
126             // comment does not have an answer for this valid question.
127             index = getName().substring(PHYSICALDRIVE_PREFIX.length(), getName().length());
128         } else {
129             // The author of this comment cannot fathom a circumstance in which
130             // the code reaches this point, but just in case it does, here's the
131             // correct response. If you get this log warning, the circumstances
132             // would be of great interest to the project's maintainers.
133             LOG.warn("Couldn't match index for {}", getName());
134             return false;
135         }
136         DiskStats stats = queryReadWriteStats(index);
137         if (stats.readMap.containsKey(index)) {
138             this.reads = stats.readMap.getOrDefault(index, 0L);
139             this.readBytes = stats.readByteMap.getOrDefault(index, 0L);
140             this.writes = stats.writeMap.getOrDefault(index, 0L);
141             this.writeBytes = stats.writeByteMap.getOrDefault(index, 0L);
142             this.currentQueueLength = stats.queueLengthMap.getOrDefault(index, 0L);
143             this.transferTime = stats.diskTimeMap.getOrDefault(index, 0L);
144             this.timeStamp = stats.timeStamp;
145             return true;
146         } else {
147             return false;
148         }
149     }
150 
151     /**
152      * Gets the disks on this machine
153      *
154      * @return a list of {@link HWDiskStore} objects representing the disks
155      */
156     public static List<HWDiskStore> getDisks() {
157         WmiQueryHandler h = Objects.requireNonNull(WmiQueryHandler.createInstance());
158         boolean comInit = false;
159         try {
160             comInit = h.initCOM();
161             List<HWDiskStore> result;
162             result = new ArrayList<>();
163             DiskStats stats = queryReadWriteStats(null);
164             PartitionMaps maps = queryPartitionMaps(h);
165 
166             WmiResult<DiskDriveProperty> vals = Win32DiskDrive.queryDiskDrive(h);
167             for (int i = 0; i < vals.getResultCount(); i++) {
168                 WindowsHWDiskStore ds = new WindowsHWDiskStore(WmiUtil.getString(vals, DiskDriveProperty.NAME, i),
169                         String.format(Locale.ROOT, "%s %s", WmiUtil.getString(vals, DiskDriveProperty.MODEL, i),
170                                 WmiUtil.getString(vals, DiskDriveProperty.MANUFACTURER, i)).trim(),
171                         // Most vendors store serial # as a hex string; convert
172                         ParseUtil.hexStringToString(WmiUtil.getString(vals, DiskDriveProperty.SERIALNUMBER, i)),
173                         WmiUtil.getUint64(vals, DiskDriveProperty.SIZE, i));
174 
175                 String index = Integer.toString(WmiUtil.getUint32(vals, DiskDriveProperty.INDEX, i));
176                 ds.reads = stats.readMap.getOrDefault(index, 0L);
177                 ds.readBytes = stats.readByteMap.getOrDefault(index, 0L);
178                 ds.writes = stats.writeMap.getOrDefault(index, 0L);
179                 ds.writeBytes = stats.writeByteMap.getOrDefault(index, 0L);
180                 ds.currentQueueLength = stats.queueLengthMap.getOrDefault(index, 0L);
181                 // DiskTime (sum of readTime+writeTime) slightly overestimates actual transfer
182                 // time because it includes waiting time in the queue and can exceed 100%.
183                 // However, alternative calculations require use of a timestamp with 1/64-second
184                 // resolution producing unacceptable variation in what should be a monotonically
185                 // increasing counter. See extended discussion and experiments here:
186                 // https://github.com/oshi/oshi/issues/1504
187                 ds.transferTime = stats.diskTimeMap.getOrDefault(index, 0L);
188                 ds.timeStamp = stats.timeStamp;
189                 // Get partitions
190                 List<HWPartition> partitions = new ArrayList<>();
191                 List<String> partList = maps.driveToPartitionMap.get(ds.getName());
192                 if (partList != null && !partList.isEmpty()) {
193                     for (String part : partList) {
194                         if (maps.partitionMap.containsKey(part)) {
195                             partitions.addAll(maps.partitionMap.get(part));
196                         }
197                     }
198                 }
199                 ds.partitionList = Collections.unmodifiableList(partitions.stream()
200                         .sorted(Comparator.comparing(HWPartition::getName)).collect(Collectors.toList()));
201                 // Add to list
202                 result.add(ds);
203             }
204             return result;
205         } catch (COMException e) {
206             LOG.warn("COM exception: {}", e.getMessage());
207             return Collections.emptyList();
208         } finally {
209             if (comInit) {
210                 h.unInitCOM();
211             }
212         }
213     }
214 
215     /**
216      * Gets disk stats for the specified index. If the index is null, populates all the maps
217      *
218      * @param index The index to populate/update maps for
219      * @return An object encapsulating maps with the stats
220      */
221     private static DiskStats queryReadWriteStats(String index) {
222         // Create object to hold and return results
223         DiskStats stats = new DiskStats();
224         Pair<List<String>, Map<PhysicalDiskProperty, List<Long>>> instanceValuePair = PhysicalDisk.queryDiskCounters();
225         List<String> instances = instanceValuePair.getA();
226         Map<PhysicalDiskProperty, List<Long>> valueMap = instanceValuePair.getB();
227         stats.timeStamp = System.currentTimeMillis();
228         List<Long> readList = valueMap.get(PhysicalDiskProperty.DISKREADSPERSEC);
229         List<Long> readByteList = valueMap.get(PhysicalDiskProperty.DISKREADBYTESPERSEC);
230         List<Long> writeList = valueMap.get(PhysicalDiskProperty.DISKWRITESPERSEC);
231         List<Long> writeByteList = valueMap.get(PhysicalDiskProperty.DISKWRITEBYTESPERSEC);
232         List<Long> queueLengthList = valueMap.get(PhysicalDiskProperty.CURRENTDISKQUEUELENGTH);
233         List<Long> diskTimeList = valueMap.get(PhysicalDiskProperty.PERCENTDISKTIME);
234 
235         if (instances.isEmpty() || readList == null || readByteList == null || writeList == null
236                 || writeByteList == null || queueLengthList == null || diskTimeList == null) {
237             return stats;
238         }
239         for (int i = 0; i < instances.size(); i++) {
240             String name = getIndexFromName(instances.get(i));
241             // If index arg passed, only update passed arg
242             if (index != null && !index.equals(name)) {
243                 continue;
244             }
245             stats.readMap.put(name, readList.get(i));
246             stats.readByteMap.put(name, readByteList.get(i));
247             stats.writeMap.put(name, writeList.get(i));
248             stats.writeByteMap.put(name, writeByteList.get(i));
249             stats.queueLengthMap.put(name, queueLengthList.get(i));
250             stats.diskTimeMap.put(name, diskTimeList.get(i) / 10_000L);
251         }
252         return stats;
253     }
254 
255     private static PartitionMaps queryPartitionMaps(WmiQueryHandler h) {
256         // Create object to hold and return results
257         PartitionMaps maps = new PartitionMaps();
258 
259         // For Regexp matching DeviceIDs
260         Matcher mAnt;
261         Matcher mDep;
262 
263         // Map drives to partitions
264         WmiResult<DriveToPartitionProperty> drivePartitionMap = Win32DiskDriveToDiskPartition.queryDriveToPartition(h);
265         for (int i = 0; i < drivePartitionMap.getResultCount(); i++) {
266             mAnt = DEVICE_ID.matcher(WmiUtil.getRefString(drivePartitionMap, DriveToPartitionProperty.ANTECEDENT, i));
267             mDep = DEVICE_ID.matcher(WmiUtil.getRefString(drivePartitionMap, DriveToPartitionProperty.DEPENDENT, i));
268             if (mAnt.matches() && mDep.matches()) {
269                 maps.driveToPartitionMap.computeIfAbsent(mAnt.group(1).replace("\\\\", "\\"), x -> new ArrayList<>())
270                         .add(mDep.group(1));
271             }
272         }
273 
274         // Map partitions to logical disks
275         WmiResult<DiskToPartitionProperty> diskPartitionMap = Win32LogicalDiskToPartition.queryDiskToPartition(h);
276         for (int i = 0; i < diskPartitionMap.getResultCount(); i++) {
277             mAnt = DEVICE_ID.matcher(WmiUtil.getRefString(diskPartitionMap, DiskToPartitionProperty.ANTECEDENT, i));
278             mDep = DEVICE_ID.matcher(WmiUtil.getRefString(diskPartitionMap, DiskToPartitionProperty.DEPENDENT, i));
279             long size = WmiUtil.getUint64(diskPartitionMap, DiskToPartitionProperty.ENDINGADDRESS, i)
280                     - WmiUtil.getUint64(diskPartitionMap, DiskToPartitionProperty.STARTINGADDRESS, i) + 1L;
281             if (mAnt.matches() && mDep.matches()) {
282                 if (maps.partitionToLogicalDriveMap.containsKey(mAnt.group(1))) {
283                     maps.partitionToLogicalDriveMap.get(mAnt.group(1)).add(new Pair<>(mDep.group(1) + "\\", size));
284                 } else {
285                     List<Pair<String, Long>> list = new ArrayList<>();
286                     list.add(new Pair<>(mDep.group(1) + "\\", size));
287                     maps.partitionToLogicalDriveMap.put(mAnt.group(1), list);
288                 }
289             }
290         }
291 
292         // Next, get all partitions and create objects
293         WmiResult<DiskPartitionProperty> hwPartitionQueryMap = Win32DiskPartition.queryPartition(h);
294         for (int i = 0; i < hwPartitionQueryMap.getResultCount(); i++) {
295             String deviceID = WmiUtil.getString(hwPartitionQueryMap, DiskPartitionProperty.DEVICEID, i);
296             List<Pair<String, Long>> logicalDrives = maps.partitionToLogicalDriveMap.get(deviceID);
297             if (logicalDrives == null) {
298                 continue;
299             }
300             for (int j = 0; j < logicalDrives.size(); j++) {
301                 Pair<String, Long> logicalDrive = logicalDrives.get(j);
302                 if (logicalDrive != null && !logicalDrive.getA().isEmpty()) {
303                     char[] volumeChr = new char[GUID_BUFSIZE];
304                     Kernel32.INSTANCE.GetVolumeNameForVolumeMountPoint(logicalDrive.getA(), volumeChr, GUID_BUFSIZE);
305                     String uuid = ParseUtil.parseUuidOrDefault(new String(volumeChr).trim(), "");
306                     HWPartition pt = new HWPartition(
307                             WmiUtil.getString(hwPartitionQueryMap, DiskPartitionProperty.NAME, i),
308                             WmiUtil.getString(hwPartitionQueryMap, DiskPartitionProperty.TYPE, i),
309                             WmiUtil.getString(hwPartitionQueryMap, DiskPartitionProperty.DESCRIPTION, i), uuid,
310                             logicalDrive.getB(),
311                             WmiUtil.getUint32(hwPartitionQueryMap, DiskPartitionProperty.DISKINDEX, i),
312                             WmiUtil.getUint32(hwPartitionQueryMap, DiskPartitionProperty.INDEX, i),
313                             logicalDrive.getA());
314                     if (maps.partitionMap.containsKey(deviceID)) {
315                         maps.partitionMap.get(deviceID).add(pt);
316                     } else {
317                         List<HWPartition> ptlist = new ArrayList<>();
318                         ptlist.add(pt);
319                         maps.partitionMap.put(deviceID, ptlist);
320                     }
321                 }
322             }
323         }
324         return maps;
325     }
326 
327     /**
328      * Parse a drive name like "0 C:" to just the index "0"
329      *
330      * @param s A drive name to parse
331      * @return The first space-delimited value
332      */
333     private static String getIndexFromName(String s) {
334         if (s.isEmpty()) {
335             return s;
336         }
337         return s.split("\\s")[0];
338     }
339 
340     /**
341      * Maps to store read/write bytes per drive index
342      */
343     private static final class DiskStats {
344         private final Map<String, Long> readMap = new HashMap<>();
345         private final Map<String, Long> readByteMap = new HashMap<>();
346         private final Map<String, Long> writeMap = new HashMap<>();
347         private final Map<String, Long> writeByteMap = new HashMap<>();
348         private final Map<String, Long> queueLengthMap = new HashMap<>();
349         private final Map<String, Long> diskTimeMap = new HashMap<>();
350         private long timeStamp;
351     }
352 
353     /**
354      * Maps for the partition structure
355      */
356     private static final class PartitionMaps {
357         private final Map<String, List<String>> driveToPartitionMap = new HashMap<>();
358         private final Map<String, List<Pair<String, Long>>> partitionToLogicalDriveMap = new HashMap<>();
359         private final Map<String, List<HWPartition>> partitionMap = new HashMap<>();
360     }
361 }