refactor: CommandClient & Connections

This commit is contained in:
世界
2026-01-16 20:08:33 +08:00
parent c479921a4f
commit 12c8bff2d4
6 changed files with 74 additions and 37 deletions
@@ -8,8 +8,10 @@ import SwiftUI
public struct ConnectionListView: View { public struct ConnectionListView: View {
@EnvironmentObject private var environments: ExtensionEnvironments @EnvironmentObject private var environments: ExtensionEnvironments
@StateObject private var viewModel = ConnectionListViewModel() @StateObject private var viewModel = ConnectionListViewModel()
@StateObject private var commandClient = CommandClient([.connections])
public init() {} public init() {}
public var body: some View { public var body: some View {
VStack { VStack {
if viewModel.isLoading { if viewModel.isLoading {
@@ -59,19 +61,24 @@ public struct ConnectionListView: View {
.alert($viewModel.alert) .alert($viewModel.alert)
.onAppear { .onAppear {
viewModel.connect() viewModel.connect()
commandClient.connect()
} }
.onReceive(environments.commandClient.$connections) { connections in .onDisappear {
viewModel.disconnect()
commandClient.disconnect()
}
.onReceive(commandClient.$connections) { connections in
Task { @MainActor in Task { @MainActor in
viewModel.setConnections(connections) viewModel.setConnections(connections)
} }
} }
.onChangeCompat(of: viewModel.connectionStateFilter) { filter in .onChangeCompat(of: viewModel.connectionStateFilter) { filter in
environments.commandClient.connectionStateFilter = filter commandClient.connectionStateFilter = filter
environments.commandClient.filterConnectionsNow() commandClient.filterConnectionsNow()
} }
.onChangeCompat(of: viewModel.connectionSort) { sort in .onChangeCompat(of: viewModel.connectionSort) { sort in
environments.commandClient.connectionSort = sort commandClient.connectionSort = sort
environments.commandClient.filterConnectionsNow() commandClient.filterConnectionsNow()
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
#if os(iOS) #if os(iOS)
@@ -24,7 +24,7 @@ public struct ButtonVisibilityState {
groupsCount = Variant.screenshotMode && actualGroupsCount == 0 groupsCount = Variant.screenshotMode && actualGroupsCount == 0
? screenshotFallbackGroupsCount ? screenshotFallbackGroupsCount
: actualGroupsCount : actualGroupsCount
connectionsCount = commandClient.connections?.count ?? 0 connectionsCount = Int(commandClient.status?.connectionsIn ?? 0)
let isConnected = Variant.screenshotMode || profile.status.isConnectedStrict let isConnected = Variant.screenshotMode || profile.status.isConnectedStrict
+3 -11
View File
@@ -202,22 +202,14 @@ public class CommandClient: ObservableObject {
} }
clientOptions.statusInterval = Int64(NSEC_PER_SEC) clientOptions.statusInterval = Int64(NSEC_PER_SEC)
let client = LibboxNewCommandClient(clientHandler(self), clientOptions)! 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 { do {
try client.connect() try client.connect()
} catch {
return
}
await MainActor.run { await MainActor.run {
commandClient = client commandClient = client
} }
return
} catch {}
try Task.checkCancellation()
}
} catch {
try? client.disconnect()
}
} }
private class clientHandler: NSObject, LibboxCommandClientHandlerProtocol { private class clientHandler: NSObject, LibboxCommandClientHandlerProtocol {
+41 -15
View File
@@ -15,43 +15,64 @@
let socketPath: String let socketPath: String
var commandServer: LibboxCommandServer? var commandServer: LibboxCommandServer?
private enum ServiceReadyState {
case pending
case ready
case failed(Error)
}
private let serviceReadyLock = NSLock() private let serviceReadyLock = NSLock()
private var _serviceReady = false private var serviceReadyState = ServiceReadyState.pending
private var serviceReadyContinuations: [CheckedContinuation<Void, Never>] = [] private var serviceReadyContinuations: [CheckedContinuation<Void, Error>] = []
private static let defaultNotReadyError = NSError(domain: "CommandXPC", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Command server not ready",
])
init(socketPath: String) { init(socketPath: String) {
self.socketPath = socketPath self.socketPath = socketPath
} }
func waitForServiceReady() async { func waitForServiceReady() async throws {
serviceReadyLock.lock() serviceReadyLock.lock()
if _serviceReady { switch serviceReadyState {
case .ready:
serviceReadyLock.unlock() serviceReadyLock.unlock()
return return
} case let .failed(error):
await withCheckedContinuation { continuation in serviceReadyLock.unlock()
throw error
case .pending:
return try await withCheckedThrowingContinuation { continuation in
serviceReadyContinuations.append(continuation) serviceReadyContinuations.append(continuation)
serviceReadyLock.unlock() serviceReadyLock.unlock()
} }
} }
}
func markServiceReady() { func markServiceReady() {
resolveServiceReady(.success(()))
}
func markServiceNotReady(_ error: Error? = nil) {
resolveServiceReady(.failure(error ?? Self.defaultNotReadyError))
}
private func resolveServiceReady(_ result: Result<Void, Error>) {
serviceReadyLock.lock() serviceReadyLock.lock()
_serviceReady = true switch result {
case .success:
serviceReadyState = .ready
case let .failure(error):
serviceReadyState = .failed(error)
}
let continuations = serviceReadyContinuations let continuations = serviceReadyContinuations
serviceReadyContinuations.removeAll() serviceReadyContinuations.removeAll()
serviceReadyLock.unlock() serviceReadyLock.unlock()
for continuation in continuations { 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 { func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
let bundleID = AppConfiguration.packageName + ".standalone" let bundleID = AppConfiguration.packageName + ".standalone"
let requirement = "identifier \"\(bundleID)\" and anchor apple generic and certificate leaf[subject.OU] = \"\(AppConfiguration.teamID)\"" 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) { func extensionRequirements(reply: @escaping (Bool, Bool, NSError?) -> Void) {
Task { Task {
await service.waitForServiceReady() do {
try await service.waitForServiceReady()
} catch {
reply(false, false, error as NSError)
return
}
guard let commandServer = service.commandServer else { guard let commandServer = service.commandServer else {
reply(false, false, NSError(domain: "CommandXPC", code: -1, userInfo: [ reply(false, false, NSError(domain: "CommandXPC", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Command server not available", NSLocalizedDescriptionKey: "Command server not available",
+1 -1
View File
@@ -131,7 +131,7 @@ public struct ImportRemoteProfileRequest: Hashable, Identifiable {
@MainActor @MainActor
public class ExtensionEnvironments: ObservableObject { 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 extensionProfileLoading = true
@Published public var extensionProfile: ExtensionProfile? @Published public var extensionProfile: ExtensionProfile?
@Published public var emptyProfiles = false @Published public var emptyProfiles = false
+12
View File
@@ -171,7 +171,16 @@ open class ExtensionProvider: NEPacketTunnelProvider {
#endif #endif
writeMessage("(packet-tunnel): Here I stand") writeMessage("(packet-tunnel): Here I stand")
do {
try await startService() try await startService()
} catch {
#if os(macOS)
if Variant.useSystemExtension {
xpcService.markServiceNotReady(error)
}
#endif
throw error
}
#if os(macOS) #if os(macOS)
if Variant.useSystemExtension { if Variant.useSystemExtension {
xpcService.markServiceReady() xpcService.markServiceReady()
@@ -251,6 +260,9 @@ open class ExtensionProvider: NEPacketTunnelProvider {
} }
#if os(macOS) #if os(macOS)
if Variant.useSystemExtension { if Variant.useSystemExtension {
xpcService.markServiceNotReady(NSError(domain: "CommandXPC", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Command server stopped",
]))
xpcListener.invalidate() xpcListener.invalidate()
xpcListener = nil xpcListener = nil
xpcService.commandServer = nil xpcService.commandServer = nil