1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "device/serial/serial_device_enumerator_mac.h"
7 #include <IOKit/serial/IOSerialKeys.h>
8 #include <IOKit/usb/IOUSBLib.h>
13 #include <unordered_set>
16 #include "base/files/file_enumerator.h"
17 #include "base/files/file_path.h"
18 #include "base/files/file_util.h"
19 #include "base/mac/scoped_cftyperef.h"
20 #include "base/mac/scoped_ioobject.h"
21 #include "base/metrics/histogram_functions.h"
22 #include "base/strings/pattern.h"
23 #include "base/strings/string_util.h"
24 #include "base/strings/sys_string_conversions.h"
25 #include "base/threading/scoped_blocking_call.h"
31 // Searches a service and all ancestor services for a property with the
32 // specified key, returning NULL if no such key was found.
33 CFTypeRef GetCFProperty(io_service_t service, const CFStringRef key) {
34 // We search for the specified property not only on the specified service, but
35 // all ancestors of that service. This is important because if a device is
36 // both serial and USB, in the registry tree it appears as a serial service
37 // with a USB service as its ancestor. Without searching ancestors services
38 // for the specified property, we'd miss all USB properties.
39 return IORegistryEntrySearchCFProperty(
40 service, kIOServicePlane, key, NULL,
41 kIORegistryIterateRecursively | kIORegistryIterateParents);
44 // Searches a service and all ancestor services for a string property with the
45 // specified key, returning NULL if no such key was found.
46 CFStringRef GetCFStringProperty(io_service_t service, const CFStringRef key) {
47 CFTypeRef value = GetCFProperty(service, key);
48 if (value && (CFGetTypeID(value) == CFStringGetTypeID()))
49 return static_cast<CFStringRef>(value);
54 // Searches a service and all ancestor services for a number property with the
55 // specified key, returning NULL if no such key was found.
56 CFNumberRef GetCFNumberProperty(io_service_t service, const CFStringRef key) {
57 CFTypeRef value = GetCFProperty(service, key);
58 if (value && (CFGetTypeID(value) == CFNumberGetTypeID()))
59 return static_cast<CFNumberRef>(value);
64 // Searches the specified service for a string property with the specified key,
65 // sets value to that property's value, and returns whether the operation was
67 bool GetStringProperty(io_service_t service,
68 const CFStringRef key,
70 CFStringRef propValue = GetCFStringProperty(service, key);
72 *value = base::SysCFStringRefToUTF8(propValue);
79 // Searches the specified service for a uint16_t property with the specified
80 // key, sets value to that property's value, and returns whether the operation
82 bool GetUInt16Property(io_service_t service,
83 const CFStringRef key,
85 CFNumberRef propValue = GetCFNumberProperty(service, key);
88 if (CFNumberGetValue(propValue, kCFNumberIntType, &intValue)) {
89 *value = static_cast<uint16_t>(intValue);
97 // Returns value clamped to the range of [min, max].
98 int Clamp(int value, int min, int max) {
99 return std::min(std::max(value, min), max);
102 // Returns an array of devices as retrieved through the new method of
103 // enumerating serial devices (IOKit). This new method gives more information
104 // about the devices than the old method.
105 std::vector<mojom::SerialDeviceInfoPtr> GetDevicesNew() {
106 std::vector<mojom::SerialDeviceInfoPtr> devices;
108 base::ScopedBlockingCall scoped_blocking_call(base::BlockingType::MAY_BLOCK);
109 // Make a service query to find all serial devices.
110 CFMutableDictionaryRef matchingDict =
111 IOServiceMatching(kIOSerialBSDServiceValue);
117 IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &it);
118 if (kr != KERN_SUCCESS)
121 base::mac::ScopedIOObject<io_iterator_t> scoped_it(it);
122 base::mac::ScopedIOObject<io_service_t> scoped_device;
123 while (scoped_device.reset(IOIteratorNext(scoped_it.get())), scoped_device) {
124 auto callout_info = mojom::SerialDeviceInfo::New();
127 if (GetUInt16Property(scoped_device.get(), CFSTR(kUSBVendorID),
129 callout_info->has_vendor_id = true;
130 callout_info->vendor_id = vendorId;
134 if (GetUInt16Property(scoped_device.get(), CFSTR(kUSBProductID),
136 callout_info->has_product_id = true;
137 callout_info->product_id = productId;
140 std::string display_name;
141 if (GetStringProperty(scoped_device.get(), CFSTR(kUSBProductString),
143 callout_info->display_name = std::move(display_name);
146 // Each serial device has two "paths" in /dev/ associated with it: a
147 // "dialin" path starting with "tty" and a "callout" path starting with
148 // "cu". Each of these is considered a different device from Chrome's
149 // standpoint, but both should share the device's USB properties.
150 std::string dialinDevice;
151 if (GetStringProperty(scoped_device.get(), CFSTR(kIODialinDeviceKey),
153 mojom::SerialDeviceInfoPtr dialin_info = callout_info.Clone();
154 dialin_info->path = dialinDevice;
155 devices.push_back(std::move(dialin_info));
158 std::string calloutDevice;
159 if (GetStringProperty(scoped_device.get(), CFSTR(kIOCalloutDeviceKey),
161 callout_info->path = calloutDevice;
162 devices.push_back(std::move(callout_info));
169 // Returns an array of devices as retrieved through the old method of
170 // enumerating serial devices (pattern matching in /dev/). This old method gives
171 // less information about the devices than the new method.
172 std::vector<mojom::SerialDeviceInfoPtr> GetDevicesOld() {
173 const base::FilePath kDevRoot("/dev");
174 const int kFilesAndSymLinks =
175 base::FileEnumerator::FILES | base::FileEnumerator::SHOW_SYM_LINKS;
177 std::set<std::string> valid_patterns;
178 valid_patterns.insert("/dev/*Bluetooth*");
179 valid_patterns.insert("/dev/*Modem*");
180 valid_patterns.insert("/dev/*bluetooth*");
181 valid_patterns.insert("/dev/*modem*");
182 valid_patterns.insert("/dev/*serial*");
183 valid_patterns.insert("/dev/tty.*");
184 valid_patterns.insert("/dev/cu.*");
186 base::ScopedBlockingCall scoped_blocking_call(base::BlockingType::MAY_BLOCK);
187 std::vector<mojom::SerialDeviceInfoPtr> devices;
188 base::FileEnumerator enumerator(kDevRoot, false, kFilesAndSymLinks);
190 const base::FilePath next_device_path(enumerator.Next());
191 const std::string next_device = next_device_path.value();
192 if (next_device.empty())
195 std::set<std::string>::const_iterator i = valid_patterns.begin();
196 for (; i != valid_patterns.end(); ++i) {
197 if (base::MatchPattern(next_device, *i)) {
198 auto info = mojom::SerialDeviceInfo::New();
199 info->path = next_device;
200 devices.push_back(std::move(info));
211 std::unique_ptr<SerialDeviceEnumerator> SerialDeviceEnumerator::Create() {
212 return std::unique_ptr<SerialDeviceEnumerator>(
213 new SerialDeviceEnumeratorMac());
216 SerialDeviceEnumeratorMac::SerialDeviceEnumeratorMac() {}
218 SerialDeviceEnumeratorMac::~SerialDeviceEnumeratorMac() {}
220 std::vector<mojom::SerialDeviceInfoPtr>
221 SerialDeviceEnumeratorMac::GetDevices() {
222 std::vector<mojom::SerialDeviceInfoPtr> devices = GetDevicesNew();
223 std::vector<mojom::SerialDeviceInfoPtr> old_devices = GetDevicesOld();
225 base::UmaHistogramSparse("Hardware.Serial.NewMinusOldDeviceListSize",
226 Clamp(devices.size() - old_devices.size(), -10, 10));
228 // Add devices found from both the new and old methods of enumeration. If a
229 // device is found using both the new and the old enumeration method, then we
230 // take the device from the new enumeration method because it's able to
231 // collect more information. We do this by inserting the new devices first,
232 // because insertions are ignored if the key already exists.
233 std::unordered_set<std::string> devices_seen;
234 for (const auto& device : devices) {
235 bool inserted = devices_seen.insert(device->path).second;
238 for (auto& device : old_devices) {
239 if (devices_seen.insert(device->path).second)
240 devices.push_back(std::move(device));
245 } // namespace device