This commit is contained in:
Max Goedjen 2025-08-26 22:49:38 -07:00
parent 07bf521414
commit 14666070e5
No known key found for this signature in database

View File

@ -2,65 +2,31 @@ import Foundation
import OSLog
import SecretKit
public struct Session: Sendable {
public let messages: AsyncStream<Data>
public let provenance: SigningRequestProvenance
private let fileHandle: FileHandle
private let continuation: AsyncStream<Data>.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<Session>
/// A continuation to create new sessions.
private let sessionsContinuation: AsyncStream<Session>.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<Session>
public let continuation: AsyncStream<Session>.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<Session>.makeStream()
(sessions, sessionsContinuation) = AsyncStream<Session>.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<Data>
/// 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<Data>.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()