// // SDLContextBootstrap.swift // Tun // // Created by Codex on 2026/5/28. // import Foundation final class SDLContextBootstrap: @unchecked Sendable { private typealias StartCompletion = (Error?) -> Void private typealias StopCompletion = () -> Void private typealias WakeCompletion = (Error?) -> Void private enum BootstrapCommand { case start(config: SDLConfiguration, rsaCipher: CCRSACipher, completion: StartCompletion) case stop(clearRuntimeConfiguration: Bool, completion: StopCompletion) case recoverAfterWake(completion: WakeCompletion) } private enum RuntimeState { case idle case starting case running case stopping } private weak var provider: PacketTunnelProvider? private let runtimeLock = NSLock() private var runtimeState: RuntimeState = .idle private var config: SDLConfiguration? private var rsaCipher: CCRSACipher? private var contextActor: SDLContextActor? private var startCompletionHandler: ((Error?) -> Void)? private let commandContinuation: AsyncStream.Continuation private var commandWorker: Task? init(provider: PacketTunnelProvider) { let commandPair = AsyncStream.makeStream(of: BootstrapCommand.self, bufferingPolicy: .unbounded) self.provider = provider self.commandContinuation = commandPair.continuation let stream = commandPair.stream self.commandWorker = Task { [weak self] in await self?.runCommandLoop(stream) } } deinit { self.commandContinuation.finish() self.commandWorker?.cancel() } func startCached(completionHandler: @escaping (Error?) -> Void) { self.runtimeLock.lock() let config = self.config let rsaCipher = self.rsaCipher self.runtimeLock.unlock() guard let config, let rsaCipher else { SDLLogger.fatal("[SDLContextBootstrap] startCached failed: missing cached runtime configuration", category: .app) completionHandler(TunnelError.invalidConfiguration) return } self.start(config: config, rsaCipher: rsaCipher, completionHandler: completionHandler) } func start(config: SDLConfiguration, rsaCipher: CCRSACipher, completionHandler: @escaping (Error?) -> Void) { self.submit(.start(config: config, rsaCipher: rsaCipher, completion: completionHandler)) } func stop(clearRuntimeConfiguration: Bool, completionHandler: @escaping () -> Void) { self.submit(.stop(clearRuntimeConfiguration: clearRuntimeConfiguration, completion: completionHandler)) } func recoverAfterWake(completionHandler: @escaping (Error?) -> Void) { self.submit(.recoverAfterWake(completion: completionHandler)) } func currentContextActor() -> SDLContextActor? { self.runtimeLock.lock() let contextActor = self.contextActor self.runtimeLock.unlock() return contextActor } private func submit(_ command: BootstrapCommand) { self.commandContinuation.yield(command) } private func runCommandLoop(_ stream: AsyncStream) async { for await command in stream { self.handle(command) } } private func handle(_ command: BootstrapCommand) { switch command { case .start(let config, let rsaCipher, let completion): self.handleStart(config: config, rsaCipher: rsaCipher, completionHandler: completion) case .stop(let clearRuntimeConfiguration, let completion): self.handleStop(clearRuntimeConfiguration: clearRuntimeConfiguration, completionHandler: completion) case .recoverAfterWake(let completion): self.handleRecoverAfterWake(completionHandler: completion) } } private func handleStart(config: SDLConfiguration, rsaCipher: CCRSACipher, completionHandler: @escaping (Error?) -> Void) { guard let provider = self.provider else { SDLLogger.fatal("[SDLContextBootstrap] start rejected: provider released", category: .app) completionHandler(TunnelError.invalidContext) return } self.runtimeLock.lock() switch self.runtimeState { case .idle: SDLTunnelAppNotifier.shared.clear() let contextActor = SDLContextActor( provider: provider, config: config, rsaCipher: rsaCipher ) self.config = config self.rsaCipher = rsaCipher self.contextActor = contextActor self.startCompletionHandler = completionHandler self.runtimeState = .starting self.runtimeLock.unlock() Task { do { try await contextActor.start() self.finishContextStart(contextActor, error: nil) } catch { SDLLogger.fatal("[SDLContextBootstrap] context start failed: \(error)", category: .app) self.finishContextStart(contextActor, error: error) } } case .starting, .running, .stopping: SDLLogger.fatal("[SDLContextBootstrap] start rejected: invalid runtime state \(self.runtimeState)", category: .app) self.runtimeLock.unlock() completionHandler(TunnelError.invalidContext) } } private func handleStop(clearRuntimeConfiguration: Bool, completionHandler: @escaping () -> Void) { self.runtimeLock.lock() let contextActor = self.contextActor let startCompletionHandler = self.startCompletionHandler guard let contextActor else { SDLLogger.fatal("[SDLContextBootstrap] stop requested while context is nil, clearRuntimeConfiguration: \(clearRuntimeConfiguration)", category: .app) self.runtimeState = .idle self.startCompletionHandler = nil if clearRuntimeConfiguration { self.config = nil self.rsaCipher = nil } self.runtimeLock.unlock() startCompletionHandler?(TunnelError.invalidContext) completionHandler() return } self.contextActor = nil self.startCompletionHandler = nil self.runtimeState = .stopping if clearRuntimeConfiguration { self.config = nil self.rsaCipher = nil } self.runtimeLock.unlock() SDLLogger.fatal("[SDLContextBootstrap] stop will stop current context, clearRuntimeConfiguration: \(clearRuntimeConfiguration)", category: .app) startCompletionHandler?(TunnelError.invalidContext) Task { await contextActor.stop() self.markContextStopped() completionHandler() } } private func handleRecoverAfterWake(completionHandler: @escaping (Error?) -> Void) { self.runtimeLock.lock() let runtimeState = self.runtimeState let contextActor = self.contextActor let config = self.config let rsaCipher = self.rsaCipher self.runtimeLock.unlock() switch runtimeState { case .running: guard let contextActor else { SDLLogger.fatal("[SDLContextBootstrap] recoverAfterWake failed: missing running context", category: .app) completionHandler(TunnelError.invalidContext) return } Task { do { try await contextActor.recoverAfterWake() completionHandler(nil) } catch { SDLLogger.fatal("[SDLContextBootstrap] recoverAfterWake failed: \(error)", category: .app) completionHandler(error) } } case .idle: guard let config, let rsaCipher else { SDLLogger.fatal("[SDLContextBootstrap] recoverAfterWake failed: missing cached runtime configuration", category: .app) completionHandler(TunnelError.invalidConfiguration) return } SDLLogger.log("[SDLContextBootstrap] recoverAfterWake will start cached context", category: .app) self.handleStart(config: config, rsaCipher: rsaCipher, completionHandler: completionHandler) case .starting: SDLLogger.log("[SDLContextBootstrap] recoverAfterWake ignored while context is starting", category: .app) completionHandler(nil) case .stopping: SDLLogger.log("[SDLContextBootstrap] recoverAfterWake ignored while context is stopping", category: .app) completionHandler(nil) } } private func finishContextStart(_ contextActor: SDLContextActor, error: Error?) { self.runtimeLock.lock() guard self.contextActor === contextActor else { self.runtimeLock.unlock() return } let startCompletionHandler = self.startCompletionHandler self.startCompletionHandler = nil if let error { self.contextActor = nil self.runtimeState = .idle } else { self.runtimeState = .running } self.runtimeLock.unlock() startCompletionHandler?(error) } private func markContextStopped() { self.runtimeLock.lock() if self.contextActor == nil { self.runtimeState = .idle } self.runtimeLock.unlock() } }