Search code examples
c++macosusbiokitusb-mass-storage

OSX, usb mass storage discovery


I'm trying to discover USB mass storage devices under MAC OSX. I hoped to get the device class, and, on base of that, decide if the device is a mass storage or not. But, for all the usb flash drives I have, I'm getting the device class == 0, which seems to be a composite device. Please, help me to figure out, what am I doing wrong, or, maybe, what is other reliable way to discover USB Mass Storage devices (I need to get PID, VID and mount point). Here is my code:

#import <iostream>
#import <IOKit/IOkitLib.h>
#import <IOKit/usb/IOUSBLib.h>
#import <IOKit/IOCFPlugIn.h>
#import <IOKit/usb/USBSpec.h>
#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
CFMutableDictionaryRef matchingDictionary = NULL;
io_iterator_t foundIterator = 0;
io_service_t usbDevice;
matchingDictionary = IOServiceMatching(kIOUSBDeviceClassName);
IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDictionary, &foundIterator);
for(usbDevice = IOIteratorNext(foundIterator); usbDevice; usbDevice = IOIteratorNext(foundIterator))
{
    IOCFPlugInInterface** plugin = NULL;
    SInt32 theScore=0;
    IOReturn err;

    err = IOCreatePlugInInterfaceForService(usbDevice, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin, &theScore);
    if (err!= 0){
        std::cout<<"error, error code: "<<err_get_code(err) <<std::endl;
    }
    else if (plugin && *plugin)
    {
        IOUSBDeviceInterface182** usbInterface = NULL;
        (*plugin)->QueryInterface(plugin, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID182),(LPVOID*)&usbInterface);
        (*plugin)->Release(plugin);
        if (usbInterface && *usbInterface)
        {
            UInt8 devClass;
            UInt8 devSubClass;
            UInt16 productId;
            UInt16 vendorID;

            //here I'm getting 0 for all my USB flash cards
            (*usbInterface)->GetDeviceClass(usbInterface,&devClass); 
            (*usbInterface)->GetDeviceVendor(usbInterface, &vendorID);
            (*usbInterface)->GetDeviceProduct(usbInterface, &productId);
            (*usbInterface)->GetDeviceSubClass(usbInterface, &devSubClass);
            std::cout<<"device class: "<<+devClass<<std::endl;
            std::cout<<"device sub class: "<<+devSubClass<<std::endl;
            std::cout<<"vendor ID: "<<vendorID<<std::endl;
            std::cout<<"product ID: "<<productId<<std::endl;
        }
    }
    IOObjectRelease(usbDevice);
}
IOObjectRelease(foundIterator);
return 0;

}


