Hi all,
I am a relatively new Swift user (I am more experienced in C++ but giving this a go).
I am working on a test framework that is supposed to do the following:
- Setup a empty PostgreSQL instance
- Connect to it and, populate data, and run tests
- Destroy the database
Now for reasons not related to Swift (the TL;DR is that postgres can't bind to port 0), I need to start postgres in a loop and check the output for errors and success messages in order to find a free port (I don't want to assume that the system has port 5432 or any other port available). This loop looks as follows:
var _: Process? // to keep the reference count up
try await withCheckedThrowingContinuation {
continuation in
do {
_ = try Process.run(URL(filePath: "\(initDB)"),
arguments: ["-D", "\(dataDir)"],
terminationHandler: { process in
assert(!process.isRunning)
assert(process.terminationStatus == 0)
continuation.resume()
})
} catch {
continuation.resume(throwing: error)
}
}
// find a network address we can use on the loopback device
server = Process()
var stdoutPipe = Pipe()
var stderrPipe = Pipe()
var port = 5432
let startServer = {
self.server.executableURL = URL(filePath: "\(postgres)")
self.server.arguments = ["-D", "\(dataDir)", "-p", "\(port)"]
self.server.standardOutput = stdoutPipe
self.server.standardError = stderrPipe
try self.server.run()
}
// we need to try out ports as multiple tests could run in parallel
while true {
try startServer()
if try await PostgresTestInstance.checkLaunchOutput(stdout: stdoutPipe, stderr: stderrPipe) {
break
}
server.terminate()
server = Process()
stdoutPipe = Pipe()
stderrPipe = Pipe()
port += 1
}
The way I am attempting to check for errors is by using this async function:
private static func checkLaunchErrOrSuccess(handle: FileHandle, prefix: String) async throws -> Bool {
for try await line in handle.bytes.lines {
print("\(prefix): \(line)")
if line.ends(with: "FATAL: could not create any TCP/IP sockets") {
fflush(stdout)
return false
} else if line.ends(with: "LOG: database system is ready to accept connections") {
fflush(stdout)
return true
}
}
fflush(stdout)
throw TestInstanceError.postgresError
}
private static func checkLaunchOutput(stdout: Pipe, stderr: Pipe) async throws -> Bool {
return try await withThrowingTaskGroup(of: Bool.self) { group in
group.addTask {
try await checkLaunchErrOrSuccess(handle: stdout.fileHandleForReading, prefix: "Postgres (out)")
}
group.addTask {
try await checkLaunchErrOrSuccess(handle: stderr.fileHandleForReading, prefix: "Postgres (err)")
}
let first = try await group.next()
group.cancelAll()
return first!
}
}
Now this doesn't work. It seems there's more than one bug in here (and the test behaves significantly different in XCode than if I run swift test
on the command line). But my biggest problem, and the one I am observing consistently, is this:
It seems that the pipe only becomes readable AFTER the process terminates. This obviously makes all of the above useless, but it also makes it very hard to debug anything. Is this expected? And if yes, what would be the correct way of running a child process and asynchronously read its output?