diff --git a/Package.resolved b/Package.resolved index 7d25b194..6ff5c87a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "a86de9754b206fa9be8fc4ccd049cb68d283a2df" + "revision" : "c8683703b2ee456ab1d31cdc3983f25705054269" } }, { diff --git a/Sources/OpenGraphCxx/DebugServer/og-debug-server.mm b/Sources/OpenGraphCxx/DebugServer/DebugServer.mm similarity index 59% rename from Sources/OpenGraphCxx/DebugServer/og-debug-server.mm rename to Sources/OpenGraphCxx/DebugServer/DebugServer.mm index efefd8a8..5c8d4200 100644 --- a/Sources/OpenGraphCxx/DebugServer/og-debug-server.mm +++ b/Sources/OpenGraphCxx/DebugServer/DebugServer.mm @@ -1,10 +1,11 @@ // -// og-debug-server.mm +// DebugServer.mm // OpenGraphCxx // -// Audited for 2021 Release +// Audited for 6.5.1 +// Status: Blocked by profile command -#include +#include #if OG_TARGET_OS_DARWIN #include @@ -22,73 +23,55 @@ #include #include -// MARK: DebugServer Implementation +// MARK: DebugServer public API Implementation -OG::DebugServer* _Nullable OG::DebugServer::_shared_server = nullptr; - -OG::DebugServer* _Nullable OG::DebugServer::start(unsigned int mode) { - bool validPort = mode & 1; - if (OG::DebugServer::_shared_server == nullptr - && validPort - /*&& os_variant_has_internal_diagnostics("com.apple.AttributeGraph")*/ - ) { - _shared_server = new DebugServer(mode); - } - return OG::DebugServer::_shared_server; -} - -void OG::DebugServer::stop() { - if (OG::DebugServer::_shared_server == nullptr) { - return; - } - _shared_server->~DebugServer(); - delete _shared_server; - _shared_server = nullptr; -} - -OG::DebugServer::DebugServer(unsigned int mode) { +OG::DebugServer::DebugServer(OGDebugServerMode mode) { sockfd = -1; ip = 0; port = 0; token = arc4random(); source = nullptr; - connections = OG::vector, 0, unsigned long>(); - - // socket + connections = OG::vector, 0, u_long>(); + + // Create socket sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("OGDebugServer: socket"); return; } - - // fcntl - fcntl(sockfd, F_SETFD, O_WRONLY); - + + // Set socket options + fcntl(sockfd, F_SETFD, FD_CLOEXEC); + // setsockopt - const int value = 1; - setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE, &value, 4); - - // bind - sockaddr_in addr = sockaddr_in(); + int value = 1; + setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&value, sizeof(value)); + + // Prepare address structure + sockaddr_in addr; addr.sin_family = AF_INET; - addr.sin_port = 0; - addr.sin_addr.s_addr = ((mode & 2) == 0) ? htonl(INADDR_LOOPBACK) : 0; - socklen_t slen = sizeof(sockaddr_in); - if (bind(sockfd, (const sockaddr *)&addr, slen)) { + addr.sin_port = 0; // Let system assign port + addr.sin_addr.s_addr = (mode & OGDebugServerModeNetworkInterface) ? INADDR_ANY : htonl(INADDR_LOOPBACK); + + // Bind socket + if (bind(sockfd, (const sockaddr *)&addr, sizeof(addr))) { perror("OGDebugServer: bind"); shutdown(); return; } - - // getsockname + + // Get assigned address and port + socklen_t slen = sizeof(addr); if (getsockname(sockfd, (sockaddr *)&addr, &slen)) { perror("OGDebugServer: getsockname"); shutdown(); return; } ip = ntohl(addr.sin_addr.s_addr); - port = ntohl(addr.sin_port) >> 16; - if (mode & 2) { + port = ntohs(addr.sin_port); + + // If network interface mode, find external IP + if (mode & OGDebugServerModeNetworkInterface) { ifaddrs *iaddrs = nullptr; if (getifaddrs(&iaddrs) == 0) { ifaddrs *current_iaddrs = iaddrs; @@ -98,6 +81,7 @@ uint32_t ip_data = ntohl(ifa_addr->sin_addr.s_addr); if (ip_data != INADDR_LOOPBACK) { ip = ip_data; + break; // Take first non-loopback interface } } current_iaddrs = current_iaddrs->ifa_next; @@ -105,26 +89,24 @@ freeifaddrs(iaddrs); } } - + // listen - int max_connection_count = 5; - if (listen(sockfd, max_connection_count)) { + if (listen(sockfd, 5)) { perror("OGDebugServer: listen"); shutdown(); return; } - - // Dispatch + + // Set up dispatch source for incoming connections source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, sockfd, 0, dispatch_get_main_queue()); dispatch_set_context(source, this); dispatch_source_set_event_handler_f(source, &accept_handler); dispatch_resume(source); - - // inet_ntop - char address[32] = {0}; - inet_ntop(AF_INET, (const void *)(&addr.sin_addr.s_addr), address, 32); - - // log + + // Log server information + char address[32]; + uint32_t converted_ip = htonl(ip); + inet_ntop(AF_INET, &converted_ip, address, sizeof(address)); os_log_info(misc_log(), "debug server graph://%s:%d/?token=%u", address, port, token); fprintf(stderr, "debug server graph://%s:%d/?token=%u\n", address, port, token); } @@ -134,109 +116,120 @@ for (auto &connection : connections) { connection.reset(); } - if (connections.data()) { - free(connections.data()); +} + +CFURLRef _Nullable OG::DebugServer::copy_url() const { + if (sockfd < 0) { + return nullptr; } + uint32_t converted_ip = htonl(ip); + char address[32]; + inet_ntop(AF_INET, &converted_ip, address, 32); + char url[100]; + snprintf_l(url, 0x100, nullptr, "graph://%s:%d/?token=%u", address, port, token); + return CFURLCreateWithBytes(NULL, (const UInt8 *)url, strlen(url), kCFStringEncodingUTF8, nullptr); } -// TODO: select will fail here -void OG::DebugServer::run(int timeout) { +// NOTE: Copilot implementation +void OG::DebugServer::run(int timeout) const { bool accepted_connection = false; while (true) { + // Early exit condition check + if (accepted_connection && connections.size() == 0) { + break; + } + + // Initialize the fd_set for write operations fd_set writefds; FD_ZERO(&writefds); - FD_SET(sockfd, &writefds); - timeval tv = timeval { timeout, 0 }; - if (select(sockfd+1, nullptr, &writefds, nullptr, &tv) <= 0) { + // Add the server socket to the fd_set + int server_fd = sockfd; + FD_SET(server_fd, &writefds); + + // Find the maximum file descriptor and add all connection sockets + int max_fd = server_fd; + size_t connection_count = connections.size(); + + if (connection_count > 0) { + auto connection_data = connections.data(); + for (size_t i = 0; i < connection_count; ++i) { + int conn_fd = connection_data[i]->sockfd; + FD_SET(conn_fd, &writefds); + if (max_fd < conn_fd) { + max_fd = conn_fd; + } + } + } + + // Set up timeout + timeval tv = {timeout, 0}; + + // Call select with write file descriptors + int select_result = select(max_fd + 1, nullptr, &writefds, nullptr, &tv); + if (select_result <= 0) { if (errno == EAGAIN) { + // Continue the loop on EAGAIN continue; - } else { - perror("OGDebugServer: select"); - return; } + perror("OGDebugServer: select"); + return; } - } -} -void OG::DebugServer::accept_handler(void *_Nullable context) { - DebugServer *server = (DebugServer *)context; - sockaddr address; - socklen_t address_len = 16; - - int sockfd = accept(server->sockfd, &address, &address_len); - if (sockfd) { - perror("OGDebugServer: accept"); - return; - } - fcntl(server->sockfd, F_SETFD, O_WRONLY); - // FIXME: Link issue about vector - // server->connections.push_back(std::unique_ptr(new Connection(server, sockfd))); -} - -CFURLRef _Nullable OG::DebugServer::copy_url() const { - if (sockfd < 0) { - return nullptr; - } - uint32_t converted_ip = htonl(ip); - char address[32] = {0}; - inet_ntop(AF_INET, &converted_ip, address, 32); - char url[100] = {0}; - snprintf_l(url, 0x100, nullptr, "graph://%s:%d/?token=%u", address, port, token); - return CFURLCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)url, strlen(url), kCFStringEncodingUTF8, nullptr); -} - -void OG::DebugServer::shutdown() { - if (source != nullptr) { - dispatch_source_set_event_handler_f(source, nullptr); - dispatch_set_context(source, nullptr); - source = nullptr; - } - if (sockfd >= 0) { - close(sockfd); - sockfd = -1; - } -} - -// TODO: part implemented -CFDataRef _Nullable OG::DebugServer::receive(Connection *, OGDebugServerMessageHeader &, CFDataRef data) { - @autoreleasepool { - id object = [NSJSONSerialization JSONObjectWithData:(__bridge NSData *)data options:0 error:NULL]; - if (object && [object isKindOfClass:NSDictionary.class]) { - NSDictionary *dic = (NSDictionary *)object; - NSString *command = dic[@"command"]; - if ([command isEqual:@"graph/description"]) { - NSMutableDictionary *mutableDic = [NSMutableDictionary dictionaryWithDictionary:dic]; - mutableDic[(__bridge NSString *)OGDescriptionFormat] = (__bridge NSString *)OGDescriptionFormatDictionary; - CFTypeRef description = OG::Graph::description(nullptr, mutableDic); - if (description) { - NSData *descriptionData = [NSJSONSerialization dataWithJSONObject:(__bridge id)description options:0 error:NULL]; - return (__bridge CFDataRef)descriptionData; + // Check if server socket is ready for new connections + if (FD_ISSET(server_fd, &writefds)) { + accept_handler((void *)this); + accepted_connection = true; + } + + // Process ready connection sockets + if (connections.size() > 0) { + size_t i = 0; + while (i < connections.size()) { + auto connection = connections.data()[i].get(); + int conn_fd = connection->sockfd; + + if (FD_ISSET(conn_fd, &writefds)) { + + // Clear the FD from the set before processing + FD_CLR(conn_fd, &writefds); + + // Handle the connection + Connection::handler(connection); + + // Reset index to 0 after handling a connection + // (connection might have been removed) + i = 0; + } else { + i++; + } + + // Break if no more connections + if (connections.size() == 0) { + break; } - } else if ([command isEqual:@"profiler/start"]) { - // TODO - } else if ([command isEqual:@"profiler/stop"]) { - // TODO - } else if ([command isEqual:@"profiler/reset"]) { - // TODO - } else if ([command isEqual:@"profiler/mark"]) { - // TODO } } - return nullptr; } } +OG::DebugServer* _Nullable OG::DebugServer::start(OGDebugServerMode mode) { + if ( + (mode & OGDebugServerModeValid) + && !OG::DebugServer::has_shared_server() + /*&& os_variant_has_internal_diagnostics("com.apple.AttributeGraph")*/ + ) { + _shared_server = new DebugServer(mode); + } + return OG::DebugServer::_shared_server; +} -void OG::DebugServer::close_connection(OG::DebugServer::Connection *connection) { - auto it = connections.begin(); - for (; it != connections.end(); it++) { - if (it->get() == connection) { - // FIXME: Link issue about vector - // connections.pop_back(); - return; - } +void OG::DebugServer::stop() { + if (!OG::DebugServer::has_shared_server()) { + return; } + delete _shared_server; + _shared_server = nullptr; } // MARK: Blocking operation @@ -340,4 +333,95 @@ bool blocking_write(int descriptor, void *buf, unsigned long count) { return; } +// MARK: DebugServer private API Implementation + +// TODO: part implemented +CFDataRef _Nullable OG::DebugServer::receive(Connection *, OGDebugServerMessageHeader &, CFDataRef data) { + @autoreleasepool { + id object = [NSJSONSerialization JSONObjectWithData:(__bridge NSData *)data options:0 error:NULL]; + if (object && [object isKindOfClass:NSDictionary.class]) { + NSDictionary *dic = (NSDictionary *)object; + NSString *command = dic[@"command"]; + if ([command isEqual:@"graph/description"]) { + NSMutableDictionary *mutableDic = [NSMutableDictionary dictionaryWithDictionary:dic]; + mutableDic[(__bridge NSString *)OGDescriptionFormat] = (__bridge NSString *)OGDescriptionFormatDictionary; + CFTypeRef description = OG::Graph::description(nullptr, mutableDic); + if (description) { + NSData *descriptionData = [NSJSONSerialization dataWithJSONObject:(__bridge id)description options:0 error:NULL]; + return (__bridge CFDataRef)descriptionData; + } + } else if ([command isEqual:@"profiler/start"]) { + // FIXME: Simply return the command str for now + CFStringRef string = CFSTR("profiler/start"); + CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)CFStringGetCStringPtr(string, kCFStringEncodingUTF8), CFStringGetLength(string)); + CFRelease(string); + return data; + } else if ([command isEqual:@"profiler/stop"]) { + // FIXME: Simply return the command str for now + CFStringRef string = CFSTR("profiler/stop"); + CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)CFStringGetCStringPtr(string, kCFStringEncodingUTF8), CFStringGetLength(string)); + CFRelease(string); + return data; + } else if ([command isEqual:@"profiler/reset"]) { + // FIXME: Simply return the command str for now + CFStringRef string = CFSTR("profiler/reset"); + CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)CFStringGetCStringPtr(string, kCFStringEncodingUTF8), CFStringGetLength(string)); + CFRelease(string); + return data; + } else if ([command isEqual:@"profiler/mark"]) { + // FIXME: Simply return the command str for now + CFStringRef string = CFSTR("profiler/mark"); + CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)CFStringGetCStringPtr(string, kCFStringEncodingUTF8), CFStringGetLength(string)); + CFRelease(string); + return data; + } + } + return nullptr; + } +} + +void OG::DebugServer::close_connection(OG::DebugServer::Connection *target) { + auto size = connections.size(); + if (size == 0) { + return; + } + auto data = connections.data(); + for (size_t i = 0; i < size; ++i) { + auto conn = data[i].get(); + if (conn == target) { + // FIXME + data[i] = std::move(data[size - 1]); + connections.pop_back(); + return; + } + } +} + +void OG::DebugServer::shutdown() { + if (source != nullptr) { + dispatch_source_set_event_handler_f(source, nullptr); + dispatch_set_context(source, nullptr); + source = nullptr; + } + if (sockfd >= 0) { + close(sockfd); + sockfd = -1; + } +} + +void OG::DebugServer::accept_handler(void *_Nullable context) { + DebugServer *server = (DebugServer *)context; + sockaddr address; + socklen_t address_len = 16; + int sockfd = accept(server->sockfd, &address, &address_len); + if (sockfd < 0) { + perror("OGDebugServer: accept"); + return; + } + fcntl(server->sockfd, F_SETFD, O_WRONLY); + server->connections.push_back(std::unique_ptr(new Connection(server, sockfd))); +} + +OG::DebugServer* _Nullable OG::DebugServer::_shared_server = nullptr; + #endif /* OG_TARGET_OS_DARWIN */ diff --git a/Sources/OpenGraphCxx/DebugServer/OGDebugServer.cpp b/Sources/OpenGraphCxx/DebugServer/OGDebugServer.cpp index 080e45dd..282104b0 100644 --- a/Sources/OpenGraphCxx/DebugServer/OGDebugServer.cpp +++ b/Sources/OpenGraphCxx/DebugServer/OGDebugServer.cpp @@ -1,16 +1,19 @@ // // OGDebugServer.cpp // OpenGraphCxx +// +// Audited for 6.5.1 +// Status: Complete #include -#include +#include #if OG_TARGET_OS_DARWIN // MARK: - Exported C functions -OGDebugServer _Nullable OGDebugServerStart(unsigned int port) { - return (OGDebugServer)OG::DebugServer::start(port); +OGDebugServerRef _Nullable OGDebugServerStart(OGDebugServerMode port) { + return (OGDebugServerRef)OG::DebugServer::start(port); } void OGDebugServerStop() { @@ -18,17 +21,17 @@ void OGDebugServerStop() { } CFURLRef _Nullable OGDebugServerCopyURL() { - if (OG::DebugServer::_shared_server == nullptr) { + if (!OG::DebugServer::has_shared_server()) { return nullptr; } - return OG::DebugServer::_shared_server->copy_url(); + return OG::DebugServer::shared_server()->copy_url(); } void OGDebugServerRun(int timeout) { - if (OG::DebugServer::_shared_server == nullptr) { + if (!OG::DebugServer::has_shared_server()) { return; } - OG::DebugServer::_shared_server->run(timeout); + OG::DebugServer::shared_server()->run(timeout); } #endif diff --git a/Sources/OpenGraphCxx/include/OpenGraph/OGDebugServer.h b/Sources/OpenGraphCxx/include/OpenGraph/OGDebugServer.h index d44eae79..7d9945f8 100644 --- a/Sources/OpenGraphCxx/include/OpenGraph/OGDebugServer.h +++ b/Sources/OpenGraphCxx/include/OpenGraph/OGDebugServer.h @@ -1,6 +1,9 @@ // // OGDebugServer.h // OpenGraphCxx +// +// Audited for 6.5.1 +// Status: Complete #ifndef OGDebugServer_h #define OGDebugServer_h @@ -9,29 +12,114 @@ #if OG_TARGET_OS_DARWIN +/** + * @header OGDebugServer.h + * @abstract OpenGraph Debug Server API for runtime debugging and inspection. + * @discussion The debug server provides runtime debugging capabilities for OpenGraph applications, + * allowing external tools to connect and inspect graph state, dependencies, and execution. + * This API is only available on Darwin platforms and requires network interface access + * when used in network mode. + */ + OG_ASSUME_NONNULL_BEGIN OG_IMPLICIT_BRIDGING_ENABLED -typedef struct OGDebugServerStorage OGDebugServerStorage; - -typedef const OGDebugServerStorage *OGDebugServer OG_SWIFT_STRUCT; +/** + * @typedef OGDebugServerRef + * @abstract An opaque reference to a debug server instance. + * @discussion The debug server manages a connection endpoint that external debugging tools + * can use to inspect OpenGraph runtime state. Only one debug server instance + * can be active at a time. + */ +typedef struct OGDebugServerStorage *OGDebugServerRef OG_SWIFT_STRUCT OG_SWIFT_NAME(DebugServer); + +/** + * @typedef OGDebugServerMode + * @abstract Configuration modes for the debug server. + * @discussion These flags control how the debug server operates and what interfaces it exposes. + * Multiple modes can be combined using bitwise OR operations. + */ +typedef OG_OPTIONS(uint32_t, OGDebugServerMode) { + /** + * @abstract No debug server functionality. + * @discussion Use this mode to disable all debug server operations. + */ + OGDebugServerModeNone = 0, + + /** + * @abstract Enable basic debug server validation and setup. + * @discussion This mode enables the debug server with minimal functionality and is required + * for any debug server operation. All other modes must be combined with this flag. + */ + OGDebugServerModeValid = 1 << 0, + + /** + * @abstract Enable network interface for remote debugging. + * @discussion When enabled, the debug server will listen on a network interface, allowing + * remote debugging tools to connect. Requires OGDebugServerModeValid to be set. + */ + OGDebugServerModeNetworkInterface = 1 << 1, +} OG_SWIFT_NAME(OGDebugServerRef.Mode); // MARK: - Exported C functions OG_EXTERN_C_BEGIN +/** + * @function OGDebugServerStart + * @abstract Starts the shared debug server with the specified mode. + * @discussion Creates and starts a new shared debug server instance. If a server is already + * running, this function will return the existing instance. + * + * The returned reference should not be manually managed. Use OGDebugServerStop() + * to properly shut down the shared server. + * @param mode Configuration flags controlling server behavior. + * Must include OGDebugServerModeValid for basic operation. + * @result A reference to the started shared debug server, or NULL if the server + * could not be started (e.g., due to network permissions, conflicts, or existing server). + */ OG_EXPORT -OGDebugServer _Nullable OGDebugServerStart(unsigned int mode) OG_SWIFT_NAME(OGDebugServer.start(mode:)); - +OG_REFINED_FOR_SWIFT +OGDebugServerRef _Nullable OGDebugServerStart(OGDebugServerMode mode) OG_SWIFT_NAME(OGDebugServerRef.start(mode:)); + +/** + * @function OGDebugServerStop + * @abstract Stops and deletes the running shared debug server. + * @discussion Shuts down the active shared debug server instance and cleans up all associated + * resources. If no shared debug server is currently running, this function has no effect. + * + * This function should be called before application termination to ensure + * proper cleanup of network resources and connections. + */ OG_EXPORT -void OGDebugServerStop(void) OG_SWIFT_NAME(OGDebugServer.stop()); - +OG_REFINED_FOR_SWIFT +void OGDebugServerStop(void) OG_SWIFT_NAME(OGDebugServerRef.stop()); + +/** + * @function OGDebugServerCopyURL + * @abstract Returns the URL for connecting to the shared debug server. + * @discussion Returns the URL that external debugging tools should use to connect to + * the currently running shared debug server. The URL format depends on the server + * configuration and may be a local or network address. + * + * The returned URL is only valid while the shared debug server is running. + * It may change if the server is restarted. + * @result A CFURLRef containing the connection URL, or NULL if no shared debug server + * is currently running or if the server doesn't expose a connectable interface. + * The caller is responsible for releasing the returned URL. + */ OG_EXPORT -CFURLRef _Nullable OGDebugServerCopyURL(void) OG_SWIFT_NAME(OGDebugServer.copyURL()); +OG_REFINED_FOR_SWIFT +CFURLRef _Nullable OGDebugServerCopyURL(void) OG_SWIFT_NAME(OGDebugServerRef.copyURL()); +/** + * @function OGDebugServerRun + * @abstract Runs the shared debug server event loop. + */ OG_EXPORT -void OGDebugServerRun(int timeout) OG_SWIFT_NAME(OGDebugServer.run(timeout:)); +OG_REFINED_FOR_SWIFT +void OGDebugServerRun(int timeout) OG_SWIFT_NAME(OGDebugServerRef.run(timeout:)); OG_EXTERN_C_END diff --git a/Sources/OpenGraphCxx/include/OpenGraphCxx/DebugServer/DebugServer.hpp b/Sources/OpenGraphCxx/include/OpenGraphCxx/DebugServer/DebugServer.hpp new file mode 100644 index 00000000..c6c7d9a4 --- /dev/null +++ b/Sources/OpenGraphCxx/include/OpenGraphCxx/DebugServer/DebugServer.hpp @@ -0,0 +1,167 @@ +// +// DebugServer.hpp +// OpenGraphCxx +// +// Audited for 6.5.1 +// Status: Complete + +#ifndef OPENGRAPH_CXX_DEBUGSERVER_DEBUGSERVER_HPP +#define OPENGRAPH_CXX_DEBUGSERVER_DEBUGSERVER_HPP + +#include +#if OG_TARGET_OS_DARWIN +#include +#include +#include +#include + +OG_ASSUME_NONNULL_BEGIN + +OG_IMPLICIT_BRIDGING_ENABLED + +/// Message header structure for debug server communication protocol. +/// Contains metadata for messages exchanged between the debug server and clients. +typedef struct OG_SWIFT_NAME(DebugServerMessageHeader) OGDebugServerMessageHeader { + /// Authentication token for the message + uint32_t token; + /// Reserved field for future use + uint32_t unknown; + /// Length of the message payload in bytes + uint32_t length; + /// Additional reserved field for protocol extensions + uint32_t unknown2; +} OGDebugServerMessageHeader; /* OGDebugServerMessageHeader */ + +namespace OG { + +/// Debug server for OpenGraph runtime inspection and debugging. +/// +/// The DebugServer provides a network interface for external tools to connect +/// and inspect the internal state of OpenGraph. It supports both listening for +/// incoming connections and handling client requests for graph data. +/// +/// The server operates in different modes as specified by OGDebugServerMode +/// and manages multiple concurrent client connections using GCD dispatch sources. +class DebugServer { +public: + /// Creates a new debug server instance with the specified mode. + /// + /// @param mode The operating mode for the debug server + DebugServer(OGDebugServerMode mode); + + /// Destroys the debug server and cleans up all resources. + /// Automatically closes all active connections and stops the server. + ~DebugServer(); + + /// Returns a copy of the URL where the debug server is accessible. + /// + /// @return A CFURLRef containing the server URL, or nullptr if not running. + /// The caller is responsible for releasing the returned URL. + CFURLRef _Nullable copy_url() const; + + /// Shuts down the debug server and closes all connections. + /// Called internally during destruction or explicit stop. + void shutdown(); + + /// Runs the debug server for the specified timeout duration. + /// + /// @param timeout Maximum time in seconds to run the server. + /// Use -1 for indefinite operation. + void run(int timeout) const; + + /// Returns the shared debug server instance. + /// + /// @return Pointer to the shared server, or nullptr if none exists + static OG_INLINE DebugServer *shared_server() { return _shared_server; } + + /// Checks if a shared debug server instance exists. + /// + /// @return true if a shared server is active, false otherwise + static OG_INLINE bool has_shared_server() { return _shared_server != nullptr; } + + /// Starts a new shared debug server with the specified mode. + /// + /// @param mode Configuration flags controlling server behavior. + /// Must include OGDebugServerModeValid for basic operation. + /// @return Pointer to the started server, or nullptr if startup failed + static DebugServer *_Nullable start(OGDebugServerMode mode); + + /// Stops the shared debug server and releases all resources. + /// This will close all client connections and shut down the server. + static void stop(); + +private: + /// Represents a single client connection to the debug server. + /// + /// Each Connection manages its own socket descriptor and dispatch source + /// for handling incoming data from a connected client. Connections are + /// automatically cleaned up when the client disconnects. + class Connection { + public: + /// Creates a new connection for the specified server and socket. + /// + /// @param server The debug server that owns this connection + /// @param descriptor The socket file descriptor for the client + Connection(DebugServer *server,int descriptor); + + /// Destroys the connection and cleans up resources. + /// Automatically closes the socket and cancels the dispatch source. + ~Connection(); + + /// Static handler function for processing incoming connection data. + /// + /// @param context Pointer to the Connection instance + static void handler(void *_Nullable context); + + friend class DebugServer; + private: + DebugServer *server; ///< Owning debug server + int sockfd; ///< Client socket file descriptor + dispatch_source_t source; ///< GCD source for socket events + }; /* Connection */ + + /// Receives and processes a message from the specified connection. + /// + /// @param connection The client connection to receive from + /// @param header Reference to store the received message header + /// @param data Existing data buffer to append to, or nullptr for new data + /// @return CFDataRef containing the complete message, or nullptr on error + CFDataRef _Nullable receive(Connection *connection, OGDebugServerMessageHeader &header, CFDataRef data); + + /// Closes and removes the specified client connection. + /// + /// @param connection The connection to close and clean up + void close_connection(Connection *connection); + + /// Static handler for accepting new client connections. + /// + /// @param context Pointer to the DebugServer instance + static void accept_handler(void *_Nullable context); + + int32_t sockfd; ///< Server socket file descriptor + uint32_t ip; ///< Server IP address + uint32_t port; ///< Server port number + uint32_t token; ///< Authentication token + _Nullable dispatch_source_t source; ///< GCD source for accepting connections + OG::vector, 0, u_long> connections; ///< Active client connections + + static DebugServer *_Nullable _shared_server; ///< Global shared server instance +}; /* DebugServer */ + +} /* OG */ + +/// C-compatible storage wrapper for DebugServer instances. +/// +/// This structure provides a C-compatible interface for storing +/// DebugServer objects in contexts where C++ objects cannot be +/// used directly. +typedef struct OGDebugServerStorage { + OG::DebugServer debugServer; ///< The wrapped DebugServer instance +} OGDebugServerStorage; + +OG_IMPLICIT_BRIDGING_DISABLED + +OG_ASSUME_NONNULL_END + +#endif /* OG_TARGET_OS_DARWIN */ +#endif /* OPENGRAPH_CXX_DEBUGSERVER_DEBUGSERVER_HPP */ diff --git a/Sources/OpenGraphCxx/include/OpenGraphCxx/DebugServer/og-debug-server.hpp b/Sources/OpenGraphCxx/include/OpenGraphCxx/DebugServer/og-debug-server.hpp deleted file mode 100644 index 2ff5b8cd..00000000 --- a/Sources/OpenGraphCxx/include/OpenGraphCxx/DebugServer/og-debug-server.hpp +++ /dev/null @@ -1,71 +0,0 @@ -// -// og-debug-server.hpp -// OpenGraphCxx -// -// Audited for 2021 Release - -#ifndef og_debug_server_hpp -#define og_debug_server_hpp - -#include -#if OG_TARGET_OS_DARWIN -#include -#include -#include -#include - -OG_ASSUME_NONNULL_BEGIN - -OG_IMPLICIT_BRIDGING_ENABLED - -namespace OG { -struct OGDebugServerMessageHeader { - uint32_t token; - uint32_t unknown; - uint32_t length; - uint32_t unknown2; -}; -class DebugServer { - class Connection { - private: - DebugServer *server; - int sockfd; - dispatch_source_t source; - public: - Connection(DebugServer *server,int descriptor); - ~Connection(); - static void handler(void *_Nullable context); - friend class DebugServer; - }; -private: - int32_t sockfd; - uint32_t ip; - uint32_t port; - uint32_t token; - _Nullable dispatch_source_t source; - OG::vector, 0, unsigned long> connections; -public: - static DebugServer *_Nullable _shared_server; - static DebugServer *_Nullable start(unsigned int port); - static void stop(); - DebugServer(unsigned int port); - ~DebugServer(); - void run(int descriptor); - static void accept_handler(void *_Nullable context); - CFURLRef _Nullable copy_url() const; - void shutdown(); - CFDataRef _Nullable receive(Connection *connection, OGDebugServerMessageHeader &header, CFDataRef data); - void close_connection(Connection *connection); -}; -} /* OG */ - -typedef struct OGDebugServerStorage { - OG::DebugServer debugServer; -} OGDebugServerStorage; - -OG_IMPLICIT_BRIDGING_DISABLED - -OG_ASSUME_NONNULL_END - -#endif /* OG_TARGET_OS_DARWIN */ -#endif /* og_debug_server_hpp */ diff --git a/Sources/OpenGraphCxx/include/OpenGraphCxx/Vector/vector.hpp b/Sources/OpenGraphCxx/include/OpenGraphCxx/Vector/vector.hpp index 8ac8d50c..0f99f3c2 100644 --- a/Sources/OpenGraphCxx/include/OpenGraphCxx/Vector/vector.hpp +++ b/Sources/OpenGraphCxx/include/OpenGraphCxx/Vector/vector.hpp @@ -224,8 +224,8 @@ class vector, 0, size_type> { void clear(); void push_back(const std::unique_ptr &value) = delete; - void push_back(std::unique_ptr &&value); - void pop_back(); + OG_INLINE void push_back(std::unique_ptr &&value); + OG_INLINE void pop_back(); void resize(size_type count); void resize(size_type count, const value_type &value); diff --git a/Sources/OpenGraphCxx/include/OpenGraphCxx/Vector/vector.tpp b/Sources/OpenGraphCxx/include/OpenGraphCxx/Vector/vector.tpp index 977c3dbb..98446f40 100644 --- a/Sources/OpenGraphCxx/include/OpenGraphCxx/Vector/vector.tpp +++ b/Sources/OpenGraphCxx/include/OpenGraphCxx/Vector/vector.tpp @@ -1,5 +1,5 @@ // -// vector.hpp +// vector.tpp // OpenGraphCxx // // Status: Complete @@ -17,7 +17,6 @@ #include #include - namespace OG { #pragma mark - Base implementation @@ -63,7 +62,7 @@ void *realloc_vector(void *buffer, void *stack_buffer, size_type stack_size, siz return new_buffer; } -} // namespace details +} /* namespace details */ template requires std::unsigned_integral @@ -199,7 +198,7 @@ void *realloc_vector(void *buffer, size_type *size, size_type preferred_new_size return new_buffer; } -} // namespace details +} /* namespace details */ template requires std::unsigned_integral @@ -308,20 +307,89 @@ void vector::resize(size_type count, const value_type &value) { template requires std::unsigned_integral vector, 0, size_type>::~vector() { - for (auto i = 0; i < _size; i++) { + clear(); + if (_buffer) { + free(_buffer); + _buffer = nullptr; + } +} + +template + requires std::unsigned_integral +void vector, 0, size_type>::clear() { + for (size_type i = 0; i < _size; ++i) { _buffer[i].reset(); + _buffer[i].~unique_ptr(); + } + _size = 0; +} + +template + requires std::unsigned_integral +void vector, 0, size_type>::reserve_slow(size_type new_cap) { + if (new_cap <= _capacity) { + return; + } + + size_type actual_new_cap = std::max(new_cap, _capacity * 2); + std::unique_ptr *new_buffer = (std::unique_ptr *)malloc(actual_new_cap * sizeof(std::unique_ptr)); + if (!new_buffer) { + return; + } + + for (size_type i = 0; i < _size; ++i) { + new (&new_buffer[i]) std::unique_ptr(std::move(_buffer[i])); + _buffer[i].~unique_ptr(); } + if (_buffer) { free(_buffer); } + + _buffer = new_buffer; + _capacity = actual_new_cap; +} + +template + requires std::unsigned_integral +void vector, 0, size_type>::reserve(size_type new_cap) { + if (new_cap > _capacity) { + reserve_slow(new_cap); + } } template requires std::unsigned_integral -void vector, 0, size_type>::push_back(std::unique_ptr &&value) { +OG_INLINE void vector, 0, size_type>::push_back(std::unique_ptr &&value) { reserve(_size + 1); - new (&_buffer[_size]) value_type(std::move(value)); - _size += 1; + new (&_buffer[_size]) std::unique_ptr(std::move(value)); + ++_size; +} + +template + requires std::unsigned_integral +OG_INLINE void vector, 0, size_type>::pop_back() { + assert(_size > 0); + _buffer[_size - 1].reset(); + _buffer[_size - 1].~unique_ptr(); + --_size; +} + +template + requires std::unsigned_integral +void vector, 0, size_type>::resize(size_type count) { + reserve(count); + if (count < _size) { + for (auto i = count; i < _size; i++) { + _buffer[i].reset(); + _buffer[i].~unique_ptr(); + } + } else if (count > _size) { + for (auto i = _size; i < count; i++) { + new (&_buffer[i]) std::unique_ptr(); + } + } + _size = count; } -} // /* OG */ +} /* OG */ diff --git a/Sources/OpenGraphShims/GraphShims.swift b/Sources/OpenGraphShims/GraphShims.swift index 4a6e0cfc..f1fa621e 100644 --- a/Sources/OpenGraphShims/GraphShims.swift +++ b/Sources/OpenGraphShims/GraphShims.swift @@ -11,7 +11,6 @@ public typealias OGAttributeTypeFlags = AGAttributeTypeFlags public typealias OGCachedValueOptions = AGCachedValueOptions public typealias OGChangedValueFlags = AGChangedValueFlags public typealias OGCounterQueryType = AGCounterQueryType -public typealias OGDebugServer = AGDebugServer public typealias OGInputOptions = AGInputOptions public typealias OGSearchOptions = AGSearchOptions public typealias OGTypeApplyOptions = AGTypeApplyOptions diff --git a/Tests/OpenGraphCompatibilityTests/Debug/DebugServerTests.swift b/Tests/OpenGraphCompatibilityTests/Debug/DebugServerTests.swift index c5c3a0b0..188aec04 100644 --- a/Tests/OpenGraphCompatibilityTests/Debug/DebugServerTests.swift +++ b/Tests/OpenGraphCompatibilityTests/Debug/DebugServerTests.swift @@ -11,10 +11,11 @@ import Testing struct DebugServerTests { @Test func testMode0() { - #expect(OGDebugServer.start(mode: 0) == nil) - #expect(OGDebugServer.copyURL() == nil) + #expect(DebugServer.start(mode: []) == nil) + #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. @@ -23,12 +24,27 @@ struct DebugServerTests { .disabled(if: compatibilityTestEnabled, "Skip on AG due to internal_diagnostics check"), ) func testMode1() throws { - let _ = try #require(OGDebugServer.start(mode: 1)) - let url = try #require(OGDebugServer.copyURL()) - let urlString = (url as URL).absoluteString - #expect(urlString.hasPrefix("graph://")) - OGDebugServer.run(timeout: 1) - OGDebugServer.stop() + let _ = try #require(DebugServer.start(mode: [.valid])) + let url = try #require(DebugServer.copyURL()) as URL + #expect(url.scheme == "graph") + let host = try #require(url.host) + #expect(host == "127.0.0.1") + DebugServer.run(timeout: 1) + DebugServer.stop() + } + + @Test( + .disabled(if: compatibilityTestEnabled, "Skip on AG due to internal_diagnostics check"), + ) + func testMode3() throws { + let _ = try #require(DebugServer.start(mode: [.valid, .networkInterface])) + let url = try #require(DebugServer.copyURL()) as URL + #expect(url.scheme == "graph") + let host = try #require(url.host) + #expect(host != "127.0.0.1") + #expect(host.hasPrefix("192.168")) + DebugServer.run(timeout: 1) + DebugServer.stop() } } #endif diff --git a/Tests/OpenGraphCompatibilityTests/GraphShims.swift b/Tests/OpenGraphCompatibilityTests/GraphShims.swift index 67c6b86f..d05d3cf3 100644 --- a/Tests/OpenGraphCompatibilityTests/GraphShims.swift +++ b/Tests/OpenGraphCompatibilityTests/GraphShims.swift @@ -11,7 +11,6 @@ public typealias OGAttributeTypeFlags = AGAttributeTypeFlags public typealias OGCachedValueOptions = AGCachedValueOptions public typealias OGChangedValueFlags = AGChangedValueFlags public typealias OGCounterQueryType = AGCounterQueryType -public typealias OGDebugServer = AGDebugServer public typealias OGInputOptions = AGInputOptions public typealias OGSearchOptions = AGSearchOptions public typealias OGTypeApplyOptions = AGTypeApplyOptions diff --git a/Tests/OpenGraphCxxTests/DebugServer/DebugClient.swift b/Tests/OpenGraphCxxTests/DebugServer/DebugClient.swift new file mode 100644 index 00000000..14715170 --- /dev/null +++ b/Tests/OpenGraphCxxTests/DebugServer/DebugClient.swift @@ -0,0 +1,127 @@ +// +// 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/DebugServerTests.swift b/Tests/OpenGraphCxxTests/DebugServer/DebugServerTests.swift new file mode 100644 index 00000000..db9b51fc --- /dev/null +++ b/Tests/OpenGraphCxxTests/DebugServer/DebugServerTests.swift @@ -0,0 +1,49 @@ +// +// DebugServerTests.swift +// OpenGraphCxxTests + +#if canImport(Darwin) +import Foundation +import OpenGraphCxx_Private.DebugServer +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" + } + + private func data(for command: Command) throws -> Data{ + let command = ["command": command.rawValue] + return try JSONSerialization.data(withJSONObject: command) + } + + @Test + func commandTest() async throws { + let debugServer = OG.DebugServer([.valid]) + let url = try #require(debugServer.copy_url()) 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 + } + 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) + } + } +} +#endif