Skip to content

Commit 27fb8c0

Browse files
committed
Add DebugClient
1 parent de6eeee commit 27fb8c0

File tree

4 files changed

+185
-8
lines changed

4 files changed

+185
-8
lines changed

Sources/OpenGraphCxx/DebugServer/DebugServer.mm

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,25 +359,25 @@ bool blocking_write(int descriptor, void *buf, unsigned long count) {
359359
}
360360
} else if ([command isEqual:@"profiler/start"]) {
361361
// FIXME: Simply return the command str for now
362-
CFStringRef string = CFSTR("start");
362+
CFStringRef string = CFSTR("profiler/start");
363363
CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)CFStringGetCStringPtr(string, kCFStringEncodingUTF8), CFStringGetLength(string));
364364
CFRelease(string);
365365
return data;
366366
} else if ([command isEqual:@"profiler/stop"]) {
367367
// FIXME: Simply return the command str for now
368-
CFStringRef string = CFSTR("stop");
368+
CFStringRef string = CFSTR("profiler/stop");
369369
CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)CFStringGetCStringPtr(string, kCFStringEncodingUTF8), CFStringGetLength(string));
370370
CFRelease(string);
371371
return data;
372372
} else if ([command isEqual:@"profiler/reset"]) {
373373
// FIXME: Simply return the command str for now
374-
CFStringRef string = CFSTR("reset");
374+
CFStringRef string = CFSTR("profiler/reset");
375375
CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)CFStringGetCStringPtr(string, kCFStringEncodingUTF8), CFStringGetLength(string));
376376
CFRelease(string);
377377
return data;
378378
} else if ([command isEqual:@"profiler/mark"]) {
379379
// FIXME: Simply return the command str for now
380-
CFStringRef string = CFSTR("mark");
380+
CFStringRef string = CFSTR("profiler/mark");
381381
CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)CFStringGetCStringPtr(string, kCFStringEncodingUTF8), CFStringGetLength(string));
382382
CFRelease(string);
383383
return data;

Sources/OpenGraphCxx/include/OpenGraphCxx/DebugServer/DebugServer.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,21 @@ OG_ASSUME_NONNULL_BEGIN
1919

2020
OG_IMPLICIT_BRIDGING_ENABLED
2121

