objective-cmacosdisk-partitioningdiskarbitration

Formatting a disk in macOS programmatically using DiskManagement.framework


I have a task to implement disk formatting functionality in my code.
I am against the use of command line wrappers (e.g. diskutil), as they are slow and unreliable.

I'm importing this private framework: /System/Library/PrivateFrameworks/DiskManagement.framework
And the following headers: DMManager.h, DMEraseDisk.h, DMFilesystem.h (GitHub Repo)

I have almost everything ready, but there is one problem that I can not overcome:
Calling the eraseDisk method in DMEraseDisk freezes the application.
At the same time, the disk is formatted successfully, I just need to mount it manually.

#import <Foundation/Foundation.h>
#import <DiskArbitration/DiskArbitration.h>

#import "DiskManagement/DMManager.h"
#import "DiskManagement/DMEraseDisk.h"
#import "DiskManagement/DMFilesystem.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        /* From the public DiskArbitration.h */
        DASessionRef diskSession = DASessionCreate(nil);
        DADiskRef currentDisk = DADiskCreateFromBSDName(NULL, diskSession, "disk9s1");
        
        /* From DiskManagement.framework private headers (DMManager.h, DMEraseDisk.h, DMFilesystem.h) */
        DMManager *dmManager = [DMManager sharedManager];
        DMEraseDisk *diskEraser = [[DMEraseDisk alloc] initWithManager:dmManager];
        
        /* Getting available file systems for a given device */
        NSArray *availableFilesystems = [DMEraseDisk eraseTypesForDisk:currentDisk];
        
        printf("Available File Systems for this device:\n");
        for (DMFilesystem *availableFilesystem in availableFilesystems) {
            printf("[Type:] %s\n", [[availableFilesystem filesystemType] UTF8String]);
            printf("[Personality:] %s\n", [[availableFilesystem filesystemPersonality] UTF8String]);
            printf("---\n");
        }
        
        /* (Type: msdos, Personality: MS-DOS FAT32) */
        DMFilesystem *selectedFilesystem = [availableFilesystems objectAtIndex:2];
        
        /*
         (Formatting this device to MS-DOS FAT32)
         Formats successfully, but stops here
         and the code after this function is not executed further
         */
        
        [diskEraser
         eraseDisk: currentDisk
         synchronous: YES                // Won't work if set to NO (even with CFRunLoopRun())
         filesystem: selectedFilesystem
         bootable: YES
         name: @"RESOPHIE"
         doNewfs: YES
         doBooterCleanup: NO
        ];
        
        printf("I will never show up :(\n");
    }
    
    return 0;
}

Formatted drive in Disk Utility (needs to be mounted manually after formatting)

How can I make the code continue to execute after calling eraseDisk method?


