diff --git a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift index ed4b9d2..0f592a0 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift @@ -2,65 +2,31 @@ import Foundation import OSLog import SecretKit -public struct Session: Sendable { - - public let messages: AsyncStream - public let provenance: SigningRequestProvenance - - private let fileHandle: FileHandle - private let continuation: AsyncStream.Continuation - private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Session") - - init(fileHandle: FileHandle) { - self.fileHandle = fileHandle - provenance = SigningRequestTracer().provenance(from: fileHandle) - (messages, continuation) = AsyncStream.makeStream() - Task { [continuation, logger] in - await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor() - for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) { - let data = fileHandle.availableData - guard !data.isEmpty else { - logger.debug("Socket controller received empty data, ending continuation.") - continuation.finish() - try fileHandle.close() - return - } - continuation.yield(data) - logger.debug("Socket controller yielded data.") - } - } - } - - public func write(_ data: Data) async throws { - try fileHandle.write(contentsOf: data) - await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor() - } - - public func close() throws { - logger.debug("Session closed.") - continuation.finish() - try fileHandle.close() - } - -} - /// A controller that manages socket configuration and request dispatching. public struct SocketController { - /// The active SocketPort. + /// A stream of Sessions. Each session represents one connection to a class communicating with the socket. Multiple Sessions may be active simultaneously. + public let sessions: AsyncStream + + /// A continuation to create new sessions. + private let sessionsContinuation: AsyncStream.Continuation + + /// The active SocketPort. Must be retained to be kept valid. private let port: SocketPort + /// The FileHandle for the main socket. private let fileHandle: FileHandle + + /// Logger for the socket controller. private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "SocketController") - private let requestTracer = SigningRequestTracer() - public let sessions: AsyncStream - public let continuation: AsyncStream.Continuation + /// Tracer which determines who originates a socket connection. + private let requestTracer = SigningRequestTracer() /// Initializes a socket controller with a specified path. /// - Parameter path: The path to use as a socket. public init(path: String) { - (sessions, continuation) = AsyncStream.makeStream() + (sessions, sessionsContinuation) = AsyncStream.makeStream() logger.debug("Socket controller setting up at \(path)") if let _ = try? FileManager.default.removeItem(atPath: path) { logger.debug("Socket controller removed existing socket") @@ -70,12 +36,12 @@ public struct SocketController { logger.debug("Socket controller path is clear") port = SocketPort(path: path) fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true) - Task { [fileHandle, continuation, logger] in + Task { [fileHandle, sessionsContinuation, logger] in for await notification in NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted) { logger.debug("Socket controller accepted connection") guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { continue } let session = Session(fileHandle: new) - continuation.yield(session) + sessionsContinuation.yield(session) await fileHandle.acceptConnectionInBackgroundAndNotifyOnMainActor() } } @@ -85,8 +51,68 @@ public struct SocketController { } -extension FileHandle { +extension SocketController { + /// A session represents a connection that has been established between the two ends of the socket. + public struct Session: Sendable { + + /// Data received by the socket. + public let messages: AsyncStream + + /// The provenance of the process that established the session. + public let provenance: SigningRequestProvenance + + /// A FileHandle used to communicate with the socket. + private let fileHandle: FileHandle + + /// A continuation for issuing new messages. + private let messagesContinuation: AsyncStream.Continuation + + /// A logger for the session. + private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Session") + + /// Initializes a new Session. + /// - Parameter fileHandle: The FileHandle used to communicate with the socket. + init(fileHandle: FileHandle) { + self.fileHandle = fileHandle + provenance = SigningRequestTracer().provenance(from: fileHandle) + (messages, messagesContinuation) = AsyncStream.makeStream() + Task { [messagesContinuation, logger] in + await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor() + for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) { + let data = fileHandle.availableData + guard !data.isEmpty else { + logger.debug("Socket controller received empty data, ending continuation.") + messagesContinuation.finish() + try fileHandle.close() + return + } + messagesContinuation.yield(data) + logger.debug("Socket controller yielded data.") + } + } + } + + /// Writes new data to the socket. + /// - Parameter data: The data to write. + public func write(_ data: Data) async throws { + try fileHandle.write(contentsOf: data) + await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor() + } + + /// Closes the socket and cleans up resources. + public func close() throws { + logger.debug("Session closed.") + messagesContinuation.finish() + try fileHandle.close() + } + + } + +} + +private extension FileHandle { + /// Ensures waitForDataInBackgroundAndNotify will be called on the main actor. @MainActor func waitForDataInBackgroundAndNotifyOnMainActor() { waitForDataInBackgroundAndNotify() @@ -101,7 +127,7 @@ extension FileHandle { } -extension SocketPort { +private extension SocketPort { convenience init(path: String) { var addr = sockaddr_un()