PadXcode-Daemon/Daemon/BuildManager.swift

66 lines
3 KiB
Swift
Raw Normal View History

2026-04-12 00:42:51 -07:00
import Vapor
import Foundation
actor BuildSessionStore {
static let shared = BuildSessionStore()
private var sessions: [String: BuildSession] = [:]
func create(session: BuildSession) { sessions[session.id] = session }
func session(for id: String) -> BuildSession? { sessions[id] }
func remove(id: String) { sessions.removeValue(forKey: id) }
}
final class BuildSession: @unchecked Sendable {
let id: String
private let logger: Logger
private var clients: [WebSocket] = []
private let lock = NSLock()
init(id: String, logger: Logger) { self.id = id; self.logger = logger }
func addClient(_ ws: WebSocket) { lock.lock(); defer { lock.unlock() }; clients.append(ws) }
func broadcast(_ message: BuildLogMessage) {
guard let data = try? JSONEncoder().encode(message),
let text = String(data: data, encoding: .utf8) else { return }
lock.lock(); let snap = clients; lock.unlock()
2026-04-12 10:16:21 -07:00
for ws in snap where !ws.isClosed { let w = ws; Task { try? await w.send(text) } }
2026-04-12 00:42:51 -07:00
}
@discardableResult
func runXcodebuild(arguments: [String]) async -> Int32 {
await withCheckedContinuation { continuation in
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/xcodebuild")
process.arguments = arguments
let outPipe = Pipe(); let errPipe = Pipe()
process.standardOutput = outPipe; process.standardError = errPipe
let group = DispatchGroup()
group.enter()
outPipe.fileHandleForReading.readabilityHandler = { [weak self] h in
let d = h.availableData
if d.isEmpty { outPipe.fileHandleForReading.readabilityHandler = nil; group.leave(); return }
if let t = String(data: d, encoding: .utf8) { self?.broadcast(BuildLogMessage(type: .stdout, text: t, exitCode: nil)) }
}
group.enter()
errPipe.fileHandleForReading.readabilityHandler = { [weak self] h in
let d = h.availableData
if d.isEmpty { errPipe.fileHandleForReading.readabilityHandler = nil; group.leave(); return }
if let t = String(data: d, encoding: .utf8) { self?.broadcast(BuildLogMessage(type: .stderr, text: t, exitCode: nil)) }
}
process.terminationHandler = { [weak self] proc in
let code = proc.terminationStatus
group.notify(queue: .global()) {
let msg = code == 0 ? "✅ Build succeeded." : "❌ Build failed (exit \(code))."
self?.broadcast(BuildLogMessage(type: .status, text: msg, exitCode: Int(code)))
continuation.resume(returning: code)
}
}
do { try process.run(); logger.info("xcodebuild started. PID: \(process.processIdentifier)") }
catch {
broadcast(BuildLogMessage(type: .stderr, text: "Failed to launch xcodebuild: \(error)", exitCode: nil))
continuation.resume(returning: -1)
}
}
}
}