View Javadoc
1   /*
2    * Copyright 2016-2023 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.HashMap;
10  import java.util.List;
11  import java.util.Locale;
12  import java.util.Map;
13  
14  import com.sun.jna.platform.mac.CoreFoundation;
15  import com.sun.jna.platform.mac.CoreFoundation.CFIndex;
16  import com.sun.jna.platform.mac.CoreFoundation.CFMutableDictionaryRef;
17  import com.sun.jna.platform.mac.CoreFoundation.CFStringRef;
18  import com.sun.jna.platform.mac.CoreFoundation.CFTypeRef;
19  import com.sun.jna.platform.mac.IOKit.IOIterator;
20  import com.sun.jna.platform.mac.IOKit.IORegistryEntry;
21  import com.sun.jna.platform.mac.IOKitUtil;
22  
23  import oshi.annotation.concurrent.Immutable;
24  import oshi.hardware.UsbDevice;
25  import oshi.hardware.common.AbstractUsbDevice;
26  
27  /**
28   * Mac Usb Device
29   */
30  @Immutable
31  public class MacUsbDevice extends AbstractUsbDevice {
32  
33      private static final CoreFoundation CF = CoreFoundation.INSTANCE;
34  
35      private static final String IOUSB = "IOUSB";
36      private static final String IOSERVICE = "IOService";
37  
38      public MacUsbDevice(String name, String vendor, String vendorId, String productId, String serialNumber,
39              String uniqueDeviceId, List<UsbDevice> connectedDevices) {
40          super(name, vendor, vendorId, productId, serialNumber, uniqueDeviceId, connectedDevices);
41      }
42  
43      /**
44       * Instantiates a list of {@link oshi.hardware.UsbDevice} objects, representing devices connected via a usb port
45       * (including internal devices).
46       * <p>
47       * If the value of {@code tree} is true, the top level devices returned from this method are the USB Controllers;
48       * connected hubs and devices in its device tree share that controller's bandwidth. If the value of {@code tree} is
49       * false, USB devices (not controllers) are listed in a single flat list.
50       *
51       * @param tree If true, returns a list of controllers, which requires recursive iteration of connected devices. If
52       *             false, returns a flat list of devices excluding controllers.
53       * @return a list of {@link oshi.hardware.UsbDevice} objects.
54       */
55      public static List<UsbDevice> getUsbDevices(boolean tree) {
56          List<UsbDevice> devices = getUsbDevices();
57          if (tree) {
58              return devices;
59          }
60          List<UsbDevice> deviceList = new ArrayList<>();
61          // Top level is controllers; they won't be added to the list, but all
62          // their connected devices will be
63          for (UsbDevice device : devices) {
64              addDevicesToList(deviceList, device.getConnectedDevices());
65          }
66          return deviceList;
67      }
68  
69      private static List<UsbDevice> getUsbDevices() {
70          // Maps to store information using RegistryEntryID as the key
71          Map<Long, String> nameMap = new HashMap<>();
72          Map<Long, String> vendorMap = new HashMap<>();
73          Map<Long, String> vendorIdMap = new HashMap<>();
74          Map<Long, String> productIdMap = new HashMap<>();
75          Map<Long, String> serialMap = new HashMap<>();
76          Map<Long, List<Long>> hubMap = new HashMap<>();
77  
78          List<Long> usbControllers = new ArrayList<>();
79          IORegistryEntry root = IOKitUtil.getRoot();
80          // Iterate over children of root in the IOUSB plane. This does not include
81          // controllers so we have to check their parents
82          IOIterator iter = root.getChildIterator(IOUSB);
83          if (iter != null) {
84              // Define keys
85              CFStringRef locationIDKey = CFStringRef.createCFString("locationID");
86              CFStringRef ioPropertyMatchKey = CFStringRef.createCFString("IOPropertyMatch");
87  
88              // Get the device directly under each controller
89              IORegistryEntry device = iter.next();
90              while (device != null) {
91                  long id = 0L;
92                  // The parent of this device in IOService plane is the controller
93                  IORegistryEntry controller = device.getParentEntry(IOSERVICE);
94                  if (controller != null) {
95                      // Unique global identifier for this controller
96                      id = controller.getRegistryEntryID();
97                      // Populate other data for the controller
98                      nameMap.put(id, controller.getName());
99                      // The only information we have in registry for this controller is
100                     // the locationID. Use that to search for matching PCI device to obtain
101                     // more information for the controller
102                     CFTypeRef ref = controller.createCFProperty(locationIDKey);
103                     if (ref != null) {
104                         getControllerIdByLocation(id, ref, locationIDKey, ioPropertyMatchKey, vendorIdMap,
105                                 productIdMap);
106                         ref.release();
107                     }
108                     controller.release();
109                 }
110                 usbControllers.add(id);
111 
112                 // Now recursively add this device and its children to the maps
113                 // id is the coontroller ID and the first parent ID
114                 addDeviceAndChildrenToMaps(device, id, nameMap, vendorMap, vendorIdMap, productIdMap, serialMap,
115                         hubMap);
116 
117                 device.release();
118                 device = iter.next();
119             }
120             locationIDKey.release();
121             ioPropertyMatchKey.release();
122             iter.release();
123         }
124         root.release();
125 
126         // Build tree and return
127         List<UsbDevice> controllerDevices = new ArrayList<>();
128         for (Long controller : usbControllers) {
129             controllerDevices.add(getDeviceAndChildren(controller, "0000", "0000", nameMap, vendorMap, vendorIdMap,
130                     productIdMap, serialMap, hubMap));
131         }
132         return controllerDevices;
133     }
134 
135     /**
136      * Recursively populate maps with information from a USB Device and its children
137      *
138      * @param device       The device which, along with its children, should be added
139      * @param parentId     The id of the device's parent.
140      * @param nameMap      the map of names
141      * @param vendorMap    the map of vendors
142      * @param vendorIdMap  the map of vendorIds
143      * @param productIdMap the map of productIds
144      * @param serialMap    the map of serial numbers
145      * @param hubMap       the map of hubs
146      */
147     private static void addDeviceAndChildrenToMaps(IORegistryEntry device, long parentId, Map<Long, String> nameMap,
148             Map<Long, String> vendorMap, Map<Long, String> vendorIdMap, Map<Long, String> productIdMap,
149             Map<Long, String> serialMap, Map<Long, List<Long>> hubMap) {
150 
151         // Unique global identifier for this device
152         long id = device.getRegistryEntryID();
153         // Store id as a child of parent in hubmap
154         hubMap.computeIfAbsent(parentId, x -> new ArrayList<>()).add(id);
155         // Get device name and store in map
156         nameMap.put(id, device.getName().trim());
157         // Get vendor and store in map
158         String vendor = device.getStringProperty("USB Vendor Name");
159         if (vendor != null) {
160             vendorMap.put(id, vendor.trim());
161         }
162         // Get vendorId and store in map
163         Long vendorId = device.getLongProperty("idVendor");
164         if (vendorId != null) {
165             vendorIdMap.put(id, String.format(Locale.ROOT, "%04x", 0xffff & vendorId));
166         }
167         // Get productId and store in map
168         Long productId = device.getLongProperty("idProduct");
169         if (productId != null) {
170             productIdMap.put(id, String.format(Locale.ROOT, "%04x", 0xffff & productId));
171         }
172         // Get serial and store in map
173         String serial = device.getStringProperty("USB Serial Number");
174         if (serial != null) {
175             serialMap.put(id, serial.trim());
176         }
177 
178         // Now get this device's children (if any) and recurse
179         IOIterator childIter = device.getChildIterator(IOUSB);
180         IORegistryEntry childDevice = childIter.next();
181         while (childDevice != null) {
182             addDeviceAndChildrenToMaps(childDevice, id, nameMap, vendorMap, vendorIdMap, productIdMap, serialMap,
183                     hubMap);
184 
185             childDevice.release();
186             childDevice = childIter.next();
187         }
188         childIter.release();
189     }
190 
191     private static void addDevicesToList(List<UsbDevice> deviceList, List<UsbDevice> list) {
192         for (UsbDevice device : list) {
193             deviceList.add(
194                     new MacUsbDevice(device.getName(), device.getVendor(), device.getVendorId(), device.getProductId(),
195                             device.getSerialNumber(), device.getUniqueDeviceId(), Collections.emptyList()));
196             addDevicesToList(deviceList, device.getConnectedDevices());
197         }
198     }
199 
200     /**
201      * Looks up vendor and product id information for a USB Host Controller by cross-referencing the location
202      *
203      * @param id                 The global unique ID for the host controller used as a key for maps
204      * @param locationId         The locationID of this controller returned from the registry
205      * @param locationIDKey      A pointer to the locationID string
206      * @param ioPropertyMatchKey A pointer to the IOPropertyMatch string
207      * @param productIdMap       the map of productIds
208      * @param vendorIdMap        the map of vendorIds
209      */
210     private static void getControllerIdByLocation(long id, CFTypeRef locationId, CFStringRef locationIDKey,
211             CFStringRef ioPropertyMatchKey, Map<Long, String> vendorIdMap, Map<Long, String> productIdMap) {
212         // Create a matching property dictionary from the locationId
213         CFMutableDictionaryRef propertyDict = CF.CFDictionaryCreateMutable(CF.CFAllocatorGetDefault(), new CFIndex(0),
214                 null, null);
215         propertyDict.setValue(locationIDKey, locationId);
216         CFMutableDictionaryRef matchingDict = CF.CFDictionaryCreateMutable(CF.CFAllocatorGetDefault(), new CFIndex(0),
217                 null, null);
218         matchingDict.setValue(ioPropertyMatchKey, propertyDict);
219 
220         // search for all IOservices that match the locationID
221         IOIterator serviceIterator = IOKitUtil.getMatchingServices(matchingDict);
222         // getMatchingServices releases matchingDict
223         propertyDict.release();
224 
225         // Iterate matching services looking for devices whose parents have the
226         // vendor and device ids
227         boolean found = false;
228         if (serviceIterator != null) {
229             IORegistryEntry matchingService = serviceIterator.next();
230             while (matchingService != null && !found) {
231                 // Get the parent, which contains the keys we need
232                 IORegistryEntry parent = matchingService.getParentEntry(IOSERVICE);
233                 // look up the vendor-id by key
234                 // vendor-id is a byte array of 4 bytes
235                 if (parent != null) {
236                     byte[] vid = parent.getByteArrayProperty("vendor-id");
237                     if (vid != null && vid.length >= 2) {
238                         vendorIdMap.put(id, String.format(Locale.ROOT, "%02x%02x", vid[1], vid[0]));
239                         found = true;
240                     }
241                     // look up the device-id by key
242                     // device-id is a byte array of 4 bytes
243                     byte[] pid = parent.getByteArrayProperty("device-id");
244                     if (pid != null && pid.length >= 2) {
245                         productIdMap.put(id, String.format(Locale.ROOT, "%02x%02x", pid[1], pid[0]));
246                         found = true;
247                     }
248                     parent.release();
249                 }
250                 // iterate
251                 matchingService.release();
252                 matchingService = serviceIterator.next();
253             }
254             serviceIterator.release();
255         }
256     }
257 
258     /**
259      * Recursively creates MacUsbDevices by fetching information from maps to populate fields
260      *
261      * @param registryEntryId The device unique registry id.
262      * @param vid             The default (parent) vendor ID
263      * @param pid             The default (parent) product ID
264      * @param nameMap         the map of names
265      * @param vendorMap       the map of vendors
266      * @param vendorIdMap     the map of vendorIds
267      * @param productIdMap    the map of productIds
268      * @param serialMap       the map of serial numbers
269      * @param hubMap          the map of hubs
270      * @return A MacUsbDevice corresponding to this device
271      */
272     private static MacUsbDevice getDeviceAndChildren(Long registryEntryId, String vid, String pid,
273             Map<Long, String> nameMap, Map<Long, String> vendorMap, Map<Long, String> vendorIdMap,
274             Map<Long, String> productIdMap, Map<Long, String> serialMap, Map<Long, List<Long>> hubMap) {
275         String vendorId = vendorIdMap.getOrDefault(registryEntryId, vid);
276         String productId = productIdMap.getOrDefault(registryEntryId, pid);
277         List<Long> childIds = hubMap.getOrDefault(registryEntryId, new ArrayList<>());
278         List<UsbDevice> usbDevices = new ArrayList<>();
279         for (Long id : childIds) {
280             usbDevices.add(getDeviceAndChildren(id, vendorId, productId, nameMap, vendorMap, vendorIdMap, productIdMap,
281                     serialMap, hubMap));
282         }
283         Collections.sort(usbDevices);
284         return new MacUsbDevice(nameMap.getOrDefault(registryEntryId, vendorId + ":" + productId),
285                 vendorMap.getOrDefault(registryEntryId, ""), vendorId, productId,
286                 serialMap.getOrDefault(registryEntryId, ""), "0x" + Long.toHexString(registryEntryId), usbDevices);
287     }
288 }