PadXcode-Daemon/Daemon/PreBuildRunner.swift
2026-04-12 22:14:13 -07:00

83 lines
3.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import Vapor
final class PreBuildRunner {
static func run(hooks: [PreBuildHook], session: BuildSession, logger: Logger) async -> Bool {
for hook in hooks {
session.broadcast(BuildLogMessage(type: .stdout, text: "\n⚙️ Hook: \(hook.name)\n", exitCode: nil))
let ok = await runHook(hook, session: session)
if !ok {
session.broadcast(BuildLogMessage(type: .stderr, text: "❌ Hook '\(hook.name)' failed.", exitCode: nil))
if !hook.continueOnFailure { return false }
} else {
session.broadcast(BuildLogMessage(type: .stdout, text: "\(hook.name) done.\n", exitCode: nil))
}
}
return true
}
private static func runHook(_ hook: PreBuildHook, session: BuildSession) async -> Bool {
await withCheckedContinuation { cont in
let p = Process()
p.executableURL = URL(fileURLWithPath: "/bin/zsh")
p.arguments = ["-l", "-c", hook.command]
p.currentDirectoryURL = URL(fileURLWithPath: hook.workingDirectory)
var env = ProcessInfo.processInfo.environment
env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:" + (env["PATH"] ?? "")
p.environment = env
let out = Pipe(); let err = Pipe()
p.standardOutput = out; p.standardError = err
// Guard against double-resume: if p.run() throws, the catch block resumes first,
// then empty-data pipe handlers fire g.leave()×2 g.notify second resume.
final class ResumeOnce {
private let lock = NSLock(); private var done = false
func resume(continuation: CheckedContinuation<Bool, Never>, returning value: Bool) {
lock.lock(); defer { lock.unlock() }
guard !done else { return }; done = true
continuation.resume(returning: value)
}
}
let guard_ = ResumeOnce()
final class TimedOutBox { var value = false }
let timedOutBox = TimedOutBox()
let wd = DispatchWorkItem { timedOutBox.value = true; p.terminate()
session.broadcast(BuildLogMessage(type: .stderr,
text: "⏱ Hook '\(hook.name)' timed out.", exitCode: nil))
}
DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(hook.timeoutSeconds), execute: wd)
let g = DispatchGroup()
g.enter()
out.fileHandleForReading.readabilityHandler = { h in
let d = h.availableData
if d.isEmpty { out.fileHandleForReading.readabilityHandler = nil; g.leave(); return }
if let t = String(data: d, encoding: .utf8) {
session.broadcast(BuildLogMessage(type: .stdout, text: t, exitCode: nil))
}
}
g.enter()
err.fileHandleForReading.readabilityHandler = { h in
let d = h.availableData
if d.isEmpty { err.fileHandleForReading.readabilityHandler = nil; g.leave(); return }
if let t = String(data: d, encoding: .utf8) {
session.broadcast(BuildLogMessage(type: .stderr, text: t, exitCode: nil))
}
}
p.terminationHandler = { proc in
wd.cancel()
g.notify(queue: .global()) {
guard_.resume(continuation: cont,
returning: proc.terminationStatus == 0 && !timedOutBox.value)
}
}
do { try p.run() } catch {
wd.cancel()
session.broadcast(BuildLogMessage(type: .stderr,
text: "Launch failed: \(error)", exitCode: nil))
guard_.resume(continuation: cont, returning: false)
}
}
}
}