Formatting a disk in macOS programmatically using DiskManagement.framework

165 Views Asked by At

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?

1

There are 1 best solutions below

0
On BEST ANSWER

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:

  • Reverse-engineered the DiskManagement.framework binary from Mac OS X Mavericks.
    (Newer versions of macOS moved away from binary frameworks, as I can see.)
    • Examined the eraseDisk:currentDisk:synchronous:filesystem:bootable:name:doNewfs: method logic on DMManager object.
      • Noticed the 'success' requirement for the - (bool)checkClientDelegate; (if synchronous == NO).
  • Created an object called SwiftDiskManager.
  • Configured the delegate methods: diskManager.setDelegate(self) and diskManager.setClientDelegate(self) in order to send all messages to the SwiftDiskManager object.
  • Overridden the func responds(to aSelector: Selector!) -> Bool method in order to track which methods DMManager is trying to access and then implemented them.
  • Got a 'true' output from the - (bool)checkClientDelegate; method.

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.