2

What's the best way I can get a list of BSD names of all USB devices (and maybe including internal Mac drives) without using a diskutil CLI wrapper?

I don't want to use any wrappers that interact with the CLI interface, as this way of interacting is quite slow and unreliable:

This is an example of why I'm not happy with using CLI wrappers
(Compare 'Time elapsed for DiskUtil CLI Wrapper.' and 'Time elapsed for Disk Arbitration')

What is the best way to implement the solution for my problem?
Use the data from IOReg?
If yes, how can I get a list of BSD names of connected devices using it?

Here is an example what I want to get:

["disk0", "disk0s1", "disk0s2", "disk0s3", "disk1", "disk1s1", "disk1s2", "disk1s3", "disk1s4", "disk2", "disk2s1", "disk2s2", "disk3", "disk3s1", "disk3s1s1", "disk3s2", "disk3s3", "disk3s4", "disk3s5", "disk3s6", "disk4", "disk4s1", "disk4s2", "disk5", "disk5s1", "disk5s2", "disk6", "disk6s1", "disk6s2", "disk10", "disk10s1", "disk10s2", "disk11", "disk11s1"]

At the moment, I have the following:

static func getMountedBSDNames() -> [String] {
    guard let session = DASessionCreate(nil) else { return [] }
    guard let mountedVolumeURLs = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil) else { return [] }
    
    var BSDNames: [String] = []
    
    for volumeURL in mountedVolumeURLs {
        if let disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, volumeURL as CFURL), let BSDName = DADiskGetBSDName(disk) {
            BSDNames.append(
                String(cString: BSDName)
            )
        }
    }
    
    return BSDNames
}

But in this case, only mounted are returning.
I want there to have even those, that were ejected

ReSophie
  • 121
  • 6

2 Answers2

2

I achieved the desired result using the IOReg lookup method:

Elapsed Time

func getDriveBSDNames() -> [String] {
    var iterator: io_iterator_t = 0

    let matching: CFDictionary = IOServiceMatching(kIOServicePlane)

    // Use 'kIOMasterPortDefault' for macOS older than 12.0 Monterey
    IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator)
    var child: io_object_t = IOIteratorNext(iterator)
    
    var BSDNames: [String] = []
    while child > 0 {
        if let BSDNameAnyObject = IORegistryEntryCreateCFProperty(child, "BSD Name" as CFString, kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)) {
            
            if let BSDNameString = (BSDNameAnyObject.takeRetainedValue() as? String), BSDNameString.starts(with: "disk") {
                BSDNames.append(
                    BSDNameString
                )
            }
        }
        
        child = IOIteratorNext(iterator)
    }
    
    return BSDNames
}

In this case, it was also necessary to filter the output of the results using:
BSDNameString.starts(with: "disk")
(otherwise, some unnecessary devices were added, such as en0, anpi0, llw0, etc.)

ReSophie
  • 121
  • 6
  • 1
    I think you might end up with serial ports, which also have BSD names, in your list of results using this method. At minimum, pass `"IOStorage"` to `IOServiceMatching()` instead of `"IOService"` (this should also improve performance). – pmdj Oct 23 '22 at 10:30
  • 2
    By the way, you can wrap your this manual iteration into a sequence, which makes it quite nice: `sequence(state: iterator, next: IOIteratorNext).prefix(while: { child in child > 0 })`. You can then use a regular `for` loop over it (instead of `while`). Even better, you can replace your loop_repeated `append` calls with a call to `map` or `compactMap`. See https://gist.github.com/amomchilov/7f53d9d7f2d20ef057be4a55871dc215 – Alexander Oct 25 '22 at 14:07
1

Note that while the Disk Arbitration framework doesn't have a function for synchronously enumerating all disks, it does effectively support asynchronous enumeration by registering a callback for disk appearance. This may or may not be useful depending on your use case - when providing the user with an interactive list of devices, this is usually exactly what you want though, as you'll automatically be notified of newly added devices.

I don't do Swift, sorry, but the following C code should be easy enough to understand to come up with something similar in other languages.

#include <DiskArbitration/DiskArbitration.h>
#include <stdio.h>

static void disk_appeared(DADiskRef disk, void* context)
{
    printf("%s\n", DADiskGetBSDName(disk) ?: "(null)");
}

int main()
{
    DASessionRef session = DASessionCreate(kCFAllocatorDefault);
    DASessionSetDispatchQueue(session, dispatch_get_main_queue());
    DARegisterDiskAppearedCallback(session, NULL, disk_appeared, NULL /*context*/);
    dispatch_main();
}

Note that the callback will also be called for APFS snapshots, which don't have a BSD name, so DADiskGetBSDName returns NULL and you'll have to do a little bit of filtering.

pmdj
  • 22,018
  • 3
  • 52
  • 103