Solution

  • For me, the following way of iterating through USB worked on OSX:

    void scanUsbMassStorage()
    {
     CFMutableDictionaryRef matchingDictionary = IOServiceMatching(kIOUSBInterfaceClassName);
     //now specify class and subclass to iterate only through USB mass storage devices:
     CFNumberRef cfValue;
     SInt32 deviceClassNum = kUSBMassStorageInterfaceClass;
     cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &deviceClassNum);
     CFDictionaryAddValue(matchingDictionary, CFSTR(kUSBInterfaceClass), cfValue);
     CFRelease(cfValue);
         SInt32 deviceSubClassNum = kUSBMassStorageSCSISubClass;
     cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &deviceSubClassNum);
     CFDictionaryAddValue(matchingDictionary, CFSTR(kUSBInterfaceSubClass), cfValue);
     CFRelease(cfValue);
     //NOTE: if you will specify only device class and will not specify subclass, it will return an empty iterator, 
     //and I don't know how to make a query for *any* subclass. 
     //BUT: all the devices I've checked had kUSBMassStorageSCSISubClass
     io_iterator_t foundIterator = 0;
     io_service_t usbInterface;
     IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDictionary, &foundIterator);
     //iterate through USB mass storage devices
     for(usbInterface = IOIteratorNext(foundIterator); usbInterface; usbInterface = IOIteratorNext(foundIterator))
     {
        CFTypeRef bsdName = IORegistryEntrySearchCFProperty(usbInterface,
                                                            kIOServicePlane,
                                                            CFSTR(kIOBSDNameKey),
                                                            kCFAllocatorDefault,
                                                            kIORegistryIterateRecursively);
    
        CFTypeRef serial = IORegistryEntrySearchCFProperty(usbInterface,
                                                           kIOServicePlane,
                                                           CFSTR(kUsbSerialPropertyName),
                                                           kCFAllocatorDefault,
                                                           kIORegistryIterateRecursively|kIORegistryIterateParents);
    
        CFTypeRef pid = IORegistryEntrySearchCFProperty(usbInterface,
                                                        kIOServicePlane,
                                                        CFSTR(kUsbPidPropertyName),
                                                        kCFAllocatorDefault,
                                                        kIORegistryIterateRecursively|kIORegistryIterateParents);
    
        CFTypeRef vid = IORegistryEntrySearchCFProperty(usbInterface,
                                                        kIOServicePlane,
                                                        CFSTR(kUsbVidPropertyName),
                                                        kCFAllocatorDefault,
                                                        kIORegistryIterateRecursively|kIORegistryIterateParents);
    
        //now we can perform checks and casts from CFTypeRef like this:
        std::string filePathStr;
        std::string serialStr;
        uint16_t pidInt;
        uint16_t vidInt;
        //getMountPathByBSDName - see below
        bool stillOk = getMountPathByBSDName(bsdName, filePath);
        if (stillOk)
        {
            stillOk = CFTypeRef2AsciiString(serial, serialStr);
        }
        if (stillOk)
        {
            stillOk = CFTypeRef2uint16(pid, pidInt);
        }
        if (stillOk)
        {
            stillOk = CFTypeRef2uint16(vid, vidInt);
        }
        if (stillOK) 
        {
          //can do something with the values here
        }
     }
    

    Getting mount path from BSD name, though, was unexpectedly tricky for me, and if someone knows a better way, please, share it. Here is my method. There is a lot of code, but at least this works in several different OSX versions:

    bool getMountPathByBSDName(CFTypeRef bsdName, std::string& dest)
    {
    std::list<std::string> bsdNames;
    //for getChildBsdNames - see below =)
    getChildBsdNames(bsdName, bsdNames);
    DASessionRef session = DASessionCreate(kCFAllocatorDefault);
    if (!session)
    {
        return false;
    }
    for (const auto& bsdNameStr : bsdNames)
    {
        DADiskRef disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, bsdNameStr.c_str());
        if (disk)
        {
            CFDictionaryRef diskInfo = DADiskCopyDescription(disk);
            if (diskInfo)
            {
                char buf[1024];
                CFURLRef fspath = (CFURLRef)CFDictionaryGetValue(diskInfo, kDADiskDescriptionVolumePathKey);
                if (CFURLGetFileSystemRepresentation(fspath, false, (UInt8 *)buf, 1024))
                {
                    //for now, return the first found partition
                    dest = std::string(buf);
                    return true;
                }
            }
         }
    }
    return false;
    }
    

    and finally - getChildBsdNames function:

    void getChildBsdNames(CFTypeRef bsdName, std::list<std::string>& tgtList)
    {
    std::string bsdNameStr;
    if(!CFTypeRef2AsciiString(bsdName, bsdNameStr))
    {
        return;
    }
    CFDictionaryRef matchingDictionary = IOBSDNameMatching(kIOMasterPortDefault, 0, bsdNameStr.c_str());
    io_iterator_t it;
    IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDictionary, &it);
    io_object_t service;
    while ((service = IOIteratorNext(it)))
    {
        io_iterator_t children;
        io_registry_entry_t child;
    
        IORegistryEntryGetChildIterator(service, kIOServicePlane, &children);
        while((child = IOIteratorNext(children)))
        {
            CFTypeRef bsdNameChild = IORegistryEntrySearchCFProperty(child,
                                                                    kIOServicePlane,
                                                                    CFSTR (kIOBSDNameKey),
                                                                    kCFAllocatorDefault,
                                                                    kIORegistryIterateRecursively);
            std::string bsdNameChildStr;
            if (CFTypeRef2AsciiString(bsdNameChild, bsdNameChildStr))
            {
                tgtList.push_back(bsdNameChildStr);
            }
        }
    }
    /**
     * The device could get name 'disk1s1, or just 'disk1'. In first case, the original bsd name would be 
     * 'disk1', and the child bsd name would be 'disk1s1'. In second case, there would be no child bsd names,
     * but the original one is valid for further work (obtaining various properties).
     */
    if (tgtList.empty())
    {
        tgtList.push_back(bsdNameStr);
    }
    }
    

    P.S. There is an event-based mechanism for that, but for some reasons for me it wasn't a solution. I've choose polling, but it is not that hard to modify this code to make it event based. Beware, though, that the event could come earlier than the device would be mounted.