diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ddd5d1..8afdf32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$") endif() cmake_minimum_required(VERSION 3.21) -project(xone_macos VERSION 0.1.3 LANGUAGES CXX Swift) +project(xone_macos VERSION 0.1.4 LANGUAGES CXX Swift) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/src/usb/usb_transport.cpp b/src/usb/usb_transport.cpp index fa7a7f2..632bbea 100644 --- a/src/usb/usb_transport.cpp +++ b/src/usb/usb_transport.cpp @@ -27,14 +27,35 @@ constexpr std::size_t num_in_reads = 4; namespace { -auto vid_match_dict() -> CFMutableDictionaryRef +// Numeric property matching on "idVendor"/"idProduct" is unreliable on some +// macOS versions (single-key matches fail while combined matches work), so +// we match the IOUSBDevice class and filter VID/PID in user space. The +// numeric values live in the "idVendor"/"idProduct" registry properties. +struct vid_pid { + std::uint16_t vid = 0; + std::uint16_t pid = 0; +}; + +auto read_vid_pid(io_service_t service) -> std::optional { - CFMutableDictionaryRef match = IOServiceMatching("IOUSBDevice"); - SInt32 vid_value = static_cast(vid); - CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &vid_value); - CFDictionaryAddValue(match, CFSTR(kUSBVendorString), number); - CFRelease(number); - return match; + CFMutableDictionaryRef props = nullptr; + if (IORegistryEntryCreateCFProperties(service, &props, kCFAllocatorDefault, 0) + != kIOReturnSuccess + || !props) + return std::nullopt; + + auto *vid_number = static_cast(CFDictionaryGetValue(props, CFSTR("idVendor"))); + auto *pid_number = static_cast(CFDictionaryGetValue(props, CFSTR("idProduct"))); + SInt32 v = 0, p = 0; + std::optional result; + if (vid_number && pid_number + && CFNumberGetValue(vid_number, kCFNumberSInt32Type, &v) + && CFNumberGetValue(pid_number, kCFNumberSInt32Type, &p)) { + result = { static_cast(v), static_cast(p) }; + } + + CFRelease(props); + return result; } auto is_supported_pid(std::uint16_t pid) -> bool @@ -43,25 +64,9 @@ auto is_supported_pid(std::uint16_t pid) -> bool || pid == pid_builtin_asus_lenovo || pid == pid_surface_book_2; } -// Read the "kUSBProductString" property (a CFNumber) of a USB device service. -// Returns 0 if the property is missing or unreadable. -auto read_pid(io_service_t service) -> std::uint16_t +auto is_dongle(vid_pid const &vp) -> bool { - CFMutableDictionaryRef props = nullptr; - if (IORegistryEntryCreateCFProperties(service, &props, kCFAllocatorDefault, 0) != kIOReturnSuccess - || !props) - return 0; - - auto *number = static_cast(CFDictionaryGetValue(props, CFSTR(kUSBProductString))); - std::uint16_t pid = 0; - if (number) { - SInt32 value = 0; - if (CFNumberGetValue(number, kCFNumberSInt32Type, &value)) - pid = static_cast(value); - } - - CFRelease(props); - return pid; + return vp.vid == vid && is_supported_pid(vp.pid); } } // namespace @@ -69,7 +74,7 @@ auto read_pid(io_service_t service) -> std::uint16_t struct transport::read_slot { transport *owner = nullptr; std::uint8_t ep = 0; - IOUSBInterfaceInterface190 *iface = nullptr; + IOUSBInterfaceInterface190 **iface_ref = nullptr; UInt8 pipe_ref = 0; std::vector buf; }; @@ -78,21 +83,21 @@ struct transport::state { std::uint16_t pid = 0; // Device connection (port of the usb_device + usb_interface binding). - // Plug-in interface references are double pointers; QI'd interfaces are - // single struct pointers. + // All interface references are double pointers: *ref is the interface + // struct, and methods are called as (*ref)->Method(ref, ...). io_service_t service = 0; IOCFPlugInInterface **dev_iodev = nullptr; - IOUSBDeviceInterface197 *dev = nullptr; + IOUSBDeviceInterface197 **dev_ref = nullptr; bool dev_opened = false; struct iface_conn { IOCFPlugInInterface **iodev = nullptr; - IOUSBInterfaceInterface190 *iface = nullptr; + IOUSBInterfaceInterface190 **iface_ref = nullptr; }; std::vector ifaces; struct pipe_ref { - IOUSBInterfaceInterface190 *iface = nullptr; + IOUSBInterfaceInterface190 **iface_ref = nullptr; UInt8 ref = 0; }; std::optional in_cmd_pipe; @@ -143,7 +148,7 @@ transport::~transport() state_->stopping = true; } for (auto &slot : state_->slots) - slot.iface->AbortPipe(slot.iface, slot.pipe_ref); + (*slot.iface_ref)->AbortPipe(slot.iface_ref, slot.pipe_ref); if (state_->thread_started) { CFRunLoopRef runloop = nullptr; @@ -168,16 +173,16 @@ transport::~transport() // Close the interfaces and their endpoint pipes. for (auto &conn : state_->ifaces) { - conn.iface->USBInterfaceClose(conn.iface); - conn.iface->Release(conn.iface); + (*conn.iface_ref)->USBInterfaceClose(conn.iface_ref); + (*conn.iface_ref)->Release(conn.iface_ref); IODestroyPlugInInterface(conn.iodev); } // Close the device connection. - if (state_->dev) { + if (state_->dev_ref) { if (state_->dev_opened) - state_->dev->USBDeviceClose(state_->dev); - state_->dev->Release(state_->dev); + (*state_->dev_ref)->USBDeviceClose(state_->dev_ref); + (*state_->dev_ref)->Release(state_->dev_ref); } if (state_->dev_iodev) IODestroyPlugInInterface(state_->dev_iodev); @@ -192,9 +197,9 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) -> state_->frames = std::move(frames); state_->disconnected = std::move(disconnected); - // Find the dongle service (VID match, filter by PID). + // Find the dongle service (class match, filter by VID/PID). io_iterator_t iter = 0; - kern_return_t kr = IOServiceGetMatchingServices(0, vid_match_dict(), &iter); + kern_return_t kr = IOServiceGetMatchingServices(0, IOServiceMatching("IOUSBDevice"), &iter); if (kr != kIOReturnSuccess) return false; @@ -203,10 +208,10 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) -> io_service_t service = IOIteratorNext(iter); if (!service) break; - std::uint16_t pid = read_pid(service); - if (is_supported_pid(pid)) { + auto vp = read_vid_pid(service); + if (vp && is_dongle(*vp)) { state_->service = service; - state_->pid = pid; + state_->pid = vp->pid; found = true; } else { IOObjectRelease(service); @@ -225,17 +230,16 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) -> return false; } - auto *dev = static_cast(nullptr); + void *slot = nullptr; HRESULT hr = (*state_->dev_iodev)->QueryInterface(state_->dev_iodev, - CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID197), - reinterpret_cast(&dev)); - if (hr != S_OK || !dev) { + CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID197), &slot); + if (hr != S_OK || !slot) { xone::log_msg(log_level::error, "usb: query device interface failed"); return false; } - state_->dev = dev; + state_->dev_ref = static_cast(slot); - kr = dev->USBDeviceOpen(dev); + kr = (*state_->dev_ref)->USBDeviceOpen(state_->dev_ref); if (kr != kIOReturnSuccess) { xone::log_msg(log_level::error, "usb: open device failed (%d)", kr); return false; @@ -267,7 +271,8 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) -> // Async completion dispatch for the opened interfaces. for (auto &conn : state_->ifaces) { CFRunLoopSourceRef source = nullptr; - if (conn.iface->CreateInterfaceAsyncEventSource(conn.iface, &source) == kIOReturnSuccess + if ((*conn.iface_ref)->CreateInterfaceAsyncEventSource(conn.iface_ref, &source) + == kIOReturnSuccess && source) state_->sources.push_back(source); } @@ -276,7 +281,8 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) -> // matching dictionary is consumed by this call. state_->notify_port = IONotificationPortCreate(0); kr = IOServiceAddMatchingNotification(state_->notify_port, kIOTerminatedNotification, - vid_match_dict(), on_dongle_terminated, this, + IOServiceMatching("IOUSBDevice"), + on_dongle_terminated, this, &state_->termination_iter); if (kr != kIOReturnSuccess) { xone::log_msg(log_level::error, "usb: add termination notification failed (%d)", kr); @@ -293,7 +299,7 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) -> std::uint8_t ep, std::size_t buf_len) -> bool { for (std::size_t i = 0; i < num_in_reads; i++) { state_->slots.push_back( - { this, ep, pipe->iface, pipe->ref, std::vector(buf_len) }); + { this, ep, pipe->iface_ref, pipe->ref, std::vector(buf_len) }); if (!submit_read(&state_->slots.back())) return false; } @@ -318,17 +324,17 @@ auto transport::open_interface(io_service_t child) -> bool != kIOReturnSuccess) return false; - auto *iface = static_cast(nullptr); + void *slot = nullptr; if ((*conn.iodev)->QueryInterface(conn.iodev, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID190), - reinterpret_cast(&iface)) != S_OK - || !iface) { + &slot) != S_OK + || !slot) { IODestroyPlugInInterface(conn.iodev); return false; } - conn.iface = iface; + conn.iface_ref = static_cast(slot); - if (iface->USBInterfaceOpen(iface) != kIOReturnSuccess) { - iface->Release(iface); + if ((*conn.iface_ref)->USBInterfaceOpen(conn.iface_ref) != kIOReturnSuccess) { + (*conn.iface_ref)->Release(conn.iface_ref); IODestroyPlugInInterface(conn.iodev); return false; } @@ -337,24 +343,24 @@ auto transport::open_interface(io_service_t child) -> bool // Scan the interface's pipes for the endpoints we need. UInt8 num_pipes = 0; - if (iface->GetNumEndpoints(iface, &num_pipes) != kIOReturnSuccess) + if ((*conn.iface_ref)->GetNumEndpoints(conn.iface_ref, &num_pipes) != kIOReturnSuccess) return true; for (UInt8 i = 1; i <= num_pipes; i++) { UInt8 direction = 0, number = 0, type = 0; UInt16 max_packet_size = 0; UInt8 interval = 0; - if (iface->GetPipeProperties(iface, i, &direction, &number, &type, - &max_packet_size, &interval) != kIOReturnSuccess) + if ((*conn.iface_ref)->GetPipeProperties(conn.iface_ref, i, &direction, &number, &type, + &max_packet_size, &interval) != kIOReturnSuccess) continue; std::uint8_t ep = static_cast(number); if (direction == kUSBIn && ep == ep_in_cmd && !state_->in_cmd_pipe.has_value()) - state_->in_cmd_pipe = { iface, i }; + state_->in_cmd_pipe = { conn.iface_ref, i }; else if (direction == kUSBIn && ep == ep_in_wlan && !state_->in_wlan_pipe.has_value()) - state_->in_wlan_pipe = { iface, i }; + state_->in_wlan_pipe = { conn.iface_ref, i }; else if (direction == kUSBOut && ep == ep_out && !state_->out_pipe.has_value()) - state_->out_pipe = { iface, i }; + state_->out_pipe = { conn.iface_ref, i }; } return true; @@ -378,7 +384,7 @@ auto transport::send_vendor_request(vendor_request req, bool is_read, std::uint1 request.wLength = static_cast(len); request.pData = data; - IOReturn ret = state_->dev->DeviceRequest(state_->dev, &request); + IOReturn ret = (*state_->dev_ref)->DeviceRequest(state_->dev_ref, &request); if (ret != kIOReturnSuccess || request.wLenDone != len) { xone::log_msg(log_level::error, "usb: vendor request 0x%02x failed (%d)", static_cast(req), ret); @@ -391,8 +397,8 @@ auto transport::send_vendor_request(vendor_request req, bool is_read, std::uint1 auto transport::bulk_write(void const *data, std::size_t len) -> int { auto &pipe = state_->out_pipe.value(); - IOReturn ret = pipe.iface->WritePipe(pipe.iface, pipe.ref, - const_cast(data), static_cast(len)); + IOReturn ret = (*pipe.iface_ref)->WritePipe(pipe.iface_ref, pipe.ref, + const_cast(data), static_cast(len)); if (ret != kIOReturnSuccess) { xone::log_msg(log_level::error, "usb: bulk write failed (%d)", ret); return -EIO; @@ -410,9 +416,9 @@ auto transport::submit_read(read_slot *slot) -> bool state_->in_flight++; } - IOReturn ret = slot->iface->ReadPipeAsync(slot->iface, slot->pipe_ref, - slot->buf.data(), static_cast(slot->buf.size()), - on_read_completion, slot); + IOReturn ret = (*slot->iface_ref)->ReadPipeAsync(slot->iface_ref, slot->pipe_ref, + slot->buf.data(), static_cast(slot->buf.size()), + on_read_completion, slot); if (ret != kIOReturnSuccess) { std::lock_guard lock(state_->lock); state_->in_flight--; @@ -453,9 +459,9 @@ void transport::handle_read(read_slot *slot, IOReturn result, std::size_t len) return; } - IOReturn ret = slot->iface->ReadPipeAsync(slot->iface, slot->pipe_ref, - slot->buf.data(), static_cast(slot->buf.size()), - on_read_completion, slot); + IOReturn ret = (*slot->iface_ref)->ReadPipeAsync(slot->iface_ref, slot->pipe_ref, + slot->buf.data(), static_cast(slot->buf.size()), + on_read_completion, slot); if (ret != kIOReturnSuccess) { state_->in_flight--; state_->cv.notify_all(); @@ -494,7 +500,8 @@ void transport::on_dongle_terminated(void *refcon, io_iterator_t iter) io_service_t service = IOIteratorNext(iter); if (!service) break; - if (read_pid(service) == t->state_->pid) + auto vp = read_vid_pid(service); + if (vp && vp->vid == vid && vp->pid == t->state_->pid) ours = true; IOObjectRelease(service); } @@ -506,7 +513,7 @@ void transport::on_dongle_terminated(void *refcon, io_iterator_t iter) auto dongle_present() -> bool { io_iterator_t iter = 0; - kern_return_t kr = IOServiceGetMatchingServices(0, vid_match_dict(), &iter); + kern_return_t kr = IOServiceGetMatchingServices(0, IOServiceMatching("IOUSBDevice"), &iter); if (kr != kIOReturnSuccess) return false; @@ -515,7 +522,8 @@ auto dongle_present() -> bool io_service_t service = IOIteratorNext(iter); if (!service) break; - if (is_supported_pid(read_pid(service))) + auto vp = read_vid_pid(service); + if (vp && is_dongle(*vp)) present = true; IOObjectRelease(service); }