Unable to display real time Process() output to NSTextView in release mode

I wanted to display output from Process() to NSTextView in real time. Below are my code snippets:

Code Block
func displayOutput (_ task:Process) {
let outputPipe = Pipe()
setvbuf(stdout, nil, _IONBF, 0)
dup2(outputPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)
task.standardOutput = outputPipe
outputPipe.fileHandleForReading.readabilityHandler = { pipe in
if let outputString = String(data: pipe.availableData, encoding: String.Encoding.utf8) {
if outputString.count > 0 {
DispatchQueue.main.async(execute: {
print(outputString, separator: "\n", to: &self.outputTextView.string)
let range = NSRange(location:self.outputTextView.string.count,length:0)
self.outputTextView.scrollRangeToVisible(range)
}
}
}
}
}

The above code snippets only works in debug mode. Nothing displays on the NSTextView besides the initial text when it's in release mode.

Replies

What is this code supposed to be doing?

Code Block
dup2(outputPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)


AFAICT it’s changing STDOUT_FILENO of the parent process, which seems like a mistake.

Also, this code is broken:

Code Block
if let outputString = String(data: pipe.availableData, encoding: String.Encoding.utf8) {


The problem is that pipes are byte streams, and thus might split a UTF-8 sequence. For example, if the task prints naïve, the byte sequence you get back is 6E 61 C3 AF 76 65. If the stream splits that byte sequence between C3 and AF, both the before and after byte sequences are invalid UTF-8 and thus String(data:encoding:) will return nil.

As to what’s causing your main problem, it’s most likely because no one is holding on to outputPipe which is causing it to be deallocated which then closes everything down. However, it’s hard to be sure without more context.

Pasted in below is my test code for this. Note that this exercises two different cases:
  • The child process terminating on its own.

  • You clicking the Stop button to terminate the child process early.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"



Code Block
class ProcessState {
var process: Process
var fileHandleForReading: FileHandle
init(process: Process, fileHandleForReading: FileHandle) {
self.process = process
self.fileHandleForReading = fileHandleForReading
}
}
var stateQ: ProcessState?
func start() -> ProcessState? {
print("will start")
let pipe = Pipe()
let state = ProcessState(process: Process(), fileHandleForReading: pipe.fileHandleForReading)
state.process.executableURL = URL(fileURLWithPath: "/bin/sh")
state.process.arguments = [
"-c", "for i in 1 2 3 4 5; do date; sleep 1; done"
]
state.process.terminationHandler = { _ in
OperationQueue.main.addOperation {
guard state === self.stateQ else { return }
self.stateQ = nil
self.stop(state: state, needsTerminate: false)
}
}
state.process.standardOutput = pipe.fileHandleForWriting
state.fileHandleForReading.readabilityHandler = { _ in self.readData(state: state) }
do {
try state.process.run()
} catch {
print("did not start")
return nil
}
return state
}
func readData(state: ProcessState) {
print(state.fileHandleForReading.availableData as NSData)
}
func stop(state: ProcessState, needsTerminate: Bool) {
state.fileHandleForReading.readabilityHandler = nil
state.fileHandleForReading.closeFile()
state.process.terminationHandler = nil
if needsTerminate {
state.process.terminate()
}
print("did stop")
}