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.