I have been working on updating an old app that makes extensive use of Objective-C's NSTask. Now using Process in Swift, I'm trying to gather updates as the process runs, using readabilityHandler and availableData. However, my process tends to exit before all data has been read. I found this post entitled "Running a Child Process with Standard Input and Output" but it doesn't seem to address gathering output from long-running tasks. Is there a straightforward way to gather ongoing output from a long running task without it prematurely exiting?
I have a tool that verifies volumes using diskutil.
OK. I think you’re in luck here, because while diskutil writes its progress to stdout, it seems to flush stdout after each write. Consider:
% diskutil verifyVolume /Volumes/Test | cat
Started file system verification on disk64s1 (Test)
…
Finished file system verification on disk64s1 (Test)
Each line of output appears as the operation occurs. If diskutil weren’t flushing stdout, it’d all appear at the end. And when that happens things get more complex.
Based on this assumption, I created a small test script that I can use to verify that I’m getting data promptly:
#! /bin/sh
echo 0
for i in $(seq 4)
do
sleep 2
echo ${i}
done
I then started poking at it with Subprocess:
import Foundation
import Subprocess
func main() async throws {
let config = Subprocess.Configuration(
executable: .path("/Users/quinn/slow-print.sh")
)
let result = try await Subprocess.run(config) { (execution, stdin, stdout, stderr) -> Void in
for try await chunk in stdout {
print(chunk.count)
}
}
print(result)
}
try await main()
That’s disappointing. The AsyncBufferSequence approach seems to accumulate all the data until it hits EOF — or, presumably, until the data hits some desired size [1] — so it won’t work for this case.
So I fell back to the file descriptor approach:
let result = try await Subprocess.run(
config,
output: FileDescriptorOutput.fileDescriptor(write, closeAfterSpawningProcess: true)
)
The problem then devolves into how to handle the file descriptor. Here’s a quick hack for that:
let (read, write) = try FileDescriptor.pipe()
Thread.detachNewThread {
do {
while true {
var buffer = [UInt8](repeating: 0, count: 1024)
let bytesRead = try buffer.withUnsafeMutableBytes { buffer in
try read.read(into: buffer)
}
if bytesRead == 0 {
print("EOF")
break
}
print("chunk, count: \(bytesRead)")
}
} catch {
print("error")
}
}
In a real program you’d want to come up with something that doesn’t require you to spin up a completely new thread, for example, by using Dispatch I/O.
All-in-all, I think it’d be reasonable you to file an issue again Subprocess requesting that AsyncBufferSequence have some better way to control the low and high water marks.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] Looking at the code, this seems to be readBufferSize, which is one page.