Running a child process and capturing its output is way more complex than it seems at first. I’ve researched this extensively as part of a DTS tech support incident and posted a summary of my results in Running a Child Process with Standard Input and Output.
As to your specific question:
I notice that you mentioned you added flush(stdout)
in your code.
Did it work?
This is fundamental to the way that the standard I/O library works on Unix-y systems. Consider this simple Swift script:
% cat test.swift
#! env swift
import Foundation
for _ in 0..<5 {
print(Date.now)
Thread.sleep(forTimeInterval: 1.0)
}
First run it directly from the command line:
% ./test.swift
2022-03-03 10:20:53 +0000
2022-03-03 10:20:54 +0000
2022-03-03 10:20:55 +0000
2022-03-03 10:20:56 +0000
2022-03-03 10:20:57 +0000
This does what you’d expect: You see each line as it’s printed. Now run it like this:
% ./test.swift | cat
Now you see nothing for 5 seconds, and then all the output. Note how the timestamps are different, so the output was generated at the right time, it was just buffered until the process terminated.
Now tweak the script to add an fflush
:
% cat test-flush.swift
#! env swift
import Foundation
for _ in 0..<5 {
print(Date.now)
fflush(stdout)
Thread.sleep(forTimeInterval: 1.0)
}
And repeat your test:
% ./test-flush.swift | cat
With this script you see the same output but it arrives one line at a time. That’s the effect of the fflush
.
There are two factoids in play here:
-
Swift’s print
statement is wired up to the Unix-y standard I/O library, that is, printf
and friends.
-
The standard I/O library may or may not buffer by default, depending on the target file descriptor. Specifically:
-
If the file descriptor is connected to a terminal, it defaults to being lined buffer.
-
Otherwise it defaults to block buffering.
This explains the behaviour shown above:
-
In the first example (test.swift
output to a terminal) stdout
was connected to a terminal and thus line buffered.
-
In the second example (test.swift
piped to cat
) it was connected to a pipe and thus block buffered.
-
In the third example (test-flush.swift
) the script overrode that decision using fflush
.
To learn more about this, read the setvbuf
man page.
So, what can you do about this? If you control the code of the child process, either add the appropriate fflush
calls or reconfigure the buffering using setvbuf
. If not, things get more complex. Let me know if that’s the case and I can assist you with the next step on this path.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"