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

1

6

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)
Dropout answered 14/9, 2018 at 15:52 Comment(0)
A
12

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)!)
}
Animatism answered 14/9, 2018 at 16:55 Comment(3)
I just tryed it and according to logs. I can receive more data, after availableData was empty. So I'm not sure that this answer is correct oneSankaran
@Nikolay: That is strange, I am fairly sure that it is correct. I have tested the code once more and it behaved as described. I cannot explain why you observed a different behavior.Animatism
thank you for reply, I just tryed to create isolated sample to demonstrate the issue, and it works fine. So I think that issue might be specifically in my implementation, rather that in provided sample.Sankaran

© 2022 - 2024 — McMap. All rights reserved.