import Darwin import Foundation import OSLog /// Manages UNIX socket connection for screen capture communication with automatic reconnection @MainActor final class UnixSocketConnection { private let logger = Logger(subsystem: BundleIdentifiers.loggerSubsystem, category: "UnixSocket") // MARK: - Properties private nonisolated(unsafe) var socketFD: Int32 = -1 private let socketPath: String private let queue = DispatchQueue(label: "sh.vibetunnel.unix-socket", qos: .userInitiated) /// Socket state private(set) var isConnected = false private var isConnecting = false /// Buffer for accumulating partial messages private var receiveBuffer = Data() /// Task for continuous message receiving private var receiveTask: Task? /// Keep-alive timer private var keepAliveTimer: Timer? private let keepAliveInterval: TimeInterval = 30.0 private var lastPongTime = Date() /// Reconnection management private var reconnectTask: Task? private var reconnectDelay: TimeInterval = 1.0 private let initialReconnectDelay: TimeInterval = 1.0 private let maxReconnectDelay: TimeInterval = 30.0 private var isReconnecting = false private var shouldReconnect = true private var consecutiveFailures = 0 /// Message queuing for reliability private var pendingMessages: [(data: Data, completion: (@Sendable (Error?) -> Void)?)] = [] private let maxPendingMessages = 100 /// Connection state tracking private var lastConnectionTime: Date? /// Message handler callback var onMessage: ((Data) -> Void)? /// Connection state change callback var onStateChange: ((ConnectionState) -> Void)? /// Connection states for the Unix socket. /// /// Represents the various states of the socket connection lifecycle, /// similar to `NWConnection.State` for consistency with Network framework patterns. enum ConnectionState { case setup case preparing case ready case failed(Error) case cancelled case waiting(Error) } // MARK: - Initialization init(socketPath: String? = nil) { // Use socket path in user's home directory to avoid /tmp issues let home = FileManager.default.homeDirectoryForCurrentUser.path self.socketPath = socketPath ?? "\(home)/.vibetunnel/control.sock" logger.info("Unix socket initialized with path: \(self.socketPath)") } deinit { shouldReconnect = false // Close socket directly in deinit since we can't call @MainActor methods if socketFD >= 0 { close(socketFD) socketFD = -1 } } // MARK: - Public Methods /// Connect to the UNIX socket with automatic reconnection func connect() { logger.info("🔌 Connecting to UNIX socket at \(self.socketPath)") guard !isConnecting else { logger.debug("Connection attempt already in progress.") return } // Reset reconnection state if this is a new top-level call if !isReconnecting { shouldReconnect = true reconnectDelay = initialReconnectDelay consecutiveFailures = 0 } isConnecting = true onStateChange?(.preparing) // Connect on background queue queue.async { [weak self] in self?.establishConnection() } } /// Establish the actual connection using C socket API private nonisolated func establishConnection() { // Close any existing socket if socketFD >= 0 { close(socketFD) socketFD = -1 } // Create socket socketFD = socket(AF_UNIX, SOCK_STREAM, 0) guard socketFD >= 0 else { let error = POSIXError(POSIXErrorCode(rawValue: errno) ?? .ECONNREFUSED) logger.error("Failed to create socket: \(error.localizedDescription)") Task { @MainActor in self.handleConnectionError(error) } return } // Set socket buffer sizes for large messages var bufferSize: Int32 = 1_024 * 1_024 // 1MB buffer if setsockopt(socketFD, SOL_SOCKET, SO_SNDBUF, &bufferSize, socklen_t(MemoryLayout.size)) < 0 { logger.warning("Failed to set send buffer size: \(String(cString: strerror(errno)))") } else { logger.info("📊 Set socket send buffer to 1MB") } if setsockopt(socketFD, SOL_SOCKET, SO_RCVBUF, &bufferSize, socklen_t(MemoryLayout.size)) < 0 { logger.warning("Failed to set receive buffer size: \(String(cString: strerror(errno)))") } else { logger.info("📊 Set socket receive buffer to 1MB") } // Set socket to non-blocking mode let flags = fcntl(socketFD, F_GETFL, 0) if flags < 0 { logger.error("Failed to get socket flags") close(socketFD) socketFD = -1 return } if fcntl(socketFD, F_SETFL, flags | O_NONBLOCK) < 0 { logger.error("Failed to set non-blocking mode") close(socketFD) socketFD = -1 return } // Create socket address var address = sockaddr_un() address.sun_family = sa_family_t(AF_UNIX) // Copy socket path let pathBytes = socketPath.utf8CString guard pathBytes.count <= MemoryLayout.size(ofValue: address.sun_path) else { logger.error("Socket path too long") close(socketFD) socketFD = -1 return } withUnsafeMutableBytes(of: &address.sun_path) { ptr in pathBytes.withUnsafeBytes { pathPtr in ptr.copyMemory(from: pathPtr) } } // Connect to socket let result = withUnsafePointer(to: &address) { ptr in ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in Darwin.connect(socketFD, sockaddrPtr, socklen_t(MemoryLayout.size)) } } if result < 0 { let errorCode = errno if errorCode == EINPROGRESS { // Connection in progress (non-blocking) logger.info("Connection in progress...") waitForConnection() } else { let error = POSIXError(POSIXErrorCode(rawValue: errorCode) ?? .ECONNREFUSED) logger.error("Failed to connect: \(error.localizedDescription) (errno: \(errorCode))") close(socketFD) socketFD = -1 Task { @MainActor in self.handleConnectionError(error) } } } else { // Connected immediately Task { @MainActor in self.handleConnectionSuccess() } } } /// Wait for non-blocking connection to complete private nonisolated func waitForConnection() { queue.asyncAfter(deadline: .now() + 0.1) { [weak self] in guard let self, self.socketFD >= 0 else { return } var error: Int32 = 0 var errorLen = socklen_t(MemoryLayout.size) let result = getsockopt(self.socketFD, SOL_SOCKET, SO_ERROR, &error, &errorLen) if result < 0 { logger.error("Failed to get socket error") close(self.socketFD) self.socketFD = -1 return } if error == 0 { // Connected successfully Task { @MainActor in self.handleConnectionSuccess() } } else if error == EINPROGRESS { // Still connecting self.waitForConnection() } else { // Connection failed let posixError = POSIXError(POSIXErrorCode(rawValue: error) ?? .ECONNREFUSED) logger.error("Connection failed: \(posixError.localizedDescription)") close(self.socketFD) self.socketFD = -1 Task { @MainActor in self.handleConnectionError(posixError) } } } } /// Handle successful connection private func handleConnectionSuccess() { logger.info("✅ UNIX socket connected") isConnected = true isConnecting = false lastConnectionTime = Date() consecutiveFailures = 0 reconnectDelay = initialReconnectDelay onStateChange?(.ready) // Start continuous receive loop startReceiveLoop() // Start keep-alive timer startKeepAlive() // Send any pending messages flushPendingMessages() } /// Handle connection error private func handleConnectionError(_ error: Error) { logger.error("❌ Connection failed: \(error)") isConnected = false isConnecting = false consecutiveFailures += 1 onStateChange?(.failed(error)) // Clean up cleanupConnection() // Schedule reconnection if appropriate if shouldReconnect { scheduleReconnect() } } /// Send a message with automatic retry on failure func send(_ message: some Encodable) async throws { logger.info("📤 Sending control message...") let encoder = JSONEncoder() let data = try encoder.encode(message) // Log the message content for debugging if let str = String(data: data, encoding: .utf8) { logger.info("📤 Message content: \(String(str.prefix(500)))") } try await sendData(data) } /// Serial queue for message sending to prevent concurrent writes private let sendQueue = DispatchQueue(label: "sh.vibetunnel.unix-socket.send", qos: .userInitiated) /// Send raw dictionary message (for compatibility) with queuing func sendMessage(_ dict: [String: Any]) async { do { let data = try JSONSerialization.data(withJSONObject: dict, options: []) await sendDataWithErrorHandling(data) } catch { logger.error("Failed to serialize message: \(error)") } } /// Send raw data with error handling func sendRawData(_ data: Data) async throws { try await sendData(data) } /// Send data with proper error handling and reconnection private func sendData(_ data: Data) async throws { guard isConnected, socketFD >= 0 else { throw UnixSocketError.notConnected } try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in sendQueue.async { [weak self] in guard let self else { continuation.resume(throwing: UnixSocketError.notConnected) return } // Ensure socket is still valid guard self.socketFD >= 0 else { continuation.resume(throwing: UnixSocketError.notConnected) return } // Create message with 4-byte length header let lengthValue = UInt32(data.count).bigEndian var headerData = Data() withUnsafeBytes(of: lengthValue) { bytes in headerData.append(contentsOf: bytes) } let fullData = headerData + data // Send data in chunks if needed var totalSent = 0 var remainingData = fullData while totalSent < fullData.count { let result = remainingData.withUnsafeBytes { ptr in Darwin.send(self.socketFD, ptr.baseAddress, remainingData.count, 0) } if result < 0 { let errorCode = errno // Check if it's EAGAIN (would block) - that's okay for non-blocking if errorCode == EAGAIN || errorCode == EWOULDBLOCK { // Socket buffer is full, wait a bit and retry usleep(1_000) // Wait 1ms continue } let error = POSIXError(POSIXErrorCode(rawValue: errorCode) ?? .ECONNREFUSED) Task { @MainActor in self.handleSendError(error, errorCode: errorCode) } continuation.resume(throwing: error) return } else if result == 0 { // Connection closed let error = UnixSocketError.connectionClosed Task { @MainActor in self.logger.error("Connection closed during send") } continuation.resume(throwing: error) return } else { totalSent += result if result < remainingData.count { // Partial send - remove sent bytes and continue remainingData = remainingData.dropFirst(result) let currentTotal = totalSent Task { @MainActor in self.logger.debug("Partial send: \(result) bytes, total: \(currentTotal)/\(data.count)") } } else { // All data sent break } } } continuation.resume() } } // Add a small delay between messages to prevent concatenation try? await Task.sleep(nanoseconds: 5_000_000) // 5ms } /// Send data with error handling but no throwing private func sendDataWithErrorHandling(_ data: Data) async { guard isConnected, socketFD >= 0 else { queueMessage(data) return } // Use send queue to ensure serialized writes await withCheckedContinuation { (continuation: CheckedContinuation) in sendQueue.async { [weak self] in guard let self else { continuation.resume() return } // Ensure socket is still valid guard self.socketFD >= 0 else { Task { @MainActor in self.queueMessage(data) } continuation.resume() return } // Create message with 4-byte length header let lengthValue = UInt32(data.count).bigEndian var headerData = Data() withUnsafeBytes(of: lengthValue) { bytes in headerData.append(contentsOf: bytes) } let fullData = headerData + data // Send data in chunks if needed var totalSent = 0 var remainingData = fullData while totalSent < fullData.count { let result = remainingData.withUnsafeBytes { ptr in Darwin.send(self.socketFD, ptr.baseAddress, remainingData.count, 0) } if result < 0 { let errorCode = errno // Check if it's EAGAIN (would block) - that's okay for non-blocking if errorCode == EAGAIN || errorCode == EWOULDBLOCK { // Socket buffer is full, wait a bit and retry usleep(1_000) // Wait 1ms continue } let error = POSIXError(POSIXErrorCode(rawValue: errorCode) ?? .ECONNREFUSED) Task { @MainActor in self.handleSendError(error, errorCode: errorCode) } break // Exit the loop on error } else if result == 0 { // Connection closed Task { @MainActor in self.logger.error("Connection closed during send") self.handleConnectionError(UnixSocketError.connectionClosed) } break } else { totalSent += result if result < remainingData.count { // Partial send - remove sent bytes and continue remainingData = remainingData.dropFirst(result) let currentTotal = totalSent Task { @MainActor in self.logger.debug("Partial send: \(result) bytes, total: \(currentTotal)/\(data.count)") } } else { // All data sent Task { @MainActor in self.logger.debug("✅ Message sent successfully: \(data.count) bytes") } break } } } continuation.resume() } } // Add a small delay between messages to prevent concatenation try? await Task.sleep(nanoseconds: 5_000_000) // 5ms } /// Handle send errors and trigger reconnection if needed private func handleSendError(_ error: Error, errorCode: Int32) { logger.error("Failed to send message: \(error)") logger.error(" Error code: \(errorCode)") // Check for broken pipe (EPIPE = 32) if errorCode == EPIPE { logger.warning("🔥 Broken pipe detected (EPIPE), triggering reconnection") scheduleReconnect() } // Check for other connection errors else if errorCode == ECONNRESET || // 54 - Connection reset errorCode == ECONNREFUSED || // 61 - Connection refused errorCode == ENOTCONN { // 57 - Not connected logger.warning("🔥 Connection error detected, triggering reconnection") scheduleReconnect() } } /// Disconnect from the socket func disconnect() { logger.info("🔌 Disconnecting from UNIX socket") // Stop reconnection attempts shouldReconnect = false // Cancel timers and tasks keepAliveTimer?.invalidate() keepAliveTimer = nil reconnectTask?.cancel() reconnectTask = nil // Cancel receive task receiveTask?.cancel() receiveTask = nil // Clear buffers receiveBuffer.removeAll() pendingMessages.removeAll() // Close socket if socketFD >= 0 { close(socketFD) socketFD = -1 } isConnected = false onStateChange?(.cancelled) } // MARK: - Private Methods /// Clean up connection resources private func cleanupConnection() { keepAliveTimer?.invalidate() keepAliveTimer = nil receiveTask?.cancel() receiveTask = nil receiveBuffer.removeAll() } /// Schedule a reconnection attempt private func scheduleReconnect() { guard shouldReconnect && !isReconnecting else { logger .debug( "Skipping reconnect: shouldReconnect=\(self.shouldReconnect), isReconnecting=\(self.isReconnecting)" ) return } isReconnecting = true // Cancel any existing reconnect task reconnectTask?.cancel() logger .info( "🔄 Scheduling reconnection in \(String(format: "%.1f", self.reconnectDelay)) seconds (attempt #\(self.consecutiveFailures + 1))" ) reconnectTask = Task { [weak self] in guard let self else { return } do { try await Task.sleep(nanoseconds: UInt64(self.reconnectDelay * 1_000_000_000)) guard !Task.isCancelled && self.shouldReconnect else { self.isReconnecting = false return } logger.info("🔁 Attempting reconnection...") self.isReconnecting = false self.connect() // Increase delay for next attempt (exponential backoff) self.reconnectDelay = min(self.reconnectDelay * 1.5, self.maxReconnectDelay) } catch { self.isReconnecting = false if !Task.isCancelled { logger.error("Reconnection task error: \(error)") } } } } /// Queue a message for later delivery private func queueMessage(_ data: Data, completion: (@Sendable (Error?) -> Void)? = nil) { guard pendingMessages.count < maxPendingMessages else { logger.warning("Pending message queue full, dropping oldest message") pendingMessages.removeFirst() return } pendingMessages.append((data: data, completion: completion)) logger.debug("Queued message (total pending: \(self.pendingMessages.count))") } /// Send all pending messages private func flushPendingMessages() { guard !pendingMessages.isEmpty else { return } logger.info("📤 Flushing \(self.pendingMessages.count) pending messages") let messages = pendingMessages pendingMessages.removeAll() Task { for (data, completion) in messages { guard isConnected, socketFD >= 0 else { // Re-queue if connection lost again queueMessage(data, completion: completion) break } // Use send queue to ensure serialized writes await withCheckedContinuation { (continuation: CheckedContinuation) in sendQueue.async { [weak self] in guard let self else { continuation.resume() return } guard self.socketFD >= 0 else { Task { @MainActor in self.queueMessage(data) } // Call completion with not connected error completion?(UnixSocketError.notConnected) continuation.resume() return } // Create message with 4-byte length header var lengthHeader = UInt32(data.count).bigEndian let headerData = Data(bytes: &lengthHeader, count: 4) let fullData = headerData + data // Send data in chunks if needed var totalSent = 0 var remainingData = fullData var sendError: Error? while totalSent < fullData.count && sendError == nil { let result = remainingData.withUnsafeBytes { ptr in Darwin.send(self.socketFD, ptr.baseAddress, remainingData.count, 0) } if result < 0 { let errorCode = errno // Check if it's EAGAIN (would block) - that's okay for non-blocking if errorCode == EAGAIN || errorCode == EWOULDBLOCK { // Socket buffer is full, wait a bit and retry usleep(1_000) // Wait 1ms continue } let error = POSIXError(POSIXErrorCode(rawValue: errorCode) ?? .ECONNREFUSED) sendError = error Task { @MainActor in self.logger.error("Failed to send pending message: \(error)") } } else if result == 0 { sendError = UnixSocketError.connectionClosed Task { @MainActor in self.logger.error("Connection closed while sending pending message") } } else { totalSent += result if result < remainingData.count { // Partial send - remove sent bytes and continue remainingData = remainingData.dropFirst(result) } else { // All data sent Task { @MainActor in self.logger.debug("✅ Sent pending message: \(data.count) bytes") } } } } completion?(sendError) continuation.resume() } } // Small delay between messages to avoid concatenation try? await Task.sleep(nanoseconds: 10_000_000) // 10ms } } } // MARK: - Keep-Alive /// Start keep-alive mechanism private func startKeepAlive() { keepAliveTimer?.invalidate() keepAliveTimer = Timer.scheduledTimer(withTimeInterval: keepAliveInterval, repeats: true) { [weak self] _ in Task { @MainActor [weak self] in await self?.sendKeepAlive() } } } /// Send keep-alive ping private func sendKeepAlive() async { guard isConnected else { return } let timeSinceLastPong = Date().timeIntervalSince(lastPongTime) if timeSinceLastPong > keepAliveInterval * 2 { logger .warning("⚠️ No pong received for \(String(format: "%.0f", timeSinceLastPong))s, connection may be dead") // Trigger reconnection scheduleReconnect() return } let pingMessage = ControlProtocol.systemPingRequest() Task { do { try await send(pingMessage) logger.debug("🏓 Sent keep-alive ping") } catch { logger.error("Failed to send keep-alive ping: \(error)") } } } /// Start continuous receive loop private func startReceiveLoop() { receiveTask?.cancel() receiveTask = Task { [weak self] in while !Task.isCancelled { guard let self else { break } await self.receiveNextMessage() } } } /// Receive next message from the connection private func receiveNextMessage() async { guard isConnected, socketFD >= 0 else { // Add a small delay to prevent busy loop try? await Task.sleep(nanoseconds: 100_000_000) // 100ms return } await withCheckedContinuation { (continuation: CheckedContinuation) in queue.async { [weak self] in guard let self, self.socketFD >= 0 else { continuation.resume() return } var buffer = [UInt8](repeating: 0, count: 65_536) // Increased from 4KB to 64KB let bytesRead = recv(self.socketFD, &buffer, buffer.count, 0) if bytesRead > 0 { let data = Data(bytes: buffer, count: bytesRead) Task { @MainActor in self.processReceivedData(data) } } else if bytesRead == 0 { // Connection closed Task { @MainActor in self.logger.warning("⚠️ Connection closed by peer (recv returned 0)") self.logger.warning(" Socket FD: \(self.socketFD)") self.logger.warning(" Was connected: \(self.isConnected)") self.logger.warning(" Receive buffer had \(self.receiveBuffer.count) bytes") self.handleConnectionError(UnixSocketError.connectionClosed) } } else { let errorCode = errno if errorCode != EAGAIN && errorCode != EWOULDBLOCK { let error = POSIXError(POSIXErrorCode(rawValue: errorCode) ?? .ECONNREFUSED) Task { @MainActor in self.logger.error("Receive error: \(error) (errno: \(errorCode))") if errorCode == EPIPE || errorCode == ECONNRESET || errorCode == ENOTCONN { self.logger.warning("Connection error during receive, triggering reconnection") self.scheduleReconnect() } } } } continuation.resume() } } // Small delay between receive attempts try? await Task.sleep(nanoseconds: 10_000_000) // 10ms } /// Process received data with proper message framing private func processReceivedData(_ data: Data) { logger.debug("📥 Received \(data.count) bytes of data") receiveBuffer.append(data) logger.debug("📦 Buffer now contains \(self.receiveBuffer.count) bytes") // Process as many messages as we can from the buffer while receiveBuffer.count >= 4 { // Read the message length header (4 bytes, big-endian UInt32) let messageLength = receiveBuffer.prefix(4) .withUnsafeBytes { UInt32(bigEndian: $0.loadUnaligned(as: UInt32.self)) } logger.debug("📏 Next message length from header: \(messageLength) bytes") // Check against reasonable upper bound to guard against corrupted headers guard messageLength < 10_000_000 else { // 10MB max message size (matching Node.js peer) logger.error("Corrupted message header: length=\(messageLength)") receiveBuffer.removeAll() // Clear corrupted buffer break } let needed = Int(messageLength) + 4 guard receiveBuffer.count >= needed else { break } // Extract the complete message body (skip the 4-byte header) let body = Data(receiveBuffer.dropFirst(4).prefix(Int(messageLength))) receiveBuffer.removeFirst(needed) // Check for keep-alive pong if let msgDict = try? JSONSerialization.jsonObject(with: body) as? [String: Any], let type = msgDict["type"] as? String, type == "response", let category = msgDict["category"] as? String, category == "system", let action = msgDict["action"] as? String, action == "ping" { lastPongTime = Date() logger.debug("🏓 Received keep-alive pong") continue } // Deliver the complete message logger.info("📨 Delivering message of size \(body.count) bytes") if let str = String(data: body, encoding: .utf8) { logger.info("📨 Message content: \(String(str.prefix(500)))") } if let handler = onMessage { handler(body) } else { logger.warning("⚠️ No message handler registered - message will be dropped!") } } // If buffer grows too large, clear it to prevent memory issues if receiveBuffer.count > 10 * 1_024 * 1_024 { // 10MB limit (matching Node.js peer) logger.warning("Receive buffer exceeded 10MB, clearing to prevent memory issues") receiveBuffer.removeAll() } } } // MARK: - Errors /// Errors specific to Unix socket operations. /// /// Provides detailed error information for Unix socket connection failures, /// data transmission issues, and connection state problems. enum UnixSocketError: LocalizedError { case notConnected case connectionFailed(Error) case sendFailed(Error) case connectionClosed var errorDescription: String? { switch self { case .notConnected: "UNIX socket not connected" case .connectionFailed(let error): "Connection failed: \(error.localizedDescription)" case .sendFailed(let error): "Send failed: \(error.localizedDescription)" case .connectionClosed: "Connection closed by peer" } } }