PadXcode-Daemon/Daemon/DaemonController.swift

118 lines
4.7 KiB
Swift
Raw Normal View History

2026-04-12 00:42:51 -07:00
import Foundation
import Vapor
import Observation
enum ServerState: Equatable {
case stopped
case starting
case running(port: Int, startedAt: Date)
case error(String)
var isRunning: Bool { if case .running = self { return true }; return false }
var statusLabel: String {
switch self {
case .stopped: return "Stopped"
case .starting: return "Starting…"
case .running: return "Running"
case .error(let m): return "Error: \(m)"
}
}
var uptime: String? {
guard case .running(_, let start) = self else { return nil }
let e = Date().timeIntervalSince(start)
if e < 60 { return String(format: "%.0fs", e) }
if e < 3600 { return String(format: "%dm %ds", Int(e/60), Int(e)%60) }
return String(format: "%dh %dm", Int(e/3600), Int(e/60)%60)
}
}
struct ConnectedClient: Identifiable {
let id = UUID(); let sessionId: String; let connectedAt: Date; let type: ClientType
enum ClientType: String { case build = "Build Stream"; case lsp = "LSP Stream" }
}
struct DaemonLogLine: Identifiable {
let id = UUID(); let date = Date(); let message: String; let level: Level
enum Level { case info, success, warning, error
var prefix: String {
switch self {
case .info: return ""; case .success: return ""
case .warning: return "⚠️"; case .error: return ""
}
}
}
var formattedTime: String {
let f = DateFormatter(); f.dateFormat = "HH:mm:ss"; return f.string(from: date)
}
}
@Observable
2026-04-12 10:16:21 -07:00
@MainActor
2026-04-12 00:42:51 -07:00
final class DaemonController {
var serverState: ServerState = .stopped
var connectedClients: [ConnectedClient] = []
var logLines: [DaemonLogLine] = []
var buildCount: Int = 0
var preferences: AppPreferences
private var vaporApp: Application?
private let maxLogLines = 500
init(preferences: AppPreferences) { self.preferences = preferences }
2026-04-12 10:16:21 -07:00
// LoggingSystem can only be bootstrapped once per process lifetime.
// Guard with a static flag so restarts don't crash NIO's internal storage box.
private static var loggingBootstrapped = false
2026-04-12 00:42:51 -07:00
func start() async {
guard !serverState.isRunning else { return }
serverState = .starting
appendLog("Starting PadXcode daemon on port \(preferences.port)", level: .info)
do {
var env = try Environment.detect()
2026-04-12 10:16:21 -07:00
if !DaemonController.loggingBootstrapped {
try LoggingSystem.bootstrap(from: &env)
DaemonController.loggingBootstrapped = true
}
let app = try await Application.make(env)
2026-04-12 00:42:51 -07:00
app.http.server.configuration.hostname = "0.0.0.0"
app.http.server.configuration.port = preferences.port
try configure(app, controller: self)
try await app.startup()
vaporApp = app
serverState = .running(port: preferences.port, startedAt: Date())
appendLog("✅ Daemon running on 0.0.0.0:\(preferences.port)", level: .success)
appendLog("📡 Tailscale IP: \(preferences.tailscaleIP)", level: .info)
} catch {
serverState = .error(error.localizedDescription)
appendLog("❌ Failed to start: \(error.localizedDescription)", level: .error)
}
}
func stop() async {
guard serverState.isRunning else { return }
appendLog("Stopping daemon…", level: .info)
2026-04-12 10:16:21 -07:00
try? await vaporApp?.asyncShutdown()
2026-04-12 00:42:51 -07:00
vaporApp = nil; serverState = .stopped; connectedClients = []
appendLog("Daemon stopped.", level: .info)
}
func restart() async { await stop(); try? await Task.sleep(for: .milliseconds(500)); await start() }
func clientConnected(sessionId: String, type: ConnectedClient.ClientType) {
connectedClients.append(ConnectedClient(sessionId: sessionId, connectedAt: Date(), type: type))
appendLog("Client connected: \(type.rawValue) [\(sessionId.prefix(8))]", level: .info)
}
func clientDisconnected(sessionId: String) {
connectedClients.removeAll { $0.sessionId == sessionId }
appendLog("Client disconnected [\(sessionId.prefix(8))]", level: .info)
}
func buildStarted(scheme: String) { buildCount += 1; appendLog("Build #\(buildCount): \(scheme)", level: .info) }
func appendLog(_ message: String, level: DaemonLogLine.Level) {
logLines.append(DaemonLogLine(message: message, level: level))
if logLines.count > maxLogLines { logLines.removeFirst(logLines.count - maxLogLines) }
}
func clearLog() { logLines = [] }
func applyPortChange() async { if serverState.isRunning { await restart() } }
}