Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
538 views
in Technique[技术] by (71.8m points)

cocoa - Swift: How to read standard output in a child process without waiting for process to finish

I have an external console application (on OS X) that emits a sequence of integers from 1 to 100 to standard output, roughly once every second.

I Swift, I need to use that stream of numbers in order to update a progress indicator.

Here is the code that I have so far:

class MasterViewController: NSViewController {

@IBOutlet weak var progressIndicator: NSProgressIndicator!

override func viewDidLoad() {
    super.viewDidLoad()

    let task = Process()
    task.launchPath = "/bin/sh"
    task.arguments = ["-c", "sleep 1; echo 10 ; sleep 1 ; echo 20 ; sleep 1 ; echo 30 ; sleep 1 ; echo 40; sleep 1; echo 50; sleep 1; echo 60; sleep 1"]  

    let pipe = Pipe()
    task.standardOutput = pipe

    task.launch()

    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    if let string = String(data: data, encoding: String.Encoding.utf8) {
        print(string)
    }
}

The code works—that is, it reads the output from the command-line utility and modifies the progress indicator accordingly—but it makes all the changes after the utility quits (and makes my UI wait in the meantime).

How would I set it up so that it reads the output from the background application and updates the progress indicator in real-time?

UPDATE

For future reference, here is how I got it to work in the end (now updated for Swift 3):

class ViewController: NSViewController {

    @IBOutlet weak var progressIndicator: NSProgressIndicator!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.


        let task = Process()
        task.launchPath = "/bin/sh"
        task.arguments = ["-c", "sleep 1; echo 10 ; sleep 1 ; echo 20 ; sleep 1 ; echo 30 ; sleep 1 ; echo 40; sleep 1; echo 50; sleep 1; echo 60; sleep 1"]

        let pipe = Pipe()
        task.standardOutput = pipe
        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) {
                    if let newValue = Double(str.trimEverything) {
                        self.progressIndicator.doubleValue = newValue
                    }
                }
                outHandle.waitForDataInBackgroundAndNotify()
            } else {
                // That means we've reached the end of the input.
                NotificationCenter.default.removeObserver(progressObserver)
            }
        }

        var terminationObserver : NSObjectProtocol!
        terminationObserver = NotificationCenter.default.addObserver(
            forName: Process.didTerminateNotification,
            object: task, queue: nil)
        {
            notification -> Void in
            // Process was terminated. Hence, progress should be 100%
            self.progressIndicator.doubleValue = 100
            NotificationCenter.default.removeObserver(terminationObserver)
        }

        task.launch()

    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }

}


// This is just a convenience extension so that I can trim
// off the extra newlines and white spaces before converting
// the input to a Double.
fileprivate extension String {
    var trimEverything: String {
        return self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
    }
}

Now, the progress bar progresses to 60% and then jumps up to 100%, once the child process has completed.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You are reading synchronously on the main thread, therefore the UI is not updated until the function returns to the main loop.

There are (at least) two possible approaches to solve the problem:

  • Do the reading from the pipe on a background thread (e.g. by dispatching it to a background queue – but don't forget to dispatch the UI updates to the main thread again).
  • Use notifications to read asynchronously from the pipe (see Real time NSTask output to NSTextView with Swift for an example).

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...