Refactor command client

This commit is contained in:
世界
2023-08-15 17:20:44 +08:00
parent 1ac5df7f72
commit dbd8700145
20 changed files with 378 additions and 572 deletions
@@ -52,11 +52,14 @@ public struct DashboardView: View {
#endif
struct DashboardView0: View {
@Environment(\.extensionProfile) private var extensionProfile
@EnvironmentObject private var environments: ExtensionEnvironments
var body: some View {
if ApplicationLibrary.inPreview {
ActiveDashboardView()
} else if let profile = extensionProfile.wrappedValue {
} else if environments.extensionProfileLoading {
ProgressView()
} else if let profile = environments.extensionProfile {
DashboardView1().environmentObject(profile)
} else {
FormView {
@@ -67,13 +70,13 @@ public struct DashboardView: View {
}
struct DashboardView1: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile
@State private var alert: Alert?
var body: some View {
VStack {
ActiveDashboardView()
.environmentObject(profile)
}
.alertBinding($alert)
.onChangeCompat(of: profile.status) { newValue in
@@ -3,16 +3,13 @@ import Library
import SwiftUI
public struct ExtensionStatusView: View {
@State private var commandClient: LibboxCommandClient?
@State private var message: LibboxStatusMessage?
@State private var connectTask: Task<Void, Error>?
@StateObject private var commandClient = CommandClient(.status)
@State private var columnCount: Int = 4
@State private var alert: Alert?
private let infoFont = Font.system(.caption, design: .monospaced)
public init() {}
public var body: some View {
viewBuilder {
VStack {
@@ -34,7 +31,7 @@ public struct ExtensionStatusView: View {
StatusLine("Uplink", "52 MiB")
StatusLine("Downlink", "5.6 GiB")
}
} else if let message {
} else if let message = commandClient.status {
StatusItem("Status") {
StatusLine("Memory", LibboxFormatBytes(message.memory))
StatusLine("Goroutines", "\(message.goroutines)")
@@ -80,53 +77,15 @@ public struct ExtensionStatusView: View {
.frame(alignment: .topLeading)
.padding([.top, .leading, .trailing])
}
.onAppear(perform: doReload)
.onAppear {
commandClient.connect()
}
.onDisappear {
connectTask?.cancel()
if let commandClient {
try? commandClient.disconnect()
}
commandClient = nil
commandClient.disconnect()
}
.alertBinding($alert)
}
private func doReload() {
connectTask?.cancel()
connectTask = Task.detached {
await connect()
}
}
private func connect() async {
let clientOptions = LibboxCommandClientOptions()
clientOptions.command = LibboxCommandStatus
clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC)
let client = LibboxNewCommandClient(FilePath.sharedDirectory.relativePath, statusHandler(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()
let isConnected: Bool
do {
try client.connect()
isConnected = true
} catch {
isConnected = false
}
try Task.checkCancellation()
if isConnected {
commandClient = client
return
}
}
} catch {
NSLog("failed to connect status: \(error.localizedDescription)")
try? client.disconnect()
}
}
private func updateColumnCount(_ width: Double) {
let v = Int(Int(width) / 155)
let new = v < 1 ? 1 : (v > 4 ? 4 : (v % 2 == 0 ? v : v - 1))
@@ -144,26 +103,6 @@ public struct ExtensionStatusView: View {
}
}
private class statusHandler: NSObject, LibboxCommandClientHandlerProtocol {
private let statusView: ExtensionStatusView
init(_ statusView: ExtensionStatusView) {
self.statusView = statusView
}
func connected() {}
func disconnected(_: String?) {}
func writeLog(_: String?) {}
func writeStatus(_ message: LibboxStatusMessage?) {
statusView.message = message
}
func writeGroups(_: LibboxOutboundGroupIteratorProtocol?) {}
}
private struct StatusItem<T>: View where T: View {
private let title: String
@ViewBuilder private let content: () -> T
@@ -2,8 +2,6 @@ import Library
import SwiftUI
public struct InstallProfileButton: View {
@Environment(\.extensionProfile) private var extensionProfile
@State private var alert: Alert?
public init() {}
@@ -3,7 +3,7 @@ import NetworkExtension
import SwiftUI
public struct StartStopButton: View {
@Environment(\.extensionProfile) private var extensionProfile
@EnvironmentObject private var environments: ExtensionEnvironments
public init() {}
@@ -20,8 +20,8 @@ public struct StartStopButton: View {
})
#endif
} else if let profile = extensionProfile.wrappedValue {
Button0(profile)
} else if let profile = environments.extensionProfile {
Button0().environmentObject(profile)
} else {
#if os(iOS) || os(tvOS)
Toggle(isOn: .constant(false)) {
@@ -39,14 +39,10 @@ public struct StartStopButton: View {
}
private struct Button0: View {
@Environment(\.logClient) private var logClient
@ObservedObject private var profile: ExtensionProfile
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile
@State private var alert: Alert?
init(_ profile: ExtensionProfile) {
self.profile = profile
}
var body: some View {
viewBuilder {
#if os(iOS) || os(tvOS)
@@ -81,7 +77,7 @@ public struct StartStopButton: View {
do {
if isEnabled {
try await profile.start()
logClient.wrappedValue?.reconnect()
environments.logClient.connect()
} else {
profile.stop()
}
@@ -30,32 +30,6 @@ public extension EnvironmentValues {
}
}
private struct extensionProfileKey: EnvironmentKey {
static let defaultValue: Binding<ExtensionProfile?> = .constant(nil)
}
var extensionProfile: Binding<ExtensionProfile?> {
get {
self[extensionProfileKey.self]
}
set {
self[extensionProfileKey.self] = newValue
}
}
private struct logClientKey: EnvironmentKey {
static let defaultValue: Binding<LogClient?> = .constant(nil)
}
var logClient: Binding<LogClient?> {
get {
self[logClientKey.self]
}
set {
self[logClientKey.self] = newValue
}
}
private struct importRemoteProfileKey: EnvironmentKey {
static var defaultValue: Binding<LibboxImportRemoteProfile?> = .constant(nil)
}
@@ -4,10 +4,8 @@ import SwiftUI
public struct GroupListView: View {
@State private var isLoading = true
@State private var connectTask: Task<Void, Error>?
@State private var commandClient: LibboxCommandClient?
@StateObject private var commandClient = CommandClient(.groups)
@State private var groups: [OutboundGroup] = []
@State private var groupExpand: [String: Bool] = [:]
public init() {}
public var body: some View {
@@ -27,17 +25,20 @@ public struct GroupListView: View {
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.onAppear(perform: doReload)
.onDisappear {
connectTask?.cancel()
if let commandClient {
try? commandClient.disconnect()
}
commandClient = nil
.onAppear {
connect()
}
.onDisappear {
commandClient.disconnect()
}
.onReceive(commandClient.$groups, perform: { groups in
if let groups {
setGroups(groups)
}
})
}
private func doReload() {
private func connect() {
if ApplicationLibrary.inPreview {
groups = [
OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [
@@ -52,47 +53,11 @@ public struct GroupListView: View {
]
isLoading = false
} else {
connectTask?.cancel()
connectTask = Task.detached {
await connect()
}
commandClient.connect()
}
}
private func connect() async {
let clientOptions = LibboxCommandClientOptions()
clientOptions.command = LibboxCommandGroup
clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC)
let client = LibboxNewCommandClient(FilePath.sharedDirectory.relativePath, groupsHandler(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()
let isConnected: Bool
do {
try client.connect()
isConnected = true
} catch {
isConnected = false
}
try Task.checkCancellation()
if isConnected {
commandClient = client
return
}
}
} catch {
NSLog("failed to connect status: \(error.localizedDescription)")
try? client.disconnect()
}
}
private func setGroups(_ groupIterator: LibboxOutboundGroupIteratorProtocol) {
var goGroups = [LibboxOutboundGroup]()
while groupIterator.hasNext() {
goGroups.append(groupIterator.next()!)
}
private func setGroups(_ goGroups: [LibboxOutboundGroup]) {
var groups = [OutboundGroup]()
for goGroup in goGroups {
var items = [OutboundGroupItem]()
@@ -106,24 +71,4 @@ public struct GroupListView: View {
self.groups = groups
isLoading = false
}
private class groupsHandler: NSObject, LibboxCommandClientHandlerProtocol {
private let groupListView: GroupListView
init(_ statusView: GroupListView) {
groupListView = statusView
}
func connected() {}
func disconnected(_: String?) {}
func writeLog(_: String?) {}
func writeStatus(_: LibboxStatusMessage?) {}
func writeGroups(_ groupIterator: LibboxOutboundGroupIteratorProtocol?) {
groupListView.setGroups(groupIterator!)
}
}
}
@@ -1,120 +0,0 @@
import Foundation
import Libbox
import Library
import SwiftUI
public class LogClient: ObservableObject {
private var maxLines: Int
@Published public var isConnected: Bool
@Published public var logList: [String]
private var commandClient: LibboxCommandClient!
private var connectTask: Task<Void, Error>?
public init(_ maxLines: Int) {
self.maxLines = maxLines
isConnected = false
logList = []
}
deinit {
if let connectTask {
connectTask.cancel()
}
if let commandClient {
try? commandClient.disconnect()
}
}
public func reconnect() {
if ApplicationLibrary.inPreview {
logList = [
"(packet-tunnel) log server started",
"INFO[0000] router: loaded geoip database: 250 codes",
"INFO[0000] router: loaded geosite database: 1400 codes",
"INFO[0000] router: updated default interface en0, index 11",
"inbound/tun[0]: started at utun3",
"sing-box started (1.666s)",
]
isConnected = true
} else {
if isConnected {
return
}
if let connectTask {
connectTask.cancel()
}
connectTask = Task.detached {
await self.connect()
}
}
}
private func connect() async {
let clientOptions = LibboxCommandClientOptions()
clientOptions.command = LibboxCommandLog
clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC)
let client = LibboxNewCommandClient(FilePath.sharedDirectory.relativePath, logHandler(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()
let isConnected: Bool
do {
try client.connect()
isConnected = true
} catch {
isConnected = false
}
try Task.checkCancellation()
if isConnected {
commandClient = client
return
}
}
} catch {
try? client.disconnect()
}
}
private class logHandler: NSObject, LibboxCommandClientHandlerProtocol {
private let logClient: LogClient
init(_ logClient: LogClient) {
self.logClient = logClient
}
@MainActor
func connected() {
logClient.logList.removeAll()
logClient.isConnected = true
}
@MainActor
func disconnected(_ message: String?) {
if let message {
logClient.logList.append("(log client closed) \(message)")
} else {
logClient.logList.append("(log client closed)")
}
try? logClient.commandClient?.disconnect()
logClient.commandClient = nil
logClient.isConnected = false
}
@MainActor
func writeLog(_ message: String?) {
guard let message else {
return
}
if logClient.logList.count > logClient.maxLines {
logClient.logList.removeFirst()
}
logClient.logList.append(message)
}
func writeStatus(_: LibboxStatusMessage?) {}
func writeGroups(_: LibboxOutboundGroupIteratorProtocol?) {}
}
}
+38 -65
View File
@@ -1,80 +1,53 @@
import Library
import SwiftUI
public struct LogView: View {
@Environment(\.logClient) private var logClient
@Environment(\.selection) private var selection
@EnvironmentObject private var environments: ExtensionEnvironments
private let logFont = Font.system(.caption2, design: .monospaced)
public init() {}
public var body: some View {
viewBuilder {
if let logClient = logClient.wrappedValue {
LogView0().environmentObject(logClient)
} else {
Text("Service not started")
}
}
}
private struct LogView0: View {
@Environment(\.selection) private var selection
@Environment(\.extensionProfile) private var extensionProfile
@EnvironmentObject private var logClient: LogClient
private let logFont = Font.system(.caption2, design: .monospaced)
var body: some View {
viewBuilder {
if logClient.logList.isEmpty {
VStack {
if logClient.isConnected {
Text("Empty logs")
} else {
Text("Service not started").onAppear(perform: connectLog)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
if environments.logClient.logList.isEmpty {
VStack {
if environments.logClient.isConnected {
Text("Empty logs")
} else {
ScrollViewReader { reader in
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in
Text(it.element)
.font(logFont)
#if os(tvOS)
.focusable()
#endif
Spacer(minLength: 8)
}
.onChangeCompat(of: logClient.logList.count) { newCount in
withAnimation {
reader.scrollTo(newCount - 1)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
#if os(tvOS)
.focusEffectDisabled()
.focusSection()
#endif
.onAppear {
reader.scrollTo(logClient.logList.count - 1)
}
Text("Service not started").onAppear {
environments.connectLog()
}
}
}
}
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
} else {
ScrollViewReader { reader in
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(environments.logClient.logList.enumerated()), id: \.offset) { it in
Text(it.element)
.font(logFont)
#if os(tvOS)
.focusable()
#endif
Spacer(minLength: 8)
}
private func connectLog() {
if ApplicationLibrary.inPreview {
logClient.reconnect()
} else {
guard let profile = extensionProfile.wrappedValue else {
return
.onChangeCompat(of: environments.logClient.logList.count) { newCount in
withAnimation {
reader.scrollTo(newCount - 1)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
if profile.status.isConnected, !logClient.isConnected {
logClient.reconnect()
#if os(tvOS)
.focusEffectDisabled()
.focusSection()
#endif
.onAppear {
reader.scrollTo(environments.logClient.logList.count - 1)
}
}
}
@@ -73,10 +73,8 @@ public struct ServiceLogView: View {
if content.isEmpty {
do {
content = try String(contentsOf: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old"))
} catch {
}
} catch {}
}
isLoading = false
}
@@ -85,6 +83,7 @@ public struct ServiceLogView: View {
try? FileManager.default.removeItem(at: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old"))
DispatchQueue.main.async {
dismiss()
isLoading = true
}
}