├── Resources ├── demo_app.png ├── demo_app_2.png ├── demo_json.png └── debug_client.png ├── .gitignore ├── Tests └── AGDebugKitTests │ ├── GraphTests.swift │ └── DebugServerTests.swift ├── Sources ├── DemoApp │ ├── ContentView.swift │ ├── DemoApp.swift │ ├── ViewExample.swift │ └── ExtraExample.swift ├── AGDebugKit │ ├── DebugServer │ │ ├── DebugServerCommand.swift │ │ ├── DebugServerMessageHeader.swift │ │ ├── DebugServerMode.swift │ │ └── DebugServer.swift │ ├── ViewModifier │ │ ├── GraphValueHelper.swift │ │ └── ExtraGraphModifier.swift │ └── GraphDebug.swift └── DebugClient │ ├── DebugClientApp.swift │ └── ContentView.swift ├── Package.resolved ├── README.md └── Package.swift /Resources/demo_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSwiftUIProject/AGDebugKit/HEAD/Resources/demo_app.png -------------------------------------------------------------------------------- /Resources/demo_app_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSwiftUIProject/AGDebugKit/HEAD/Resources/demo_app_2.png -------------------------------------------------------------------------------- /Resources/demo_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSwiftUIProject/AGDebugKit/HEAD/Resources/demo_json.png -------------------------------------------------------------------------------- /Resources/debug_client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSwiftUIProject/AGDebugKit/HEAD/Resources/debug_client.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Tests/AGDebugKitTests/GraphTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphTests.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/18. 6 | // 7 | 8 | import XCTest 9 | import AGDebugKit 10 | 11 | final class GraphTests: XCTestCase { 12 | func testArchiveGraph() throws { 13 | // Graph.archiveGraph(name: "test.json") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/DemoApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // AGDebugKit 4 | // 5 | // Created by Kyle on 2025/7/29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | VStack { 13 | ExtraExample() 14 | ViewExample.Inner() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/AGDebugKit/DebugServer/DebugServerCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugServerCommand.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/22. 6 | // 7 | 8 | extension DebugServer { 9 | public enum Command: String, CaseIterable, Hashable, Identifiable { 10 | case graphDescription = "graph/description" 11 | case profilerStart = "profiler/start" 12 | case profilerStop = "profiler/stop" 13 | case profilerReset = "profiler/reset" 14 | case profilerMark = "profiler/mark" 15 | 16 | public var id: String { rawValue } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/AGDebugKitTests/DebugServerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugServerTests.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/21. 6 | // 7 | 8 | import XCTest 9 | import AGDebugKit 10 | 11 | final class DebugServerTests: XCTestCase { 12 | func testExample() throws { 13 | let server = DebugServer.shared 14 | XCTAssertNil(server.url) 15 | server.start() 16 | // Need workaround internal_diagnostics check 17 | // breakpoint on _ZN2AG11DebugServer5startEj 18 | // let url = try XCTUnwrap(server.url) 19 | // print(url.absoluteString) 20 | server.stop() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/DemoApp/DemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/18. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | @available(macOS 14.0, *) 12 | struct DemoApp: App { 13 | init() { 14 | // Fixing the App Activation from: https://www.alwaysrightinstitute.com/tows 15 | DispatchQueue.main.async { 16 | NSApp.setActivationPolicy(.regular) 17 | NSApp.activate(ignoringOtherApps: true) 18 | NSApp.windows.first?.makeKeyAndOrderFront(nil) 19 | } 20 | } 21 | 22 | var body: some Scene { 23 | WindowGroup { 24 | ContentView() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/DebugClient/DebugClientApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugClientApp.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | @available(macOS 14.0, *) 12 | struct DebugClientApp: App { 13 | init() { 14 | // Fixing the App Activation from: https://www.alwaysrightinstitute.com/tows 15 | DispatchQueue.main.async { 16 | NSApp.setActivationPolicy(.regular) 17 | NSApp.activate(ignoringOtherApps: true) 18 | NSApp.windows.first?.makeKeyAndOrderFront(nil) 19 | } 20 | } 21 | 22 | var body: some Scene { 23 | WindowGroup { 24 | ContentView() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/AGDebugKit/DebugServer/DebugServerMessageHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugServerMessageHeader.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/22. 6 | // 7 | 8 | extension DebugServer { 9 | public struct MessageHeader: Codable { 10 | public var token: UInt32 11 | public var reserved: UInt32 12 | public var length: UInt32 13 | public var reserved2: UInt32 14 | public init(token: UInt32, length: UInt32) { 15 | self.token = token 16 | self.reserved = 0 17 | self.length = length 18 | self.reserved2 = 0 19 | } 20 | 21 | public static var size: Int { MemoryLayout.size } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/AGDebugKit/DebugServer/DebugServerMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugServerMode.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/22. 6 | // 7 | 8 | extension DebugServer { 9 | /// The run mode of DebugServer 10 | /// 11 | public struct Mode: RawRepresentable, Hashable { 12 | public let rawValue: UInt32 13 | 14 | public init(rawValue: UInt32) { 15 | self.rawValue = rawValue 16 | } 17 | 18 | /// Localhost mode: example host is `127.0.0.1` 19 | public static let local = Mode(rawValue: 1) 20 | 21 | /// Network mode: example host is `192.168.8.230` 22 | public static let network = Mode(rawValue: 3) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/AGDebugKit/ViewModifier/GraphValueHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphHelper.swift 3 | // AGDebugKit 4 | // 5 | // Created by Kyle on 2025/7/29. 6 | // 7 | 8 | package import SwiftUI 9 | package import AttributeGraph 10 | 11 | extension _GraphValue { 12 | package init(_ value: Attribute) { 13 | self = Swift.unsafeBitCast(value, to: Self.self) 14 | } 15 | 16 | package var value: Attribute { 17 | Swift.unsafeBitCast(self, to: Attribute.self) 18 | } 19 | 20 | package func unsafeCast(to _: T.Type = T.self) -> _GraphValue { 21 | _GraphValue(value.unsafeCast(to: T.self)) 22 | } 23 | 24 | package func unsafeBitCast(to _: T.Type) -> _GraphValue { 25 | _GraphValue(value.unsafeBitCast(to: T.self)) 26 | } 27 | } 28 | 29 | extension Attribute { 30 | package func unsafeBitCast(to _: T.Type) -> Attribute { 31 | unsafeOffset(at: 0, as: T.self) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "fd872cf156709daa1f2c87d1fd6723604d3fd11ef31d419639422ab82b0df9ac", 3 | "pins" : [ 4 | { 5 | "identity" : "darwinprivateframeworks", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", 8 | "state" : { 9 | "revision" : "5eb0f26ea5a5bbd5068f6b3daf3a97dd3682b234", 10 | "version" : "0.0.4" 11 | } 12 | }, 13 | { 14 | "identity" : "socket", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/OpenSwiftUIProject/Socket.git", 17 | "state" : { 18 | "revision" : "e81b4bd0415060e89decb90461cfc117f0e6d8d0", 19 | "version" : "0.3.3" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-system", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-system", 26 | "state" : { 27 | "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", 28 | "version" : "1.2.1" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /Sources/AGDebugKit/GraphDebug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphDebug.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/21. 6 | // 7 | 8 | public import AttributeGraph 9 | import Foundation 10 | 11 | extension Graph { 12 | public var dict: [String: Any]? { 13 | let options = [ 14 | Graph.descriptionFormat: Graph.descriptionFormatDictionary 15 | ] as NSDictionary 16 | guard let description = Graph.description(nil, options: options) else { 17 | return nil 18 | } 19 | guard let dictionary = description as? NSDictionary else { 20 | return nil 21 | } 22 | return dictionary as? [String: Any] 23 | } 24 | 25 | // style: 26 | // - bold: empty input/output edge 27 | // - dashed: indirect or has no value 28 | // color: 29 | // - red: is_changed 30 | public var dot: String? { 31 | let options = [ 32 | Graph.descriptionFormat: Graph.descriptionFormatDot 33 | ] as NSDictionary 34 | guard let description = Graph.description(self, options: options) 35 | else { 36 | return nil 37 | } 38 | return description as? String 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [WIP] AGDebugKit 2 | 3 | A package to get debug information from the private AttributeGraph framework behind SwiftUI. 4 | 5 | If you need JSON result visualization, you can refer to [GraphConverter](https://github.com/OpenSwiftUIProject/GraphConverter) 6 | 7 | If you need SwiftUI debug information, you can refer to [SwiftUIViewDebug](https://github.com/OpenSwiftUIProject/SwiftUIViewDebug) 8 | 9 | Note that only macOS and iPhone simulator are supported for now. And I only tested it on macOS 15.5 and iOS 18.5. 10 | 11 | ## Example 12 | 13 | ![Demo App Console](Resources/demo_app.png) 14 | 15 | ![Demo App Screenshot](Resources/demo_app_2.png) 16 | 17 | ![Demo JSON](Resources/demo_json.png) 18 | 19 | ![Debug Client Screenshot](Resources/debug_client.png) 20 | 21 | ## Star History 22 | 23 | 24 | 25 | 26 | 27 | Star History Chart 28 | 29 | 30 | -------------------------------------------------------------------------------- /Sources/AGDebugKit/DebugServer/DebugServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugServer.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/21. 6 | // 7 | 8 | import AttributeGraph 9 | public import Foundation 10 | 11 | public final class DebugServer { 12 | private var server: AGDebugServer? 13 | 14 | public static let shared = DebugServer() 15 | 16 | public func start(_ mode: Mode = .local) { 17 | server = AGDebugServer.start(mode: mode.rawValue) 18 | } 19 | 20 | public func stop() { 21 | AGDebugServer.stop() 22 | server = nil 23 | } 24 | 25 | public func run(timeout: Int32) { 26 | guard let _ = server else { return } 27 | AGDebugServer.run(timeout: timeout) 28 | } 29 | 30 | public var url: URL? { 31 | guard let _ = server, 32 | let url = AGDebugServer.copyURL() 33 | else { return nil } 34 | return url as URL 35 | } 36 | 37 | /// A Bool value indicating whether the server has been started successfully 38 | /// 39 | /// To make AttributeGraph start debugServer successfully, we need to pass its internal diagnostics check. 40 | /// In debug mode, add a symbolic breakpoint on `_ZN2AG11DebugServer5startEj`, run `start(_:)` and 41 | /// executable `reg write w0 1` after `os_variant_has_internal_diagnostics` call. 42 | public var startSuccess: Bool { 43 | server != nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/DemoApp/ViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewExample.swift 3 | // AGDebugKit 4 | // 5 | // Created by Kyle on 2025/7/29. 6 | // 7 | 8 | import SwiftUI 9 | import AttributeGraph 10 | 11 | struct ViewExample: View { 12 | fileprivate var inner: Inner 13 | 14 | var body: some View { 15 | VStack { 16 | Image(systemName: "globe") 17 | .imageScale(.large) 18 | .foregroundStyle(.tint) 19 | Text("Hello, world!") 20 | } 21 | .padding() 22 | } 23 | 24 | struct Inner: View { 25 | var body: Never { fatalError() } 26 | 27 | static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { 28 | let outputs = ViewExample._makeView( 29 | view: .init(Attribute(Child(inner: view.value))), 30 | inputs: inputs 31 | ) 32 | return outputs 33 | } 34 | 35 | static func _makeViewList(view: _GraphValue, inputs: _ViewListInputs) -> _ViewListOutputs { 36 | let outputs = ViewExample._makeViewList( 37 | view: .init(Attribute(Child(inner: view.value))), 38 | inputs: inputs 39 | ) 40 | return outputs 41 | } 42 | 43 | static func _viewListCount(inputs: _ViewListCountInputs) -> Int? { 44 | let result = ViewExample._viewListCount(inputs: inputs) 45 | return result 46 | } 47 | } 48 | 49 | struct Child: Rule { 50 | @Attribute var inner: Inner 51 | var value: ViewExample { ViewExample(inner: inner) } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/DemoApp/ExtraExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/21. 6 | // 7 | 8 | import SwiftUI 9 | import AttributeGraph 10 | import AGDebugKit 11 | 12 | struct ExtraExample: View { 13 | 14 | var body: some View { 15 | ZStack { 16 | StaticView() 17 | DynamicView() 18 | } 19 | } 20 | 21 | struct StaticView: View { 22 | var body: some View { 23 | ChildView() 24 | } 25 | } 26 | 27 | struct DynamicView: View { 28 | @State private var count = 0 29 | var body: some View { 30 | VStack { 31 | Text("Count: \(count)") 32 | Button("Increase") { 33 | count += 1 34 | } 35 | ChildView() 36 | } 37 | } 38 | } 39 | } 40 | 41 | 42 | struct ChildView: View { 43 | var body: some View { 44 | Color.red 45 | .extraGraph { graph, inputs, body in 46 | _ = graph.value.breadthFirstSearch(options: ._1) { anyAttribute in 47 | print("[makeView] Body type \(anyAttribute._bodyType)") 48 | print("[makeView] Value type \(anyAttribute.valueType)") 49 | return false 50 | } 51 | } makeViewListCallback: { graph, inputs, body in 52 | _ = graph.value.breadthFirstSearch(options: ._1) { anyAttribute in 53 | print("[makeViewList] Body type \(anyAttribute._bodyType)") 54 | print("[makeViewList] Value type \(anyAttribute.valueType)") 55 | return false 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.1 2 | import PackageDescription 3 | 4 | let releaseVersion = Context.environment["DARWINPRIVATEFRAMEWORKS_TARGET_RELEASE"].flatMap { Int($0) } ?? 2024 5 | let platforms: [SupportedPlatform] = switch releaseVersion { 6 | case 2024: [.iOS(.v18), .macOS(.v15), .macCatalyst(.v18), .tvOS(.v18), .watchOS(.v10), .visionOS(.v2)] 7 | case 2021: [.iOS(.v15), .macOS(.v12), .macCatalyst(.v15), .tvOS(.v15), .watchOS(.v7)] 8 | default: [] 9 | } 10 | 11 | let sharedSwiftSettings: [SwiftSetting] = [ 12 | .swiftLanguageMode(.v5), 13 | ] 14 | 15 | let package = Package( 16 | name: "AGDebugKit", 17 | platforms: platforms, 18 | products: [ 19 | .library(name: "AGDebugKit", targets: ["AGDebugKit"]), 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", exact: "0.0.4"), 23 | .package(url: "https://github.com/OpenSwiftUIProject/Socket.git", from: "0.3.3"), 24 | ], 25 | targets: [ 26 | .target( 27 | name: "AGDebugKit", 28 | dependencies: [ 29 | .product(name: "AttributeGraph", package: "DarwinPrivateFrameworks"), 30 | ], 31 | swiftSettings: sharedSwiftSettings + [ 32 | .enableExperimentalFeature("AccessLevelOnImport"), 33 | .enableUpcomingFeature("InternalImportsByDefault"), 34 | ] 35 | ), 36 | // A demo app showing how to use AGDebugKit 37 | .executableTarget( 38 | name: "DemoApp", 39 | dependencies: [ 40 | "AGDebugKit", 41 | ], 42 | swiftSettings: sharedSwiftSettings 43 | ), 44 | // A client sending command to AGDebugServer 45 | .executableTarget( 46 | name: "DebugClient", 47 | dependencies: [ 48 | "AGDebugKit", 49 | .product(name: "Socket", package: "Socket"), 50 | ], 51 | swiftSettings: sharedSwiftSettings 52 | ), 53 | .testTarget( 54 | name: "AGDebugKitTests", 55 | dependencies: ["AGDebugKit"], 56 | swiftSettings: sharedSwiftSettings 57 | ), 58 | ] 59 | ) 60 | -------------------------------------------------------------------------------- /Sources/AGDebugKit/ViewModifier/ExtraGraphModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtraGraphModifier.swift 3 | // AGDebugKit 4 | // 5 | // Created by Kyle on 2025/7/29. 6 | // 7 | 8 | public import SwiftUI 9 | import AttributeGraph 10 | 11 | public struct ExtraGraphModifier: ViewModifier { 12 | private var emptyModifier: EmptyModifier = .init() 13 | 14 | let makeViewCallback: (_GraphValue, _ViewInputs, @escaping (_Graph, _ViewInputs) -> _ViewOutputs) -> Void 15 | let makeViewListCallback: (_GraphValue, _ViewListInputs, @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs) -> Void 16 | 17 | public init(makeViewCallback: @escaping (_GraphValue, _ViewInputs, @escaping (_Graph, _ViewInputs) -> _ViewOutputs) -> Void, makeViewListCallback: @escaping (_GraphValue, _ViewListInputs, @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs) -> Void) { 18 | self.makeViewCallback = makeViewCallback 19 | self.makeViewListCallback = makeViewListCallback 20 | } 21 | 22 | public typealias Body = Never 23 | 24 | nonisolated public static func _makeView( 25 | modifier: _GraphValue, 26 | inputs: _ViewInputs, 27 | body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs 28 | ) -> _ViewOutputs { 29 | let outputs = EmptyModifier._makeView( 30 | modifier: modifier[\.emptyModifier], 31 | inputs: inputs, 32 | body: body 33 | ) 34 | modifier.value.value.makeViewCallback(modifier, inputs, body) 35 | return outputs 36 | } 37 | 38 | nonisolated public static func _makeViewList( 39 | modifier: _GraphValue, 40 | inputs: _ViewListInputs, 41 | body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs 42 | ) -> _ViewListOutputs { 43 | let outputs = EmptyModifier._makeViewList( 44 | modifier: modifier[\.emptyModifier], 45 | inputs: inputs, 46 | body: body 47 | ) 48 | modifier.value.value.makeViewListCallback(modifier, inputs, body) 49 | return outputs 50 | } 51 | 52 | nonisolated public static func _viewListCount( 53 | inputs: _ViewListCountInputs, 54 | body: (_ViewListCountInputs) -> Int? 55 | ) -> Int? { 56 | EmptyModifier._viewListCount(inputs: inputs, body: body) 57 | } 58 | } 59 | 60 | extension View { 61 | public func extraGraph( 62 | makeViewCallback: @escaping (_GraphValue, _ViewInputs, @escaping (_Graph, _ViewInputs) -> _ViewOutputs) -> Void, 63 | makeViewListCallback: @escaping (_GraphValue, _ViewListInputs, @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs) -> Void 64 | ) -> some View { 65 | modifier(ExtraGraphModifier( 66 | makeViewCallback: makeViewCallback, 67 | makeViewListCallback: makeViewListCallback 68 | )) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/DebugClient/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // 4 | // 5 | // Created by Kyle on 2024/1/21. 6 | // 7 | 8 | import AGDebugKit 9 | import os.log 10 | import Socket 11 | import SwiftUI 12 | 13 | @available(macOS 14.0, *) 14 | struct ContentView: View { 15 | private let logger = Logger(subsystem: "org.OpenSwiftUIProject.AGDebugKit", category: "DebugClient") 16 | 17 | @State private var started = false 18 | @State private var selectedMode: Mode = .local 19 | @State private var timeout: Int32 = 1 20 | 21 | @State private var host = "" 22 | @State private var port: UInt16 = 0 23 | 24 | @State private var socket: Socket? 25 | private var connectServerDisable: Bool { 26 | IPv4Address(rawValue: host) == nil || port == 0 || !started 27 | } 28 | 29 | @State private var token = 0 30 | private var serverIODisable: Bool { 31 | socket == nil || token == 0 32 | } 33 | 34 | @State private var selectedCommand: Command = .graphDescription 35 | @State private var commandLocked = false 36 | 37 | @State private var output = "" 38 | @State private var outputDate: Date? 39 | 40 | var body: some View { 41 | Form { 42 | Section { 43 | Text("Status: \(started.description) ") + Text("⏺").foregroundStyle(started ? .green : .red) 44 | HStack { 45 | Button { 46 | DebugServer.shared.start(selectedMode) 47 | started = DebugServer.shared.startSuccess 48 | if started, 49 | let url = DebugServer.shared.url, 50 | let components = URLComponents(url: url, resolvingAgainstBaseURL: false) { 51 | if let host = components.host { 52 | self.host = host 53 | } 54 | if let port = components.port { 55 | self.port = UInt16(port) 56 | } 57 | if let queryItems = components.queryItems, 58 | let tokenItem = queryItems.first(where: { $0.name == "token" }), 59 | let tokenValue = tokenItem.value, 60 | let token = Int(tokenValue) { 61 | self.token = token 62 | } 63 | } 64 | } label: { 65 | Text("Start debug server") 66 | } 67 | Spacer() 68 | Picker(selection: $selectedMode) { 69 | Text("local").tag(Mode.local) 70 | Text("network").tag(Mode.network) 71 | } label: { 72 | Text("Mode") 73 | } 74 | .pickerStyle(.segmented) 75 | .disabled(started) 76 | } 77 | HStack { 78 | Button("Run debug server") { 79 | DebugServer.shared.run(timeout: timeout) 80 | } 81 | Spacer() 82 | Stepper("Timeout \(timeout)", value: $timeout) 83 | } 84 | 85 | Button("Stop debug server") { 86 | DebugServer.shared.stop() 87 | started = DebugServer.shared.startSuccess 88 | } 89 | } 90 | Section { 91 | TextField("Host", text: $host) 92 | TextField("Port", value: $port, formatter: NumberFormatter()) 93 | Button("Connect server") { 94 | Task { try await connectServer() } 95 | } 96 | .disabled(connectServerDisable) 97 | } 98 | Section { 99 | TextField("Token", value: $token, formatter: NumberFormatter()) 100 | Picker(selection: $selectedCommand) { 101 | ForEach(Command.allCases) { command in 102 | Text(command.rawValue).tag(command) 103 | } 104 | } label: { 105 | Text("Command") 106 | } 107 | .pickerStyle(.segmented) 108 | .disabled(commandLocked) 109 | Button("Write Data") { 110 | Task { 111 | try await writeData() 112 | commandLocked = true 113 | } 114 | } 115 | .disabled(serverIODisable) 116 | Button("Read Data") { 117 | Task { 118 | try await readData() 119 | commandLocked = false 120 | } 121 | } 122 | .disabled(serverIODisable) 123 | } 124 | Section { 125 | Text(output) 126 | .multilineTextAlignment(.leading) 127 | } footer: { 128 | if let outputDate { 129 | Text("\(outputDate, format: .dateTime)") 130 | } 131 | } 132 | } 133 | .buttonStyle(.bordered) 134 | .formStyle(.grouped) 135 | } 136 | 137 | func connectServer() async throws { 138 | guard let addr = IPv4Address(rawValue: host) else { 139 | return 140 | } 141 | let socket = try await Socket(IPv4Protocol.tcp) 142 | self.socket = socket 143 | do { 144 | try await socket.connect(to: IPv4SocketAddress(address: addr, port: port)) 145 | } catch { 146 | logger.error("\(error.localizedDescription, privacy: .public)") 147 | throw error 148 | } 149 | } 150 | 151 | typealias Mode = DebugServer.Mode 152 | typealias Header = DebugServer.MessageHeader 153 | typealias Command = DebugServer.Command 154 | 155 | /// "graph/description" command is the same as `Graph().dict` 156 | func writeData(command: Command = .graphDescription) async throws { 157 | guard let socket else { return } 158 | let command = ["command": command.rawValue] 159 | let commandData = try JSONSerialization.data(withJSONObject: command) 160 | let size = commandData.count 161 | 162 | let header = Header(token: UInt32(token), length: UInt32(size)) 163 | let headerData = withUnsafePointer(to: header) { 164 | Data(bytes: UnsafeRawPointer($0), count: Header.size) 165 | } 166 | do { 167 | let byteCount = try await socket.write(headerData) 168 | logger.info("Send: \(byteCount, privacy: .public) bytes") 169 | } catch { 170 | logger.error("\(error.localizedDescription, privacy: .public)") 171 | throw error 172 | } 173 | do { 174 | let byteCount = try await socket.write(commandData) 175 | logger.info("Send: \(byteCount, privacy: .public) bytes") 176 | } catch { 177 | logger.error("\(error.localizedDescription, privacy: .public)") 178 | throw error 179 | } 180 | } 181 | 182 | func readData() async throws { 183 | guard let socket else { return } 184 | let headerData = try await socket.read(Header.size) 185 | 186 | let header = headerData.withUnsafeBytes { pointer in 187 | pointer.baseAddress! 188 | .assumingMemoryBound(to: Header.self) 189 | .pointee 190 | } 191 | guard header.token == token else { 192 | logger.error("Token mismatch: header's token-\(header.token, privacy: .public) token-\(token)") 193 | return 194 | } 195 | let size = header.length 196 | let dictionaryData = try await socket.read(Int(size)) 197 | let dict = try JSONSerialization.jsonObject(with: dictionaryData) as? NSDictionary 198 | if let dict { 199 | let dictDescription = dict.description 200 | logger.info("Received: \(dictDescription)") 201 | output = dictDescription 202 | outputDate = Date.now 203 | } 204 | } 205 | } 206 | --------------------------------------------------------------------------------