I have this code to continously read the output of a started process:
let task = Process()
task.arguments = ["-c", command]
task.launchPath = "/bin/zsh"
let pipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = pipe
task.standardError = errorPipe
let outHandle = pipe.fileHandleForReading
let errorHandle = errorPipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()
errorHandle.waitForDataInBackgroundAndNotify()
var updateObserver: NSObjectProtocol!
updateObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: outHandle, queue: nil, using: { notification in
let data = outHandle.availableData
if !data.isEmpty {
if let str = String(data: data, encoding: .utf8) {
print(str) // This is differently here.
}
outHandle.waitForDataInBackgroundAndNotify()
} else {
NotificationCenter.default.removeObserver(updateObserver!)
}
})
var errorObserver: NSObjectProtocol!
errorObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: errorHandle, queue: nil, using: { notification in
let data = errorHandle.availableData
if !data.isEmpty {
if let str = String(data: data, encoding: .utf8) {
print(str) // This is differently here.
}
errorHandle.waitForDataInBackgroundAndNotify()
} else {
NotificationCenter.default.removeObserver(errorObserver!)
}
})
var taskObserver : NSObjectProtocol!
taskObserver = NotificationCenter.default.addObserver(forName: Process.didTerminateNotification, object: task, queue: nil, using: { notification in
print("terminated")
NotificationCenter.default.removeObserver(taskObserver!)
})
task.launch()
Now this works for processes that print a new line with every change.
What does not work is to get outputs of processes that edit already printed lines (changin percentage or something like that).
In that case, the pipe is not pushing anything.
How would I handle that case. I thought about doing the output into a file, read that and at the end deleting it. Is that a possible solution?
Related
Everything is working fine, but I don't understand why the wrong label "loading" doesn't update the percentage. while i print the output value is continuously changing
do {
#discardableResult
func shell(_ args: String...) -> (String?, Int32) {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = args
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
task.waitUntilExit()
return (output, task.terminationStatus)
}
shell(....)
let strURL = "http://download.com/load.zip"
let url = URL(string: strURL)
FileDownloader(url! as NSObject).download(url: url!)
var result = ""
func checkdl() {
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
let fileURL = dir!.appendingPathComponent("load.txt")
result = try! String(contentsOf: fileURL, encoding: .utf8)
print(result)
if result.contains("100.0") {
try! "".write(to: fileURL, atomically: true, encoding: String.Encoding.utf8)
print("download done")
} else {
sleep(2)
loading.stringValue = result+"%"
checkdl()
}
}
checkdl()
shell(....)
}
Everything is working fine, but I don't understand why the wrong label "loading" doesn't update the percentage. while i print the output value is continuously changing
I have the following Code:
func syncShellExec(path: String, args: [String] = []) {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = [path] + args
let outputPipe = Pipe()
let filelHandler = outputPipe.fileHandleForReading
process.standardOutput = outputPipe
process.launch()
filelHandler.readabilityHandler = { pipe in
let data = pipe.availableData
if let line = String(data: data, encoding: String.Encoding.utf8) {
DispatchQueue.main.sync {
self.output_window.string += line
self.output_window.scrollToEndOfDocument(nil)
}
} else {
print("Error decoding data: \(data.base64EncodedString())")
}
}
process.waitUntilExit()
filelHandler.readabilityHandler = nil
}
If the amount of round about 330.000 Characters is reached the output stopped immediately. Is there a way to increase the Buffer for this Operation?
There are two problems:
The process may terminate before all data has been read from the pipe.
Your function blocks the main thread, so that the UI eventually freezes.
Similarly as in How can I tell when a FileHandle has nothing left to be read? you should wait asynchronously for the process to terminate, and also wait for “end-of-file” on the pipe:
func asyncShellExec(path: String, args: [String] = []) {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = [path] + args
let outputPipe = Pipe()
let filelHandler = outputPipe.fileHandleForReading
process.standardOutput = outputPipe
process.launch()
let group = DispatchGroup()
group.enter()
filelHandler.readabilityHandler = { pipe in
let data = pipe.availableData
if data.isEmpty { // EOF
filelHandler.readabilityHandler = nil
group.leave()
return
}
if let line = String(data: data, encoding: String.Encoding.utf8) {
DispatchQueue.main.sync {
self.output_window.string += line
self.output_window.scrollToEndOfDocument(nil)
}
} else {
print("Error decoding data: \(data.base64EncodedString())")
}
}
process.terminationHandler = { process in
group.wait()
DispatchQueue.main.sync {
// Update UI that process has finished.
}
}
}
I have a java program running in the background of my swift program. The java program can read user input in a command line. How can I pass "commands", or text line into the jar after it has been launched?
Just like the way the program down below read from the java program, how can I then "reply" to it?
let b = Bundle.main
let path = b.path(forResource: "myjar", ofType: "jar")!
NSLog("%#", "jar path : \(path)")
task.launchPath = "/usr/bin/java"
task.arguments = ["-jar", path]
let pipe = Pipe()
task.standardOutput = pipe
let errorPipe = Pipe()
task.standardError = errorPipe
task.launch()
let outHandle = pipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()
var progressObserver : NSObjectProtocol!
progressObserver = 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 = String(data: data, encoding: String.Encoding.utf8) {
NSLog("%#", str)
}
outHandle.waitForDataInBackgroundAndNotify()
} else { NotificationCenter.default.removeObserver(progressObserver)
}
}
I then tried the following without luck:
pipe.fileHandleForWriting.write("text to send to java program".data(using: String.Encoding.utf8)
You're pretty close. If you just added the code pipe.fileHandleForWriting.write("text to send to java program"... that's not going to work because that pipe is the one you assigned to standardOutput.
You need to create yet another Pipe object, assign it to standardInput, and write to that:
...
>> let inputPipe = Pipe()
>> task.standardInput = inputPipe
task.launch()
>> inputPipe.fileHandleForWriting.write("text to send to java program".data(using: String.Encoding.utf8)
...
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?
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.
(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).
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()
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.
I've written the function that launches Tor process. It's a process that won't stop until SIGTERM is sent to it, so, to avoid app freezing, I run this process in a background queue (Tor needs to be launched when the application is started and finished when application is terminated, and user needs to do some other things meanwhile). That's my code for Tor launching:
func launchTor(hashedPassword hash : String) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
let task = NSTask()
task.launchPath = "/bin/bash"
print("Hashed password : \(hash)")
task.arguments = (["-c", "/usr/local/bin/tor HashedControlPassword \(hash)"])
let pipe = NSPipe()
task.standardOutput = pipe
let handle = pipe.fileHandleForReading
handle.waitForDataInBackgroundAndNotify()
let errPipe = NSPipe()
task.standardError = errPipe
let errHandle = errPipe.fileHandleForReading
errHandle.waitForDataInBackgroundAndNotify()
var startObserver : NSObjectProtocol!
startObserver = NSNotificationCenter.defaultCenter().addObserverForName(NSFileHandleDataAvailableNotification, object: nil, queue: nil) { notification -> Void in
let data = handle.availableData
if data.length > 0 {
if let output = String(data: data, encoding: NSUTF8StringEncoding) {
print("Output : \(output)")
}
}
else {
print("EOF on stdout")
NSNotificationCenter.defaultCenter().removeObserver(startObserver)
}
}
var endObserver : NSObjectProtocol!
endObserver = NSNotificationCenter.defaultCenter().addObserverForName(NSTaskDidTerminateNotification, object: nil, queue: nil) {
notification -> Void in
print("Task terminated with code \(task.terminationStatus)")
NSNotificationCenter.defaultCenter().removeObserver(endObserver)
}
var errObserver : NSObjectProtocol!
errObserver = NSNotificationCenter.defaultCenter().addObserverForName(NSTaskDidTerminateNotification, object: nil, queue: nil) {
notification -> Void in
let data = errHandle.availableData
if (data.length > 0) {
if let output = String(data: data, encoding: NSUTF8StringEncoding) {
print("Error : \(output)")
NSNotificationCenter.defaultCenter().removeObserver(errObserver)
}
}
}
task.launch()
_ = NSNotificationCenter.defaultCenter().addObserverForName("AppTerminates", object: nil, queue: nil) {
notification -> Void in
task.terminate()
}
task.waitUntilExit()
}
}
When it's launched, everything is OK, but then the whole app freezes. When I stop an app, I always see that let data = handle.availableData line is running. How to fix this issue?