NSTask output buffer size issue (Running SPApplicationsDataType command )

657 views Asked by At

Try to read some information from system profiler. For this purpose i m running some terminal line commands with NSTask. If i run some command which output not too big there is no problem.(For example : SPInstallHistoryDataType) But if i run "SPApplicationsDataType" command to collect installed application list, NSTask waits too much without any result and any error.

So i started to thing there should be a buffer size or something like that and i could not find anything about that. I don't know maybe i m on wrong way.

func readData (dataType: String) -> NSArray? {
let out = NSPipe()
let task = NSTask()
task.launchPath = "/usr/sbin/system_profiler"
task.arguments = ["-xml",dataType]
task.standardOutput = out
task.launch()

task.waitUntilExit()

if task.terminationStatus != 0 {
    NSLog("system_profiler returned error status")
    return nil
}

let data = out.fileHandleForReading.readDataToEndOfFile()
let plist : AnyObject?
do {
    plist = try NSPropertyListSerialization.propertyListWithData(data,
        options: [.Immutable],
        format: nil)
} catch let error as NSError {
    NSLog("%@", "Failed to parse system_profiler results. \(error.localizedDescription)")
    return nil
}

return plist as? NSArray
}
let r = readData("SPInstallHistoryDataType")// There is no problem
let r2 = readData("SPApplicationsDataType") // Crash

Note : Yes i could write this data to file and read from that file. But i try to understand what is the problem.

2

There are 2 answers

1
Alex M On

It's definitely a buffer issue. When you read a chunk at a time, it works.

func getApplications() -> String?
{
    var retval=""
    let theTask = NSTask()
    let taskOutput = NSPipe()
    theTask.launchPath = "/usr/sbin/system_profiler"
    theTask.standardOutput = taskOutput
    theTask.standardError = taskOutput
    theTask.arguments = ["-xml", "SPApplicationsDataType"]
    theTask.launch()

    while (true) {
        let data = taskOutput.fileHandleForReading.readDataOfLength(1024)
        if (data.length <= 0) { break }
        let str = String(data: data, encoding: NSUTF8StringEncoding)!
        retval += str

        //print (str)
    }

    theTask.waitUntilExit()

    return retval
}
0
j.s.com On

I have a similar problem, on a new Mac Pro, but even worse. With macOS 10.15.3 Catalina I am not able to get system_profiler data for "SPAudioDataType". Other processes like curl etc. can be called but system_profiler is a problem.

The very funny thing with my problem was, that is only occurred about 10 minutes after making a fresh restart. In the first 10 minutes everything worked, with or without using handlers and even with the code "getApplications" from the answer above.

And yes, of course I run it in the main thread, but it makes no difference running in main thread or not.

I experimented lot to look, what is the source for this behavior. I found out, that my programs hangs while reading data with the command

let data = taskOutput.fileHandleForReading.readDataOfLength(1024)

in the case that there is error data available and vice versa the program hangs while reading error messages with the command

let data = taskError.fileHandleForReading.readDataOfLength(1024)

in the case that there is normal data available (but no error data).

The Program even hangs if I tried to get the amount of data, which is currently available:

let c = taskError.fileHandleForReading.availableData.count

Regardless what I test first, the program hangs if there is no data available.

So I completely rewrote my function to using async handlers:

@discardableResult func launchprogram (_ launchpath: String, _ arguments: [String]) -> (result: String, error: Int)
{
    var out: String  = ""           // Output
    var err: String  = ""           // Error Messages
    var fin: Bool    = false        // If the process exits normally
    let pro: Process = Process()

    pro.arguments      = arguments
    pro.launchPath     = launchpath
    pro.standardOutput = Pipe()
    pro.standardError  = Pipe()
    let proOut: Pipe   = pro.standardOutput as! Pipe
    let proIn: Pipe    = pro.standardError  as! Pipe

    proOut.fileHandleForReading.readabilityHandler =
    {
        pipe in
        if let line = String(data: pipe.availableData, encoding: String.Encoding.utf8)
        {
            if line.count > 0 // Neuen Ausgabe-Text hinzufügen
            {
                out += line
            }
        }
    }

    proIn.fileHandleForReading.readabilityHandler =
    {
        pipe in
        if let line = String(data: pipe.availableData, encoding: String.Encoding.utf8)
        {
            if line.count > 0 // Neuen Fehler-Text hinzufügen
            {
                err += line
            }
        }
    }

    pro.terminationHandler =
    {
        (process) in
        fin = not(process.isRunning)
    }

    pro.launch()
    pro.waitUntilExit()

    if err == ""
    {
        if fin
        {
            return (out, 0)
        }
        else
        {
            return (out, -1)
        }
    }
    else if out == ""
    {
        let message: String = "Error while executing:" + char(13) + char(13)
        return (message + err, -2)
    }
    else
    {
        let message: String = char(13) + char(13) + "Error while executing:" + char(13) + char(13)
        return (out + message + err, -3)
    }
}

The fundamental difference between this function and the function "getApplications" from the last post is, that I use "handler" to manage the output und error message streams. This always works. Deployment Target can bei 10.9 or above. I did not test it with 10.8 and earlier. So my problem was, that in Catalina under some circumstances it is not longer possible to get the information in "normal" synchronous order but only async with using handlers. If I break the execution, I always be in something like "libsystem_kernel.dylibread" withe the calling function "Foundation_NSReadFromFileDescriptorWithProgress". I would be glad to know, if this is a Catalina issue (with a new Mac Pro) or a fundamental change in what Apple wants us to use.