import Darwin import Foundation import MacToolsPluginKit protocol ActivityBarSocketServing: AnyObject { var isRunning: Bool { get } func start() throws func stop() } enum ActivityBarSocketError: LocalizedError, Equatable { case pathTooLong(String) case socketFailed(Int32) case bindFailed(Int32) case listenFailed(Int32) var errorDescription: String? { localizedDescription(localization: PluginLocalization(bundle: .main)) } func localizedDescription(localization: PluginLocalization) -> String { switch self { case let .pathTooLong(path): return localization.format("error.socket.pathTooLong", defaultValue: "Socket 路径过长:%@", path) case let .socketFailed(code): return localization.format("error.socket.createFailed", defaultValue: "创建 Socket 失败:%d", code) case let .bindFailed(code): return localization.format("error.socket.bindFailed", defaultValue: "绑定 Socket 失败:%d", code) case let .listenFailed(code): return localization.format("error.socket.listenFailed", defaultValue: "监听 Socket 失败:%d", code) } } } final class ActivityBarHookSocketServer: ActivityBarSocketServing, @unchecked Sendable { private let socketPath: String private let onEvent: @MainActor (ActivityBarHookEvent) -> Void private let stateLock = NSLock() private var serverFD: Int32 = -1 private var running = false var isRunning: Bool { stateLock.withLock { running } } init( socketPath: String = ActivityBarConstants.defaultSocketPath, onEvent: @escaping @MainActor (ActivityBarHookEvent) -> Void ) { self.socketPath = socketPath self.onEvent = onEvent } func start() throws { guard !isRunning else { return } removeSocketFile() let fileDescriptor = socket(AF_UNIX, SOCK_STREAM, 0) guard fileDescriptor >= 0 else { throw ActivityBarSocketError.socketFailed(errno) } var address = sockaddr_un() address.sun_family = sa_family_t(AF_UNIX) let pathBytes = socketPath.utf8CString let maxPathLength = MemoryLayout.size(ofValue: address.sun_path) guard pathBytes.count < maxPathLength else { close(fileDescriptor) throw ActivityBarSocketError.pathTooLong(socketPath) } withUnsafeMutableBytes(of: &address.sun_path) { buffer in for index in 0...size)) } } guard bindResult == 0 else { let code = errno close(fileDescriptor) throw ActivityBarSocketError.bindFailed(code) } guard listen(fileDescriptor, 5) == 0 else { let code = errno close(fileDescriptor) throw ActivityBarSocketError.listenFailed(code) } let flags = fcntl(fileDescriptor, F_GETFL) _ = fcntl(fileDescriptor, F_SETFL, flags | O_NONBLOCK) stateLock.withLock { serverFD = fileDescriptor running = true } ActivityBarLog.socket.info("Activity bar socket listening at \(self.socketPath, privacy: .public)") DispatchQueue.global(qos: .utility).async { [weak self] in self?.acceptLoop() } } func stop() { let fileDescriptor = stateLock.withLock { () -> Int32 in running = false let fd = serverFD serverFD = -1 return fd } if fileDescriptor >= 0 { close(fileDescriptor) } removeSocketFile() } private func acceptLoop() { while stateLock.withLock({ running }) { var clientAddress = sockaddr_un() var clientLength = socklen_t(MemoryLayout.size) let fd = stateLock.withLock { serverFD } guard fd >= 0 else { return } let clientFD = withUnsafeMutablePointer(to: &clientAddress) { pointer in pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { socketAddress in accept(fd, socketAddress, &clientLength) } } if clientFD >= 0 { DispatchQueue.global(qos: .utility).async { [weak self] in self?.handleClient(clientFD) } } else if errno == EAGAIN || errno == EWOULDBLOCK { Thread.sleep(forTimeInterval: 0.05) } else if stateLock.withLock({ running }) { ActivityBarLog.socket.error("Activity bar socket accept failed: \(errno)") Thread.sleep(forTimeInterval: 0.1) } } } private func handleClient(_ fileDescriptor: Int32) { defer { close(fileDescriptor) } var timeout = timeval(tv_sec: 2, tv_usec: 0) setsockopt( fileDescriptor, SOL_SOCKET, SO_RCVTIMEO, &timeout, socklen_t(MemoryLayout.size) ) var data = Data() let bufferSize = 8_192 let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) defer { buffer.deallocate() } while true { let bytesRead = read(fileDescriptor, buffer, bufferSize) if bytesRead > 0 { data.append(buffer, count: bytesRead) } else { break } } guard !data.isEmpty else { return } do { let event = try JSONDecoder().decode(ActivityBarHookEvent.self, from: data) let callback = onEvent Task { @MainActor in callback(event) } } catch { ActivityBarLog.socket.error("Activity bar hook decode failed: \(error.localizedDescription, privacy: .public)") } } private func removeSocketFile() { unlink(socketPath) } } private extension NSLock { func withLock(_ body: () throws -> T) rethrows -> T { lock() defer { unlock() } return try body() } }