diff --git a/Package.resolved b/Package.resolved index 6ff5c87a..34c805da 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "33172a013ea78db9fbfb2be84e0c38eb0aa2efdaf6e752b088afb5349346644a", + "originHash" : "e6d3adb6e1120d8ec888e92e5977e2ce73440a10c3e2f3088b9fb458e554f65f", "pins" : [ { "identity" : "darwinprivateframeworks", diff --git a/Package.swift b/Package.swift index 29f4c244..1f355eca 100644 --- a/Package.swift +++ b/Package.swift @@ -262,6 +262,7 @@ if attributeGraphCondition { } } else { openGraphShimsTarget.dependencies.append("OpenGraph") + package.platforms = [.iOS(.v13), .macOS(.v10_15), .macCatalyst(.v13), .tvOS(.v13), .watchOS(.v5)] } let compatibilityTestCondition = envEnable("OPENGRAPH_COMPATIBILITY_TEST") diff --git a/Sources/OpenGraphCxx/DebugServer/DebugServer.mm b/Sources/OpenGraphCxx/DebugServer/DebugServer.mm index 08c0ca93..2aa163e1 100644 --- a/Sources/OpenGraphCxx/DebugServer/DebugServer.mm +++ b/Sources/OpenGraphCxx/DebugServer/DebugServer.mm @@ -28,6 +28,11 @@ #include #include +OG_EXTERN_C_BEGIN +// DYLD_INTERPOSE does not work. Directly use the hook one here to match the semantics. +bool og_variant_has_internal_diagnostics(const char *subsystem); +OG_EXTERN_C_END + // MARK: DebugServer public API Implementation OG::DebugServer::DebugServer(OGDebugServerMode mode) { @@ -260,7 +265,7 @@ if ( (mode & OGDebugServerModeValid) && !OG::DebugServer::has_shared_server() - /*&& os_variant_has_internal_diagnostics("com.apple.AttributeGraph")*/ + && og_variant_has_internal_diagnostics("org.OpenSwiftUIProject.OpenGraph") ) { _shared_server = new DebugServer(mode); } diff --git a/Sources/OpenGraphCxx/DebugServer/interpose.c b/Sources/OpenGraphCxx/DebugServer/interpose.c new file mode 100644 index 00000000..637bd2bc --- /dev/null +++ b/Sources/OpenGraphCxx/DebugServer/interpose.c @@ -0,0 +1,34 @@ +// +// interpose.c +// OpenGraphCxx + +#include +#include "stdio.h" +#include "stdbool.h" +#include "string.h" + +#if OG_TARGET_OS_DARWIN +extern bool os_variant_has_internal_diagnostics(const char *subsystem); +#endif + +bool og_variant_has_internal_diagnostics(const char *subsystem) { + if (strcmp(subsystem, "org.OpenSwiftUIProject.OpenGraph") == 0) { + return true; + } else if (strcmp(subsystem, "com.apple.AttributeGraph") == 0) { + return true; + } else { + #if OG_TARGET_OS_DARWIN + return os_variant_has_internal_diagnostics(subsystem); + #else + return false; + #endif + } +} + +#if OG_TARGET_OS_DARWIN +#define DYLD_INTERPOSE(_replacement,_replacee) \ + __attribute__((used)) static struct{ const void* replacement; const void* replacee; } _interpose_##_replacee \ + __attribute__ ((section ("__DATA,__interpose"))) = { (const void*)(unsigned long)&_replacement, (const void*)(unsigned long)&_replacee }; + +DYLD_INTERPOSE(og_variant_has_internal_diagnostics, os_variant_has_internal_diagnostics) +#endif diff --git a/Sources/OpenGraphShims/DebugClient.swift b/Sources/OpenGraphShims/DebugClient.swift new file mode 100644 index 00000000..ab9181ff --- /dev/null +++ b/Sources/OpenGraphShims/DebugClient.swift @@ -0,0 +1,146 @@ +// +// DebugServerTests.swift +// OpenGraphShims + +#if canImport(Darwin) +public import Foundation +public import Network + +public struct ConnectionUpdates: AsyncSequence { + public typealias Element = NWConnection.State + + private let stream: AsyncStream + + fileprivate init(stream: AsyncStream) { + self.stream = stream + } + + public func makeAsyncIterator() -> AsyncStream.AsyncIterator { + stream.makeAsyncIterator() + } +} + +@_spi(Debug) +public final class DebugClient { + public enum Command: String, CaseIterable, Hashable { + case graphDescription = "graph/description" + case profilerStart = "profiler/start" + case profilerStop = "profiler/stop" + case profilerReset = "profiler/reset" + case profilerMark = "profiler/mark" + } + + private var connection: NWConnection? + private let queue = DispatchQueue(label: "org.openswiftuiproject.opengraph.debugclient") + + public init() {} + + public func connect(to url: URL) -> ConnectionUpdates { + guard let host = url.host, let port = url.port else { + return ConnectionUpdates(stream: AsyncStream { continuation in + continuation.yield(.failed(NWError.posix(.EINVAL))) + continuation.finish() + }) + } + let nwHost = NWEndpoint.Host(host) + let nwPort = NWEndpoint.Port(integerLiteral: UInt16(port)) + connection = NWConnection(host: nwHost, port: nwPort, using: .tcp) + let stream = AsyncStream { continuation in + connection?.stateUpdateHandler = { state in + continuation.yield(state) + if case .cancelled = state { + continuation.finish() + } + } + connection?.start(queue: queue) + } + return ConnectionUpdates(stream: stream) + } + + public func sendMessage(token: UInt32, data: Data) async throws { + guard let connection else { + throw ClientError.notConnected + } + let header = DebugServerMessageHeader( + token: token, + unknown: 0, + length: numericCast(data.count), + unknown2: 0 + ) + let headerData = withUnsafePointer(to: header) { + Data(bytes: UnsafeRawPointer($0), count: MemoryLayout.size) + } + try await send(data: headerData, on: connection) + guard header.length > 0 else { + return + } + try await send(data: data, on: connection) + } + + public func receiveMessage() async throws -> (header: DebugServerMessageHeader, data: Data) { + guard let connection = connection else { + throw ClientError.notConnected + } + let headerData = try await receive( + length: MemoryLayout.size, + from: connection + ) + let header = headerData.withUnsafeBytes { bytes in + let buffer = bytes.bindMemory(to: UInt32.self) + return DebugServerMessageHeader( + token: buffer[0], + unknown: buffer[1], + length: buffer[2], + unknown2: buffer[3] + ) + } + guard header.length > 0 else { + return (header: header, data: Data()) + } + let payloadData = try await receive( + length: numericCast(header.length), + from: connection + ) + return (header: header, data: payloadData) + } + + public func disconnect() { + connection?.cancel() + connection = nil + } + + private func send(data: Data, on connection: NWConnection) async throws { + return try await withCheckedThrowingContinuation { continuation in + connection.send(content: data, completion: .contentProcessed { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + }) + } + } + + private func receive(length: Int, from connection: NWConnection) async throws -> Data { + return try await withCheckedThrowingContinuation { continuation in + connection.receive(minimumIncompleteLength: length, maximumLength: length) { data, _, isComplete, error in + if let error { + continuation.resume(throwing: error) + } else if let data { + continuation.resume(returning: data) + } else { + continuation.resume(throwing: ClientError.noDataReceived) + } + } + } + } +} + +enum ClientError: Error { + case invalidURL + case notConnected + case connectionCancelled + case noDataReceived +} + +#endif diff --git a/Sources/OpenGraphShims/DebugServerMessageHeader.swift b/Sources/OpenGraphShims/DebugServerMessageHeader.swift new file mode 100644 index 00000000..bc6df06d --- /dev/null +++ b/Sources/OpenGraphShims/DebugServerMessageHeader.swift @@ -0,0 +1,18 @@ +// +// DebugServerMessageHeader.swift +// OpenGraphShims + +@_spi(Debug) +public struct DebugServerMessageHeader { + public let token: UInt32 + public let unknown: UInt32 + public let length: UInt32 + public let unknown2: UInt32 + + public init(token: UInt32, unknown: UInt32, length: UInt32, unknown2: UInt32) { + self.token = token + self.unknown = unknown + self.length = length + self.unknown2 = unknown2 + } +} diff --git a/Tests/OpenGraphCompatibilityTests/Debug/DebugServerTests.swift b/Tests/OpenGraphCompatibilityTests/Debug/DebugServerTests.swift index 188aec04..4eecefa9 100644 --- a/Tests/OpenGraphCompatibilityTests/Debug/DebugServerTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Debug/DebugServerTests.swift @@ -15,14 +15,13 @@ struct DebugServerTests { #expect(DebugServer.copyURL() == nil) } - // TODO: hook via private API of dyld // To make AG start debugServer, we need to pass internal_diagnostics check. // In debug mode, we can breakpoint on `_ZN2AG11DebugServer5startEj` and // executable `reg write w0 1` after `internal_diagnostics` call. - // Or we can disable SIP on the target darwinOS and run `sudo sysctl kern.osvariant_status=xx` to workaround - @Test( - .disabled(if: compatibilityTestEnabled, "Skip on AG due to internal_diagnostics check"), - ) + // Or we can disable SIP on the target darwinOS and run `sudo sysctl kern.osvariant_status=xx` to workaround. + // Or you can add `breakpoint set -n os_variant_has_internal_diagnostics -C "thread return 1"` + // to your lldbinit or run it before AGDebugServerStart call. + @Test(.disabled(if: compatibilityTestEnabled, "Skip on AG on CI due to internal_diagnostics check")) func testMode1() throws { let _ = try #require(DebugServer.start(mode: [.valid])) let url = try #require(DebugServer.copyURL()) as URL @@ -33,9 +32,7 @@ struct DebugServerTests { DebugServer.stop() } - @Test( - .disabled(if: compatibilityTestEnabled, "Skip on AG due to internal_diagnostics check"), - ) + @Test(.disabled(if: compatibilityTestEnabled, "Skip on AG on CI due to internal_diagnostics check")) func testMode3() throws { let _ = try #require(DebugServer.start(mode: [.valid, .networkInterface])) let url = try #require(DebugServer.copyURL()) as URL diff --git a/Tests/OpenGraphCxxTests/DebugServer/DebugClient.swift b/Tests/OpenGraphCxxTests/DebugServer/DebugClient.swift deleted file mode 100644 index 14715170..00000000 --- a/Tests/OpenGraphCxxTests/DebugServer/DebugClient.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// DebugServerTests.swift -// OpenGraphCxxTests - -#if canImport(Darwin) -import Foundation -import Network -import OpenGraphCxx_Private.DebugServer - -final class DebugClient { - private var connection: NWConnection? - private let queue = DispatchQueue(label: "opengraph.debugserver.client.queue") - - func connect(to url: URL) async throws { - guard let host = url.host, let port = url.port else { - throw ClientError.invalidURL - } - - let nwHost = NWEndpoint.Host(host) - let nwPort = NWEndpoint.Port(integerLiteral: UInt16(port)) - - connection = NWConnection(host: nwHost, port: nwPort, using: .tcp) - - return try await withCheckedThrowingContinuation { continuation in - connection?.stateUpdateHandler = { state in - switch state { - case .ready: - continuation.resume() - case let .failed(error): - continuation.resume(throwing: error) - case .cancelled: - continuation.resume(throwing: ClientError.connectionCancelled) - default: - break - } - } - connection?.start(queue: queue) - } - } - - func sendMessage(token: UInt32, data: Data) async throws { - guard let connection else { - throw ClientError.notConnected - } - let header = DebugServerMessageHeader( - token: token, - unknown: 0, - length: numericCast(data.count), - unknown2: 0 - ) - let headerData = withUnsafePointer(to: header) { - Data(bytes: UnsafeRawPointer($0), count: MemoryLayout.size) - } - try await send(data: headerData, on: connection) - guard header.length > 0 else { - return - } - try await send(data: data, on: connection) - } - - func receiveMessage() async throws -> (header: DebugServerMessageHeader, data: Data) { - guard let connection = connection else { - throw ClientError.notConnected - } - let headerData = try await receive( - length: MemoryLayout.size, - from: connection - ) - let header = headerData.withUnsafeBytes { bytes in - let buffer = bytes.bindMemory(to: UInt32.self) - return DebugServerMessageHeader( - token: buffer[0], - unknown: buffer[1], - length: buffer[2], - unknown2: buffer[3] - ) - } - guard header.length > 0 else { - return (header: header, data: Data()) - } - let payloadData = try await receive( - length: numericCast(header.length), - from: connection - ) - return (header: header, data: payloadData) - } - - func disconnect() { - connection?.cancel() - connection = nil - } - - private func send(data: Data, on connection: NWConnection) async throws { - return try await withCheckedThrowingContinuation { continuation in - connection.send(content: data, completion: .contentProcessed { error in - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume() - } - }) - } - } - - private func receive(length: Int, from connection: NWConnection) async throws -> Data { - return try await withCheckedThrowingContinuation { continuation in - connection.receive(minimumIncompleteLength: length, maximumLength: length) { data, _, isComplete, error in - if let error { - continuation.resume(throwing: error) - } else if let data { - continuation.resume(returning: data) - } else { - continuation.resume(throwing: ClientError.noDataReceived) - } - } - } - } -} - -enum ClientError: Error { - case invalidURL - case notConnected - case connectionCancelled - case noDataReceived -} - -#endif diff --git a/Tests/OpenGraphCxxTests/DebugServer/DebugClient.swift b/Tests/OpenGraphCxxTests/DebugServer/DebugClient.swift new file mode 120000 index 00000000..14c281b3 --- /dev/null +++ b/Tests/OpenGraphCxxTests/DebugServer/DebugClient.swift @@ -0,0 +1 @@ +../../../Sources/OpenGraphShims/DebugClient.swift \ No newline at end of file diff --git a/Tests/OpenGraphCxxTests/DebugServer/DebugServerMessageHeader.swift b/Tests/OpenGraphCxxTests/DebugServer/DebugServerMessageHeader.swift new file mode 120000 index 00000000..abce3da1 --- /dev/null +++ b/Tests/OpenGraphCxxTests/DebugServer/DebugServerMessageHeader.swift @@ -0,0 +1 @@ +../../../Sources/OpenGraphShims/DebugServerMessageHeader.swift \ No newline at end of file diff --git a/Tests/OpenGraphCxxTests/DebugServer/DebugServerTests.swift b/Tests/OpenGraphCxxTests/DebugServer/DebugServerTests.swift index c1b17df8..57f715b8 100644 --- a/Tests/OpenGraphCxxTests/DebugServer/DebugServerTests.swift +++ b/Tests/OpenGraphCxxTests/DebugServer/DebugServerTests.swift @@ -9,13 +9,7 @@ import Testing @MainActor struct DebugServerTests { - private enum Command: String, CaseIterable, Hashable { - case graphDescription = "graph/description" - case profilerStart = "profiler/start" - case profilerStop = "profiler/stop" - case profilerReset = "profiler/reset" - case profilerMark = "profiler/mark" - } + typealias Command = DebugClient.Command private func data(for command: Command) throws -> Data{ let command = ["command": command.rawValue] @@ -24,26 +18,37 @@ struct DebugServerTests { @Test func commandTest() async throws { - let debugServer = OG.DebugServer([.valid]) + var debugServer = OG.DebugServer([.valid]) let cfURL = debugServer.copy_url() let url = try #require(cfURL) as URL let components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: false)) let token = try #require(components.queryItems?.first { $0.name == "token" }?.value.flatMap { UInt32($0) }) debugServer.run(1) let client = DebugClient() - try await client.connect(to: url) - - for command in Command.allCases { - if command == .graphDescription { - continue + let updates = client.connect(to: url) + try await confirmation { confirm in + for try await update in updates { + switch update { + case .ready: + confirm() + for command in Command.allCases { + if command == .graphDescription { + continue + } + try await client.sendMessage( + token: token, + data: data(for: command) + ) + let (_, responseData) = try await client.receiveMessage() + let response = try #require(String(data: responseData, encoding: .utf8)) + #expect(response == command.rawValue) + } + debugServer.shutdown() + // TODO: The shutdown should close the connection, but it does not for OGDebugServer currently. + default: + break + } } - try await client.sendMessage( - token: token, - data: data(for: command) - ) - let (_, responseData) = try await client.receiveMessage() - let response = try #require(String(data: responseData, encoding: .utf8)) - #expect(response == command.rawValue) } } }