1

I'm writing a Swift command line tool that uses NSTask to interact with git. In the simplest scenario I want to run three commands: init, add ., and commit -m Initial Commit. I intend to use a separate NSTask for each command, and want to house each command in its own function - returning true if the task succeeded or false if it didn't. This set-up would allow my main function to look like this:

func main() {

    if runInit() {
        if runStage() {
            if runCommit() {
                 NSLog("success!")
            }
        }
    }
}

To accomplish this each of the three functions must do the following before returning (i) launch the task (ii) wait for it to complete, (iii) obtain whatever is in stdout, and (iv) set the return value (true or false). Here's what I've got for the commit stage:

func runCommit() -> Bool {

    var retval = false

    var commitTask = NSTask()
    commitTask.standardOutput = NSPipe()
    commitTask.launchPath = gitPath
    commitTask.arguments = ["commit", "-m", "Initial Commit"]
    commitTask.currentDirectoryPath = demoProjectURL.path!

    commitTask.standardOutput.fileHandleForReading.readToEndOfFileInBackgroundAndNotify()

    nc.addObserverForName(NSFileHandleReadToEndOfFileCompletionNotification,
        object: commitTask.standardOutput.fileHandleForReading,
        queue: nil) { (note) -> Void in
            // get the output, log it, then...
            if commitTask.terminationStatus == EXIT_SUCCESS {
                retval = true
            } 
    }

    commitTask.launch()
    commitTask.waitUntilExit()

    return retval

}

My question is essentially about how waitUntilExit works, particularly in conjunction with the notification I sign up for to enable me to get the output. Apple's docs say:

This method first checks to see if the receiver is still running using isRunning. Then it polls the current run loop using NSDefaultRunLoopMode until the task completes.

I'm a bit out of my depth when it comes to run loop mechanics, and was wondering what this means in this context - can I safely assume that my notification block will always be executed before the enclosing function returns?

Paul Patterson
  • 6,840
  • 3
  • 42
  • 56

1 Answers1

3

waitUntilExit returns when the SIGCHILD signal has been received to indicate that the child process has terminated. The notification block is executed when EOF is read from the pipe to the child process. It is not specified which of these events occurs first.

Therefore you have to wait for both. There are several possible solutions, here is one using a "signalling semaphore", you could also use a "dispatch group".

Another error in your code is that the observer is never removed.

func runCommit() -> Bool {

    let commitTask = NSTask()
    commitTask.standardOutput = NSPipe()
    commitTask.launchPath = gitPath
    commitTask.arguments = ["commit", "-m", "Initial Commit"]
    commitTask.currentDirectoryPath = demoProjectURL.path!

    commitTask.standardOutput!.fileHandleForReading.readToEndOfFileInBackgroundAndNotify()

    let sema = dispatch_semaphore_create(0)
    var obs : NSObjectProtocol!
    obs = nc.addObserverForName(NSFileHandleReadToEndOfFileCompletionNotification,
        object: commitTask.standardOutput!.fileHandleForReading, queue: nil) {
            (note) -> Void in
            // Get data and log it.
            if let data = note.userInfo?[NSFileHandleNotificationDataItem] as? NSData,
                let string = String(data: data, encoding: NSUTF8StringEncoding) {
                    print(string)
            }
            // Signal semaphore.
            dispatch_semaphore_signal(sema)
            nc.removeObserver(obs)
    }

    commitTask.launch()
    // Wait for process to terminate.
    commitTask.waitUntilExit()
    // Wait for semaphore to be signalled.
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)
    let retval = commitTask.terminationStatus == EXIT_SUCCESS
    return retval
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382