diff --git a/VibeTunnel/Core/Services/GoServer.swift b/VibeTunnel/Core/Services/GoServer.swift index ca982627..a8f4e5bd 100644 --- a/VibeTunnel/Core/Services/GoServer.swift +++ b/VibeTunnel/Core/Services/GoServer.swift @@ -1,14 +1,34 @@ import Foundation import OSLog +/// Log entry from the server. +struct ServerLogEntry { + /// Severity level of the log entry. + enum Level { + case debug + case info + case warning + case error + } + + let timestamp: Date + let level: Level + let message: String + + init(level: Level = .info, message: String) { + self.timestamp = Date() + self.level = level + self.message = message + } +} + /// Go vibetunnel server implementation. /// /// Manages the external vibetunnel Go binary as a subprocess. This implementation /// provides high-performance terminal multiplexing by leveraging the Go-based -/// vibetunnel server. It handles process lifecycle, log streaming, and error recovery -/// while maintaining compatibility with the ServerProtocol interface. +/// vibetunnel server. It handles process lifecycle, log streaming, and error recovery. @MainActor -final class GoServer: ServerProtocol { +final class GoServer { private var process: Process? private var stdoutPipe: Pipe? private var stderrPipe: Pipe? @@ -63,8 +83,6 @@ final class GoServer: ServerProtocol { private let processHandler = ProcessHandler() - var serverType: ServerMode { .go } - private(set) var isRunning = false var port: String = "" { @@ -97,15 +115,14 @@ final class GoServer: ServerProtocol { guard !port.isEmpty else { let error = GoServerError.invalidPort logger.error("Port not configured") - logContinuation?.yield(ServerLogEntry(level: .error, message: error.localizedDescription, source: .go)) + logContinuation?.yield(ServerLogEntry(level: .error, message: error.localizedDescription)) throw error } logger.info("Starting Go vibetunnel server on port \(self.port)") logContinuation?.yield(ServerLogEntry( level: .info, - message: "Initializing Go vibetunnel server...", - source: .go + message: "Initializing Go vibetunnel server..." )) // Get the vibetunnel binary path @@ -118,8 +135,7 @@ final class GoServer: ServerProtocol { logger.error("Go was not available during build") logContinuation?.yield(ServerLogEntry( level: .error, - message: "Go server is not available. Please install Go and rebuild the app to enable Go server support.", - source: .go + message: "Go server is not available. Please install Go and rebuild the app to enable Go server support." )) throw error } @@ -127,7 +143,7 @@ final class GoServer: ServerProtocol { guard let binaryPath else { let error = GoServerError.binaryNotFound logger.error("vibetunnel binary not found in bundle") - logContinuation?.yield(ServerLogEntry(level: .error, message: error.localizedDescription, source: .go)) + logContinuation?.yield(ServerLogEntry(level: .error, message: error.localizedDescription)) throw error } @@ -151,15 +167,13 @@ final class GoServer: ServerProtocol { // Log binary architecture info logContinuation?.yield(ServerLogEntry( level: .debug, - message: "Binary path: \(binaryPath)", - source: .go + message: "Binary path: \(binaryPath)" )) } else if !fileExists { logger.error("vibetunnel binary NOT FOUND at: \(binaryPath)") logContinuation?.yield(ServerLogEntry( level: .error, - message: "Binary not found at: \(binaryPath)", - source: .go + message: "Binary not found at: \(binaryPath)" )) } @@ -182,8 +196,7 @@ final class GoServer: ServerProtocol { logger.error("Web directory not found at expected location: \(staticPath)") logContinuation?.yield(ServerLogEntry( level: .error, - message: "Web directory not found at: \(staticPath)", - source: .go + message: "Web directory not found at: \(staticPath)" )) } @@ -265,43 +278,36 @@ final class GoServer: ServerProtocol { logContinuation?.yield(ServerLogEntry( level: .error, - message: "Server failed to start: \(errorDetails)", - source: .go + message: "Server failed to start: \(errorDetails)" )) throw GoServerError.processFailedToStart } logger.info("Go server process started, performing health check...") - logContinuation?.yield(ServerLogEntry(level: .info, message: "Performing health check...", source: .go)) + logContinuation?.yield(ServerLogEntry(level: .info, message: "Performing health check...")) // Perform health check to ensure server is actually responding let isHealthy = await performHealthCheck(maxAttempts: 10, delaySeconds: 0.5) if isHealthy { logger.info("Go server started successfully and is responding") - logContinuation?.yield(ServerLogEntry(level: .info, message: "Health check passed ✓", source: .go)) + logContinuation?.yield(ServerLogEntry(level: .info, message: "Health check passed ✓")) logContinuation?.yield(ServerLogEntry( level: .info, - message: "Go vibetunnel server is ready", - source: .go + message: "Go vibetunnel server is ready" )) - // Monitor process termination with task context + // Monitor process termination Task { - await ServerTaskContext.$taskName.withValue("GoServer-monitor-\(port)") { - await ServerTaskContext.$serverType.withValue(.go) { - await monitorProcessTermination() - } - } + await monitorProcessTermination() } } else { // Server process is running but not responding logger.error("Go server process started but is not responding to health checks") logContinuation?.yield(ServerLogEntry( level: .error, - message: "Health check failed - server not responding", - source: .go + message: "Health check failed - server not responding" )) // Clean up the non-responsive process @@ -332,8 +338,7 @@ final class GoServer: ServerProtocol { logger.error("Failed to start Go server: \(errorMessage)") logContinuation?.yield(ServerLogEntry( level: .error, - message: "Failed to start Go server: \(errorMessage)", - source: .go + message: "Failed to start Go server: \(errorMessage)" )) throw error } @@ -348,8 +353,7 @@ final class GoServer: ServerProtocol { logger.info("Stopping Go server") logContinuation?.yield(ServerLogEntry( level: .info, - message: "Shutting down Go vibetunnel server...", - source: .go + message: "Shutting down Go vibetunnel server..." )) // Cancel output monitoring tasks @@ -370,8 +374,7 @@ final class GoServer: ServerProtocol { logger.warning("Force killed Go server after timeout") logContinuation?.yield(ServerLogEntry( level: .warning, - message: "Force killed server after timeout", - source: .go + message: "Force killed server after timeout" )) } @@ -386,14 +389,13 @@ final class GoServer: ServerProtocol { logger.info("Go server stopped") logContinuation?.yield(ServerLogEntry( level: .info, - message: "Go vibetunnel server shutdown complete", - source: .go + message: "Go vibetunnel server shutdown complete" )) } func restart() async throws { logger.info("Restarting Go server") - logContinuation?.yield(ServerLogEntry(level: .info, message: "Restarting server", source: .go)) + logContinuation?.yield(ServerLogEntry(level: .info, message: "Restarting server")) await stop() try await start() @@ -414,8 +416,7 @@ final class GoServer: ServerProtocol { logContinuation?.yield(ServerLogEntry( level: .debug, - message: "Health check attempt \(attempt)/\(maxAttempts)...", - source: .go + message: "Health check attempt \(attempt)/\(maxAttempts)..." )) let (_, response) = try await URLSession.shared.data(for: request) @@ -429,8 +430,7 @@ final class GoServer: ServerProtocol { if attempt == maxAttempts { logContinuation?.yield(ServerLogEntry( level: .warning, - message: "Health check failed after \(maxAttempts) attempts", - source: .go + message: "Health check failed after \(maxAttempts) attempts" )) } } @@ -452,79 +452,69 @@ final class GoServer: ServerProtocol { // Monitor stdout on background thread outputTask = Task.detached { [weak self] in - ServerTaskContext.$taskName.withValue("GoServer-stdout-\(currentPort)") { - ServerTaskContext.$serverType.withValue(.go) { - guard let self, let pipe = stdoutPipe else { return } + guard let self, let pipe = stdoutPipe else { return } - let handle = pipe.fileHandleForReading - self.logger.debug("Starting stdout monitoring for Go server on port \(currentPort)") + let handle = pipe.fileHandleForReading + self.logger.debug("Starting stdout monitoring for Go server on port \(currentPort)") - while !Task.isCancelled { - autoreleasepool { - let data = handle.availableData - if !data.isEmpty, let output = String(data: data, encoding: .utf8) { - let lines = output.trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: .newlines) - for line in lines where !line.isEmpty { - // Skip shell initialization messages - if line.contains("zsh:") || line.hasPrefix("Last login:") { - continue - } - Task { @MainActor [weak self] in - guard let self else { return } - let level = self.detectLogLevel(from: line) - self.logContinuation?.yield(ServerLogEntry( - level: level, - message: line, - source: .go - )) - } - } + while !Task.isCancelled { + autoreleasepool { + let data = handle.availableData + if !data.isEmpty, let output = String(data: data, encoding: .utf8) { + let lines = output.trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: .newlines) + for line in lines where !line.isEmpty { + // Skip shell initialization messages + if line.contains("zsh:") || line.hasPrefix("Last login:") { + continue + } + Task { @MainActor [weak self] in + guard let self else { return } + let level = self.detectLogLevel(from: line) + self.logContinuation?.yield(ServerLogEntry( + level: level, + message: line + )) } } } - - self.logger.debug("Stopped stdout monitoring for Go server") } } + + self.logger.debug("Stopped stdout monitoring for Go server") } // Monitor stderr on background thread errorTask = Task.detached { [weak self] in - ServerTaskContext.$taskName.withValue("GoServer-stderr-\(currentPort)") { - ServerTaskContext.$serverType.withValue(.go) { - guard let self, let pipe = stderrPipe else { return } + guard let self, let pipe = stderrPipe else { return } - let handle = pipe.fileHandleForReading - self.logger.debug("Starting stderr monitoring for Go server on port \(currentPort)") + let handle = pipe.fileHandleForReading + self.logger.debug("Starting stderr monitoring for Go server on port \(currentPort)") - while !Task.isCancelled { - autoreleasepool { - let data = handle.availableData - if !data.isEmpty, let output = String(data: data, encoding: .utf8) { - let lines = output.trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: .newlines) - for line in lines where !line.isEmpty { - // Skip shell initialization messages - if line.contains("zsh:") || line.hasPrefix("Last login:") { - continue - } - Task { @MainActor [weak self] in - guard let self else { return } - self.logContinuation?.yield(ServerLogEntry( - level: .error, - message: line, - source: .go - )) - } - } + while !Task.isCancelled { + autoreleasepool { + let data = handle.availableData + if !data.isEmpty, let output = String(data: data, encoding: .utf8) { + let lines = output.trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: .newlines) + for line in lines where !line.isEmpty { + // Skip shell initialization messages + if line.contains("zsh:") || line.hasPrefix("Last login:") { + continue + } + Task { @MainActor [weak self] in + guard let self else { return } + self.logContinuation?.yield(ServerLogEntry( + level: .error, + message: line + )) } } } - - self.logger.debug("Stopped stderr monitoring for Go server") } } + + self.logger.debug("Stopped stderr monitoring for Go server") } } @@ -540,8 +530,7 @@ final class GoServer: ServerProtocol { self.logger.error("Go server terminated unexpectedly with exit code: \(exitCode)") self.logContinuation?.yield(ServerLogEntry( level: .error, - message: "Server terminated unexpectedly with exit code: \(exitCode)", - source: .go + message: "Server terminated unexpectedly with exit code: \(exitCode)" )) self.isRunning = false @@ -553,8 +542,7 @@ final class GoServer: ServerProtocol { self.logger.info("Auto-restarting Go server after crash") self.logContinuation?.yield(ServerLogEntry( level: .info, - message: "Auto-restarting server after crash", - source: .go + message: "Auto-restarting server after crash" )) try? await self.start() } diff --git a/VibeTunnel/Core/Services/RustServer.swift b/VibeTunnel/Core/Services/RustServer.swift deleted file mode 100644 index 0b4f51d2..00000000 --- a/VibeTunnel/Core/Services/RustServer.swift +++ /dev/null @@ -1,627 +0,0 @@ -import Foundation -import OSLog - -/// Task tracking for better debugging. -/// -/// Provides task-local storage for debugging context during -/// asynchronous server operations. -enum ServerTaskContext { - @TaskLocal static var taskName: String? - - @TaskLocal static var serverType: ServerMode? -} - -/// Rust tty-fwd server implementation. -/// -/// Manages the external tty-fwd Rust binary as a subprocess. This implementation -/// provides high-performance terminal multiplexing by leveraging the battle-tested -/// tty-fwd server. It handles process lifecycle, log streaming, and error recovery -/// while maintaining compatibility with the ServerProtocol interface. -@MainActor -final class RustServer: ServerProtocol { - private var process: Process? - private var stdoutPipe: Pipe? - private var stderrPipe: Pipe? - private var outputTask: Task? - private var errorTask: Task? - - private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "RustServer") - private var logContinuation: AsyncStream.Continuation? - private let processQueue = DispatchQueue(label: "sh.vibetunnel.vibetunnel.RustServer", qos: .userInitiated) - - /// Actor to handle process operations on background thread. - /// - /// Isolates process management operations to prevent blocking the main thread - /// while maintaining Swift concurrency safety. - private actor ProcessHandler { - private let queue = DispatchQueue( - label: "sh.vibetunnel.vibetunnel.RustServer.ProcessHandler", - qos: .userInitiated - ) - - func runProcess(_ process: Process) async throws { - try await withCheckedThrowingContinuation { continuation in - queue.async { - do { - try process.run() - continuation.resume() - } catch { - continuation.resume(throwing: error) - } - } - } - } - - func waitForExit(_ process: Process) async { - await withCheckedContinuation { continuation in - queue.async { - process.waitUntilExit() - continuation.resume() - } - } - } - - func terminateProcess(_ process: Process) async { - await withCheckedContinuation { continuation in - queue.async { - process.terminate() - continuation.resume() - } - } - } - } - - private let processHandler = ProcessHandler() - - var serverType: ServerMode { .rust } - - private(set) var isRunning = false - - var port: String = "" { - didSet { - // If server is running and port changed, we need to restart - if isRunning && oldValue != port { - Task { - try? await restart() - } - } - } - } - - let logStream: AsyncStream - - init() { - var localContinuation: AsyncStream.Continuation? - self.logStream = AsyncStream { continuation in - localContinuation = continuation - } - self.logContinuation = localContinuation - } - - func start() async throws { - guard !isRunning else { - logger.warning("Rust server already running") - return - } - - guard !port.isEmpty else { - let error = RustServerError.invalidPort - logger.error("Port not configured") - logContinuation?.yield(ServerLogEntry(level: .error, message: error.localizedDescription, source: .rust)) - throw error - } - - logger.info("Starting Rust tty-fwd server on port \(self.port)") - logContinuation?.yield(ServerLogEntry( - level: .info, - message: "Initializing Rust tty-fwd server...", - source: .rust - )) - - // Get the tty-fwd binary path - let binaryPath = Bundle.main.path(forResource: "tty-fwd", ofType: nil) - guard let binaryPath else { - let error = RustServerError.binaryNotFound - logger.error("tty-fwd binary not found in bundle") - logContinuation?.yield(ServerLogEntry(level: .error, message: error.localizedDescription, source: .rust)) - throw error - } - - // Ensure binary is executable - try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binaryPath) - - // Verify binary exists and is executable - var isDirectory: ObjCBool = false - let fileExists = FileManager.default.fileExists(atPath: binaryPath, isDirectory: &isDirectory) - logger.info("tty-fwd binary exists: \(fileExists), is directory: \(isDirectory.boolValue)") - - if fileExists && !isDirectory.boolValue { - let attributes = try FileManager.default.attributesOfItem(atPath: binaryPath) - if let permissions = attributes[.posixPermissions] as? NSNumber { - logger.info("tty-fwd binary permissions: \(String(permissions.intValue, radix: 8))") - } - if let fileSize = attributes[.size] as? NSNumber { - logger.info("tty-fwd binary size: \(fileSize.intValue) bytes") - } - - // Log binary architecture info - logContinuation?.yield(ServerLogEntry( - level: .debug, - message: "Binary path: \(binaryPath)", - source: .rust - )) - } else if !fileExists { - logger.error("tty-fwd binary NOT FOUND at: \(binaryPath)") - logContinuation?.yield(ServerLogEntry( - level: .error, - message: "Binary not found at: \(binaryPath)", - source: .rust - )) - } - - // Create the process using login shell - let process = Process() - process.executableURL = URL(fileURLWithPath: "/bin/zsh") - - // Get the Resources directory path - let bundlePath = Bundle.main.bundlePath - let resourcesPath = Bundle.main.resourcePath ?? bundlePath - - // Set working directory to Resources directory where both tty-fwd and web folder exist - process.currentDirectoryURL = URL(fileURLWithPath: resourcesPath) - logger.info("Setting working directory to: \(resourcesPath)") - - // The web/public directory should be at web/public relative to Resources - let webPublicPath = URL(fileURLWithPath: resourcesPath).appendingPathComponent("web/public") - let webPublicExists = FileManager.default.fileExists(atPath: webPublicPath.path) - logger.info("Web public directory at \(webPublicPath.path) exists: \(webPublicExists)") - - if !webPublicExists { - logger.error("Web public directory NOT FOUND at: \(webPublicPath.path)") - logContinuation?.yield(ServerLogEntry( - level: .error, - message: "Web public directory not found at: \(webPublicPath.path)", - source: .rust - )) - // List contents of Resources directory for debugging - if let contents = try? FileManager.default.contentsOfDirectory(atPath: resourcesPath) { - logger.debug("Resources directory contents: \(contents.joined(separator: ", "))") - } - } - - // Use absolute path for static directory - let staticPath = webPublicPath.path - - // Build command to run tty-fwd through login shell - // Use bind address from ServerManager to control server accessibility - let bindAddress = ServerManager.shared.bindAddress - - var ttyFwdCommand = "\"\(binaryPath)\" --static-path \"\(staticPath)\" --serve \(bindAddress):\(port)" - - // Add password flag if password protection is enabled - // Only check if password exists, don't retrieve it yet - if UserDefaults.standard.bool(forKey: "dashboardPasswordEnabled") && DashboardKeychain.shared.hasPassword() { - // Defer actual password retrieval until first authenticated request - // For now, we'll use a placeholder that the Rust server will replace - // when it needs to authenticate - logger.info("Password protection enabled, deferring keychain access") - // Note: The Rust server needs to be updated to support lazy password loading - // For now, we still need to access the keychain here - if let password = DashboardKeychain.shared.getPassword() { - // Escape the password for shell - let escapedPassword = password.replacingOccurrences(of: "\"", with: "\\\"") - .replacingOccurrences(of: "$", with: "\\$") - .replacingOccurrences(of: "`", with: "\\`") - .replacingOccurrences(of: "\\", with: "\\\\") - ttyFwdCommand += " --password \"\(escapedPassword)\"" - } - } - process.arguments = ["-l", "-c", ttyFwdCommand] - - logger.info("Executing command: /bin/zsh -l -c \"\(ttyFwdCommand)\"") - logger.info("Working directory: \(resourcesPath)") - - // Set up environment - login shell will load the rest - var environment = ProcessInfo.processInfo.environment - environment["RUST_LOG"] = "info" - process.environment = environment - - // Set up pipes for stdout and stderr - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - process.standardOutput = stdoutPipe - process.standardError = stderrPipe - - self.process = process - self.stdoutPipe = stdoutPipe - self.stderrPipe = stderrPipe - - // Start monitoring output - startOutputMonitoring() - - do { - // Start the process (this just launches it and returns immediately) - try await processHandler.runProcess(process) - - // Mark server as running - isRunning = true - - logger.info("Rust server process started") - - // Give the process a moment to start before checking for early failures - try await Task.sleep(for: .milliseconds(100)) - - // Check if process exited immediately (indicating failure) - if !process.isRunning { - isRunning = false - let exitCode = process.terminationStatus - logger.error("Process exited immediately with code: \(exitCode)") - - // Try to read any error output - var errorDetails = "Exit code: \(exitCode)" - if let stderrPipe = self.stderrPipe { - let errorData = stderrPipe.fileHandleForReading.availableData - if !errorData.isEmpty, let errorOutput = String(data: errorData, encoding: .utf8) { - errorDetails += "\nError: \(errorOutput.trimmingCharacters(in: .whitespacesAndNewlines))" - } - } - - logContinuation?.yield(ServerLogEntry( - level: .error, - message: "Server failed to start: \(errorDetails)", - source: .rust - )) - - throw RustServerError.processFailedToStart - } - - logger.info("Rust server process started, performing health check...") - logContinuation?.yield(ServerLogEntry(level: .info, message: "Performing health check...", source: .rust)) - - // Perform health check to ensure server is actually responding - let isHealthy = await performHealthCheck(maxAttempts: 10, delaySeconds: 0.5) - - if isHealthy { - logger.info("Rust server started successfully and is responding") - logContinuation?.yield(ServerLogEntry(level: .info, message: "Health check passed ✓", source: .rust)) - logContinuation?.yield(ServerLogEntry( - level: .info, - message: "Rust tty-fwd server is ready", - source: .rust - )) - - // Monitor process termination with task context - Task { - await ServerTaskContext.$taskName.withValue("RustServer-monitor-\(port)") { - await ServerTaskContext.$serverType.withValue(.rust) { - await monitorProcessTermination() - } - } - } - } else { - // Server process is running but not responding - logger.error("Rust server process started but is not responding to health checks") - logContinuation?.yield(ServerLogEntry( - level: .error, - message: "Health check failed - server not responding", - source: .rust - )) - - // Clean up the non-responsive process - process.terminate() - self.process = nil - self.stdoutPipe = nil - self.stderrPipe = nil - isRunning = false - - throw RustServerError.serverNotResponding - } - } catch { - isRunning = false - - // Log more detailed error information - let errorMessage: String - if let rustError = error as? RustServerError { - errorMessage = rustError.localizedDescription - } else if let nsError = error as NSError? { - errorMessage = "\(nsError.localizedDescription) (Code: \(nsError.code), Domain: \(nsError.domain))" - if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] { - logger.error("Underlying error: \(String(describing: underlyingError))") - } - } else { - errorMessage = String(describing: error) - } - - logger.error("Failed to start Rust server: \(errorMessage)") - logContinuation?.yield(ServerLogEntry( - level: .error, - message: "Failed to start Rust server: \(errorMessage)", - source: .rust - )) - throw error - } - } - - func stop() async { - guard let process, isRunning else { - logger.warning("Rust server not running") - return - } - - logger.info("Stopping Rust server") - logContinuation?.yield(ServerLogEntry( - level: .info, - message: "Shutting down Rust tty-fwd server...", - source: .rust - )) - - // Cancel output monitoring tasks - outputTask?.cancel() - errorTask?.cancel() - - // Terminate the process on background thread - await processHandler.terminateProcess(process) - - // Wait for process to terminate (with timeout) - let terminated: Void? = await withTimeoutOrNil(seconds: 5) { [self] in - await self.processHandler.waitForExit(process) - } - - if terminated == nil { - // Force kill if termination timeout - process.interrupt() - logger.warning("Force killed Rust server after timeout") - logContinuation?.yield(ServerLogEntry( - level: .warning, - message: "Force killed server after timeout", - source: .rust - )) - } - - // Clean up - self.process = nil - self.stdoutPipe = nil - self.stderrPipe = nil - self.outputTask = nil - self.errorTask = nil - isRunning = false - - logger.info("Rust server stopped") - logContinuation?.yield(ServerLogEntry( - level: .info, - message: "Rust tty-fwd server shutdown complete", - source: .rust - )) - } - - func restart() async throws { - logger.info("Restarting Rust server") - logContinuation?.yield(ServerLogEntry(level: .info, message: "Restarting server", source: .rust)) - - await stop() - try await start() - } - - // MARK: - Private Methods - - private func performHealthCheck(maxAttempts: Int, delaySeconds: Double) async -> Bool { - guard let healthURL = URL(string: "http://127.0.0.1:\(port)/api/health") else { - return false - } - - for attempt in 1...maxAttempts { - do { - // Create request with short timeout - var request = URLRequest(url: healthURL) - request.timeoutInterval = 2.0 - - logContinuation?.yield(ServerLogEntry( - level: .debug, - message: "Health check attempt \(attempt)/\(maxAttempts)...", - source: .rust - )) - - let (_, response) = try await URLSession.shared.data(for: request) - - if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { - logger.debug("Health check succeeded on attempt \(attempt)") - return true - } - } catch { - logger.debug("Health check attempt \(attempt) failed: \(error.localizedDescription)") - if attempt == maxAttempts { - logContinuation?.yield(ServerLogEntry( - level: .warning, - message: "Health check failed after \(maxAttempts) attempts", - source: .rust - )) - } - } - - // Wait before next attempt (except on last attempt) - if attempt < maxAttempts { - try? await Task.sleep(for: .seconds(delaySeconds)) - } - } - - return false - } - - private func startOutputMonitoring() { - // Capture pipes and port before starting detached tasks - let stdoutPipe = self.stdoutPipe - let stderrPipe = self.stderrPipe - let currentPort = self.port - - // Monitor stdout on background thread - outputTask = Task.detached { [weak self] in - ServerTaskContext.$taskName.withValue("RustServer-stdout-\(currentPort)") { - ServerTaskContext.$serverType.withValue(.rust) { - guard let self, let pipe = stdoutPipe else { return } - - let handle = pipe.fileHandleForReading - self.logger.debug("Starting stdout monitoring for Rust server on port \(currentPort)") - - while !Task.isCancelled { - autoreleasepool { - let data = handle.availableData - if !data.isEmpty, let output = String(data: data, encoding: .utf8) { - let lines = output.trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: .newlines) - for line in lines where !line.isEmpty { - // Skip shell initialization messages - if line.contains("zsh:") || line.hasPrefix("Last login:") { - continue - } - Task { @MainActor [weak self] in - guard let self else { return } - let level = self.detectLogLevel(from: line) - self.logContinuation?.yield(ServerLogEntry( - level: level, - message: line, - source: .rust - )) - } - } - } - } - } - - self.logger.debug("Stopped stdout monitoring for Rust server") - } - } - } - - // Monitor stderr on background thread - errorTask = Task.detached { [weak self] in - ServerTaskContext.$taskName.withValue("RustServer-stderr-\(currentPort)") { - ServerTaskContext.$serverType.withValue(.rust) { - guard let self, let pipe = stderrPipe else { return } - - let handle = pipe.fileHandleForReading - self.logger.debug("Starting stderr monitoring for Rust server on port \(currentPort)") - - while !Task.isCancelled { - autoreleasepool { - let data = handle.availableData - if !data.isEmpty, let output = String(data: data, encoding: .utf8) { - let lines = output.trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: .newlines) - for line in lines where !line.isEmpty { - // Skip shell initialization messages - if line.contains("zsh:") || line.hasPrefix("Last login:") { - continue - } - Task { @MainActor [weak self] in - guard let self else { return } - self.logContinuation?.yield(ServerLogEntry( - level: .error, - message: line, - source: .rust - )) - } - } - } - } - } - - self.logger.debug("Stopped stderr monitoring for Rust server") - } - } - } - } - - private func monitorProcessTermination() async { - guard let process else { return } - - // Wait for process exit on background thread - await processHandler.waitForExit(process) - - if self.isRunning { - // Unexpected termination - let exitCode = process.terminationStatus - self.logger.error("Rust server terminated unexpectedly with exit code: \(exitCode)") - self.logContinuation?.yield(ServerLogEntry( - level: .error, - message: "Server terminated unexpectedly with exit code: \(exitCode)", - source: .rust - )) - - self.isRunning = false - - // Auto-restart on unexpected termination - Task { - try? await Task.sleep(for: .seconds(2)) - if self.process == nil { // Only restart if not manually stopped - self.logger.info("Auto-restarting Rust server after crash") - self.logContinuation?.yield(ServerLogEntry( - level: .info, - message: "Auto-restarting server after crash", - source: .rust - )) - try? await self.start() - } - } - } - } - - private func detectLogLevel(from line: String) -> ServerLogEntry.Level { - let lowercased = line.lowercased() - if lowercased.contains("error") || lowercased.contains("fatal") { - return .error - } else if lowercased.contains("warn") || lowercased.contains("warning") { - return .warning - } else if lowercased.contains("debug") || lowercased.contains("trace") { - return .debug - } else { - return .info - } - } - - private func withTimeoutOrNil( - seconds: TimeInterval, - operation: @escaping @Sendable () async -> T - ) - async -> T? - { - await withTaskGroup(of: T?.self) { group in - group.addTask { - await operation() - } - - group.addTask { - try? await Task.sleep(for: .seconds(seconds)) - return nil - } - - if let result = await group.next() { - group.cancelAll() - return result - } - group.cancelAll() - return nil - } - } -} - -// MARK: - Errors - -enum RustServerError: LocalizedError { - case binaryNotFound - case processFailedToStart - case serverNotResponding - case invalidPort - - var errorDescription: String? { - switch self { - case .binaryNotFound: - "The tty-fwd binary was not found in the app bundle" - case .processFailedToStart: - "The server process failed to start" - case .serverNotResponding: - "The server process started but is not responding to health checks" - case .invalidPort: - "Server port is not configured" - } - } -} diff --git a/VibeTunnel/Core/Services/ServerManager.swift b/VibeTunnel/Core/Services/ServerManager.swift index 91757f1d..f51468bd 100644 --- a/VibeTunnel/Core/Services/ServerManager.swift +++ b/VibeTunnel/Core/Services/ServerManager.swift @@ -3,22 +3,16 @@ import Observation import OSLog import SwiftUI -/// Manages the active server and handles switching between modes. +/// Manages the VibeTunnel server lifecycle. /// /// `ServerManager` is the central coordinator for server lifecycle management in VibeTunnel. -/// It handles starting, stopping, and switching between different server implementations (Rust/Hummingbird), -/// manages server configuration, and provides logging capabilities. The manager ensures only one -/// server instance runs at a time and coordinates smooth transitions between server modes. +/// It handles starting, stopping, and restarting the Go server, manages server configuration, +/// and provides logging capabilities. @MainActor @Observable class ServerManager { @MainActor static let shared = ServerManager() - private var serverModeString: String { - get { UserDefaults.standard.string(forKey: "serverMode") ?? ServerMode.rust.rawValue } - set { UserDefaults.standard.set(newValue, forKey: "serverMode") } - } - var port: String { get { UserDefaults.standard.string(forKey: "serverPort") ?? "4020" } set { UserDefaults.standard.set(newValue, forKey: "serverPort") } @@ -44,9 +38,8 @@ class ServerManager { set { UserDefaults.standard.set(newValue, forKey: "cleanupOnStartup") } } - private(set) var currentServer: ServerProtocol? + private(set) var currentServer: GoServer? private(set) var isRunning = false - private(set) var isSwitching = false private(set) var isRestarting = false private(set) var lastError: Error? private(set) var crashCount = 0 @@ -59,11 +52,6 @@ class ServerManager { private var serverLogTask: Task? private(set) var logStream: AsyncStream! - var serverMode: ServerMode { - get { ServerMode(rawValue: serverModeString) ?? .rust } - set { serverModeString = newValue.rawValue } - } - private init() { setupLogStream() @@ -103,9 +91,7 @@ class ServerManager { @objc private nonisolated func userDefaultsDidChange() { - Task { @MainActor in - await handleServerModeChange() - } + // Server mode is now fixed to Go, no need to handle changes } /// Start the server with current configuration @@ -117,13 +103,11 @@ class ServerManager { // Ensure our state is synced isRunning = true lastError = nil - ServerMonitor.shared.isServerRunning = true // Log for clarity logContinuation?.yield(ServerLogEntry( level: .info, - message: "\(serverMode.displayName) server already running on port \(self.port)", - source: serverMode + message: "Server already running on port \(self.port)" )) return } @@ -138,16 +122,14 @@ class ServerManager { logger.info("Attempting to kill conflicting process: \(processName) (PID: \(pid))") logContinuation?.yield(ServerLogEntry( level: .warning, - message: "Port \(self.port) is used by another instance. Terminating conflicting process...", - source: serverMode + message: "Port \(self.port) is used by another instance. Terminating conflicting process..." )) do { try await PortConflictResolver.shared.resolveConflict(conflict) logContinuation?.yield(ServerLogEntry( level: .info, - message: "Conflicting process terminated successfully", - source: serverMode + message: "Conflicting process terminated successfully" )) // Wait a moment for port to be fully released @@ -157,8 +139,7 @@ class ServerManager { lastError = PortConflictError.failedToKillProcess(pid: pid) logContinuation?.yield(ServerLogEntry( level: .error, - message: "Failed to terminate conflicting process. Please try a different port.", - source: serverMode + message: "Failed to terminate conflicting process. Please try a different port." )) return } @@ -172,8 +153,7 @@ class ServerManager { ) logContinuation?.yield(ServerLogEntry( level: .error, - message: "Port \(self.port) is used by \(appName). Please choose a different port.", - source: serverMode + message: "Port \(self.port) is used by \(appName). Please choose a different port." )) return @@ -186,12 +166,11 @@ class ServerManager { // Log that we're starting a server logContinuation?.yield(ServerLogEntry( level: .info, - message: "Starting \(serverMode.displayName) server on port \(self.port)...", - source: serverMode + message: "Starting server on port \(self.port)..." )) do { - let server = createServer(for: serverMode) + let server = GoServer() server.port = port // Subscribe to server logs @@ -207,10 +186,7 @@ class ServerManager { isRunning = true lastError = nil - logger.info("Started \(self.serverMode.displayName) server on port \(self.port)") - - // Update ServerMonitor for compatibility - ServerMonitor.shared.isServerRunning = true + logger.info("Started server on port \(self.port)") // Trigger cleanup of old sessions after server starts await triggerInitialCleanup() @@ -218,8 +194,7 @@ class ServerManager { logger.error("Failed to start server: \(error.localizedDescription)") logContinuation?.yield(ServerLogEntry( level: .error, - message: "Failed to start \(serverMode.displayName) server: \(error.localizedDescription)", - source: serverMode + message: "Failed to start server: \(error.localizedDescription)" )) lastError = error @@ -227,10 +202,8 @@ class ServerManager { if let server = currentServer, server.isRunning { logger.warning("Server reported as running despite startup error, syncing state") isRunning = true - ServerMonitor.shared.isServerRunning = true } else { isRunning = false - ServerMonitor.shared.isServerRunning = false } } } @@ -242,14 +215,12 @@ class ServerManager { return } - let serverType = server.serverType - logger.info("Stopping \(serverType.displayName) server") + logger.info("Stopping server") // Log that we're stopping the server logContinuation?.yield(ServerLogEntry( level: .info, - message: "Stopping \(serverType.displayName) server...", - source: serverType + message: "Stopping server..." )) await server.stop() @@ -261,15 +232,8 @@ class ServerManager { // Log that the server has stopped logContinuation?.yield(ServerLogEntry( level: .info, - message: "\(serverType.displayName) server stopped", - source: serverType + message: "Server stopped" )) - - // Update ServerMonitor for compatibility - // Only set to false if we're not in the middle of a restart - if !isRestarting { - ServerMonitor.shared.isServerRunning = false - } } /// Restart the current server @@ -281,92 +245,13 @@ class ServerManager { // Log that we're restarting logContinuation?.yield(ServerLogEntry( level: .info, - message: "Restarting server...", - source: serverMode + message: "Restarting server..." )) await stop() await start() } - /// Switch to a different server mode - func switchMode(to mode: ServerMode) async { - guard mode != serverMode else { return } - - isSwitching = true - defer { isSwitching = false } - - let oldMode = serverMode - logger.info("Switching from \(oldMode.displayName) to \(mode.displayName)") - - // Log the mode switch with a clear separator - logContinuation?.yield(ServerLogEntry( - level: .info, - message: "════════════════════════════════════════════════════════", - source: oldMode - )) - logContinuation?.yield(ServerLogEntry( - level: .info, - message: "Switching server mode: \(oldMode.displayName) → \(mode.displayName)", - source: oldMode - )) - logContinuation?.yield(ServerLogEntry( - level: .info, - message: "════════════════════════════════════════════════════════", - source: oldMode - )) - - // Stop current server if running - if currentServer != nil { - await stop() - } - - // Add a small delay for visual clarity in logs - try? await Task.sleep(for: .milliseconds(500)) - - // Update mode - serverMode = mode - - // Update VT config file to match - await updateVTConfig(mode: mode) - - // Start new server - await start() - - // Log completion - logContinuation?.yield(ServerLogEntry( - level: .info, - message: "════════════════════════════════════════════════════════", - source: mode - )) - logContinuation?.yield(ServerLogEntry( - level: .info, - message: "Server mode switch completed successfully", - source: mode - )) - logContinuation?.yield(ServerLogEntry( - level: .info, - message: "════════════════════════════════════════════════════════", - source: mode - )) - } - - private func handleServerModeChange() async { - // This is called when serverMode changes via AppStorage - // If we have a running server, switch to the new mode - if currentServer != nil { - await switchMode(to: serverMode) - } - } - - private func createServer(for mode: ServerMode) -> ServerProtocol { - switch mode { - case .rust: - RustServer() - case .go: - GoServer() - } - } /// Trigger cleanup of exited sessions after server startup private func triggerInitialCleanup() async { @@ -403,15 +288,13 @@ class ServerManager { logger.info("Initial cleanup completed: cleaned \(cleanedCount) exited sessions") logContinuation?.yield(ServerLogEntry( level: .info, - message: "Cleaned up \(cleanedCount) exited sessions on startup", - source: serverMode + message: "Cleaned up \(cleanedCount) exited sessions on startup" )) } else { logger.info("Initial cleanup completed successfully") logContinuation?.yield(ServerLogEntry( level: .info, - message: "Cleaned up exited sessions on startup", - source: serverMode + message: "Cleaned up exited sessions on startup" )) } } else { @@ -423,8 +306,7 @@ class ServerManager { logger.warning("Failed to trigger initial cleanup: \(error.localizedDescription)") logContinuation?.yield(ServerLogEntry( level: .warning, - message: "Could not clean up old sessions: \(error.localizedDescription)", - source: serverMode + message: "Could not clean up old sessions: \(error.localizedDescription)" )) } } @@ -440,10 +322,8 @@ class ServerManager { guard let self else { return } - // Only monitor if we're in Rust mode and server should be running - guard serverMode == .rust, - isRunning, - !isSwitching, + // Only monitor if server should be running + guard isRunning, !isRestarting else { continue } // Check if server is responding @@ -494,8 +374,7 @@ class ServerManager { logger.error("Server crashed (crash #\(self.crashCount))") logContinuation?.yield(ServerLogEntry( level: .error, - message: "Server crashed unexpectedly (crash #\(self.crashCount))", - source: serverMode + message: "Server crashed unexpectedly (crash #\(self.crashCount))" )) // Clear the current server reference @@ -510,20 +389,18 @@ class ServerManager { logger.info("Waiting \(delay) seconds before restart attempt...") logContinuation?.yield(ServerLogEntry( level: .info, - message: "Waiting \(Int(delay)) seconds before restart attempt...", - source: serverMode + message: "Waiting \(Int(delay)) seconds before restart attempt..." )) // Wait with exponential backoff try? await Task.sleep(for: .seconds(delay)) // Attempt to restart - if !Task.isCancelled && serverMode == .rust { + if !Task.isCancelled { logger.info("Attempting to restart server after crash...") logContinuation?.yield(ServerLogEntry( level: .info, - message: "Attempting automatic restart after crash...", - source: serverMode + message: "Attempting automatic restart after crash..." )) await start() @@ -556,30 +433,6 @@ class ServerManager { logger.info("Authentication cache clearing requested - handled by external server") } - /// Update VT config file with the preferred server - private func updateVTConfig(mode: ServerMode) async { - let configPath = FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent(".vibetunnel") - .appendingPathComponent("config.json") - - // Ensure .vibetunnel directory exists - let vibetunnelDir = configPath.deletingLastPathComponent() - try? FileManager.default.createDirectory(at: vibetunnelDir, withIntermediateDirectories: true) - - // Prepare config data - let serverValue = mode == .rust ? "rust" : "go" // Map hummingbird to go as well - let config = ["server": serverValue] - - do { - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - let data = try encoder.encode(config) - try data.write(to: configPath) - logger.info("Updated VT config to use \(serverValue) server") - } catch { - logger.error("Failed to update VT config: \(error)") - } - } } // MARK: - Port Conflict Error Extension diff --git a/VibeTunnel/Core/Services/ServerMonitor.swift b/VibeTunnel/Core/Services/ServerMonitor.swift deleted file mode 100644 index 873d9669..00000000 --- a/VibeTunnel/Core/Services/ServerMonitor.swift +++ /dev/null @@ -1,92 +0,0 @@ -import Foundation -import Observation - -/// Monitors the HTTP server status and provides observable state for the UI. -/// -/// This class now acts as a facade over ServerManager for backward compatibility -/// while providing a simplified interface for UI components to observe server state. -/// It bridges the gap between the older server architecture and the new ServerManager. -@MainActor -@Observable -public final class ServerMonitor { - public static let shared = ServerMonitor() - - /// Observable properties - public var isRunning: Bool { - isServerRunning - } - - public var port: Int { - Int(ServerManager.shared.port) ?? 4_020 - } - - public var lastError: Error? { - ServerManager.shared.lastError - } - - /// Internal state tracking - public var isServerRunning = false - - private init() { - // Sync initial state with ServerManager - Task { - await syncWithServerManager() - } - } - - /// Updates the current status from the server - public func updateStatus() { - Task { - await syncWithServerManager() - } - } - - /// Syncs state with ServerManager - private func syncWithServerManager() async { - // Consider the server as running if it's actually running OR if it's restarting - // This prevents the UI from showing "stopped" during restart - isServerRunning = ServerManager.shared.isRunning || ServerManager.shared.isRestarting - } - - /// Starts the server if not already running - public func startServer() async throws { - // Delegate to ServerManager - await ServerManager.shared.start() - await syncWithServerManager() - } - - /// Stops the server if running - public func stopServer() async throws { - // Delegate to ServerManager - await ServerManager.shared.stop() - await syncWithServerManager() - } - - /// Restarts the server - public func restartServer() async throws { - // During restart, we maintain the running state to prevent UI flicker - await ServerManager.shared.restart() - // Sync after restart completes - await syncWithServerManager() - } - - /// Checks if the server is healthy by making a health check request - public func checkHealth() async -> Bool { - guard isRunning else { return false } - - do { - guard let url = URL(string: "http://127.0.0.1:\(port)/api/health") else { - return false - } - let request = URLRequest(url: url, timeoutInterval: 2.0) - let (_, response) = try await URLSession.shared.data(for: request) - - if let httpResponse = response as? HTTPURLResponse { - return httpResponse.statusCode == 200 - } - } catch { - // Server not responding - } - return false - } -} diff --git a/VibeTunnel/Core/Services/ServerProtocol.swift b/VibeTunnel/Core/Services/ServerProtocol.swift deleted file mode 100644 index 15cbf568..00000000 --- a/VibeTunnel/Core/Services/ServerProtocol.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation - -/// Common interface for server implementations. -/// -/// Defines the contract that all VibeTunnel server implementations must follow. -/// This protocol ensures consistent behavior across different server backends -/// (Hummingbird, Rust) while allowing for implementation-specific details. -@MainActor -protocol ServerProtocol: AnyObject { - /// Current running state of the server - var isRunning: Bool { get } - - /// Port the server is configured to use - var port: String { get set } - - /// Server type identifier - var serverType: ServerMode { get } - - /// Start the server - func start() async throws - - /// Stop the server - func stop() async - - /// Restart the server - func restart() async throws - - /// Stream for receiving log messages - var logStream: AsyncStream { get } -} - -/// Server mode options. -/// -/// Represents the available server implementations that VibeTunnel can use. -/// Each mode corresponds to a different backend technology with its own -/// performance characteristics and feature set. -enum ServerMode: String, CaseIterable { - case rust - case go - - var displayName: String { - switch self { - case .rust: - "Rust" - case .go: - "Go" - } - } - - var description: String { - switch self { - case .rust: - "External tty-fwd binary" - case .go: - "External Go binary" - } - } -} - -/// Log entry from server. -/// -/// Represents a single log message from a server implementation, -/// including severity level, timestamp, and source identification. -struct ServerLogEntry { - /// Severity level of the log entry. - enum Level { - case debug - case info - case warning - case error - } - - let timestamp: Date - let level: Level - let message: String - let source: ServerMode - - init(level: Level = .info, message: String, source: ServerMode) { - self.timestamp = Date() - self.level = level - self.message = message - self.source = source - } -} diff --git a/VibeTunnel/Presentation/Views/MenuBarView.swift b/VibeTunnel/Presentation/Views/MenuBarView.swift index 9f21c629..0c9d1a86 100644 --- a/VibeTunnel/Presentation/Views/MenuBarView.swift +++ b/VibeTunnel/Presentation/Views/MenuBarView.swift @@ -8,28 +8,27 @@ import SwiftUI struct MenuBarView: View { @Environment(SessionMonitor.self) var sessionMonitor - @Environment(ServerMonitor.self) - var serverMonitor + @State private var serverManager = ServerManager.shared @AppStorage("showInDock") private var showInDock = false var body: some View { VStack(alignment: .leading, spacing: 0) { // Server status header - ServerStatusView(isRunning: serverMonitor.isRunning, port: serverMonitor.port) + ServerStatusView(isRunning: serverManager.isRunning, port: Int(serverManager.port) ?? 4020) .padding(.horizontal, 12) .padding(.vertical, 8) // Open Dashboard button Button(action: { - if let dashboardURL = URL(string: "http://127.0.0.1:\(serverMonitor.port)") { + if let dashboardURL = URL(string: "http://127.0.0.1:\(serverManager.port)") { NSWorkspace.shared.open(dashboardURL) } }, label: { Label("Open Dashboard", systemImage: "safari") }) .buttonStyle(MenuButtonStyle()) - .disabled(!serverMonitor.isRunning) + .disabled(!serverManager.isRunning) Divider() .padding(.vertical, 4) diff --git a/VibeTunnel/Presentation/Views/ServerConsoleView.swift b/VibeTunnel/Presentation/Views/ServerConsoleView.swift index 66e0f212..518e900b 100644 --- a/VibeTunnel/Presentation/Views/ServerConsoleView.swift +++ b/VibeTunnel/Presentation/Views/ServerConsoleView.swift @@ -140,15 +140,6 @@ struct ServerLogEntryView: View { .frame(width: 6, height: 6) .padding(.top, 6) - // Source badge - Text(entry.source.displayName) - .font(.caption2) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(entry.source.color.opacity(0.2)) - .foregroundStyle(entry.source.color) - .clipShape(Capsule()) - // Message Text(entry.message) .textSelection(.enabled) @@ -205,8 +196,7 @@ class ServerConsoleViewModel { let logText = logs.map { entry in let timestamp = dateFormatter.string(from: entry.timestamp) let level = String(describing: entry.level).uppercased().padding(toLength: 7, withPad: " ", startingAt: 0) - let source = entry.source.displayName.padding(toLength: 12, withPad: " ", startingAt: 0) - return "[\(timestamp)] [\(level)] [\(source)] \(entry.message)" + return "[\(timestamp)] [\(level)] \(entry.message)" } .joined(separator: "\n") @@ -215,7 +205,7 @@ class ServerConsoleViewModel { savePanel.nameFieldStringValue = "vibetunnel-server-logs.txt" if savePanel.runModal() == .OK, let url = savePanel.url { - try? logText.write(to: url, atomically: true, encoding: .utf8) + try? logText.write(to: url, atomically: true, encoding: String.Encoding.utf8) } } } @@ -248,11 +238,3 @@ extension ServerLogEntry.Level { } } -extension ServerMode { - var color: Color { - switch self { - case .rust: .orange - case .go: .cyan - } - } -} diff --git a/VibeTunnel/Presentation/Views/Settings/DashboardSettingsView.swift b/VibeTunnel/Presentation/Views/Settings/DashboardSettingsView.swift index a625b8d0..3d313ae4 100644 --- a/VibeTunnel/Presentation/Views/Settings/DashboardSettingsView.swift +++ b/VibeTunnel/Presentation/Views/Settings/DashboardSettingsView.swift @@ -49,14 +49,9 @@ struct DashboardSettingsView: View { static func updateServerForPasswordChange(action: PasswordAction, logger: Logger) async { let serverManager = ServerManager.shared - if serverManager.serverMode == .rust { - // Rust server requires restart to apply password changes - logger.info("Restarting Rust server to \(action.logMessage)") - await serverManager.restart() - } else { - // Hummingbird server just needs cache clear - await serverManager.clearAuthCache() - } + // Go server handles authentication internally + logger.info("Clearing auth cache to \(action.logMessage)") + await serverManager.clearAuthCache() } enum PasswordAction { diff --git a/VibeTunnel/Presentation/Views/Settings/DebugSettingsView.swift b/VibeTunnel/Presentation/Views/Settings/DebugSettingsView.swift index cd1ba23b..f1305b25 100644 --- a/VibeTunnel/Presentation/Views/Settings/DebugSettingsView.swift +++ b/VibeTunnel/Presentation/Views/Settings/DebugSettingsView.swift @@ -4,13 +4,10 @@ import SwiftUI /// Debug settings tab for development and troubleshooting struct DebugSettingsView: View { - @State private var serverMonitor = ServerMonitor.shared @AppStorage("debugMode") private var debugMode = false @AppStorage("logLevel") private var logLevel = "info" - @AppStorage("serverMode") - private var serverModeString = ServerMode.rust.rawValue @State private var serverManager = ServerManager.shared @State private var isServerHealthy = false @State private var heartbeatTask: Task? @@ -19,11 +16,11 @@ struct DebugSettingsView: View { private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "DebugSettings") private var isServerRunning: Bool { - serverMonitor.isRunning + serverManager.isRunning } private var serverPort: Int { - serverMonitor.port + Int(serverManager.port) ?? 4020 } var body: some View { @@ -33,7 +30,6 @@ struct DebugSettingsView: View { isServerHealthy: isServerHealthy, isServerRunning: isServerRunning, serverPort: serverPort, - serverModeString: $serverModeString, serverManager: serverManager, getCurrentServerMode: getCurrentServerMode ) @@ -54,8 +50,6 @@ struct DebugSettingsView: View { .scrollContentBackground(.hidden) .navigationTitle("Debug Settings") .onAppear { - // Ensure ServerMonitor is synced with ServerManager - serverMonitor.updateStatus() // Start heartbeat monitoring startHeartbeatMonitoring() } @@ -68,10 +62,6 @@ struct DebugSettingsView: View { // Restart heartbeat monitoring when server state changes startHeartbeatMonitoring() } - .onChange(of: serverModeString) { _, _ in - // Clear health status when switching modes - isServerHealthy = false - } .alert("Purge All User Defaults?", isPresented: $showPurgeConfirmation) { Button("Cancel", role: .cancel) {} Button("Purge", role: .destructive) { @@ -151,13 +141,8 @@ struct DebugSettingsView: View { } private func getCurrentServerMode() -> String { - // If server is switching, show transitioning state - if serverManager.isSwitching { - return "Switching..." - } - - // Always use the configured mode from settings to ensure immediate UI update - return ServerMode(rawValue: serverModeString)?.displayName ?? "None" + // Server mode is fixed to Go + return "Go" } private func openConsole() { @@ -199,7 +184,6 @@ private struct ServerSection: View { let isServerHealthy: Bool let isServerRunning: Bool let serverPort: Int - @Binding var serverModeString: String let serverManager: ServerManager let getCurrentServerMode: () -> String @@ -281,49 +265,6 @@ private struct ServerSection: View { .buttonStyle(.borderedProminent) } - Divider() - - // Server Mode Configuration - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("Server Mode") - Text("Multiple server implementations cause reasons™.") - .font(.caption) - .foregroundStyle(.secondary) - } - Spacer() - Picker("", selection: Binding( - get: { ServerMode(rawValue: serverModeString) ?? .rust }, - set: { newMode in - serverModeString = newMode.rawValue - Task { - await serverManager.switchMode(to: newMode) - } - } - )) { - ForEach(ServerMode.allCases, id: \.self) { mode in - VStack(alignment: .leading) { - Text(mode.displayName) - Text(mode.description) - .font(.caption) - .foregroundStyle(.secondary) - } - .tag(mode) - } - } - .pickerStyle(.menu) - .labelsHidden() - .disabled(serverManager.isSwitching) - } - - // Server mode switching status with consistent height - HStack { - if serverManager.isSwitching { - TextShimmer(text: "Switching server mode...", font: .caption) - .foregroundStyle(.secondary) - } - } - // Port conflict warning if let conflict = portConflict { VStack(alignment: .leading, spacing: 6) { diff --git a/VibeTunnel/Utilities/TerminalLauncher.swift b/VibeTunnel/Utilities/TerminalLauncher.swift index 1814e3b2..81e1113c 100644 --- a/VibeTunnel/Utilities/TerminalLauncher.swift +++ b/VibeTunnel/Utilities/TerminalLauncher.swift @@ -183,7 +183,7 @@ enum Terminal: String, CaseIterable { case .wezterm: // Use unified AppleScript approach for consistency .appleScript(script: unifiedAppleScript(for: config)) - + case .kitty: // Use unified AppleScript approach for consistency .appleScript(script: unifiedAppleScript(for: config)) diff --git a/VibeTunnel/VibeTunnelApp.swift b/VibeTunnel/VibeTunnelApp.swift index faeb14d0..b99f19f0 100644 --- a/VibeTunnel/VibeTunnelApp.swift +++ b/VibeTunnel/VibeTunnelApp.swift @@ -9,7 +9,6 @@ struct VibeTunnelApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @State private var sessionMonitor = SessionMonitor.shared - @State private var serverMonitor = ServerMonitor.shared init() { // No special initialization needed @@ -73,7 +72,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser private(set) var sparkleUpdaterManager: SparkleUpdaterManager? private let serverManager = ServerManager.shared private let sessionMonitor = SessionMonitor.shared - private let serverMonitor = ServerMonitor.shared private let ngrokService = NgrokService.shared private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "AppDelegate")