Swift GDC background process freezes app - swift

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?

Related

Swift start Process and read continously changed output

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?

Getting nil data when try to cancel a task and restart it again Swift

What I am trying to do:
Sometimes my URLSession call request takes a too long time to give a response back. That's why I am trying to call the call request again if it doesn't give a response back within 60 seconds. After 60 seconds it will cancel the call request, then give an alert to the user to try again. When the user taps on the try again alert button it will call the call request again from the beginning.
How I tried:
I declare a global variable for the session task like this:
private weak var IAPTask: URLSessionTask?
This is my call request function:
Code 1
func receiptValidation(completion: #escaping(_ isPurchaseSchemeActive: Bool, _ error: Error?) -> ()) {
let receiptFileURL = Bundle.main.appStoreReceiptURL
guard let receiptData = try? Data(contentsOf: receiptFileURL!) else {
//This is the First launch app VC pointer call
completion(false, nil)
return
}
let recieptString = receiptData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
let jsonDict: [String: AnyObject] = ["receipt-data" : recieptString as AnyObject, "password" : AppSpecificSharedSecret as AnyObject]
do {
let requestData = try JSONSerialization.data(withJSONObject: jsonDict, options: JSONSerialization.WritingOptions.prettyPrinted)
let storeURL = URL(string: self.verifyReceiptURL)!
var storeRequest = URLRequest(url: storeURL)
storeRequest.httpMethod = "POST"
storeRequest.httpBody = requestData
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: storeRequest, completionHandler: { [weak self] (data, response, error) in
do {
if let jsonResponse = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
if let latestInfoReceiptObjects = self?.getLatestInfoReceiptObjects(jsonResponse: jsonResponse) {
self?.getCurrentTimeFromServer(completionHandler: { currentDateFromServer in
let purchaseStatus = self?.isPurchaseActive(currentDateFromServer: currentDateFromServer, latestReceiptInfoArray: latestInfoReceiptObjects)
completion(purchaseStatus!, nil)
})
}
}
} catch let parseError {
completion(false, parseError)
}
})
task.resume()
self.IAPTask = task
} catch let parseError {
completion(false, parseError)
}
}
I am calling this request after every 60 seconds with a timer. But whenever I try to call it for a second time, I am getting nil value for data.
let task = session.dataTask(with: storeRequest, completionHandler: { [weak self] (data, response, error) in
//Getting data = nil here for second time
})
Let me show how I am canceling the global variable step by step.
First calling this where I am setting the timer and call for the call request. If I get the response within 10 seconds (For testing purposes I set it as 10), I am canceling the timer and the call request task and doing further procedures. :
Code 2
func IAPResponseCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom) {
let infoDic: [String : String] = ["IAPReceiptValidationFrom" : iapReceiptValidationFrom.rawValue]
self.IAPTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(self.IAPTimerAction), userInfo: infoDic, repeats: false)
IAPStatusCheck(iapReceiptValidationFrom: iapReceiptValidationFrom) { isSuccessful in
if isSuccessful == true {
self.IAPTimer.invalidate()
self.IAPTask?.cancel()
self.getTopVisibleViewController { topViewController in
if let viewController = topViewController {
viewController.dismiss(animated: true, completion: nil)
self.hideActivityIndicator()
}
}
}
}
}
This is the completion where I call the call request. I set a true value for the completion if it just gives me the response.
Code 3
func IAPStatusCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom, complition: #escaping (_ isSuccessful: Bool)->()) {
receiptValidation() { isPurchaseSchemeActive, error in
if let err = error {
self.onBuyProductHandler?(.failure(err))
} else {
self.onBuyProductHandler?(.success(isPurchaseSchemeActive))
}
complition(true)
}
}
This is the timer action from where I am invalidating the timer and canceling the task call request and then showing the pop up alert to the user:
Code 4
#objc func IAPTimerAction(sender: Timer) {
if let dic = (sender.userInfo)! as? Dictionary<String, String> {
let val = dic["IAPReceiptValidationFrom"]!
let possibleType = IAPReceiptValidationFrom(rawValue: val)
self.IAPTimer.invalidate()
self.IAPTask?.cancel()
self.showAlertForRetryIAP(iapReceiptValidationFrom: possibleType!)
}
}
And finally, call the same initial response check function in the alert "Try again" action.
Code 5
func showAlertForRetryIAP(iapReceiptValidationFrom: IAPReceiptValidationFrom) {
DispatchQueue.main.async {
let alertVC = UIAlertController(title: "Time Out!" , message: "Apple server seems busy. Please wait or try again.", preferredStyle: UIAlertController.Style.alert)
alertVC.view.tintColor = UIColor.black
let okAction = UIAlertAction(title: "Try again", style: UIAlertAction.Style.cancel) { (alert) in
self.showActivityIndicator()
self.IAPResponseCheck(iapReceiptValidationFrom: iapReceiptValidationFrom)
}
alertVC.addAction(okAction)
DispatchQueue.main.async {
self.getTopVisibleViewController { topViewController in
if let viewController = topViewController {
var presentVC = viewController
while let next = presentVC.presentedViewController {
presentVC = next
}
presentVC.present(alertVC, animated: true, completion: nil)
}
}
}
}
}
This is the response I am getting:
▿ Optional<NSURLResponse>
- some : <NSHTTPURLResponse: 0x281d07680> { URL: https://sandbox.itunes.apple.com/verifyReceipt } { Status Code: 200, Headers {
Connection = (
"keep-alive"
);
"Content-Type" = (
"application/json"
);
Date = (
"Sun, 27 Feb 2022 17:28:17 GMT"
);
Server = (
"daiquiri/3.0.0"
);
"Strict-Transport-Security" = (
"max-age=31536000; includeSubDomains"
);
"Transfer-Encoding" = (
Identity
);
"apple-originating-system" = (
CommerceGateway
);
"apple-seq" = (
"0.0"
);
"apple-timing-app" = (
"154 ms"
);
"apple-tk" = (
false
);
b3 = (
"48d6c45fe76eb9d8445d83e518f01866-c8bfb32d2a7305e2"
);
"x-apple-jingle-correlation-key" = (
JDLMIX7HN245QRC5QPSRR4AYMY
);
"x-apple-request-uuid" = (
"48d6c45f-e76e-b9d8-445d-83e518f01866"
);
"x-b3-spanid" = (
c8bfb32d2a7305e2
);
"x-b3-traceid" = (
48d6c45fe76eb9d8445d83e518f01866
);
"x-daiquiri-instance" = (
"daiquiri:45824002:st44p00it-hyhk15104701:7987:22RELEASE11:daiquiri-amp-commerce-clients-ext-001-st"
);
"x-responding-instance" = (
"CommerceGateway:020115:::"
);
} }
And this is the error:
▿ Optional<Error>
- some : Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=https://sandbox.itunes.apple.com/verifyReceipt, NSErrorFailingURLKey=https://sandbox.itunes.apple.com/verifyReceipt, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <17863FCF-EC4C-43CC-B408-81EC19B04ED7>.<1>"
), _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <17863FCF-EC4C-43CC-B408-81EC19B04ED7>.<1>, NSLocalizedDescription=cancelled}
It seems that the way you reinitialize the URLSessionTask looks fine to me but I think there is something when cancelling a task and invalidating the timer seems to happen twice as you mentioned in code 2 and code 4 so it is a little tricky to debug.
From the error it seems like a race condition (I could be wrong) type situation where while you are initializing a new URLSession the url session gets cancelled.
What I can offer is a simpler alternative if you would like to try:
When creating your URLRequest, instead of using a timer, use the timeoutInterval
storeRequest.httpMethod = "POST"
storeRequest.httpBody = requestData
// add this
storeRequest.timeoutInterval = timeOutInterval
Then inside your data task, handler you could check if you encounter the error due to a timeout:
let task = session.dataTask(with: storeRequest) { [weak self] (data, response, error) in
do {
if let error = error
{
// This checks if the request timed out based on your interval
if (error as? URLError)?.code == .timedOut
{
// retry your request
self?.receiptValidation(completion: completion)
}
}
The full mechanism for retrying seems to become a lot more simpler in my opinion and you don't need to manage a URLSessionTask object
This is the full code:
private func receiptValidation(completion: #escaping(_ isPurchaseSchemeActive: Bool,
_ error: Error?) -> ())
{
// all your initial work to prepare your data and jsonDict data
// goes here
do {
let requestData = try JSONSerialization.data(withJSONObject: jsonDict!,
options: .prettyPrinted)
let storeURL = URL(string: self.verifyReceiptURL)!
var storeRequest = URLRequest(url: storeURL)
storeRequest.httpMethod = "POST"
storeRequest.httpBody = requestData
storeRequest.timeoutInterval = timeOutInterval
let session = URLSession(configuration: .default)
let task = session.dataTask(with: storeRequest)
{ [weak self] (data, response, error) in
do {
if let error = error
{
// Error due to a timeout
if (error as? URLError)?.code == .timedOut
{
// retry your request
self?.receiptValidation(completion: completion)
}
// some other error
}
if let data = data,
let jsonResponse
= try JSONSerialization.jsonObject(with: data,
options: .mutableContainers) as? NSDictionary
{
// do your work or call completion handler
print("JSON: \(jsonResponse)")
}
}
catch
{
print("Response error: \(error)")
}
}
task.resume()
}
catch
{
print("Request creation error: \(error)")
}
}
I am retrying the request immediately when I figure out the request timed out, however you would show your alert instead to ask the user to retry.
Once they hit retry, all you need to do is call your function again receiptValidation(completion: completion) so probably the completion closure is what needs to be stored so that receiptValidation can be relaunched again.
I know this does not exactly find the error in your code but have a look if this could help with your use case and simplify things ?

Redirect the output of Terminal command to TextView

I want to execute a Terminal command in my Application and redirect the Terminal output of this command to a TextView (content_scroller). If I run the Application with Apple+R from within Xcode the Progress of this Terminal command is refreshed as it should. But ... If I started the Application the normal way only the first line of terminal output is shown but there is no refresh/new lines anymore. But why? Is there a way to loop the request of the actual output? Here is mit Swift 5 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: .utf8) {
DispatchQueue.main.sync {
self.content_scroller.string += line
self.content_scroller.scrollToEndOfDocument(nil)
}
}
process.waitUntilExit()
filelHandler.readabilityHandler = nil
}
Should be able to direct output straight to text view if I understand your question correctly. Something like the following outputs an error (I didn't test it.)
import Cocoa
func syncShellExec(path: String, args: [String] = []) {
var status : Int32
var dataRead : Data
var stringRead :String?
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = [path] + args
let outputPipe = Pipe()
let txtView = NSTextView()
let fileHandler = outputPipe.fileHandleForReading
process.standardOutput = outputPipe
process.launch()
process.waitUntilExit()
status = process.terminationStatus
dataRead = fileHandler.readDataToEndOfFile()
stringRead = String.init(data: dataRead, encoding: String.Encoding.utf8)
if (status != 0) {
txtView.string.append("Terminated with error.\n")
txtView.string.append(stringRead!)
}
}

More Buffer for FileHandler Output?

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.
}
}
}

SWIFT 3 - Can I "stream" the output from a bash command to an output window? [duplicate]

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.