View Javadoc
1   /*
2    * Copyright 2020-2022 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.hardware.platform.mac;
6   
7   import java.util.ArrayList;
8   import java.util.Collections;
9   import java.util.Comparator;
10  import java.util.EnumMap;
11  import java.util.List;
12  import java.util.Map;
13  import java.util.stream.Collectors;
14  
15  import org.slf4j.Logger;
16  import org.slf4j.LoggerFactory;
17  
18  import com.sun.jna.Pointer;
19  import com.sun.jna.platform.mac.CoreFoundation;
20  import com.sun.jna.platform.mac.CoreFoundation.CFBooleanRef;
21  import com.sun.jna.platform.mac.CoreFoundation.CFDictionaryRef;
22  import com.sun.jna.platform.mac.CoreFoundation.CFIndex;
23  import com.sun.jna.platform.mac.CoreFoundation.CFMutableDictionaryRef;
24  import com.sun.jna.platform.mac.CoreFoundation.CFNumberRef;
25  import com.sun.jna.platform.mac.CoreFoundation.CFStringRef;
26  import com.sun.jna.platform.mac.CoreFoundation.CFTypeRef;
27  import com.sun.jna.platform.mac.DiskArbitration;
28  import com.sun.jna.platform.mac.DiskArbitration.DADiskRef;
29  import com.sun.jna.platform.mac.DiskArbitration.DASessionRef;
30  import com.sun.jna.platform.mac.IOKit;
31  import com.sun.jna.platform.mac.IOKit.IOIterator;
32  import com.sun.jna.platform.mac.IOKit.IORegistryEntry;
33  import com.sun.jna.platform.mac.IOKitUtil;
34  
35  import oshi.annotation.concurrent.ThreadSafe;
36  import oshi.driver.mac.disk.Fsstat;
37  import oshi.hardware.HWDiskStore;
38  import oshi.hardware.HWPartition;
39  import oshi.hardware.common.AbstractHWDiskStore;
40  import oshi.util.Constants;
41  import oshi.util.platform.mac.CFUtil;
42  
43  /**
44   * Mac hard disk implementation.
45   */
46  @ThreadSafe
47  public final class MacHWDiskStore extends AbstractHWDiskStore {
48  
49      private static final CoreFoundation CF = CoreFoundation.INSTANCE;
50      private static final DiskArbitration DA = DiskArbitration.INSTANCE;
51  
52      private static final Logger LOG = LoggerFactory.getLogger(MacHWDiskStore.class);
53  
54      private long reads = 0L;
55      private long readBytes = 0L;
56      private long writes = 0L;
57      private long writeBytes = 0L;
58      private long currentQueueLength = 0L;
59      private long transferTime = 0L;
60      private long timeStamp = 0L;
61      private List<HWPartition> partitionList;
62  
63      private MacHWDiskStore(String name, String model, String serial, long size, DASessionRef session,
64              Map<String, String> mountPointMap, Map<CFKey, CFStringRef> cfKeyMap) {
65          super(name, model, serial, size);
66          updateDiskStats(session, mountPointMap, cfKeyMap);
67      }
68  
69      @Override
70      public long getReads() {
71          return reads;
72      }
73  
74      @Override
75      public long getReadBytes() {
76          return readBytes;
77      }
78  
79      @Override
80      public long getWrites() {
81          return writes;
82      }
83  
84      @Override
85      public long getWriteBytes() {
86          return writeBytes;
87      }
88  
89      @Override
90      public long getCurrentQueueLength() {
91          return currentQueueLength;
92      }
93  
94      @Override
95      public long getTransferTime() {
96          return transferTime;
97      }
98  
99      @Override
100     public long getTimeStamp() {
101         return timeStamp;
102     }
103 
104     @Override
105     public List<HWPartition> getPartitions() {
106         return this.partitionList;
107     }
108 
109     @Override
110     public boolean updateAttributes() {
111         // Open a session and create CFStrings
112         DASessionRef session = DA.DASessionCreate(CF.CFAllocatorGetDefault());
113         if (session == null) {
114             LOG.error("Unable to open session to DiskArbitration framework.");
115             return false;
116         }
117         Map<CFKey, CFStringRef> cfKeyMap = mapCFKeys();
118         // Execute the update
119         boolean diskFound = updateDiskStats(session, Fsstat.queryPartitionToMountMap(), cfKeyMap);
120         // Release the session and CFStrings
121         session.release();
122         for (CFTypeRef value : cfKeyMap.values()) {
123             value.release();
124         }
125 
126         return diskFound;
127     }
128 
129     private boolean updateDiskStats(DASessionRef session, Map<String, String> mountPointMap,
130             Map<CFKey, CFStringRef> cfKeyMap) {
131         // Now look up the device using the BSD Name to get its
132         // statistics
133         String bsdName = getName();
134         CFMutableDictionaryRef matchingDict = IOKitUtil.getBSDNameMatchingDict(bsdName);
135         if (matchingDict != null) {
136             // search for all IOservices that match the bsd name
137             IOIterator driveListIter = IOKitUtil.getMatchingServices(matchingDict);
138             if (driveListIter != null) {
139                 // getMatchingServices releases matchingDict
140                 IORegistryEntry drive = driveListIter.next();
141                 // Should only match one drive
142                 if (drive != null) {
143                     // Should be an IOMedia object with a parent
144                     // IOBlockStorageDriver or AppleAPFSContainerScheme object
145                     // Get the properties from the parent
146                     if (drive.conformsTo("IOMedia")) {
147                         IORegistryEntry parent = drive.getParentEntry("IOService");
148                         if (parent != null && (parent.conformsTo("IOBlockStorageDriver")
149                                 || parent.conformsTo("AppleAPFSContainerScheme"))) {
150                             CFMutableDictionaryRef properties = parent.createCFProperties();
151                             // We now have a properties object with the
152                             // statistics we need on it. Fetch them
153                             Pointer result = properties.getValue(cfKeyMap.get(CFKey.STATISTICS));
154                             CFDictionaryRef statistics = new CFDictionaryRef(result);
155                             this.timeStamp = System.currentTimeMillis();
156 
157                             // Now get the stats we want
158                             result = statistics.getValue(cfKeyMap.get(CFKey.READ_OPS));
159                             CFNumberRef stat = new CFNumberRef(result);
160                             this.reads = stat.longValue();
161                             result = statistics.getValue(cfKeyMap.get(CFKey.READ_BYTES));
162                             stat.setPointer(result);
163                             this.readBytes = stat.longValue();
164 
165                             result = statistics.getValue(cfKeyMap.get(CFKey.WRITE_OPS));
166                             stat.setPointer(result);
167                             this.writes = stat.longValue();
168                             result = statistics.getValue(cfKeyMap.get(CFKey.WRITE_BYTES));
169                             stat.setPointer(result);
170                             this.writeBytes = stat.longValue();
171 
172                             // Total time is in nanoseconds. Add read+write
173                             // and convert total to ms
174                             final Pointer readTimeResult = statistics.getValue(cfKeyMap.get(CFKey.READ_TIME));
175                             final Pointer writeTimeResult = statistics.getValue(cfKeyMap.get(CFKey.WRITE_TIME));
176                             // AppleAPFSContainerScheme does not have timer statistics
177                             if (readTimeResult != null && writeTimeResult != null) {
178                                 stat.setPointer(readTimeResult);
179                                 long xferTime = stat.longValue();
180                                 stat.setPointer(writeTimeResult);
181                                 xferTime += stat.longValue();
182                                 this.transferTime = xferTime / 1_000_000L;
183                             }
184 
185                             properties.release();
186                         } else {
187                             // This is normal for FileVault drives, Fusion
188                             // drives, and other virtual bsd names
189                             LOG.debug("Unable to find block storage driver properties for {}", bsdName);
190                         }
191                         // Now get partitions for this disk.
192                         List<HWPartition> partitions = new ArrayList<>();
193 
194                         CFMutableDictionaryRef properties = drive.createCFProperties();
195                         // Partitions will match BSD Unit property
196                         Pointer result = properties.getValue(cfKeyMap.get(CFKey.BSD_UNIT));
197                         CFNumberRef bsdUnit = new CFNumberRef(result);
198                         // We need a CFBoolean that's false.
199                         // Whole disk has 'true' for Whole and 'false'
200                         // for leaf; store the boolean false
201                         result = properties.getValue(cfKeyMap.get(CFKey.LEAF));
202                         CFBooleanRef cfFalse = new CFBooleanRef(result);
203                         // create a matching dict for BSD Unit
204                         CFMutableDictionaryRef propertyDict = CF.CFDictionaryCreateMutable(CF.CFAllocatorGetDefault(),
205                                 new CFIndex(0), null, null);
206                         propertyDict.setValue(cfKeyMap.get(CFKey.BSD_UNIT), bsdUnit);
207                         propertyDict.setValue(cfKeyMap.get(CFKey.WHOLE), cfFalse);
208                         matchingDict = CF.CFDictionaryCreateMutable(CF.CFAllocatorGetDefault(), new CFIndex(0), null,
209                                 null);
210                         matchingDict.setValue(cfKeyMap.get(CFKey.IO_PROPERTY_MATCH), propertyDict);
211 
212                         // search for IOservices that match the BSD Unit
213                         // with whole=false; these are partitions
214                         IOIterator serviceIterator = IOKitUtil.getMatchingServices(matchingDict);
215                         // getMatchingServices releases matchingDict
216                         properties.release();
217                         propertyDict.release();
218 
219                         if (serviceIterator != null) {
220                             // Iterate disks
221                             IORegistryEntry sdService = IOKit.INSTANCE.IOIteratorNext(serviceIterator);
222                             while (sdService != null) {
223                                 // look up the BSD Name
224                                 String partBsdName = sdService.getStringProperty("BSD Name");
225                                 String name = partBsdName;
226                                 String type = "";
227                                 // Get the DiskArbitration dictionary for
228                                 // this partition
229                                 DADiskRef disk = DA.DADiskCreateFromBSDName(CF.CFAllocatorGetDefault(), session,
230                                         partBsdName);
231                                 if (disk != null) {
232                                     CFDictionaryRef diskInfo = DA.DADiskCopyDescription(disk);
233                                     if (diskInfo != null) {
234                                         // get volume name from its key
235                                         result = diskInfo.getValue(cfKeyMap.get(CFKey.DA_MEDIA_NAME));
236                                         type = CFUtil.cfPointerToString(result);
237                                         result = diskInfo.getValue(cfKeyMap.get(CFKey.DA_VOLUME_NAME));
238                                         if (result == null) {
239                                             name = type;
240                                         } else {
241                                             name = CFUtil.cfPointerToString(result);
242                                         }
243                                         diskInfo.release();
244                                     }
245                                     disk.release();
246                                 }
247                                 String mountPoint = mountPointMap.getOrDefault(partBsdName, "");
248                                 Long size = sdService.getLongProperty("Size");
249                                 Integer bsdMajor = sdService.getIntegerProperty("BSD Major");
250                                 Integer bsdMinor = sdService.getIntegerProperty("BSD Minor");
251                                 String uuid = sdService.getStringProperty("UUID");
252                                 partitions.add(new HWPartition(partBsdName, name, type,
253                                         uuid == null ? Constants.UNKNOWN : uuid, size == null ? 0L : size,
254                                         bsdMajor == null ? 0 : bsdMajor, bsdMinor == null ? 0 : bsdMinor, mountPoint));
255                                 // iterate
256                                 sdService.release();
257                                 sdService = IOKit.INSTANCE.IOIteratorNext(serviceIterator);
258                             }
259                             serviceIterator.release();
260                         }
261                         this.partitionList = Collections.unmodifiableList(partitions.stream()
262                                 .sorted(Comparator.comparing(HWPartition::getName)).collect(Collectors.toList()));
263                         if (parent != null) {
264                             parent.release();
265                         }
266                     } else {
267                         LOG.error("Unable to find IOMedia device or parent for {}", bsdName);
268                     }
269                     drive.release();
270                 }
271                 driveListIter.release();
272                 return true;
273             }
274         }
275         return false;
276     }
277 
278     /**
279      * Gets the disks on this machine
280      *
281      * @return a list of {@link HWDiskStore} objects representing the disks
282      */
283     public static List<HWDiskStore> getDisks() {
284         Map<String, String> mountPointMap = Fsstat.queryPartitionToMountMap();
285         Map<CFKey, CFStringRef> cfKeyMap = mapCFKeys();
286 
287         List<HWDiskStore> diskList = new ArrayList<>();
288 
289         // Open a DiskArbitration session
290         DASessionRef session = DA.DASessionCreate(CF.CFAllocatorGetDefault());
291         if (session == null) {
292             LOG.error("Unable to open session to DiskArbitration framework.");
293             return Collections.emptyList();
294         }
295 
296         // Get IOMedia objects representing whole drives
297         List<String> bsdNames = new ArrayList<>();
298         IOIterator iter = IOKitUtil.getMatchingServices("IOMedia");
299         if (iter != null) {
300             IORegistryEntry media = iter.next();
301             while (media != null) {
302                 Boolean whole = media.getBooleanProperty("Whole");
303                 if (whole != null && whole) {
304                     DADiskRef disk = DA.DADiskCreateFromIOMedia(CF.CFAllocatorGetDefault(), session, media);
305                     bsdNames.add(DA.DADiskGetBSDName(disk));
306                     disk.release();
307                 }
308                 media.release();
309                 media = iter.next();
310             }
311             iter.release();
312         }
313 
314         // Now iterate the bsdNames
315         for (String bsdName : bsdNames) {
316             String model = "";
317             String serial = "";
318             long size = 0L;
319 
320             // Get a reference to the disk - only matching /dev/disk*
321             String path = "/dev/" + bsdName;
322 
323             // Get the DiskArbitration dictionary for this disk, which has model
324             // and size (capacity)
325             DADiskRef disk = DA.DADiskCreateFromBSDName(CF.CFAllocatorGetDefault(), session, path);
326             if (disk != null) {
327                 CFDictionaryRef diskInfo = DA.DADiskCopyDescription(disk);
328                 if (diskInfo != null) {
329                     // Parse out model and size from their respective keys
330                     Pointer result = diskInfo.getValue(cfKeyMap.get(CFKey.DA_DEVICE_MODEL));
331                     model = CFUtil.cfPointerToString(result);
332                     result = diskInfo.getValue(cfKeyMap.get(CFKey.DA_MEDIA_SIZE));
333                     CFNumberRef sizePtr = new CFNumberRef(result);
334                     size = sizePtr.longValue();
335                     diskInfo.release();
336 
337                     // Use the model as a key to get serial from IOKit
338                     if (!"Disk Image".equals(model)) {
339                         CFStringRef modelNameRef = CFStringRef.createCFString(model);
340                         CFMutableDictionaryRef propertyDict = CF.CFDictionaryCreateMutable(CF.CFAllocatorGetDefault(),
341                                 new CFIndex(0), null, null);
342                         propertyDict.setValue(cfKeyMap.get(CFKey.MODEL), modelNameRef);
343                         CFMutableDictionaryRef matchingDict = CF.CFDictionaryCreateMutable(CF.CFAllocatorGetDefault(),
344                                 new CFIndex(0), null, null);
345                         matchingDict.setValue(cfKeyMap.get(CFKey.IO_PROPERTY_MATCH), propertyDict);
346 
347                         // search for all IOservices that match the model
348                         IOIterator serviceIterator = IOKitUtil.getMatchingServices(matchingDict);
349                         // getMatchingServices releases matchingDict
350                         modelNameRef.release();
351                         propertyDict.release();
352 
353                         if (serviceIterator != null) {
354                             IORegistryEntry sdService = serviceIterator.next();
355                             while (sdService != null) {
356                                 // look up the serial number
357                                 serial = sdService.getStringProperty("Serial Number");
358                                 sdService.release();
359                                 if (serial != null) {
360                                     break;
361                                 }
362                                 // iterate
363                                 sdService.release();
364                                 sdService = serviceIterator.next();
365                             }
366                             serviceIterator.release();
367                         }
368                         if (serial == null) {
369                             serial = "";
370                         }
371                     }
372                 }
373                 disk.release();
374 
375                 // If empty, ignore
376                 if (size <= 0) {
377                     continue;
378                 }
379                 HWDiskStore diskStore = new MacHWDiskStore(bsdName, model.trim(), serial.trim(), size, session,
380                         mountPointMap, cfKeyMap);
381                 diskList.add(diskStore);
382             }
383         }
384         // Close DA session
385         session.release();
386         for (CFTypeRef value : cfKeyMap.values()) {
387             value.release();
388         }
389         return diskList;
390     }
391 
392     /**
393      * Temporarily cache pointers to keys. The values from this map must be released after use.}
394      *
395      * @return A map of keys in the {@link CFKey} enum to corresponding {@link CFStringRef}.
396      */
397     private static Map<CFKey, CFStringRef> mapCFKeys() {
398         Map<CFKey, CFStringRef> keyMap = new EnumMap<>(CFKey.class);
399         for (CFKey cfKey : CFKey.values()) {
400             keyMap.put(cfKey, CFStringRef.createCFString(cfKey.getKey()));
401         }
402         return keyMap;
403     }
404 
405     /*
406      * Strings to convert to CFStringRef for pointer lookups
407      */
408     private enum CFKey {
409         IO_PROPERTY_MATCH("IOPropertyMatch"), //
410 
411         STATISTICS("Statistics"), //
412         READ_OPS("Operations (Read)"), READ_BYTES("Bytes (Read)"), READ_TIME("Total Time (Read)"), //
413         WRITE_OPS("Operations (Write)"), WRITE_BYTES("Bytes (Write)"), WRITE_TIME("Total Time (Write)"), //
414 
415         BSD_UNIT("BSD Unit"), LEAF("Leaf"), WHOLE("Whole"), //
416 
417         DA_MEDIA_NAME("DAMediaName"), DA_VOLUME_NAME("DAVolumeName"), DA_MEDIA_SIZE("DAMediaSize"), //
418         DA_DEVICE_MODEL("DADeviceModel"), MODEL("Model");
419 
420         private final String key;
421 
422         CFKey(String key) {
423             this.key = key;
424         }
425 
426         public String getKey() {
427             return this.key;
428         }
429     }
430 }