Reputation: 808
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?
Upvotes: 13
Views: 11150
Reputation: 14983
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: .utf8) {
// 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.
Upvotes: 43
Reputation: 1792
I have an answer which I believe is more clean than the notification approach, based on a readabilityHandler. Here it is, in Swift 5:
class ProcessViewController: NSViewController {
var executeCommandProcess: Process!
func executeProcess() {
DispatchQueue.global().async {
self.executeCommandProcess = Process()
let pipe = Pipe()
self.executeCommandProcess.standardOutput = pipe
self.executeCommandProcess.launchPath = ""
self.executeCommandProcess.arguments = []
var bigOutputString: String = ""
pipe.fileHandleForReading.readabilityHandler = { (fileHandle) -> Void in
let availableData = fileHandle.availableData
let newOutput = String.init(data: availableData, encoding: .utf8)
bigOutputString.append(newOutput!)
print("\(newOutput!)")
// Display the new output appropriately in a NSTextView for example
}
self.executeCommandProcess.launch()
self.executeCommandProcess.waitUntilExit()
DispatchQueue.main.async {
// End of the Process, give feedback to the user.
}
}
}
}
Please note that the Process has to be a property, because in the above example, given that the command is executed in background, the process would be deallocated immediately if it was a local variable. Thanks for your attention.
Upvotes: 3
Reputation: 81
This is the update version of Martin's answer above for the latest version of Swift.
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "echo 1 ; sleep 1 ; echo 2 ; sleep 1 ; echo 3 ; sleep 1 ; echo 4"]
let pipe = Pipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()
var obs1 : NSObjectProtocol!
obs1 = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable,
object: outHandle, queue: nil) { notification -> Void in
let data = outHandle.availableData
if data.count > 0 {
if let str = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
print("got output: \(str)")
}
outHandle.waitForDataInBackgroundAndNotify()
} else {
print("EOF on stdout from process")
NotificationCenter.default.removeObserver(obs1)
}
}
var obs2 : NSObjectProtocol!
obs2 = NotificationCenter.default.addObserver(forName: Process.didTerminateNotification,
object: task, queue: nil) { notification -> Void in
print("terminated")
NotificationCenter.default.removeObserver(obs2)
}
task.launch()
Upvotes: 8
Reputation: 539975
(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).
Upvotes: 25