Updates to UI objects not occurring when sending long task to GCD until after task completed

Banging my head against a brick wall with this one.

Sending a command to the system and it takes a while to come back. I want to update an NSText field with a status before each system command.

When I do this, it doesn't update until after these tasks have completed, when it appears to catch up and do them all at once. So "send them to a DispatchQueue" I thought. I hadn't used them before, but I have read pages and pages and looked at example.

I am sending the system command with qos: utility and from what I've read the UI commands should stay in the main thread and get priority.

Even if I send the textbox.StringValue explicitly to DispatchQueue.main.async the same thing occurs

I know I am missing something, but the whole point of GCD seems to be to get around this issue, so I am very confused. I have tried a number of tests to work around this, with various qos levels and with sync and async queus, nothing seems to work.

Any help GREATLY appreciated!

Full simplified example ViewController.swift

queue (currently) declared on line 2 and called on line 24 (Swift 3 in XCode 8.2.1)

import Cocoa
let queue = DispatchQueue(label: "com.app.testqueue",qos: .utility)
class ViewController: NSViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    override var representedObject: Any? {
        didSet 
        }
    }
    func runCommand(cmd : String, args : String...) -> (output: [String], error: [String], exitCode: Int32) {
        var output : [String] = []
        var error : [String] = []
        
        let task = Process()
        task.launchPath = cmd
        task.arguments = args
        
        let outpipe = Pipe()
        task.standardOutput = outpipe
        let errpipe = Pipe()
        task.standardError = errpipe
        
        queue.async {
            task.launch()
        }
        
        let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
        if var string = String(data: outdata, encoding: .utf8) {
            string = string.trimmingCharacters(in: .newlines)
            output = string.components(separatedBy: "\n")
        }
        
        let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
        if var string = String(data: errdata, encoding: .utf8) {
            string = string.trimmingCharacters(in: .newlines)
            error = string.components(separatedBy: "\n")
        }
        
        task.waitUntilExit()
        let status = task.terminationStatus
        
        return (output, error, status)
        
    }
    
    @IBOutlet weak var textbox: NSTextField!
   
    func msg(txt : String) -> () {
            self.textbox.stringValue = txt
    }
        
  
    @IBAction func buttonPress(_ sender: NSButton) {
        
        msg(txt: "Task 1")
        
        let (output, error, status) = runCommand(cmd: "/usr/bin/say", args: "the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog")
        
        print("program exited with status \(status)")
        print (output)
       
        msg(txt: "Task 2")
        
        let (output2, error2, status2) = runCommand(cmd: "/usr/bin/say", args: "the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog the quick brown fox jumps over the lazy dog")
        print("program exited with status \(status2)")
        print (output2)
        
        
    }
}

Replies

You don't have to launch the task in an async queue. A task is a separate process and always runs asynchronously. The problem is that in your "runCommand" method, you are waiting forever for the task to complete.


Also, you are trying to read from the task's stdout (and stderr). Doing this operation is very tricky. I've seen and tried many examples. I've only found one method that works reliably - the posix_spawn function. Whatever method you use, it is going to be a fair amount of work to read from the child processes stdout and stderr streams. Plus you have to either handle that on a background thread (which you are attempting) or in the main run loop.


GCD is one method that will do it (assuming you don't do it too much, GCD flakes out after a while). Technically, however, you aren't even using the GCD method. You are just using GCD to spawn a background operation.


I suggest looking for some open-source library to handle this. You could copy the code from my app EtreCheck, but it is GPL, which you might not want. I might release some of the utility code under a less restrictive license if someone asked.

The problem is that in your "runCommand" method, you are waiting forever for the task to complete.

Agreed. This is key.

Doing this operation is very tricky.

Indeed.

I've seen and tried many examples. I've only found one method that works reliably - the posix_spawn function.

Why do you say that? In my experience NSTask, exposed as

Process
in Swift, makes this stuff relatively straightforward.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Trial and error. I have an app that spawns lots of processes (> 500). I used NSPipe/NSTask for a long time but I started getting reports of lock-ups after an OS update. I don't remember which OS it was. I had been suspicious of NSFileHandle for some time so I tried using GCD to read the pipes but it didn't help. Using posix_spawn with old-school select() definitively fixed the problem. As an added bonus, I figured out how to use posix_openpt and setup a pseudo-tty so I could call "top" reliabily. It has been rock-solid ever since.


I wouldn't agree that using NSTask for this is straightforward. It is definitely easier and requires less code. But there is a degree of magic involved that always seems just out of my control. My current code is straightforward. I know exactly where each byte of data is going and how each memory allocation is handled. And most importantly of all, I don't get complaints from users.

Thanks for the thorough answer. I will give this a try.

What I still don't understand is why updating the NStextfield, which I do BEFORE the NSTask, doesn't complete until much later (after the next task completes)

Is there some way I can ensure this happens first?