PadXcode-Daemon/Daemon/DaemonController.swift

117 lines
4.7 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
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
@MainActor
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 }
// 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
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()
if !DaemonController.loggingBootstrapped {
try LoggingSystem.bootstrap(from: &env)
DaemonController.loggingBootstrapped = true
}
let app = try await Application.make(env)
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)
try? await vaporApp?.asyncShutdown()
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() } }
}