mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Use modern Swift
This commit is contained in:
parent
831361f5b2
commit
7b02733207
10 changed files with 92 additions and 184 deletions
|
|
@ -219,7 +219,7 @@ struct CastFileGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sleep briefly before checking again
|
// Sleep briefly before checking again
|
||||||
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
try? await Task.sleep(for: .milliseconds(100))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import Combine
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Hummingbird
|
import Hummingbird
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
@ -13,7 +12,7 @@ import OSLog
|
||||||
final class HummingbirdServer: ServerProtocol {
|
final class HummingbirdServer: ServerProtocol {
|
||||||
private var tunnelServer: TunnelServer?
|
private var tunnelServer: TunnelServer?
|
||||||
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "HummingbirdServer")
|
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "HummingbirdServer")
|
||||||
private let logSubject = PassthroughSubject<ServerLogEntry, Never>()
|
private var logContinuation: AsyncStream<ServerLogEntry>.Continuation?
|
||||||
|
|
||||||
var serverType: ServerMode { .hummingbird }
|
var serverType: ServerMode { .hummingbird }
|
||||||
|
|
||||||
|
|
@ -32,8 +31,14 @@ final class HummingbirdServer: ServerProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var logPublisher: AnyPublisher<ServerLogEntry, Never> {
|
let logStream: AsyncStream<ServerLogEntry>
|
||||||
logSubject.eraseToAnyPublisher()
|
|
||||||
|
init() {
|
||||||
|
var localContinuation: AsyncStream<ServerLogEntry>.Continuation?
|
||||||
|
self.logStream = AsyncStream { continuation in
|
||||||
|
localContinuation = continuation
|
||||||
|
}
|
||||||
|
self.logContinuation = localContinuation
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() async throws {
|
func start() async throws {
|
||||||
|
|
@ -43,7 +48,7 @@ final class HummingbirdServer: ServerProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Starting Hummingbird server on port \(self.port)")
|
logger.info("Starting Hummingbird server on port \(self.port)")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Initializing Hummingbird server...",
|
message: "Initializing Hummingbird server...",
|
||||||
source: .hummingbird
|
source: .hummingbird
|
||||||
|
|
@ -58,10 +63,10 @@ final class HummingbirdServer: ServerProtocol {
|
||||||
try await server.start()
|
try await server.start()
|
||||||
|
|
||||||
logger.info("Hummingbird server started successfully")
|
logger.info("Hummingbird server started successfully")
|
||||||
logSubject.send(ServerLogEntry(level: .info, message: "Hummingbird server is ready", source: .hummingbird))
|
logContinuation?.yield(ServerLogEntry(level: .info, message: "Hummingbird server is ready", source: .hummingbird))
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to start Hummingbird server: \(error.localizedDescription)")
|
logger.error("Failed to start Hummingbird server: \(error.localizedDescription)")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .error,
|
level: .error,
|
||||||
message: "Failed to start: \(error.localizedDescription)",
|
message: "Failed to start: \(error.localizedDescription)",
|
||||||
source: .hummingbird
|
source: .hummingbird
|
||||||
|
|
@ -77,7 +82,7 @@ final class HummingbirdServer: ServerProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Stopping Hummingbird server")
|
logger.info("Stopping Hummingbird server")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Shutting down Hummingbird server...",
|
message: "Shutting down Hummingbird server...",
|
||||||
source: .hummingbird
|
source: .hummingbird
|
||||||
|
|
@ -88,14 +93,14 @@ final class HummingbirdServer: ServerProtocol {
|
||||||
tunnelServer = nil
|
tunnelServer = nil
|
||||||
|
|
||||||
logger.info("Hummingbird server stopped")
|
logger.info("Hummingbird server stopped")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Hummingbird server shutdown complete",
|
message: "Hummingbird server shutdown complete",
|
||||||
source: .hummingbird
|
source: .hummingbird
|
||||||
))
|
))
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Error stopping Hummingbird server: \(error.localizedDescription)")
|
logger.error("Error stopping Hummingbird server: \(error.localizedDescription)")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .error,
|
level: .error,
|
||||||
message: "Error stopping: \(error.localizedDescription)",
|
message: "Error stopping: \(error.localizedDescription)",
|
||||||
source: .hummingbird
|
source: .hummingbird
|
||||||
|
|
@ -105,7 +110,7 @@ final class HummingbirdServer: ServerProtocol {
|
||||||
|
|
||||||
func restart() async throws {
|
func restart() async throws {
|
||||||
logger.info("Restarting Hummingbird server")
|
logger.info("Restarting Hummingbird server")
|
||||||
logSubject.send(ServerLogEntry(level: .info, message: "Restarting server", source: .hummingbird))
|
logContinuation?.yield(ServerLogEntry(level: .info, message: "Restarting server", source: .hummingbird))
|
||||||
|
|
||||||
await stop()
|
await stop()
|
||||||
try await start()
|
try await start()
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ final class NgrokService: NgrokTunnelProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
group.addTask {
|
group.addTask {
|
||||||
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
try await Task.sleep(for: .seconds(seconds))
|
||||||
throw NgrokError.networkError("Operation timed out")
|
throw NgrokError.networkError("Operation timed out")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import Combine
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
|
|
@ -27,7 +26,7 @@ final class RustServer: ServerProtocol {
|
||||||
private var errorTask: Task<Void, Never>?
|
private var errorTask: Task<Void, Never>?
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "RustServer")
|
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "RustServer")
|
||||||
private let logSubject = PassthroughSubject<ServerLogEntry, Never>()
|
private var logContinuation: AsyncStream<ServerLogEntry>.Continuation?
|
||||||
private let processQueue = DispatchQueue(label: "com.steipete.VibeTunnel.RustServer", qos: .userInitiated)
|
private let processQueue = DispatchQueue(label: "com.steipete.VibeTunnel.RustServer", qos: .userInitiated)
|
||||||
|
|
||||||
/// Actor to handle process operations on background thread.
|
/// Actor to handle process operations on background thread.
|
||||||
|
|
@ -89,8 +88,14 @@ final class RustServer: ServerProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var logPublisher: AnyPublisher<ServerLogEntry, Never> {
|
let logStream: AsyncStream<ServerLogEntry>
|
||||||
logSubject.eraseToAnyPublisher()
|
|
||||||
|
init() {
|
||||||
|
var localContinuation: AsyncStream<ServerLogEntry>.Continuation?
|
||||||
|
self.logStream = AsyncStream { continuation in
|
||||||
|
localContinuation = continuation
|
||||||
|
}
|
||||||
|
self.logContinuation = localContinuation
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() async throws {
|
func start() async throws {
|
||||||
|
|
@ -102,19 +107,19 @@ final class RustServer: ServerProtocol {
|
||||||
guard !port.isEmpty else {
|
guard !port.isEmpty else {
|
||||||
let error = RustServerError.invalidPort
|
let error = RustServerError.invalidPort
|
||||||
logger.error("Port not configured")
|
logger.error("Port not configured")
|
||||||
logSubject.send(ServerLogEntry(level: .error, message: error.localizedDescription, source: .rust))
|
logContinuation?.yield(ServerLogEntry(level: .error, message: error.localizedDescription, source: .rust))
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Starting Rust tty-fwd server on port \(self.port)")
|
logger.info("Starting Rust tty-fwd server on port \(self.port)")
|
||||||
logSubject.send(ServerLogEntry(level: .info, message: "Initializing Rust tty-fwd server...", source: .rust))
|
logContinuation?.yield(ServerLogEntry(level: .info, message: "Initializing Rust tty-fwd server...", source: .rust))
|
||||||
|
|
||||||
// Get the tty-fwd binary path
|
// Get the tty-fwd binary path
|
||||||
let binaryPath = Bundle.main.path(forResource: "tty-fwd", ofType: nil)
|
let binaryPath = Bundle.main.path(forResource: "tty-fwd", ofType: nil)
|
||||||
guard let binaryPath else {
|
guard let binaryPath else {
|
||||||
let error = RustServerError.binaryNotFound
|
let error = RustServerError.binaryNotFound
|
||||||
logger.error("tty-fwd binary not found in bundle")
|
logger.error("tty-fwd binary not found in bundle")
|
||||||
logSubject.send(ServerLogEntry(level: .error, message: error.localizedDescription, source: .rust))
|
logContinuation?.yield(ServerLogEntry(level: .error, message: error.localizedDescription, source: .rust))
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,7 +222,7 @@ final class RustServer: ServerProtocol {
|
||||||
let errorData = stderrPipe.fileHandleForReading.availableData
|
let errorData = stderrPipe.fileHandleForReading.availableData
|
||||||
if !errorData.isEmpty, let errorOutput = String(data: errorData, encoding: .utf8) {
|
if !errorData.isEmpty, let errorOutput = String(data: errorData, encoding: .utf8) {
|
||||||
logger.error("Process stderr: \(errorOutput)")
|
logger.error("Process stderr: \(errorOutput)")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .error,
|
level: .error,
|
||||||
message: "Process error: \(errorOutput)",
|
message: "Process error: \(errorOutput)",
|
||||||
source: .rust
|
source: .rust
|
||||||
|
|
@ -229,15 +234,15 @@ final class RustServer: ServerProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Rust server process started, performing health check...")
|
logger.info("Rust server process started, performing health check...")
|
||||||
logSubject.send(ServerLogEntry(level: .info, message: "Performing health check...", source: .rust))
|
logContinuation?.yield(ServerLogEntry(level: .info, message: "Performing health check...", source: .rust))
|
||||||
|
|
||||||
// Perform health check to ensure server is actually responding
|
// Perform health check to ensure server is actually responding
|
||||||
let isHealthy = await performHealthCheck(maxAttempts: 10, delaySeconds: 0.5)
|
let isHealthy = await performHealthCheck(maxAttempts: 10, delaySeconds: 0.5)
|
||||||
|
|
||||||
if isHealthy {
|
if isHealthy {
|
||||||
logger.info("Rust server started successfully and is responding")
|
logger.info("Rust server started successfully and is responding")
|
||||||
logSubject.send(ServerLogEntry(level: .info, message: "Health check passed ✓", source: .rust))
|
logContinuation?.yield(ServerLogEntry(level: .info, message: "Health check passed ✓", source: .rust))
|
||||||
logSubject.send(ServerLogEntry(level: .info, message: "Rust tty-fwd server is ready", source: .rust))
|
logContinuation?.yield(ServerLogEntry(level: .info, message: "Rust tty-fwd server is ready", source: .rust))
|
||||||
|
|
||||||
// Monitor process termination with task context
|
// Monitor process termination with task context
|
||||||
Task {
|
Task {
|
||||||
|
|
@ -250,7 +255,7 @@ final class RustServer: ServerProtocol {
|
||||||
} else {
|
} else {
|
||||||
// Server process is running but not responding
|
// Server process is running but not responding
|
||||||
logger.error("Rust server process started but is not responding to health checks")
|
logger.error("Rust server process started but is not responding to health checks")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .error,
|
level: .error,
|
||||||
message: "Health check failed - server not responding",
|
message: "Health check failed - server not responding",
|
||||||
source: .rust
|
source: .rust
|
||||||
|
|
@ -268,7 +273,7 @@ final class RustServer: ServerProtocol {
|
||||||
} catch {
|
} catch {
|
||||||
isRunning = false
|
isRunning = false
|
||||||
logger.error("Failed to start Rust server: \(error.localizedDescription)")
|
logger.error("Failed to start Rust server: \(error.localizedDescription)")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .error,
|
level: .error,
|
||||||
message: "Failed to start: \(error.localizedDescription)",
|
message: "Failed to start: \(error.localizedDescription)",
|
||||||
source: .rust
|
source: .rust
|
||||||
|
|
@ -284,7 +289,7 @@ final class RustServer: ServerProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Stopping Rust server")
|
logger.info("Stopping Rust server")
|
||||||
logSubject.send(ServerLogEntry(level: .info, message: "Shutting down Rust tty-fwd server...", source: .rust))
|
logContinuation?.yield(ServerLogEntry(level: .info, message: "Shutting down Rust tty-fwd server...", source: .rust))
|
||||||
|
|
||||||
// Cancel output monitoring tasks
|
// Cancel output monitoring tasks
|
||||||
outputTask?.cancel()
|
outputTask?.cancel()
|
||||||
|
|
@ -302,7 +307,7 @@ final class RustServer: ServerProtocol {
|
||||||
// Force kill if termination timeout
|
// Force kill if termination timeout
|
||||||
process.interrupt()
|
process.interrupt()
|
||||||
logger.warning("Force killed Rust server after timeout")
|
logger.warning("Force killed Rust server after timeout")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .warning,
|
level: .warning,
|
||||||
message: "Force killed server after timeout",
|
message: "Force killed server after timeout",
|
||||||
source: .rust
|
source: .rust
|
||||||
|
|
@ -318,12 +323,12 @@ final class RustServer: ServerProtocol {
|
||||||
isRunning = false
|
isRunning = false
|
||||||
|
|
||||||
logger.info("Rust server stopped")
|
logger.info("Rust server stopped")
|
||||||
logSubject.send(ServerLogEntry(level: .info, message: "Rust tty-fwd server shutdown complete", source: .rust))
|
logContinuation?.yield(ServerLogEntry(level: .info, message: "Rust tty-fwd server shutdown complete", source: .rust))
|
||||||
}
|
}
|
||||||
|
|
||||||
func restart() async throws {
|
func restart() async throws {
|
||||||
logger.info("Restarting Rust server")
|
logger.info("Restarting Rust server")
|
||||||
logSubject.send(ServerLogEntry(level: .info, message: "Restarting server", source: .rust))
|
logContinuation?.yield(ServerLogEntry(level: .info, message: "Restarting server", source: .rust))
|
||||||
|
|
||||||
await stop()
|
await stop()
|
||||||
try await start()
|
try await start()
|
||||||
|
|
@ -342,7 +347,7 @@ final class RustServer: ServerProtocol {
|
||||||
var request = URLRequest(url: healthURL)
|
var request = URLRequest(url: healthURL)
|
||||||
request.timeoutInterval = 2.0
|
request.timeoutInterval = 2.0
|
||||||
|
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .debug,
|
level: .debug,
|
||||||
message: "Health check attempt \(attempt)/\(maxAttempts)...",
|
message: "Health check attempt \(attempt)/\(maxAttempts)...",
|
||||||
source: .rust
|
source: .rust
|
||||||
|
|
@ -357,7 +362,7 @@ final class RustServer: ServerProtocol {
|
||||||
} catch {
|
} catch {
|
||||||
logger.debug("Health check attempt \(attempt) failed: \(error.localizedDescription)")
|
logger.debug("Health check attempt \(attempt) failed: \(error.localizedDescription)")
|
||||||
if attempt == maxAttempts {
|
if attempt == maxAttempts {
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .warning,
|
level: .warning,
|
||||||
message: "Health check failed after \(maxAttempts) attempts",
|
message: "Health check failed after \(maxAttempts) attempts",
|
||||||
source: .rust
|
source: .rust
|
||||||
|
|
@ -403,7 +408,7 @@ final class RustServer: ServerProtocol {
|
||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
let level = self.detectLogLevel(from: line)
|
let level = self.detectLogLevel(from: line)
|
||||||
self.logSubject.send(ServerLogEntry(level: level, message: line, source: .rust))
|
self.logContinuation?.yield(ServerLogEntry(level: level, message: line, source: .rust))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -437,7 +442,7 @@ final class RustServer: ServerProtocol {
|
||||||
}
|
}
|
||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
self.logSubject.send(ServerLogEntry(
|
self.logContinuation?.yield(ServerLogEntry(
|
||||||
level: .error,
|
level: .error,
|
||||||
message: line,
|
message: line,
|
||||||
source: .rust
|
source: .rust
|
||||||
|
|
@ -464,7 +469,7 @@ final class RustServer: ServerProtocol {
|
||||||
// Unexpected termination
|
// Unexpected termination
|
||||||
let exitCode = process.terminationStatus
|
let exitCode = process.terminationStatus
|
||||||
self.logger.error("Rust server terminated unexpectedly with exit code: \(exitCode)")
|
self.logger.error("Rust server terminated unexpectedly with exit code: \(exitCode)")
|
||||||
self.logSubject.send(ServerLogEntry(
|
self.logContinuation?.yield(ServerLogEntry(
|
||||||
level: .error,
|
level: .error,
|
||||||
message: "Server terminated unexpectedly with exit code: \(exitCode)",
|
message: "Server terminated unexpectedly with exit code: \(exitCode)",
|
||||||
source: .rust
|
source: .rust
|
||||||
|
|
@ -477,7 +482,7 @@ final class RustServer: ServerProtocol {
|
||||||
try? await Task.sleep(for: .seconds(2))
|
try? await Task.sleep(for: .seconds(2))
|
||||||
if self.process == nil { // Only restart if not manually stopped
|
if self.process == nil { // Only restart if not manually stopped
|
||||||
self.logger.info("Auto-restarting Rust server after crash")
|
self.logger.info("Auto-restarting Rust server after crash")
|
||||||
self.logSubject.send(ServerLogEntry(
|
self.logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Auto-restarting server after crash",
|
message: "Auto-restarting server after crash",
|
||||||
source: .rust
|
source: .rust
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import Combine
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
@ -52,45 +51,33 @@ class ServerManager {
|
||||||
private(set) var lastError: Error?
|
private(set) var lastError: Error?
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "ServerManager")
|
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "ServerManager")
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var logContinuation: AsyncStream<ServerLogEntry>.Continuation?
|
||||||
private let logSubject = PassthroughSubject<ServerLogEntry, Never>()
|
private var serverLogTask: Task<Void, Never>?
|
||||||
|
private(set) var logStream: AsyncStream<ServerLogEntry>!
|
||||||
|
|
||||||
var serverMode: ServerMode {
|
var serverMode: ServerMode {
|
||||||
get { ServerMode(rawValue: serverModeString) ?? .rust }
|
get { ServerMode(rawValue: serverModeString) ?? .rust }
|
||||||
set { serverModeString = newValue.rawValue }
|
set { serverModeString = newValue.rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
var logPublisher: AnyPublisher<ServerLogEntry, Never> {
|
|
||||||
logSubject.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modern async stream for logs
|
|
||||||
var logStream: AsyncStream<ServerLogEntry> {
|
|
||||||
AsyncStream { continuation in
|
|
||||||
// Use logPublisher directly without storing the cancellable
|
|
||||||
Task { @MainActor in
|
|
||||||
for await entry in logPublisher.values {
|
|
||||||
continuation.yield(entry)
|
|
||||||
}
|
|
||||||
continuation.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
|
setupLogStream()
|
||||||
setupObservers()
|
setupObservers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setupLogStream() {
|
||||||
|
logStream = AsyncStream { continuation in
|
||||||
|
self.logContinuation = continuation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func setupObservers() {
|
private func setupObservers() {
|
||||||
// Watch for server mode changes when the value actually changes
|
// Watch for server mode changes when the value actually changes
|
||||||
// Since we're using @AppStorage, we need to observe changes differently
|
Task { @MainActor in
|
||||||
NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)
|
for await _ in NotificationCenter.default.notifications(named: UserDefaults.didChangeNotification) {
|
||||||
.sink { [weak self] _ in
|
await handleServerModeChange()
|
||||||
Task { @MainActor [weak self] in
|
|
||||||
await self?.handleServerModeChange()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start the server with current configuration
|
/// Start the server with current configuration
|
||||||
|
|
@ -105,7 +92,7 @@ class ServerManager {
|
||||||
ServerMonitor.shared.isServerRunning = true
|
ServerMonitor.shared.isServerRunning = true
|
||||||
|
|
||||||
// Log for clarity
|
// Log for clarity
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "\(serverMode.displayName) server already running on port \(port)",
|
message: "\(serverMode.displayName) server already running on port \(port)",
|
||||||
source: serverMode
|
source: serverMode
|
||||||
|
|
@ -114,7 +101,7 @@ class ServerManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log that we're starting a server
|
// Log that we're starting a server
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Starting \(serverMode.displayName) server on port \(port)...",
|
message: "Starting \(serverMode.displayName) server on port \(port)...",
|
||||||
source: serverMode
|
source: serverMode
|
||||||
|
|
@ -125,11 +112,11 @@ class ServerManager {
|
||||||
server.port = port
|
server.port = port
|
||||||
|
|
||||||
// Subscribe to server logs
|
// Subscribe to server logs
|
||||||
server.logPublisher
|
serverLogTask = Task { [weak self] in
|
||||||
.sink { [weak self] entry in
|
for await entry in server.logStream {
|
||||||
self?.logSubject.send(entry)
|
self?.logContinuation?.yield(entry)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
}
|
||||||
|
|
||||||
try await server.start()
|
try await server.start()
|
||||||
|
|
||||||
|
|
@ -146,7 +133,7 @@ class ServerManager {
|
||||||
await triggerInitialCleanup()
|
await triggerInitialCleanup()
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to start server: \(error.localizedDescription)")
|
logger.error("Failed to start server: \(error.localizedDescription)")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .error,
|
level: .error,
|
||||||
message: "Failed to start \(serverMode.displayName) server: \(error.localizedDescription)",
|
message: "Failed to start \(serverMode.displayName) server: \(error.localizedDescription)",
|
||||||
source: serverMode
|
source: serverMode
|
||||||
|
|
@ -176,18 +163,20 @@ class ServerManager {
|
||||||
logger.info("Stopping \(serverType.displayName) server")
|
logger.info("Stopping \(serverType.displayName) server")
|
||||||
|
|
||||||
// Log that we're stopping the server
|
// Log that we're stopping the server
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Stopping \(serverType.displayName) server...",
|
message: "Stopping \(serverType.displayName) server...",
|
||||||
source: serverType
|
source: serverType
|
||||||
))
|
))
|
||||||
|
|
||||||
await server.stop()
|
await server.stop()
|
||||||
|
serverLogTask?.cancel()
|
||||||
|
serverLogTask = nil
|
||||||
currentServer = nil
|
currentServer = nil
|
||||||
isRunning = false
|
isRunning = false
|
||||||
|
|
||||||
// Log that the server has stopped
|
// Log that the server has stopped
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "\(serverType.displayName) server stopped",
|
message: "\(serverType.displayName) server stopped",
|
||||||
source: serverType
|
source: serverType
|
||||||
|
|
@ -207,7 +196,7 @@ class ServerManager {
|
||||||
defer { isRestarting = false }
|
defer { isRestarting = false }
|
||||||
|
|
||||||
// Log that we're restarting
|
// Log that we're restarting
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Restarting server...",
|
message: "Restarting server...",
|
||||||
source: serverMode
|
source: serverMode
|
||||||
|
|
@ -228,17 +217,17 @@ class ServerManager {
|
||||||
logger.info("Switching from \(oldMode.displayName) to \(mode.displayName)")
|
logger.info("Switching from \(oldMode.displayName) to \(mode.displayName)")
|
||||||
|
|
||||||
// Log the mode switch with a clear separator
|
// Log the mode switch with a clear separator
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "════════════════════════════════════════════════════════",
|
message: "════════════════════════════════════════════════════════",
|
||||||
source: oldMode
|
source: oldMode
|
||||||
))
|
))
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Switching server mode: \(oldMode.displayName) → \(mode.displayName)",
|
message: "Switching server mode: \(oldMode.displayName) → \(mode.displayName)",
|
||||||
source: oldMode
|
source: oldMode
|
||||||
))
|
))
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "════════════════════════════════════════════════════════",
|
message: "════════════════════════════════════════════════════════",
|
||||||
source: oldMode
|
source: oldMode
|
||||||
|
|
@ -259,17 +248,17 @@ class ServerManager {
|
||||||
await start()
|
await start()
|
||||||
|
|
||||||
// Log completion
|
// Log completion
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "════════════════════════════════════════════════════════",
|
message: "════════════════════════════════════════════════════════",
|
||||||
source: mode
|
source: mode
|
||||||
))
|
))
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Server mode switch completed successfully",
|
message: "Server mode switch completed successfully",
|
||||||
source: mode
|
source: mode
|
||||||
))
|
))
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "════════════════════════════════════════════════════════",
|
message: "════════════════════════════════════════════════════════",
|
||||||
source: mode
|
source: mode
|
||||||
|
|
@ -326,14 +315,14 @@ class ServerManager {
|
||||||
let cleanedCount = jsonData["cleaned_count"] as? Int
|
let cleanedCount = jsonData["cleaned_count"] as? Int
|
||||||
{
|
{
|
||||||
logger.info("Initial cleanup completed: cleaned \(cleanedCount) exited sessions")
|
logger.info("Initial cleanup completed: cleaned \(cleanedCount) exited sessions")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Cleaned up \(cleanedCount) exited sessions on startup",
|
message: "Cleaned up \(cleanedCount) exited sessions on startup",
|
||||||
source: serverMode
|
source: serverMode
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
logger.info("Initial cleanup completed successfully")
|
logger.info("Initial cleanup completed successfully")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Cleaned up exited sessions on startup",
|
message: "Cleaned up exited sessions on startup",
|
||||||
source: serverMode
|
source: serverMode
|
||||||
|
|
@ -346,7 +335,7 @@ class ServerManager {
|
||||||
} catch {
|
} catch {
|
||||||
// Log the error but don't fail startup
|
// Log the error but don't fail startup
|
||||||
logger.warning("Failed to trigger initial cleanup: \(error.localizedDescription)")
|
logger.warning("Failed to trigger initial cleanup: \(error.localizedDescription)")
|
||||||
logSubject.send(ServerLogEntry(
|
logContinuation?.yield(ServerLogEntry(
|
||||||
level: .warning,
|
level: .warning,
|
||||||
message: "Could not clean up old sessions: \(error.localizedDescription)",
|
message: "Could not clean up old sessions: \(error.localizedDescription)",
|
||||||
source: serverMode
|
source: serverMode
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import Combine
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Common interface for server implementations.
|
/// Common interface for server implementations.
|
||||||
|
|
@ -26,8 +25,8 @@ protocol ServerProtocol: AnyObject {
|
||||||
/// Restart the server
|
/// Restart the server
|
||||||
func restart() async throws
|
func restart() async throws
|
||||||
|
|
||||||
/// Publisher for streaming log messages
|
/// Stream for receiving log messages
|
||||||
var logPublisher: AnyPublisher<ServerLogEntry, Never> { get }
|
var logStream: AsyncStream<ServerLogEntry> { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Server mode options.
|
/// Server mode options.
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ actor TerminalManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
group.addTask {
|
group.addTask {
|
||||||
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
try await Task.sleep(for: .seconds(seconds))
|
||||||
throw TunnelError.timeout
|
throw TunnelError.timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -395,7 +395,7 @@ public final class TunnelServer {
|
||||||
// Wait for the server to actually start listening
|
// Wait for the server to actually start listening
|
||||||
var serverStarted = false
|
var serverStarted = false
|
||||||
for _ in 0..<10 { // Try for up to 1 second
|
for _ in 0..<10 { // Try for up to 1 second
|
||||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
|
||||||
// Check if the server is actually listening
|
// Check if the server is actually listening
|
||||||
if await isServerListening(on: port) {
|
if await isServerListening(on: port) {
|
||||||
|
|
@ -769,7 +769,7 @@ public final class TunnelServer {
|
||||||
// Wait up to 3 seconds for session ID
|
// Wait up to 3 seconds for session ID
|
||||||
let maxAttempts = 30
|
let maxAttempts = 30
|
||||||
for _ in 0..<maxAttempts {
|
for _ in 0..<maxAttempts {
|
||||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
|
||||||
let moreData = outputPipe.fileHandleForReading.availableData
|
let moreData = outputPipe.fileHandleForReading.availableData
|
||||||
if !moreData.isEmpty {
|
if !moreData.isEmpty {
|
||||||
|
|
@ -975,7 +975,7 @@ public final class TunnelServer {
|
||||||
// Send heartbeat every 15 seconds to keep connection alive
|
// Send heartbeat every 15 seconds to keep connection alive
|
||||||
while !Task.isCancelled {
|
while !Task.isCancelled {
|
||||||
do {
|
do {
|
||||||
try await Task.sleep(nanoseconds: 15_000_000_000) // 15 seconds
|
try await Task.sleep(for: .seconds(15))
|
||||||
|
|
||||||
// Send SSE comment as heartbeat (comments start with ':')
|
// Send SSE comment as heartbeat (comments start with ':')
|
||||||
var heartbeat = ByteBuffer()
|
var heartbeat = ByteBuffer()
|
||||||
|
|
|
||||||
|
|
@ -219,8 +219,11 @@ struct DebugSettingsView: View {
|
||||||
UserDefaults.standard.synchronize()
|
UserDefaults.standard.synchronize()
|
||||||
|
|
||||||
// Quit the app after a short delay to ensure the purge completes
|
// Quit the app after a short delay to ensure the purge completes
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
Task {
|
||||||
NSApplication.shared.terminate(nil)
|
try? await Task.sleep(for: .milliseconds(500))
|
||||||
|
await MainActor.run {
|
||||||
|
NSApplication.shared.terminate(nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
# Modern Swift Refactoring Summary
|
|
||||||
|
|
||||||
This document summarizes the modernization changes made to the VibeTunnel codebase to align with modern Swift best practices as outlined in `modern-swift.md`.
|
|
||||||
|
|
||||||
## Key Changes
|
|
||||||
|
|
||||||
### 1. Converted @ObservableObject to @Observable
|
|
||||||
|
|
||||||
- **SessionMonitor.swift**: Converted from `@ObservableObject` with `@Published` properties to `@Observable` class
|
|
||||||
- Removed `import Combine`
|
|
||||||
- Replaced `@Published` with regular properties
|
|
||||||
- Changed from `ObservableObject` to `@Observable`
|
|
||||||
|
|
||||||
- **TunnelServerDemo.swift**: Converted from `@ObservableObject` to `@Observable`
|
|
||||||
- Removed `import Combine`
|
|
||||||
- Simplified property declarations
|
|
||||||
|
|
||||||
- **SparkleViewModel**: Converted stub implementation to use `@Observable`
|
|
||||||
|
|
||||||
### 2. Replaced Combine with Async/Await
|
|
||||||
|
|
||||||
- **SessionMonitor.swift**:
|
|
||||||
- Replaced `Timer` with `Task` for periodic monitoring
|
|
||||||
- Used `Task.sleep(for:)` instead of Timer callbacks
|
|
||||||
- Eliminated nested `Task { }` blocks
|
|
||||||
|
|
||||||
- **TunnelClient.swift**:
|
|
||||||
- Removed `PassthroughSubject` from WebSocket implementation
|
|
||||||
- Replaced with `AsyncStream<WSMessage>` for message handling
|
|
||||||
- Updated delegate methods to use `continuation.yield()` instead of `subject.send()`
|
|
||||||
|
|
||||||
### 3. Simplified State Management
|
|
||||||
|
|
||||||
- **VibeTunnelApp.swift**:
|
|
||||||
- Changed `@StateObject` to `@State` for SessionMonitor
|
|
||||||
- Updated `.environmentObject()` to `.environment()` for modern environment injection
|
|
||||||
|
|
||||||
- **MenuBarView.swift**:
|
|
||||||
- Changed `@EnvironmentObject` to `@Environment(SessionMonitor.self)`
|
|
||||||
|
|
||||||
- **SettingsView.swift**:
|
|
||||||
- Removed `ServerObserver` ViewModel completely
|
|
||||||
- Moved server state directly into view as `@State private var httpServer`
|
|
||||||
- Simplified server state access with computed properties
|
|
||||||
|
|
||||||
### 4. Modernized Async Operations
|
|
||||||
|
|
||||||
- **VibeTunnelApp.swift**:
|
|
||||||
- Replaced `DispatchQueue.main.asyncAfter` with `Task.sleep(for:)`
|
|
||||||
- Updated all `Task.sleep(nanoseconds:)` to `Task.sleep(for:)` with Duration
|
|
||||||
|
|
||||||
- **SettingsView.swift**:
|
|
||||||
- Replaced `.onAppear` with `.task` for async initialization
|
|
||||||
- Modernized Task.sleep usage throughout
|
|
||||||
|
|
||||||
### 5. Removed Unnecessary Abstractions
|
|
||||||
|
|
||||||
- Eliminated `ServerObserver` class - moved logic directly into views
|
|
||||||
- Removed Combine imports where no longer needed
|
|
||||||
- Simplified state ownership by keeping it close to where it's used
|
|
||||||
|
|
||||||
### 6. Updated Error Handling
|
|
||||||
|
|
||||||
- Maintained proper async/await error handling with try/catch
|
|
||||||
- Removed completion handler patterns where applicable
|
|
||||||
- Simplified error state management
|
|
||||||
|
|
||||||
## Benefits Achieved
|
|
||||||
|
|
||||||
1. **Reduced Dependencies**: Removed Combine dependency from most files
|
|
||||||
2. **Simpler Code**: Eliminated unnecessary ViewModels and abstractions
|
|
||||||
3. **Better Performance**: Native SwiftUI state management with @Observable
|
|
||||||
4. **Modern Patterns**: Consistent use of async/await throughout
|
|
||||||
5. **Cleaner Architecture**: State lives closer to where it's used
|
|
||||||
|
|
||||||
## Migration Notes
|
|
||||||
|
|
||||||
- All @Observable classes require iOS 17+ / macOS 14+
|
|
||||||
- AsyncStream provides a more natural API than Combine subjects
|
|
||||||
- Task-based monitoring is more efficient than Timer-based
|
|
||||||
- SwiftUI's built-in state management eliminates need for custom ViewModels
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
1. `/VibeTunnel/Core/Services/SessionMonitor.swift`
|
|
||||||
2. `/VibeTunnel/Core/Services/TunnelClient.swift`
|
|
||||||
3. `/VibeTunnel/Core/Services/TunnelServerDemo.swift`
|
|
||||||
4. `/VibeTunnel/Core/Services/SparkleUpdaterManager.swift`
|
|
||||||
5. `/VibeTunnel/VibeTunnelApp.swift`
|
|
||||||
6. `/VibeTunnel/Views/MenuBarView.swift`
|
|
||||||
7. `/VibeTunnel/SettingsView.swift`
|
|
||||||
|
|
||||||
All changes follow the principles outlined in the Modern Swift guidelines, embracing SwiftUI's native patterns and avoiding unnecessary complexity.
|
|
||||||
Loading…
Reference in a new issue