2026-03-11 14:44:19 -07:00
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
enum DockerExec {
|
|
|
|
|
static func run(
|
|
|
|
|
composeFile: String, service: String,
|
|
|
|
|
command: [String],
|
2026-06-09 03:55:45 -07:00
|
|
|
timeout: TimeInterval = 60,
|
2026-03-11 14:44:19 -07:00
|
|
|
) throws {
|
|
|
|
|
let process = Process()
|
|
|
|
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
|
|
|
|
process.arguments = [
|
|
|
|
|
"docker", "compose", "-f", composeFile,
|
|
|
|
|
"exec", "-T", "-u", "git", service,
|
|
|
|
|
] + command
|
|
|
|
|
|
|
|
|
|
let stderrPipe = Pipe()
|
|
|
|
|
process.standardError = stderrPipe
|
|
|
|
|
process.standardOutput = FileHandle.nullDevice
|
|
|
|
|
|
2026-06-09 03:55:45 -07:00
|
|
|
let didExit = DispatchSemaphore(value: 0)
|
|
|
|
|
process.terminationHandler = { _ in didExit.signal() }
|
|
|
|
|
|
2026-03-11 14:44:19 -07:00
|
|
|
try process.run()
|
2026-06-09 03:55:45 -07:00
|
|
|
|
|
|
|
|
// Enforce a timeout so a hung `docker compose exec` (a known Docker-on-macOS flake)
|
|
|
|
|
// fails fast and can be retried, instead of blocking the whole seed indefinitely.
|
|
|
|
|
if didExit.wait(timeout: .now() + timeout) == .timedOut {
|
|
|
|
|
process.terminate()
|
|
|
|
|
_ = didExit.wait(timeout: .now() + 5)
|
|
|
|
|
throw SeedError.dockerExecTimedOut(
|
|
|
|
|
command: command.joined(separator: " "),
|
|
|
|
|
seconds: timeout,
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-03-11 14:44:19 -07:00
|
|
|
|
|
|
|
|
if process.terminationStatus != 0 {
|
|
|
|
|
let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
|
|
|
|
|
let stderr = String(data: stderrData, encoding: .utf8) ?? ""
|
|
|
|
|
throw SeedError.dockerExecFailed(
|
|
|
|
|
command: command.joined(separator: " "),
|
|
|
|
|
exitCode: process.terminationStatus,
|
|
|
|
|
stderr: stderr,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|