From 12c8bff2d4ee42c0608ae00dd93b93ee7dee3d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 15 Jan 2026 14:04:43 +0800 Subject: [PATCH] refactor: CommandClient & Connections --- .../Connections/ConnectionListView.swift | 17 ++++-- .../Dashboard/ButtonVisibilityState.swift | 2 +- Library/Network/CommandClient.swift | 18 ++---- Library/Network/CommandXPC.swift | 58 ++++++++++++++----- Library/Network/ExtensionEnvironments.swift | 2 +- Library/Network/ExtensionProvider.swift | 14 ++++- 6 files changed, 74 insertions(+), 37 deletions(-) diff --git a/ApplicationLibrary/Views/Connections/ConnectionListView.swift b/ApplicationLibrary/Views/Connections/ConnectionListView.swift index d1ccf7e..2f5ef8b 100644 --- a/ApplicationLibrary/Views/Connections/ConnectionListView.swift +++ b/ApplicationLibrary/Views/Connections/ConnectionListView.swift @@ -8,8 +8,10 @@ import SwiftUI public struct ConnectionListView: View { @EnvironmentObject private var environments: ExtensionEnvironments @StateObject private var viewModel = ConnectionListViewModel() + @StateObject private var commandClient = CommandClient([.connections]) public init() {} + public var body: some View { VStack { if viewModel.isLoading { @@ -59,19 +61,24 @@ public struct ConnectionListView: View { .alert($viewModel.alert) .onAppear { viewModel.connect() + commandClient.connect() } - .onReceive(environments.commandClient.$connections) { connections in + .onDisappear { + viewModel.disconnect() + commandClient.disconnect() + } + .onReceive(commandClient.$connections) { connections in Task { @MainActor in viewModel.setConnections(connections) } } .onChangeCompat(of: viewModel.connectionStateFilter) { filter in - environments.commandClient.connectionStateFilter = filter - environments.commandClient.filterConnectionsNow() + commandClient.connectionStateFilter = filter + commandClient.filterConnectionsNow() } .onChangeCompat(of: viewModel.connectionSort) { sort in - environments.commandClient.connectionSort = sort - environments.commandClient.filterConnectionsNow() + commandClient.connectionSort = sort + commandClient.filterConnectionsNow() } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) #if os(iOS) diff --git a/ApplicationLibrary/Views/Dashboard/ButtonVisibilityState.swift b/ApplicationLibrary/Views/Dashboard/ButtonVisibilityState.swift index b0894ee..94da4ab 100644 --- a/ApplicationLibrary/Views/Dashboard/ButtonVisibilityState.swift +++ b/ApplicationLibrary/Views/Dashboard/ButtonVisibilityState.swift @@ -24,7 +24,7 @@ public struct ButtonVisibilityState { groupsCount = Variant.screenshotMode && actualGroupsCount == 0 ? screenshotFallbackGroupsCount : actualGroupsCount - connectionsCount = commandClient.connections?.count ?? 0 + connectionsCount = Int(commandClient.status?.connectionsIn ?? 0) let isConnected = Variant.screenshotMode || profile.status.isConnectedStrict diff --git a/Library/Network/CommandClient.swift b/Library/Network/CommandClient.swift index b02b073..7b1b4b5 100644 --- a/Library/Network/CommandClient.swift +++ b/Library/Network/CommandClient.swift @@ -203,20 +203,12 @@ public class CommandClient: ObservableObject { clientOptions.statusInterval = Int64(NSEC_PER_SEC) let client = LibboxNewCommandClient(clientHandler(self), clientOptions)! do { - for i in 0 ..< 10 { - try await Task.sleep(nanoseconds: UInt64(Double(100 + (i * 50)) * Double(NSEC_PER_MSEC))) - try Task.checkCancellation() - do { - try client.connect() - await MainActor.run { - commandClient = client - } - return - } catch {} - try Task.checkCancellation() - } + try client.connect() } catch { - try? client.disconnect() + return + } + await MainActor.run { + commandClient = client } } diff --git a/Library/Network/CommandXPC.swift b/Library/Network/CommandXPC.swift index 2a7da4d..1dcb8c6 100644 --- a/Library/Network/CommandXPC.swift +++ b/Library/Network/CommandXPC.swift @@ -15,43 +15,64 @@ let socketPath: String var commandServer: LibboxCommandServer? + private enum ServiceReadyState { + case pending + case ready + case failed(Error) + } + private let serviceReadyLock = NSLock() - private var _serviceReady = false - private var serviceReadyContinuations: [CheckedContinuation] = [] + private var serviceReadyState = ServiceReadyState.pending + private var serviceReadyContinuations: [CheckedContinuation] = [] + private static let defaultNotReadyError = NSError(domain: "CommandXPC", code: -1, userInfo: [ + NSLocalizedDescriptionKey: "Command server not ready", + ]) init(socketPath: String) { self.socketPath = socketPath } - func waitForServiceReady() async { + func waitForServiceReady() async throws { serviceReadyLock.lock() - if _serviceReady { + switch serviceReadyState { + case .ready: serviceReadyLock.unlock() return - } - await withCheckedContinuation { continuation in - serviceReadyContinuations.append(continuation) + case let .failed(error): serviceReadyLock.unlock() + throw error + case .pending: + return try await withCheckedThrowingContinuation { continuation in + serviceReadyContinuations.append(continuation) + serviceReadyLock.unlock() + } } } func markServiceReady() { + resolveServiceReady(.success(())) + } + + func markServiceNotReady(_ error: Error? = nil) { + resolveServiceReady(.failure(error ?? Self.defaultNotReadyError)) + } + + private func resolveServiceReady(_ result: Result) { serviceReadyLock.lock() - _serviceReady = true + switch result { + case .success: + serviceReadyState = .ready + case let .failure(error): + serviceReadyState = .failed(error) + } let continuations = serviceReadyContinuations serviceReadyContinuations.removeAll() serviceReadyLock.unlock() for continuation in continuations { - continuation.resume() + continuation.resume(with: result) } } - func markServiceNotReady() { - serviceReadyLock.lock() - _serviceReady = false - serviceReadyLock.unlock() - } - func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { let bundleID = AppConfiguration.packageName + ".standalone" let requirement = "identifier \"\(bundleID)\" and anchor apple generic and certificate leaf[subject.OU] = \"\(AppConfiguration.teamID)\"" @@ -98,7 +119,12 @@ func extensionRequirements(reply: @escaping (Bool, Bool, NSError?) -> Void) { Task { - await service.waitForServiceReady() + do { + try await service.waitForServiceReady() + } catch { + reply(false, false, error as NSError) + return + } guard let commandServer = service.commandServer else { reply(false, false, NSError(domain: "CommandXPC", code: -1, userInfo: [ NSLocalizedDescriptionKey: "Command server not available", diff --git a/Library/Network/ExtensionEnvironments.swift b/Library/Network/ExtensionEnvironments.swift index 2cf3653..51f78d9 100644 --- a/Library/Network/ExtensionEnvironments.swift +++ b/Library/Network/ExtensionEnvironments.swift @@ -131,7 +131,7 @@ public struct ImportRemoteProfileRequest: Hashable, Identifiable { @MainActor public class ExtensionEnvironments: ObservableObject { - @Published public var commandClient = CommandClient([.log, .status, .groups, .clashMode, .connections]) + @Published public var commandClient = CommandClient([.log, .status, .groups, .clashMode]) @Published public var extensionProfileLoading = true @Published public var extensionProfile: ExtensionProfile? @Published public var emptyProfiles = false diff --git a/Library/Network/ExtensionProvider.swift b/Library/Network/ExtensionProvider.swift index 904d27c..ac29086 100644 --- a/Library/Network/ExtensionProvider.swift +++ b/Library/Network/ExtensionProvider.swift @@ -171,7 +171,16 @@ open class ExtensionProvider: NEPacketTunnelProvider { #endif writeMessage("(packet-tunnel): Here I stand") - try await startService() + do { + try await startService() + } catch { + #if os(macOS) + if Variant.useSystemExtension { + xpcService.markServiceNotReady(error) + } + #endif + throw error + } #if os(macOS) if Variant.useSystemExtension { xpcService.markServiceReady() @@ -251,6 +260,9 @@ open class ExtensionProvider: NEPacketTunnelProvider { } #if os(macOS) if Variant.useSystemExtension { + xpcService.markServiceNotReady(NSError(domain: "CommandXPC", code: -1, userInfo: [ + NSLocalizedDescriptionKey: "Command server stopped", + ])) xpcListener.invalidate() xpcListener = nil xpcService.commandServer = nil