Search code examples
swiftnstask

Multiple NSTasks Causing Inconsistent Output


I am working on a Swift app that interacts with external binaries (via NSTask) and returns the output as an array of strings, for each line.

When calling the executeCommand function multiple times, commands start failing to return expected data.

In the example Playground below I'm polling diskutil for volume names.

In the Debug Area the first response I get is:

  • Not applicable (no file system)
  • Macintosh HD
  • Unable to obtain volume name
  • Unable to obtain volume name
  • Unable to obtain volume name

Simply re-running the Playground results in:

  • Not applicable (no file system)
  • Unable to obtain volume name <-- What happened to Macintosh HD?
  • Unable to obtain volume name
  • Unable to obtain volume name
  • Unable to obtain volume name

        import Cocoa
    
    func executeCommand(launchPath: String, arguments: [String], additionalDelay: Int=0) -> [String] {
    
        let outputPipe = NSPipe()
        var outputArray = [String]()
    
        let task = NSTask()
        task.launchPath = launchPath
        task.arguments = arguments
        task.standardOutput = outputPipe
    
        outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
    
        NSNotificationCenter.defaultCenter().addObserverForName(NSFileHandleDataAvailableNotification, object: outputPipe.fileHandleForReading , queue: nil) {
            notification in
    
            let output = outputPipe.fileHandleForReading.availableData
            let outputString = String(data: output, encoding: NSUTF8StringEncoding) ?? ""
    
            outputArray = outputString.componentsSeparatedByCharactersInSet(.newlineCharacterSet())
    
            outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
    
        }
    
        task.launch()
        sleep(UInt32(additionalDelay)) // Additional delay
        task.waitUntilExit()
    
        // Remove last empty line from output array.
        if outputArray.count > 0 {
            outputArray.removeLast()
        }
    
        return outputArray
    
    }
    
    
    // Example usage
    
    
    
    // Output volume name from provided disk identifier
    func volumeName(identifier: String) -> String {
        let volumeNameCommand = "/usr/sbin/diskutil info \(identifier) | awk '/Volume Name:/' | sed 's/Volume Name://g'| sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'"
        let volumeNameOutput = executeCommand("/bin/bash", arguments: ["-c", volumeNameCommand])
    
        var volumeName = "Unable to obtain volume name"
    
        if volumeNameOutput.count > 0 {
            volumeName = volumeNameOutput[0]
        }
    
        return volumeName
    }
    
    let identifiers = ["/dev/disk0","/dev/disk1","/dev/disk2","/dev/disk3","/dev/disk4"]
    
    for identifier in identifiers {
        print(volumeName(identifier))
    }
    

I really need consistent results, but don't understand where I'm going wrong.

Any help would be greatly appreciated!


Solution

  • I believe the asynchronous execution of tasks was causing the inconsistent results. Below is a new function working as expected:

    func executeCommand(launchPath: String, arguments: [String]) -> [String] {
    
        let task: NSTask = NSTask()
        let pipe: NSPipe = NSPipe()
    
        task.launchPath = launchPath
        task.arguments = arguments
        task.standardOutput = pipe
        task.launch()
        task.waitUntilExit()
    
        let handle = pipe.fileHandleForReading
        let data = handle.readDataToEndOfFile()
        let outputString = String(data: data, encoding: NSUTF8StringEncoding) ?? ""
    
        var outputArray = [String]()
        outputArray = outputString.componentsSeparatedByCharactersInSet(.newlineCharacterSet())
        return outputArray
    
    }