'MacOS IOkit get services by property name in Swift

I would like to get all services that contain specific properties.

 AppleDeviceManagementHIDEventService  <class AppleDeviceManagementHIDEventService, id 0x100000a79, registered, matched, active, busy 0 (0 ms), retain 7>
| | | |   |   |     {
| | | |   |   |       "IOMatchedAtBoot" = Yes
| | | |   |   |       "LowBatteryNotificationPercentage" = 2
| | | |   |   |       "PrimaryUsagePage" = 65280
| | | |   |   |       "BatteryFaultNotificationType" = "TPBatteryFault"
| | | |   |   |       "HasBattery" = Yes
| | | |   |   |       "VendorID" = 76
| | | |   |   |       "VersionNumber" = 0
| | | |   |   |       "Built-In" = No
| | | |   |   |       "DeviceAddress" = "10-94-bb-ab-b9-53"
| | | |   |   |       "WakeReason" = "Host (0x01)"
| | | |   |   |       "Product" = "Magic Trackpad"
| | | |   |   |       "SerialNumber" = "10-94-bb-ab-b9-53"
| | | |   |   |       "Transport" = "Bluetooth"
| | | |   |   |       "BatteryLowNotificationType" = "TPLowBattery"
| | | |   |   |       "ProductID" = 613
| | | |   |   |       "DeviceUsagePairs" = ({"DeviceUsagePage"=65280,"DeviceUsage"=11},{"DeviceUsagePage"=65280,"DeviceUsage"=20})
| | | |   |   |       "IOPersonalityPublisher" = "com.apple.driver.AppleTopCaseHIDEventDriver"
| | | |   |   |       "MTFW Version" = 920
| | | |   |   |       "BD_ADDR" = <1094bbabb953>
| | | |   |   |       "BatteryPercent" = 42
| | | |   |   |       "BatteryStatusNotificationType" = "BatteryStatusChanged"
| | | |   |   |       "CriticallyLowBatteryNotificationPercentage" = 1
| | | |   |   |       "ReportInterval" = 11250
| | | |   |   |       "RadioFW Version" = 368
| | | |   |   |       "VendorIDSource" = 1
| | | |   |   |       "STFW Version" = 2144
| | | |   |   |       "CFBundleIdentifier" = "com.apple.driver.AppleTopCaseHIDEventDriver"
| | | |   |   |       "IOProviderClass" = "IOHIDInterface"
| | | |   |   |       "LocationID" = 1001109843
| | | |   |   |       "BluetoothDevice" = Yes
| | | |   |   |       "IOClass" = "AppleDeviceManagementHIDEventService"
| | | |   |   |       "HIDServiceSupport" = No
| | | |   |   |       "CFBundleIdentifierKernel" = "com.apple.driver.AppleTopCaseHIDEventDriver"
| | | |   |   |       "ProductIDArray" = (613)
| | | |   |   |       "BatteryStatusFlags" = 0
| | | |   |   |       "ColorID" = 5
| | | |   |   |       "IOMatchCategory" = "IODefaultMatchCategory"
| | | |   |   |       "CountryCode" = 0
| | | |   |   |       "IOProbeScore" = 7175
| | | |   |   |       "PrimaryUsage" = 11
| | | |   |   |       "IOGeneralInterest" = "IOCommand is not serializable"
| | | |   |   |       "BTFW Version" = 368
| | | |   |   |     }

Eg. every service that contains the child property "BatteryPercent".

I know that I can get specific service by using IOServiceNameMatching and then IOServiceGetMatchingServices, but this seems to not be working on the properties inside the service. Is it possible to that? I want to match by BatteryPercent and then also get ProductName.

EDIT: This is the code I'm currently using to get the trackpad battery.