22-
namespace OG {
23-
struct OGDebugServerMessageHeader {
22+
typedef struct OG_SWIFT_NAME(DebugServerMessageHeader) OGDebugServerMessageHeader {
2423
uint32_t token;
2524
uint32_t unknown;
2625
uint32_t length;
2726
uint32_t unknown2;
28-
}; /* OGDebugServerMessageHeader */
27+
} OGDebugServerMessageHeader; /* OGDebugServerMessageHeader */
2928

29+
namespace OG {
3030
class DebugServer {
3131
public:
3232
DebugServer(OGDebugServerMode mode);
3333
~DebugServer();
3434

3535
CFURLRef _Nullable copy_url() const;
36-
void run(int descriptor);
36+
void run(int timeout) const;
3737

3838
static OG_INLINE DebugServer *shared_server() { return _shared_server; }
3939
static OG_INLINE bool has_shared_server() { return _shared_server != nullptr; }
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//
2+
// DebugServerTests.swift
3+
// OpenGraphCxxTests
4+
5+
#if canImport(Darwin)
6+
import Foundation
7+
import Network
8+
import OpenGraphCxx_Private.DebugServer
9+
10+
final class DebugClient {
11+
private var connection: NWConnection?
12+
private let queue = DispatchQueue(label: "opengraph.debugserver.client.queue")
13+
14+
func connect(to url: URL) async throws {
15+
guard let host = url.host, let port = url.port else {
16+
throw ClientError.invalidURL
17+
}
18+
19+
let nwHost = NWEndpoint.Host(host)
20+
let nwPort = NWEndpoint.Port(integerLiteral: UInt16(port))
21+
22+
connection = NWConnection(host: nwHost, port: nwPort, using: .tcp)
23+
24+
return try await withCheckedThrowingContinuation { continuation in
25+
connection?.stateUpdateHandler = { state in
26+
switch state {
27+
case .ready:
28+
continuation.resume()
29+
case let .failed(error):
30+
continuation.resume(throwing: error)
31+
case .cancelled:
32+
continuation.resume(throwing: ClientError.connectionCancelled)
33+
default:
34+
break
35+
}
36+
}
37+
connection?.start(queue: queue)
38+
}
39+
}
40+
41+
func sendMessage(token: UInt32, data: Data) async throws {
42+
guard let connection else {
43+
throw ClientError.notConnected
44+
}
45+
let header = DebugServerMessageHeader(
46+
token: token,
47+
unknown: 0,
48+
length: numericCast(data.count),
49+
unknown2: 0
50+
)
51+
let headerData = withUnsafePointer(to: header) {
52+
Data(bytes: UnsafeRawPointer($0), count: MemoryLayout<DebugServerMessageHeader>.size)
53+
}
54+
try await send(data: headerData, on: connection)
55+
guard header.length > 0 else {
56+
return
57+
}
58+
try await send(data: data, on: connection)
59+
}
60+
61+
func receiveMessage() async throws -> (header: DebugServerMessageHeader, data: Data) {
62+
guard let connection = connection else {
63+
throw ClientError.notConnected
64+
}
65+
let headerData = try await receive(
66+
length: MemoryLayout<DebugServerMessageHeader>.size,
67+
from: connection
68+
)
69+
let header = headerData.withUnsafeBytes { bytes in
70+
let buffer = bytes.bindMemory(to: UInt32.self)
71+
return DebugServerMessageHeader(
72+
token: buffer[0],
73+
unknown: buffer[1],
74+
length: buffer[2],
75+
unknown2: buffer[3]
76+
)
77+
}
78+
guard header.length > 0 else {
79+
return (header: header, data: Data())
80+
}
81+
let payloadData = try await receive(
82+
length: numericCast(header.length),
83+
from: connection
84+
)
85+
return (header: header, data: payloadData)
86+
}
87+
88+
func disconnect() {
89+
connection?.cancel()
90+
connection = nil
91+
}
92+
93+
private func send(data: Data, on connection: NWConnection) async throws {
94+
return try await withCheckedThrowingContinuation { continuation in
95+
connection.send(content: data, completion: .contentProcessed { error in
96+
if let error {
97+
continuation.resume(throwing: error)
98+
} else {
99+
continuation.resume()
100+
}
101+
})
102+
}
103+
}
104+
105+
private func receive(length: Int, from connection: NWConnection) async throws -> Data {
106+
return try await withCheckedThrowingContinuation { continuation in
107+
connection.receive(minimumIncompleteLength: length, maximumLength: length) { data, _, isComplete, error in
108+
if let error {
109+
continuation.resume(throwing: error)
110+
} else if let data {
111+
continuation.resume(returning: data)
112+
} else {
113+
continuation.resume(throwing: ClientError.noDataReceived)
114+
}
115+
}
116+
}
117+
}
118+
}
119+
120+
enum ClientError: Error {
121+
case invalidURL
122+
case notConnected
123+
case connectionCancelled
124+
case noDataReceived
125+
}
126+
127+
#endif
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// DebugServerTests.swift
3+
// OpenGraphCxxTests
4+
5+
#if canImport(Darwin)
6+
import Foundation
7+
import OpenGraphCxx_Private.DebugServer
8+
import Testing
9+
10+
@MainActor
11+
struct DebugServerTests {
12+
private enum Command: String, CaseIterable, Hashable {
13+
case graphDescription = "graph/description"
14+
case profilerStart = "profiler/start"
15+
case profilerStop = "profiler/stop"
16+
case profilerReset = "profiler/reset"
17+
case profilerMark = "profiler/mark"
18+
}
19+
20+
private func data(for command: Command) throws -> Data{
21+
let command = ["command": command.rawValue]
22+
return try JSONSerialization.data(withJSONObject: command)
23+
}
24+
25+
@Test
26+
func commandTest() async throws {
27+
let debugServer = OG.DebugServer([.valid])
28+
let url = try #require(debugServer.copy_url()) as URL
29+
let components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: false))
30+
let token = try #require(components.queryItems?.first { $0.name == "token" }?.value.flatMap { UInt32($0) })
31+
32+
debugServer.run(3)
33+
let client = DebugClient()
34+
try await client.connect(to: url)
35+
36+
for command in Command.allCases {
37+
if command == .graphDescription {
38+
continue
39+
}
40+
try await client.sendMessage(
41+
token: token,
42+
data: data(for: command)
43+
)
44+
let (_, responseData) = try await client.receiveMessage()
45+
let response = try #require(String(data: responseData, encoding: .utf8))
46+
#expect(response == command.rawValue)
47+
}
48+
}
49+
}
50+
#endif

0 commit comments

Comments
 (0)