Solution

  • I returned to solve the problem a year later.
    I migrated to Swift for personal reasons. But the logic will be the same for Objective-C, if someone actually needs this.

    The steps I have taken to solve the problem:

    SwiftDiskManager.swift

    import Foundation
    
    extension String: Error {}
    
    class SwiftDiskManager {
        typealias Callback = (CallbackData) -> Void
        
        // Because I don't know what type to use ¯\_(ツ)_/¯. NSErrorPointer doesn't work as expected, so lets just ignore it.
        typealias SuppressedDataType = AutoreleasingUnsafeMutablePointer<NSObject?>?
        
        struct CallbackData {
            let bsdName: String
            let operationName: String
            let isError: Bool
            
            init(bsdName: String, operationName: String, isError: Bool = false) {
                self.bsdName = bsdName
                self.operationName = operationName.trimmingCharacters(in: .whitespacesAndNewlines);
                self.isError = isError
            }
        }
        
        private let bsdName: String
        private let sessionDisk: DASession
        private let currentDisk: DADisk
    
        private let diskManager: DMManager
        private let diskErase: DMEraseDisk
        
        private let runLoop: CFRunLoop
        
        private var callback: Callback?
        
        static func frameworkLinkFailureString(className: String) -> String {
            return "Can't initialize '\(className)' due to DiskManagement.framework private framework linking failure."
        }
        
        init(bsdName: String) throws {
            self.bsdName = bsdName
            runLoop = CFRunLoopGetCurrent()
    
            if (!DMManager.responds(to: NSSelectorFromString("init"))) {
                throw SwiftDiskManager.frameworkLinkFailureString(className: DMManager.className())
            }
            diskManager = .init()
        
            if (!DMEraseDisk.responds(to: NSSelectorFromString("init"))) {
                throw SwiftDiskManager.frameworkLinkFailureString(className: DMEraseDisk.className())
            }
            diskErase = .init(manager: diskManager)
            
            
            guard let _sessionDisk = DASessionCreate(kCFAllocatorDefault) else {
                throw "Can't create DASession."
            }
            sessionDisk = _sessionDisk
            
            guard let _currentDisk = DADiskCreateFromBSDName(kCFAllocatorDefault, sessionDisk, bsdName) else {
                throw "Can't create DADisk with '\(bsdName)' BSD name."
            }
            currentDisk = _currentDisk
            
            // Checking if BSD device is accessible
            guard let _ = DADiskCopyDescription(currentDisk) else {
                throw "Can't get DADisk description."
            }
            
            diskManager.setDelegate(self)
            diskManager.setClientDelegate(self)
            
        }
        
        @objc private func dmAsyncStartedForDisk(_ disk: DADisk) {
            callback?(CallbackData(bsdName: bsdName, operationName: "Operation started"))
        }
        
        @objc private func dmAsyncProgressForDisk(_ disk: DADisk, barberPole: AnyObject, percent: String) {
            callback?(CallbackData(bsdName: bsdName, operationName: "In progress"))
        }
        
        @objc private func dmAsyncMessageForDisk(_ disk: DADisk, string: String, dictionary: NSDictionary) {
            callback?(CallbackData(bsdName: bsdName, operationName: string))
        }
        
        @objc private func dmAsyncFinishedForDisk(_ disk: DADisk, mainError: SuppressedDataType, detailError: SuppressedDataType, dictionary: NSDictionary) {
            let errorEncountered: Bool = (mainError != nil) || (detailError != nil)
            callback?(CallbackData(bsdName: bsdName, operationName: "Operation finished", isError: errorEncountered))
            
            CFRunLoopStop(runLoop)
        }
        
        func eraseDisk(name: String, bootable: Bool, filesystem: DMFilesystem, callback: @escaping Callback) {
            self.callback = callback
            
            diskErase.eraseDisk(
                currentDisk,
                synchronous: false,
                filesystem: filesystem,
                bootable: bootable,
                name: name,
                doNewfs: true
            )
            
            CFRunLoopRun()
    
            self.callback = nil
        }
        
    }
    

    main.swift

    import Foundation
    
    let filesystem: DMFilesystem = DMFilesystem.filesystem(forPersonality: "HFS+") as! DMFilesystem
    
    do {
        let diskManager: SwiftDiskManager = try .init(bsdName: "disk4s1")
        
        diskManager.eraseDisk(name: "HELLO_WRLD", bootable: true, filesystem: filesystem) { callbackData in
            print(callbackData)
        }
    } catch {
        print("[Error: \(error)]")
    }
    

    LanguageBridgingHeader.h

    //
    //  Use this file to import your target's public headers that you would like to expose to Swift.
    //
    
    #import "DMManager.h"
    #import "DMEraseDisk.h"
    #import "DMFilesystem.h"
    

    Console Output

    CallbackData(bsdName: "disk4s1", operationName: "Operation started", isError: false)
    CallbackData(bsdName: "disk4s1", operationName: "Unmounting disk", isError: false)
    CallbackData(bsdName: "disk4s1", operationName: "In progress", isError: false)
    CallbackData(bsdName: "disk4s1", operationName: "Erasing", isError: false)
    CallbackData(bsdName: "disk4s1", operationName: "In progress", isError: false)
    CallbackData(bsdName: "disk4s1", operationName: "Initialized /dev/rdisk4s1 as a 7 GB case-insensitive HFS Plus volume", isError: false)
    CallbackData(bsdName: "disk4s1", operationName: "Mounting disk", isError: false)
    CallbackData(bsdName: "disk4s1", operationName: "In progress", isError: false)
    CallbackData(bsdName: "disk4s1", operationName: "Operation finished", isError: false)
    Program ended with exit code: 0
    

    Disk Utility Disk Utility Screenshot

    At the end, it works as expected, so the issue was solved.
    Important notice: I made this class synchronous, but you're always free to remove the CFRunLoop and add your corrections.