Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Real time NSTask output to NSTextView with Swift

I'm using an NSTask to run rsync, and I'd like the status to show up in the text view of a scroll view inside a window. Right now I have this:

let pipe = NSPipe()
task2.standardOutput = pipe
task2.launch()

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: NSASCIIStringEncoding)! as String

textView.string = output

And that get's me the some of the statistics about the transfer, but I'd like to get the output in real time, like what get's printed out when I run the app in Xcode, and put it into the text view. Is there a way to do this?

like image 423
The Beanstalk Avatar asked Apr 09 '15 20:04

The Beanstalk


2 Answers

Since macOS 10.7, there's also the readabilityHandler property on NSPipe which you can use to set a callback for when new data is available:

let task = NSTask()

task.launchPath = "/bin/sh"
task.arguments = ["-c", "echo 1 ; sleep 1 ; echo 2 ; sleep 1 ; echo 3 ; sleep 1 ; echo 4"]

let pipe = NSPipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading

outHandle.readabilityHandler = { pipe in
    if let line = String(data: pipe.availableData, encoding: NSUTF8StringEncoding) {
        // Update your view with the new text here
        print("New ouput: \(line)")
    } else {
        print("Error decoding data: \(pipe.availableData)")
    }
}

task.launch()

I'm surprised nobody mentioned this, as it's a lot simpler.

like image 184
Kametrixom Avatar answered Nov 06 '22 02:11

Kametrixom


(See Patrick F.'s answer for an update to Swift 3/4.)

You can read asynchronously from a pipe, using notifications. Here is a simple example demonstrating how it works, hopefully that helps you to get started:

let task = NSTask()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "echo 1 ; sleep 1 ; echo 2 ; sleep 1 ; echo 3 ; sleep 1 ; echo 4"]

let pipe = NSPipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()

var obs1 : NSObjectProtocol!
obs1 = NSNotificationCenter.defaultCenter().addObserverForName(NSFileHandleDataAvailableNotification,
    object: outHandle, queue: nil) {  notification -> Void in
        let data = outHandle.availableData
        if data.length > 0 {
            if let str = NSString(data: data, encoding: NSUTF8StringEncoding) {
                print("got output: \(str)")
            }
            outHandle.waitForDataInBackgroundAndNotify()
        } else {
            print("EOF on stdout from process")
            NSNotificationCenter.defaultCenter().removeObserver(obs1)
        }
}

var obs2 : NSObjectProtocol!
obs2 = NSNotificationCenter.defaultCenter().addObserverForName(NSTaskDidTerminateNotification,
    object: task, queue: nil) { notification -> Void in
        print("terminated")
        NSNotificationCenter.defaultCenter().removeObserver(obs2)
}

task.launch()

Instead of print("got output: \(str)") you can append the received string to your text view.

The above code assumes that a runloop is active (which is the case in a default Cocoa application).

like image 28
Martin R Avatar answered Nov 06 '22 04:11

Martin R