Search code examples
swiftcocoanstasknsfilehandlenspipe

How can I tell when a FileHandle has nothing left to be read?


I'm trying to use a Pipe's fileHandleForReading's readabilityHandler to read both the standardOutput and standardError of a Process. However, the moment the terminationHandler is called is actually before the moment my readabilityHandler is called for the first time.

I'm not sure why the process does this, but it means I'm not getting all the data, because I assume process termination means all output has been flushed to the pipe. Since this isn't the case, is there a way for me to tell when there is no more output to be read? I assume that involves checking if a FileHandle is still open, but I don't see an API for that.

Here's an example of the basic idea of what my code looks like:

let stdOutPipe = Pipe()
let stdErrPipe = Pipe()

stdOutPipe.fileHandleForReading.readabilityHandler = { stdOutFileHandle in
    let stdOutPartialData = stdOutFileHandle.readDataToEndOfFile()

    guard !stdOutPartialData.isEmpty else {
        print("Time to read, but nothing to be read?") // happens a lot
        return
    }

    self.tempStdOutStorage.append(stdOutPartialData)
}

stdErrPipe.fileHandleForReading.readabilityHandler = { stdErrFileHandle in
    let stdErrPartialData = stdErrFileHandle.readDataToEndOfFile()

    guard !stdErrPartialData.isEmpty else {
        print("Time to read, but nothing to be read?") // happens a lot
        return
    }

    self.tempStdErrStorage.append(stdErrPartialData)
}

process.standardOutput = stdOutPipe
process.standardError = stdErrPipe


process.terminationHandler = { process in
    notifyOfCompleteRead(stdOut: self.tempStdOutStorage, stdErr: self.tempStdErrStorage)
}

mySpecializedDispatchQueue.async(execute: process.launch)

Solution

  • In the readabilityHandler you should use availableData to get the currently available data without blocking. Empty available data indicates EOF on the file handle, in that case the readability handler should be removed.

    A dispatch group can be used to wait for EOF on both standard output and standard error after the process has finished.

    Example:

    let group = DispatchGroup()
    
    group.enter()
    stdOutPipe.fileHandleForReading.readabilityHandler = { stdOutFileHandle in
        let stdOutPartialData = stdOutFileHandle.availableData
        if stdOutPartialData.isEmpty  {
            print("EOF on stdin")
            stdOutPipe.fileHandleForReading.readabilityHandler = nil
            group.leave()
        } else {
            tempStdOutStorage.append(stdOutPartialData)
        }
    }
    
    group.enter()
    stdErrPipe.fileHandleForReading.readabilityHandler = { stdErrFileHandle in
        let stdErrPartialData = stdErrFileHandle.availableData
    
        if stdErrPartialData.isEmpty  {
            print("EOF on stderr")
            stdErrPipe.fileHandleForReading.readabilityHandler = nil
            group.leave()
        } else {
            tempStdErrStorage.append(stdErrPartialData)
        }
    }
    
    process.standardOutput = stdOutPipe
    process.standardError = stdErrPipe
    
    process.launch()
    
    process.terminationHandler = { process in
        group.wait()
        print("OUTPUT:", String(data: tempStdOutStorage, encoding: .utf8)!)
        print("ERROR: ", String(data: tempStdErrStorage, encoding: .utf8)!)
    }