| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/device/serial/serial_device_enumerator_mac.h" |
| |
| #include <IOKit/serial/IOSerialKeys.h> |
| #include <IOKit/usb/IOUSBLib.h> |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <unordered_set> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #include "base/mac/scoped_ioobject.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/pattern.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "components/device_event_log/device_event_log.h" |
| |
| namespace device { |
| |
| namespace { |
| |
| std::string HexErrorCode(IOReturn error_code) { |
| return base::StringPrintf("0x%08x", error_code); |
| } |
| |
| // Searches a service and all ancestor services for a property with the |
| // specified key, returning NULL if no such key was found. |
| CFTypeRef GetCFProperty(io_service_t service, const CFStringRef key) { |
| // We search for the specified property not only on the specified service, but |
| // all ancestors of that service. This is important because if a device is |
| // both serial and USB, in the registry tree it appears as a serial service |
| // with a USB service as its ancestor. Without searching ancestors services |
| // for the specified property, we'd miss all USB properties. |
| return IORegistryEntrySearchCFProperty( |
| service, kIOServicePlane, key, NULL, |
| kIORegistryIterateRecursively | kIORegistryIterateParents); |
| } |
| |
| // Searches a service and all ancestor services for a string property with the |
| // specified key, returning NULL if no such key was found. |
| CFStringRef GetCFStringProperty(io_service_t service, const CFStringRef key) { |
| CFTypeRef value = GetCFProperty(service, key); |
| if (value && (CFGetTypeID(value) == CFStringGetTypeID())) |
| return static_cast<CFStringRef>(value); |
| |
| return NULL; |
| } |
| |
| // Searches a service and all ancestor services for a number property with the |
| // specified key, returning NULL if no such key was found. |
| CFNumberRef GetCFNumberProperty(io_service_t service, const CFStringRef key) { |
| CFTypeRef value = GetCFProperty(service, key); |
| if (value && (CFGetTypeID(value) == CFNumberGetTypeID())) |
| return static_cast<CFNumberRef>(value); |
| |
| return NULL; |
| } |
| |
| // Searches the specified service for a string property with the specified key. |
| absl::optional<std::string> GetStringProperty(io_service_t service, |
| const CFStringRef key) { |
| CFStringRef propValue = GetCFStringProperty(service, key); |
| if (propValue) |
| return base::SysCFStringRefToUTF8(propValue); |
| |
| return absl::nullopt; |
| } |
| |
| // Searches the specified service for a uint16_t property with the specified |
| // key. |
| absl::optional<uint16_t> GetUInt16Property(io_service_t service, |
| const CFStringRef key) { |
| CFNumberRef propValue = GetCFNumberProperty(service, key); |
| if (propValue) { |
| int intValue; |
| if (CFNumberGetValue(propValue, kCFNumberIntType, &intValue)) |
| return static_cast<uint16_t>(intValue); |
| } |
| |
| return absl::nullopt; |
| } |
| |
| // Finds the name of the USB driver for |device| by walking up the |
| // IORegistry tree to find the first entry provided by the IOUSBInterface |
| // class. For drivers compiled for macOS 10.11 and later this was renamed |
| // to IOUSBHostInterface. |
| absl::optional<std::string> GetUsbDriverName( |
| base::mac::ScopedIOObject<io_object_t> device) { |
| base::mac::ScopedIOObject<io_iterator_t> iterator; |
| kern_return_t kr = IORegistryEntryCreateIterator( |
| device.get(), kIOServicePlane, |
| kIORegistryIterateRecursively | kIORegistryIterateParents, |
| iterator.InitializeInto()); |
| if (kr != KERN_SUCCESS) |
| return absl::nullopt; |
| |
| base::mac::ScopedIOObject<io_service_t> ancestor; |
| while (ancestor.reset(IOIteratorNext(iterator)), ancestor) { |
| absl::optional<std::string> provider_class = |
| GetStringProperty(ancestor.get(), CFSTR(kIOProviderClassKey)); |
| if (provider_class && (*provider_class == "IOUSBInterface" || |
| *provider_class == "IOUSBHostInterface")) { |
| return GetStringProperty(ancestor.get(), kCFBundleIdentifierKey); |
| } |
| } |
| |
| return absl::nullopt; |
| } |
| |
| } // namespace |
| |
| SerialDeviceEnumeratorMac::SerialDeviceEnumeratorMac() { |
| notify_port_.reset(IONotificationPortCreate(kIOMasterPortDefault)); |
| CFRunLoopAddSource(CFRunLoopGetMain(), |
| IONotificationPortGetRunLoopSource(notify_port_.get()), |
| kCFRunLoopDefaultMode); |
| |
| IOReturn result = IOServiceAddMatchingNotification( |
| notify_port_.get(), kIOFirstMatchNotification, |
| IOServiceMatching(kIOSerialBSDServiceValue), FirstMatchCallback, this, |
| devices_added_iterator_.InitializeInto()); |
| if (result != kIOReturnSuccess) { |
| DLOG(ERROR) << "Failed to listen for device arrival: " |
| << HexErrorCode(result); |
| return; |
| } |
| |
| // Drain |devices_added_iterator_| to arm the notification. |
| AddDevices(); |
| |
| result = IOServiceAddMatchingNotification( |
| notify_port_.get(), kIOTerminatedNotification, |
| IOServiceMatching(kIOSerialBSDServiceValue), TerminatedCallback, this, |
| devices_removed_iterator_.InitializeInto()); |
| if (result != kIOReturnSuccess) { |
| DLOG(ERROR) << "Failed to listen for device removal: " |
| << HexErrorCode(result); |
| return; |
| } |
| |
| // Drain |devices_removed_iterator_| to arm the notification. |
| RemoveDevices(); |
| } |
| |
| SerialDeviceEnumeratorMac::~SerialDeviceEnumeratorMac() = default; |
| |
| // static |
| void SerialDeviceEnumeratorMac::FirstMatchCallback(void* context, |
| io_iterator_t iterator) { |
| auto* enumerator = static_cast<SerialDeviceEnumeratorMac*>(context); |
| DCHECK_EQ(enumerator->devices_added_iterator_, iterator); |
| enumerator->AddDevices(); |
| } |
| |
| // static |
| void SerialDeviceEnumeratorMac::TerminatedCallback(void* context, |
| io_iterator_t iterator) { |
| auto* enumerator = static_cast<SerialDeviceEnumeratorMac*>(context); |
| DCHECK_EQ(enumerator->devices_removed_iterator_, iterator); |
| enumerator->RemoveDevices(); |
| } |
| |
| void SerialDeviceEnumeratorMac::AddDevices() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| base::mac::ScopedIOObject<io_service_t> device; |
| while (device.reset(IOIteratorNext(devices_added_iterator_)), device) { |
| uint64_t entry_id; |
| IOReturn result = IORegistryEntryGetRegistryEntryID(device, &entry_id); |
| if (result != kIOReturnSuccess) |
| continue; |
| |
| auto info = mojom::SerialPortInfo::New(); |
| absl::optional<uint16_t> vendor_id = |
| GetUInt16Property(device.get(), CFSTR(kUSBVendorID)); |
| absl::optional<std::string> vendor_id_str; |
| if (vendor_id) { |
| info->has_vendor_id = true; |
| info->vendor_id = *vendor_id; |
| vendor_id_str = base::StringPrintf("%04X", *vendor_id); |
| } |
| |
| absl::optional<uint16_t> product_id = |
| GetUInt16Property(device.get(), CFSTR(kUSBProductID)); |
| absl::optional<std::string> product_id_str; |
| if (product_id) { |
| info->has_product_id = true; |
| info->product_id = *product_id; |
| product_id_str = base::StringPrintf("%04X", *product_id); |
| } |
| |
| info->display_name = |
| GetStringProperty(device.get(), CFSTR(kUSBProductString)); |
| info->serial_number = |
| GetStringProperty(device.get(), CFSTR(kUSBSerialNumberString)); |
| info->usb_driver_name = GetUsbDriverName(device); |
| |
| // Each serial device has two paths associated with it: a "dialin" path |
| // starting with "tty" and a "callout" path starting with "cu". The |
| // call-out device is typically preferred but requesting the dial-in device |
| // is supported for the legacy Chrome Apps API. |
| absl::optional<std::string> dialin_device = |
| GetStringProperty(device.get(), CFSTR(kIODialinDeviceKey)); |
| absl::optional<std::string> callout_device = |
| GetStringProperty(device.get(), CFSTR(kIOCalloutDeviceKey)); |
| |
| if (callout_device) { |
| info->path = base::FilePath(*callout_device); |
| if (dialin_device) |
| info->alternate_path = base::FilePath(*dialin_device); |
| } else if (dialin_device) { |
| info->path = base::FilePath(*dialin_device); |
| } else { |
| continue; |
| } |
| |
| auto token = base::UnguessableToken::Create(); |
| info->token = token; |
| |
| SERIAL_LOG(EVENT) << "Serial device added: dialin=" |
| << dialin_device.value_or("(none)") |
| << " callout=" << callout_device.value_or("(none)") |
| << " vid=" << vendor_id_str.value_or("(none)") |
| << " pid=" << product_id_str.value_or("(none)") |
| << " usb_serial=" |
| << info->serial_number.value_or("(none)") |
| << " usb_driver=" |
| << info->usb_driver_name.value_or("(none)"); |
| |
| entries_.insert({entry_id, token}); |
| AddPort(std::move(info)); |
| } |
| } |
| |
| void SerialDeviceEnumeratorMac::RemoveDevices() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| base::mac::ScopedIOObject<io_service_t> device; |
| while (device.reset(IOIteratorNext(devices_removed_iterator_)), device) { |
| uint64_t entry_id; |
| IOReturn result = IORegistryEntryGetRegistryEntryID(device, &entry_id); |
| if (result != kIOReturnSuccess) |
| continue; |
| |
| auto it = entries_.find(entry_id); |
| if (it == entries_.end()) |
| continue; |
| |
| base::UnguessableToken token = it->second; |
| entries_.erase(it); |
| |
| RemovePort(token); |
| } |
| } |
| |
| } // namespace device |