func getTrackpadBattery() -> (String, Int) {

    var serialPortIterator = io_iterator_t()
    var object : io_object_t
    var percent: Int = 0
    var productName: String = ""
    let masterPort: mach_port_t = kIOMainPortDefault
    let matchingDict : CFDictionary = IOServiceMatching("AppleDeviceManagementHIDEventService")
    let kernResult = IOServiceGetMatchingServices(masterPort, matchingDict, &serialPortIterator)

    if KERN_SUCCESS == kernResult {
        repeat {
            object = IOIteratorNext(serialPortIterator)
            if object != 0 {
                let percentProperty = IORegistryEntryCreateCFProperty(object, "BatteryPercent" as CFString, kCFAllocatorDefault, 0)
                if (percentProperty != nil) {
                    percent = (percentProperty?.takeRetainedValue() as? Int)!
                    productName = (IORegistryEntryCreateCFProperty(object, "Product" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? String)!
                }
                
            }
        } while percent == 0
        IOObjectRelease(object)
    }
    IOObjectRelease(serialPortIterator)
    return (productName, percent)
}


Solution 1:[1]

(Sorry, I don't really know Swift, so my example is in Objective-C and I've attempted to provide it in Swift too but it might not be right.)

The kIOPropertyExistsMatchKey matching key should be able to do what you're after. Use it in your matching dictionary to constrain the match results to IOKit services which have the property given. For example:

    CFDictionaryRef match_dict =
        (__bridge_retained CFDictionaryRef)
        @{ @kIOPropertyExistsMatchKey : @"BatteryPercent" };

    io_service_t service = IOServiceGetMatchingService(
        kIOMasterPortDefault, match_dict);

Note that IOKit properties are for the most part not standardised, so unless you know that a specific service (super-)class publishes the property in a way that your code can process, services could export just about any data via properties with arbitrary names, which does not necessarily map to the meaning you were expecting. So you might want to constrain the registry entries you process in this way to a specific allow-list of IOService classes. (By adding the "IOProviderClass"/kIOProviderClassKey matching key to your match dictionary, which is also what IOServiceMatching() spits out.) At minimum you might want to warn the user when interpreting data from an unknown service class.

In Swift (may be wrong):

For returning all I/O registry entries with the "BatteryPercent" property, try something like:

    […]
    let matchingDict = [ kIOPropertyExistsMatchKey: "BatteryPercent" ] as NSDictionary
    let kernResult = IOServiceGetMatchingServices(masterPort, matchingDict, &iterator)
    […]

(You'll need to import Foundation to be able to use NSDictionary.)

For only returning those objects with a specific class (here: AppleDeviceManagementHIDEventService) and the "BatteryPercent" property:

    […]
    let matchingDict = IOServiceMatching("AppleDeviceManagementHIDEventService")
    CFDictionarySetValue(matchingDict, kIOPropertyExistsMatchKey, "BatteryPercent")
    let kernResult = IOServiceGetMatchingServices(masterPort, matchingDict, &iterator)
    […]

Solution 2:[2]

First of all there is a typo, replace

while percent == 0

with

while object != 0

As you have only one percent and one productName property I assume that you expect only one match. If so, add a break statement after extracting both values. My suggestion is this code

func getTrackpadBattery() -> (String, Int) {

    var serialPortIterator = io_iterator_t()
    var object : io_object_t
    var percent = 0
    var productName = ""
    let masterPort: mach_port_t = kIOMainPortDefault
    let matchingDict : CFDictionary = IOServiceMatching("AppleDeviceManagementHIDEventService")
    let kernResult = IOServiceGetMatchingServices(masterPort, matchingDict, &serialPortIterator)

    if KERN_SUCCESS == kernResult {
        repeat {
            object = IOIteratorNext(serialPortIterator)
            if object != 0 {
                if let percentProperty = IORegistryEntryCreateCFProperty(object, "BatteryPercent" as CFString, kCFAllocatorDefault, 0) {
                    percent = percentProperty.takeRetainedValue() as! Int
                }
                if let productProperty = IORegistryEntryCreateCFProperty(object, "Product" as CFString, kCFAllocatorDefault, 0) {
                    productName =  productProperty.takeRetainedValue() as! String
                }
                break
            }
        } while object != 0
        IOObjectRelease(object)
    }
    IOObjectRelease(serialPortIterator)
    return (productName, percent)
}

If you want to retrieve multiple matches remove the break statement, create an array of a custom struct and append the matches

struct Device {
    let name : String
    let batteryPercent : Int
}

func getTrackpadBattery() -> [Device] {

    var serialPortIterator = io_iterator_t()
    var object : io_object_t
    var devices = [Device]()
    let masterPort: mach_port_t = kIOMainPortDefault
    let matchingDict : CFDictionary = IOServiceMatching("AppleDeviceManagementHIDEventService")
    let kernResult = IOServiceGetMatchingServices(masterPort, matchingDict, &serialPortIterator)

    if KERN_SUCCESS == kernResult {
        repeat {
            object = IOIteratorNext(serialPortIterator)
            if object != 0 {
                var percent = 0
                var productName = ""
                if let percentProperty = IORegistryEntryCreateCFProperty(object, "BatteryPercent" as CFString, kCFAllocatorDefault, 0) {
                    percent = percentProperty.takeRetainedValue() as! Int
                }
                if let productProperty = IORegistryEntryCreateCFProperty(object, "Product" as CFString, kCFAllocatorDefault, 0) {
                    productName =  productProperty.takeRetainedValue() as! String
                }
                devices.append(Device(name: productName, batteryPercent: percent))
            }
        } while object != 0
        IOObjectRelease(object)
    }
    IOObjectRelease(serialPortIterator)
    return devices
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2