From 9ef13a0b8befd0b81755acadbf3ab5deca363e47 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 28 Apr 2025 18:34:51 +0000 Subject: [PATCH] Enhance debugger with FLEX integration and advanced debugging capabilities --- iOS/Debugger/Core/AppDelegate+Debugger.swift | 81 ++++ iOS/Debugger/Core/DebuggerManager.swift | 107 ++++- iOS/Debugger/Core/FLEXIntegration.swift | 192 ++++++++ iOS/Debugger/Core/NetworkMonitor.swift | 317 ++++++++++++++ iOS/Debugger/Core/RuntimeInspector.swift | 409 ++++++++++++++++++ .../Core/UIViewController+Debugger.swift | 111 +++++ iOS/Debugger/UI/DebuggerViewController.swift | 81 +++- .../UI/FileBrowserViewController.swift | 354 +++++++++++++++ iOS/Debugger/UI/FloatingDebuggerButton.swift | 98 ++++- .../UI/NetworkMonitorViewController.swift | 242 ++++++++++- .../UI/RuntimeBrowserViewController.swift | 364 ++++++++++++++++ iOS/Debugger/UI/SystemLogViewController.swift | 401 +++++++++++++++++ iOS/Debugger/UI/VariablesViewController.swift | 88 ++++ 13 files changed, 2835 insertions(+), 10 deletions(-) create mode 100644 iOS/Debugger/Core/FLEXIntegration.swift create mode 100644 iOS/Debugger/Core/NetworkMonitor.swift create mode 100644 iOS/Debugger/Core/RuntimeInspector.swift create mode 100644 iOS/Debugger/Core/UIViewController+Debugger.swift create mode 100644 iOS/Debugger/UI/FileBrowserViewController.swift create mode 100644 iOS/Debugger/UI/RuntimeBrowserViewController.swift create mode 100644 iOS/Debugger/UI/SystemLogViewController.swift diff --git a/iOS/Debugger/Core/AppDelegate+Debugger.swift b/iOS/Debugger/Core/AppDelegate+Debugger.swift index 9885079..3c5451b 100644 --- a/iOS/Debugger/Core/AppDelegate+Debugger.swift +++ b/iOS/Debugger/Core/AppDelegate+Debugger.swift @@ -1,4 +1,5 @@ import UIKit +import ObjectiveC /// Extension to AppDelegate for initializing the debugger @@ -7,8 +8,88 @@ import UIKit func initializeDebugger() { // Initialize the debugger manager DebuggerManager.shared.initialize() + + // Enable network monitoring + NetworkMonitor.shared.enable() + + // Set up method swizzling for view controller lifecycle + swizzleViewControllerLifecycle() // Log initialization Debug.shared.log(message: "Debugger initialized", type: .info) } + + /// Swizzle view controller lifecycle methods for debugging + private func swizzleViewControllerLifecycle() { + // Swizzle viewDidLoad + swizzleMethod( + originalClass: UIViewController.self, + originalSelector: #selector(UIViewController.viewDidLoad), + swizzledClass: UIViewController.self, + swizzledSelector: #selector(UIViewController.debugger_viewDidLoad) + ) + + // Swizzle viewWillAppear + swizzleMethod( + originalClass: UIViewController.self, + originalSelector: #selector(UIViewController.viewWillAppear(_:)), + swizzledClass: UIViewController.self, + swizzledSelector: #selector(UIViewController.debugger_viewWillAppear(_:)) + ) + + // Swizzle viewDidAppear + swizzleMethod( + originalClass: UIViewController.self, + originalSelector: #selector(UIViewController.viewDidAppear(_:)), + swizzledClass: UIViewController.self, + swizzledSelector: #selector(UIViewController.debugger_viewDidAppear(_:)) + ) + + // Swizzle viewWillDisappear + swizzleMethod( + originalClass: UIViewController.self, + originalSelector: #selector(UIViewController.viewWillDisappear(_:)), + swizzledClass: UIViewController.self, + swizzledSelector: #selector(UIViewController.debugger_viewWillDisappear(_:)) + ) + + // Swizzle viewDidDisappear + swizzleMethod( + originalClass: UIViewController.self, + originalSelector: #selector(UIViewController.viewDidDisappear(_:)), + swizzledClass: UIViewController.self, + swizzledSelector: #selector(UIViewController.debugger_viewDidDisappear(_:)) + ) + } + + /// Swizzle a method + private func swizzleMethod( + originalClass: AnyClass, + originalSelector: Selector, + swizzledClass: AnyClass, + swizzledSelector: Selector + ) { + guard let originalMethod = class_getInstanceMethod(originalClass, originalSelector), + let swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector) else { + return + } + + let didAddMethod = class_addMethod( + originalClass, + swizzledSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod) + ) + + if didAddMethod { + class_replaceMethod( + originalClass, + swizzledSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod) + ) + } else { + method_exchangeImplementations(originalMethod, swizzledMethod) + } + } } \ No newline at end of file diff --git a/iOS/Debugger/Core/DebuggerManager.swift b/iOS/Debugger/Core/DebuggerManager.swift index 2228dc6..a57ea09 100644 --- a/iOS/Debugger/Core/DebuggerManager.swift +++ b/iOS/Debugger/Core/DebuggerManager.swift @@ -1,4 +1,5 @@ import UIKit +import ObjectiveC /// Manager class for the runtime debugger /// Handles the floating button and debugger UI @@ -18,6 +19,9 @@ public final class DebuggerManager { /// The debugger engine private let debuggerEngine = DebuggerEngine.shared + + /// FLEX integration + private let flexIntegration = FLEXIntegration.shared /// Current debugger view controller private weak var debuggerViewController: DebuggerViewController? @@ -36,6 +40,27 @@ public final class DebuggerManager { get { stateQueue.sync { _isSetUp } } set { stateQueue.sync { _isSetUp = newValue } } } + + /// Debug mode - determines which UI to show + public enum DebugMode { + case standard // Our custom debugger UI + case flex // FLEX explorer UI + } + + /// Current debug mode + private var _debugMode: DebugMode = .standard + public var debugMode: DebugMode { + get { stateQueue.sync { _debugMode } } + set { + stateQueue.sync { _debugMode = newValue } + // Update UI based on mode change + if newValue == .flex { + logger.log(message: "Switching to FLEX debug mode", type: .info) + } else { + logger.log(message: "Switching to standard debug mode", type: .info) + } + } + } /// Weak references to parent views private weak var parentViewController: UIViewController? @@ -62,6 +87,13 @@ public final class DebuggerManager { /// Show the debugger UI public func showDebugger() { + // If in FLEX mode, show FLEX explorer + if debugMode == .flex { + flexIntegration.showExplorer() + return + } + + // Otherwise show our standard debugger guard !isDebuggerVisible else { return } DispatchQueue.main.async { [weak self] in @@ -111,6 +143,13 @@ public final class DebuggerManager { /// Hide the debugger UI public func hideDebugger() { + // If in FLEX mode, hide FLEX explorer + if debugMode == .flex { + flexIntegration.hideExplorer() + return + } + + // Otherwise hide our standard debugger guard isDebuggerVisible, let debuggerVC = debuggerViewController else { return } DispatchQueue.main.async { @@ -120,6 +159,19 @@ public final class DebuggerManager { } } } + + /// Toggle the debugger UI + public func toggleDebugger() { + if debugMode == .flex { + flexIntegration.toggleExplorer() + } else { + if isDebuggerVisible { + hideDebugger() + } else { + showDebugger() + } + } + } /// Show the floating button public func showFloatingButton() { @@ -137,6 +189,9 @@ public final class DebuggerManager { // Add to the top view controller's view topVC.view.addSubview(self.floatingButton) + + // Configure the button based on the current debug mode + self.floatingButton.updateAppearance(forMode: self.debugMode) self.logger.log(message: "Floating debugger button added", type: .info) } @@ -149,6 +204,42 @@ public final class DebuggerManager { self?.logger.log(message: "Floating debugger button removed", type: .info) } } + + /// Switch between debug modes + public func switchDebugMode(_ mode: DebugMode) { + // If we're already in this mode, do nothing + if debugMode == mode { + return + } + + // Hide current debugger UI + if debugMode == .flex { + flexIntegration.hideExplorer() + } else if isDebuggerVisible { + hideDebugger() + } + + // Set new mode + debugMode = mode + + // Update floating button appearance + floatingButton.updateAppearance(forMode: mode) + } + + /// Present an object explorer for the given object + /// - Parameter object: The object to explore + public func presentObjectExplorer(_ object: Any) { + if debugMode == .flex { + flexIntegration.presentObjectExplorer(object) + } else { + // Use our own object explorer or show variables view + showDebugger() + // Select the variables tab and focus on this object + if let debuggerVC = debuggerViewController { + debuggerVC.showVariablesTab(withObject: object) + } + } + } // MARK: - Private Methods @@ -160,6 +251,14 @@ public final class DebuggerManager { name: .showDebugger, object: nil ) + + // Listen for mode switch + NotificationCenter.default.addObserver( + self, + selector: #selector(handleSwitchDebugMode), + name: .switchDebugMode, + object: nil + ) // Listen for show/hide button notifications NotificationCenter.default.addObserver( @@ -203,6 +302,12 @@ public final class DebuggerManager { @objc private func handleShowDebugger() { showDebugger() } + + @objc private func handleSwitchDebugMode(_ notification: Notification) { + if let mode = notification.object as? DebugMode { + switchDebugMode(mode) + } + } @objc private func handleShowFloatingButton() { showFloatingButton() @@ -223,7 +328,7 @@ public final class DebuggerManager { @objc private func handleAppDidBecomeActive() { // Show the floating button when app becomes active - if !isDebuggerVisible { + if !isDebuggerVisible && debugMode == .standard { DispatchQueue.main.async { [weak self] in self?.showFloatingButton() } diff --git a/iOS/Debugger/Core/FLEXIntegration.swift b/iOS/Debugger/Core/FLEXIntegration.swift new file mode 100644 index 0000000..bef6dd4 --- /dev/null +++ b/iOS/Debugger/Core/FLEXIntegration.swift @@ -0,0 +1,192 @@ +import UIKit + +/// Bridge class to integrate FLEX functionality into our debugger +/// This class provides a wrapper around FLEX's functionality to make it compatible with our debugger +public final class FLEXIntegration { + // MARK: - Singleton + + /// Shared instance of the FLEX integration + public static let shared = FLEXIntegration() + + // MARK: - Properties + + /// Logger for debugger operations + private let logger = Debug.shared + + /// Flag indicating whether FLEX is available + private var isFLEXAvailable: Bool = false + + /// Dynamic reference to FLEXManager to avoid compile-time dependency + private var flexManager: AnyObject? + + // MARK: - Initialization + + private init() { + setupFLEX() + } + + // MARK: - Setup + + /// Set up FLEX integration + private func setupFLEX() { + // Check if FLEX is available at runtime + if let flexManagerClass = NSClassFromString("FLEXManager") { + // Get the shared manager using runtime invocation + if let sharedManagerMethod = class_getClassMethod(flexManagerClass, NSSelectorFromString("sharedManager")), + let sharedManager = objc_msgSend(flexManagerClass, sharedManagerMethod) as? AnyObject { + flexManager = sharedManager + isFLEXAvailable = true + logger.log(message: "FLEX integration initialized successfully", type: .info) + } + } else { + logger.log(message: "FLEX not available - some advanced debugging features will be disabled", type: .warning) + } + } + + // MARK: - Public Methods + + /// Show the FLEX explorer + public func showExplorer() { + guard isFLEXAvailable, let flexManager = flexManager else { + logger.log(message: "FLEX not available", type: .warning) + return + } + + // Call showExplorer method using runtime invocation + if flexManager.responds(to: NSSelectorFromString("showExplorer")) { + _ = flexManager.perform(NSSelectorFromString("showExplorer")) + logger.log(message: "FLEX explorer shown", type: .info) + } + } + + /// Hide the FLEX explorer + public func hideExplorer() { + guard isFLEXAvailable, let flexManager = flexManager else { + return + } + + // Call hideExplorer method using runtime invocation + if flexManager.responds(to: NSSelectorFromString("hideExplorer")) { + _ = flexManager.perform(NSSelectorFromString("hideExplorer")) + logger.log(message: "FLEX explorer hidden", type: .info) + } + } + + /// Toggle the FLEX explorer + public func toggleExplorer() { + guard isFLEXAvailable, let flexManager = flexManager else { + logger.log(message: "FLEX not available", type: .warning) + return + } + + // Call toggleExplorer method using runtime invocation + if flexManager.responds(to: NSSelectorFromString("toggleExplorer")) { + _ = flexManager.perform(NSSelectorFromString("toggleExplorer")) + logger.log(message: "FLEX explorer toggled", type: .info) + } + } + + /// Present a specific FLEX tool + /// - Parameters: + /// - viewController: The view controller to present + /// - completion: Completion handler called when the tool is presented + public func presentTool(_ viewController: UIViewController, completion: (() -> Void)? = nil) { + guard isFLEXAvailable, let flexManager = flexManager else { + logger.log(message: "FLEX not available", type: .warning) + completion?() + return + } + + // Wrap in navigation controller + let navController = UINavigationController(rootViewController: viewController) + + // Create a block that returns the navigation controller + let viewControllerProvider: @convention(block) () -> UINavigationController = { + return navController + } + + // Create an Objective-C block for the completion handler + let completionBlock: @convention(block) () -> Void = { + completion?() + } + + // Call presentTool:completion: method using runtime invocation + if flexManager.responds(to: NSSelectorFromString("presentTool:completion:")) { + _ = flexManager.perform( + NSSelectorFromString("presentTool:completion:"), + with: viewControllerProvider, + with: completionBlock + ) + logger.log(message: "FLEX tool presented", type: .info) + } + } + + /// Check if FLEX is currently visible + /// - Returns: True if FLEX is visible, false otherwise + public func isExplorerVisible() -> Bool { + guard isFLEXAvailable, let flexManager = flexManager else { + return false + } + + // Check isHidden property and invert it + if flexManager.responds(to: NSSelectorFromString("isHidden")), + let isHidden = flexManager.perform(NSSelectorFromString("isHidden"))?.takeRetainedValue() as? Bool { + return !isHidden + } + + return false + } + + /// Get the FLEX toolbar + /// - Returns: The FLEX toolbar as AnyObject, or nil if not available + public func getToolbar() -> AnyObject? { + guard isFLEXAvailable, let flexManager = flexManager else { + return nil + } + + // Get toolbar property + if flexManager.responds(to: NSSelectorFromString("toolbar")), + let toolbar = flexManager.perform(NSSelectorFromString("toolbar"))?.takeRetainedValue() { + return toolbar + } + + return nil + } + + /// Get the FLEX manager + /// - Returns: The FLEX manager as AnyObject, or nil if not available + public func getFlexManager() -> AnyObject? { + guard isFLEXAvailable else { + return nil + } + + return flexManager + } + + /// Present an object explorer for the given object + /// - Parameters: + /// - object: The object to explore + /// - completion: Completion handler called when the explorer is presented + public func presentObjectExplorer(_ object: Any, completion: (() -> Void)? = nil) { + guard isFLEXAvailable, let flexManager = flexManager else { + logger.log(message: "FLEX not available", type: .warning) + completion?() + return + } + + // Create an Objective-C block for the completion handler + let completionBlock: @convention(block) (UINavigationController) -> Void = { _ in + completion?() + } + + // Call presentObjectExplorer:completion: method using runtime invocation + if flexManager.responds(to: NSSelectorFromString("presentObjectExplorer:completion:")) { + _ = flexManager.perform( + NSSelectorFromString("presentObjectExplorer:completion:"), + with: object, + with: completionBlock + ) + logger.log(message: "FLEX object explorer presented", type: .info) + } + } +} \ No newline at end of file diff --git a/iOS/Debugger/Core/NetworkMonitor.swift b/iOS/Debugger/Core/NetworkMonitor.swift new file mode 100644 index 0000000..0c48ba7 --- /dev/null +++ b/iOS/Debugger/Core/NetworkMonitor.swift @@ -0,0 +1,317 @@ +import Foundation + +/// Network monitoring class that intercepts and records network requests +class NetworkMonitor { + // MARK: - Singleton + + /// Shared instance of the network monitor + static let shared = NetworkMonitor() + + // MARK: - Properties + + /// Logger instance + private let logger = Debug.shared + + /// Network requests + private var networkRequests: [NetworkRequest] = [] + + /// Lock for thread safety + private let lock = NSLock() + + /// Flag indicating whether network monitoring is enabled + private var isEnabled = false + + /// Custom URL protocol class + private var urlProtocolClass: AnyClass? + + // MARK: - Initialization + + private init() { + setupNetworkMonitoring() + } + + // MARK: - Setup + + private func setupNetworkMonitoring() { + // Register for app lifecycle notifications + NotificationCenter.default.addObserver( + self, + selector: #selector(handleAppDidBecomeActive), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleAppWillResignActive), + name: UIApplication.willResignActiveNotification, + object: nil + ) + + // Create custom URL protocol class + createCustomURLProtocolClass() + } + + private func createCustomURLProtocolClass() { + // Define the custom URL protocol class + let className = "DebuggerURLProtocol" + let superClass: AnyClass = URLProtocol.self + + // Create the class + guard let urlProtocolClass = objc_allocateClassPair(superClass, className, 0) else { + logger.log(message: "Failed to create custom URL protocol class", type: .error) + return + } + + // Add methods + + // canInit method + let canInitMethod = class_getClassMethod(URLProtocol.self, #selector(URLProtocol.canInit(with:)))! + let canInitImp: @convention(c) (AnyObject, Selector, URLRequest) -> Bool = { _, _, request in + // Accept all requests + return true + } + class_addMethod(object_getClass(urlProtocolClass), #selector(URLProtocol.canInit(with:)), unsafeBitCast(canInitImp, to: IMP.self), method_getTypeEncoding(canInitMethod)) + + // canonicalRequest method + let canonicalRequestMethod = class_getInstanceMethod(URLProtocol.self, #selector(URLProtocol.canonicalRequest(for:)))! + let canonicalRequestImp: @convention(c) (AnyObject, Selector, URLRequest) -> URLRequest = { _, _, request in + // Return the request as is + return request + } + class_addMethod(urlProtocolClass, #selector(URLProtocol.canonicalRequest(for:)), unsafeBitCast(canonicalRequestImp, to: IMP.self), method_getTypeEncoding(canonicalRequestMethod)) + + // startLoading method + let startLoadingMethod = class_getInstanceMethod(URLProtocol.self, #selector(URLProtocol.startLoading))! + let startLoadingImp: @convention(c) (AnyObject, Selector) -> Void = { `self`, _ in + // Get the request + guard let urlProtocol = `self` as? URLProtocol else { return } + let request = urlProtocol.request + + // Create a network request + let networkRequest = NetworkRequest( + url: request.url!, + method: request.httpMethod ?? "GET", + requestHeaders: request.allHTTPHeaderFields ?? [:], + requestBody: request.httpBody != nil ? String(data: request.httpBody!, encoding: .utf8) : nil, + responseStatus: 0, + responseHeaders: [:], + responseBody: nil, + timestamp: Date(), + duration: 0 + ) + + // Add to network requests + NetworkMonitor.shared.addNetworkRequest(networkRequest) + + // Create a new request to avoid infinite recursion + var newRequest = request + URLProtocol.setProperty(true, forKey: "DebuggerURLProtocolHandled", in: &newRequest) + + // Create a new session + let config = URLSessionConfiguration.default + let session = URLSession(configuration: config, delegate: urlProtocol, delegateQueue: nil) + + // Start the task + let task = session.dataTask(with: newRequest) + URLProtocol.setProperty(task, forKey: "DebuggerURLProtocolTask", in: &newRequest) + task.resume() + } + class_addMethod(urlProtocolClass, #selector(URLProtocol.startLoading), unsafeBitCast(startLoadingImp, to: IMP.self), method_getTypeEncoding(startLoadingMethod)) + + // stopLoading method + let stopLoadingMethod = class_getInstanceMethod(URLProtocol.self, #selector(URLProtocol.stopLoading))! + let stopLoadingImp: @convention(c) (AnyObject, Selector) -> Void = { `self`, _ in + // Cancel the task + guard let urlProtocol = `self` as? URLProtocol else { return } + if let task = URLProtocol.property(forKey: "DebuggerURLProtocolTask", in: urlProtocol.request) as? URLSessionTask { + task.cancel() + } + } + class_addMethod(urlProtocolClass, #selector(URLProtocol.stopLoading), unsafeBitCast(stopLoadingImp, to: IMP.self), method_getTypeEncoding(stopLoadingMethod)) + + // Register the class + objc_registerClassPair(urlProtocolClass) + + // Store the class + self.urlProtocolClass = urlProtocolClass + } + + // MARK: - Public Methods + + /// Enable network monitoring + func enable() { + guard !isEnabled, let urlProtocolClass = urlProtocolClass else { return } + + // Register the URL protocol + URLProtocol.registerClass(urlProtocolClass) + + // Set flag + isEnabled = true + + // Log + logger.log(message: "Network monitoring enabled", type: .info) + } + + /// Disable network monitoring + func disable() { + guard isEnabled, let urlProtocolClass = urlProtocolClass else { return } + + // Unregister the URL protocol + URLProtocol.unregisterClass(urlProtocolClass) + + // Set flag + isEnabled = false + + // Log + logger.log(message: "Network monitoring disabled", type: .info) + } + + /// Get all network requests + /// - Returns: Array of network requests + func getNetworkRequests() -> [NetworkRequest] { + lock.lock() + defer { lock.unlock() } + + return networkRequests + } + + /// Add a network request + /// - Parameter request: The network request + func addNetworkRequest(_ request: NetworkRequest) { + lock.lock() + defer { lock.unlock() } + + // Add to network requests + networkRequests.append(request) + + // Post notification + NotificationCenter.default.post(name: .networkRequestAdded, object: request) + + // Log + logger.log(message: "Added network request: \(request.url.absoluteString)", type: .info) + } + + /// Update a network request + /// - Parameters: + /// - url: URL of the request to update + /// - responseStatus: Response status code + /// - responseHeaders: Response headers + /// - responseBody: Response body + /// - duration: Request duration + func updateNetworkRequest( + url: URL, + responseStatus: Int, + responseHeaders: [String: String], + responseBody: String?, + duration: TimeInterval + ) { + lock.lock() + defer { lock.unlock() } + + // Find the request + guard let index = networkRequests.firstIndex(where: { $0.url == url }) else { + logger.log(message: "Network request not found: \(url.absoluteString)", type: .error) + return + } + + // Update the request + let request = networkRequests[index] + let updatedRequest = NetworkRequest( + url: request.url, + method: request.method, + requestHeaders: request.requestHeaders, + requestBody: request.requestBody, + responseStatus: responseStatus, + responseHeaders: responseHeaders, + responseBody: responseBody, + timestamp: request.timestamp, + duration: duration + ) + networkRequests[index] = updatedRequest + + // Post notification + NotificationCenter.default.post(name: .networkRequestUpdated, object: updatedRequest) + + // Log + logger.log(message: "Updated network request: \(url.absoluteString)", type: .info) + } + + /// Clear all network requests + func clearNetworkRequests() { + lock.lock() + defer { lock.unlock() } + + // Clear network requests + networkRequests.removeAll() + + // Post notification + NotificationCenter.default.post(name: .networkRequestsCleared, object: nil) + + // Log + logger.log(message: "Cleared network requests", type: .info) + } + + // MARK: - App Lifecycle + + @objc private func handleAppDidBecomeActive() { + // Re-enable network monitoring if it was enabled + if isEnabled { + enable() + } + } + + @objc private func handleAppWillResignActive() { + // No need to do anything when app resigns active + } +} + +// MARK: - Notification Names + +extension Notification.Name { + static let networkRequestAdded = Notification.Name("networkRequestAdded") + static let networkRequestUpdated = Notification.Name("networkRequestUpdated") + static let networkRequestsCleared = Notification.Name("networkRequestsCleared") +} + +// MARK: - URLSessionDelegate + +extension NetworkMonitor: URLSessionDataDelegate { + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + // Get the URL protocol + guard let urlProtocol = session.configuration.protocolClasses?.first as? URLProtocol else { + completionHandler(.allow) + return + } + + // Forward the response to the URL protocol client + urlProtocol.client?.urlProtocol(urlProtocol, didReceive: response, cacheStoragePolicy: .notAllowed) + + // Allow the response + completionHandler(.allow) + } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + // Get the URL protocol + guard let urlProtocol = session.configuration.protocolClasses?.first as? URLProtocol else { + return + } + + // Forward the data to the URL protocol client + urlProtocol.client?.urlProtocol(urlProtocol, didLoad: data) + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + // Get the URL protocol + guard let urlProtocol = session.configuration.protocolClasses?.first as? URLProtocol else { + return + } + + // Forward the completion to the URL protocol client + if let error = error { + urlProtocol.client?.urlProtocol(urlProtocol, didFailWithError: error) + } else { + urlProtocol.client?.urlProtocolDidFinishLoading(urlProtocol) + } + } +} \ No newline at end of file diff --git a/iOS/Debugger/Core/RuntimeInspector.swift b/iOS/Debugger/Core/RuntimeInspector.swift new file mode 100644 index 0000000..08e9bf2 --- /dev/null +++ b/iOS/Debugger/Core/RuntimeInspector.swift @@ -0,0 +1,409 @@ +import UIKit +import ObjectiveC + +/// Runtime inspection class for examining objects at runtime +class RuntimeInspector { + // MARK: - Singleton + + /// Shared instance of the runtime inspector + static let shared = RuntimeInspector() + + // MARK: - Properties + + /// Logger instance + private let logger = Debug.shared + + /// Runtime class cache + private var runtimeClassCache: [String: AnyClass] = [:] + + // MARK: - Initialization + + private init() { + // Initialize the runtime inspector + logger.log(message: "Runtime inspector initialized", type: .info) + } + + // MARK: - Class Inspection + + /// Get all loaded classes + /// - Returns: Array of class names + func getLoadedClasses() -> [String] { + var count: UInt32 = 0 + guard let classes = objc_copyClassList(&count) else { + return [] + } + + var classNames: [String] = [] + for i in 0.. [String] { + // Get class from cache or lookup + let cls: AnyClass + if let cachedClass = runtimeClassCache[className] { + cls = cachedClass + } else if let foundClass = NSClassFromString(className) { + cls = foundClass + runtimeClassCache[className] = foundClass + } else { + return [] + } + + var methodCount: UInt32 = 0 + guard let methods = class_copyMethodList(cls, &methodCount) else { + return [] + } + + var methodNames: [String] = [] + for i in 0.. [String] { + // Get class from cache or lookup + let cls: AnyClass + if let cachedClass = runtimeClassCache[className] { + cls = cachedClass + } else if let foundClass = NSClassFromString(className) { + cls = foundClass + runtimeClassCache[className] = foundClass + } else { + return [] + } + + var propertyCount: UInt32 = 0 + guard let properties = class_copyPropertyList(cls, &propertyCount) else { + return [] + } + + var propertyNames: [String] = [] + for i in 0.. [String] { + // Get class from cache or lookup + let cls: AnyClass + if let cachedClass = runtimeClassCache[className] { + cls = cachedClass + } else if let foundClass = NSClassFromString(className) { + cls = foundClass + runtimeClassCache[className] = foundClass + } else { + return [] + } + + var ivarCount: UInt32 = 0 + guard let ivars = class_copyIvarList(cls, &ivarCount) else { + return [] + } + + var ivarNames: [String] = [] + for i in 0.. [String] { + // Get class from cache or lookup + let cls: AnyClass + if let cachedClass = runtimeClassCache[className] { + cls = cachedClass + } else if let foundClass = NSClassFromString(className) { + cls = foundClass + runtimeClassCache[className] = foundClass + } else { + return [] + } + + var protocolCount: UInt32 = 0 + guard let protocols = class_copyProtocolList(cls, &protocolCount) else { + return [] + } + + var protocolNames: [String] = [] + for i in 0.. [String] { + var hierarchy: [String] = [] + var cls: AnyClass? = object_getClass(object) + + while let currentClass = cls { + hierarchy.append(NSStringFromClass(currentClass)) + cls = class_getSuperclass(currentClass) + } + + return hierarchy + } + + /// Get all properties and their values for an object + /// - Parameter object: The object + /// - Returns: Dictionary of property names and values + func getPropertyValues(for object: AnyObject) -> [String: String] { + var propertyValues: [String: String] = [:] + + // Get the class + guard let cls = object_getClass(object) else { + return propertyValues + } + + // Get all properties + var propertyCount: UInt32 = 0 + guard let properties = class_copyPropertyList(cls, &propertyCount) else { + return propertyValues + } + + // Get property values + for i in 0.. String { + // Use key-value coding to get the property value + if let value = object.value(forKey: propertyName) { + return String(describing: value) + } else { + return "nil" + } + } + + /// Set the value of a property on an object + /// - Parameters: + /// - propertyName: Name of the property + /// - value: The value to set + /// - object: The object + /// - Returns: True if successful, false otherwise + func setPropertyValue(_ propertyName: String, value: Any, onObject object: AnyObject) -> Bool { + do { + // Use key-value coding to set the property value + try object.setValue(value, forKey: propertyName) + return true + } catch { + logger.log(message: "Error setting property value: \(error.localizedDescription)", type: .error) + return false + } + } + + /// Get all ivar values for an object + /// - Parameter object: The object + /// - Returns: Dictionary of ivar names and values + func getIvarValues(for object: AnyObject) -> [String: String] { + var ivarValues: [String: String] = [:] + + // Get the class + guard let cls = object_getClass(object) else { + return ivarValues + } + + // Get all ivars + var ivarCount: UInt32 = 0 + guard let ivars = class_copyIvarList(cls, &ivarCount) else { + return ivarValues + } + + // Get ivar values + for i in 0.. String { + // Create selector + let selector = NSSelectorFromString(methodName) + + // Check if the object responds to the selector + guard object.responds(to: selector) else { + return "Error: Object does not respond to selector \(methodName)" + } + + // Invoke the method + do { + let result = object.perform(selector, with: arguments.first)?.takeRetainedValue() + return String(describing: result as Any) + } catch { + return "Error: \(error.localizedDescription)" + } + } + + // MARK: - View Hierarchy + + /// Get the view hierarchy + /// - Returns: The root view controller + func getViewHierarchy() -> UIViewController? { + return UIApplication.shared.keyWindow?.rootViewController + } + + /// Get all windows + /// - Returns: Array of windows + func getAllWindows() -> [UIWindow] { + return UIApplication.shared.windows + } + + /// Get all view controllers + /// - Returns: Array of view controllers + func getAllViewControllers() -> [UIViewController] { + var viewControllers: [UIViewController] = [] + + // Get all windows + let windows = getAllWindows() + + // Get root view controllers + for window in windows { + if let rootViewController = window.rootViewController { + viewControllers.append(rootViewController) + + // Add presented view controllers + viewControllers.append(contentsOf: getAllPresentedViewControllers(from: rootViewController)) + } + } + + return viewControllers + } + + /// Get all presented view controllers + /// - Parameter viewController: The parent view controller + /// - Returns: Array of presented view controllers + private func getAllPresentedViewControllers(from viewController: UIViewController) -> [UIViewController] { + var viewControllers: [UIViewController] = [] + + // Add child view controllers + for childViewController in viewController.children { + viewControllers.append(childViewController) + viewControllers.append(contentsOf: getAllPresentedViewControllers(from: childViewController)) + } + + // Add presented view controller + if let presentedViewController = viewController.presentedViewController { + viewControllers.append(presentedViewController) + viewControllers.append(contentsOf: getAllPresentedViewControllers(from: presentedViewController)) + } + + return viewControllers + } + + /// Get the view hierarchy for a view + /// - Parameter view: The view + /// - Returns: Dictionary representing the view hierarchy + func getViewHierarchy(for view: UIView) -> [String: Any] { + var hierarchy: [String: Any] = [ + "class": NSStringFromClass(type(of: view)), + "frame": NSStringFromCGRect(view.frame), + "tag": view.tag, + "isHidden": view.isHidden, + "alpha": view.alpha, + "backgroundColor": view.backgroundColor?.description ?? "nil" + ] + + // Add subviews + var subviews: [[String: Any]] = [] + for subview in view.subviews { + subviews.append(getViewHierarchy(for: subview)) + } + hierarchy["subviews"] = subviews + + return hierarchy + } +} \ No newline at end of file diff --git a/iOS/Debugger/Core/UIViewController+Debugger.swift b/iOS/Debugger/Core/UIViewController+Debugger.swift new file mode 100644 index 0000000..a0b8f46 --- /dev/null +++ b/iOS/Debugger/Core/UIViewController+Debugger.swift @@ -0,0 +1,111 @@ +import UIKit + +/// Extension for UIViewController to add debugging functionality +extension UIViewController { + // MARK: - Swizzled Methods + + /// Swizzled viewDidLoad method + @objc func debugger_viewDidLoad() { + // Call original implementation + debugger_viewDidLoad() + + // Log view controller lifecycle + Debug.shared.log(message: "[\(type(of: self))] viewDidLoad", type: .info) + } + + /// Swizzled viewWillAppear method + @objc func debugger_viewWillAppear(_ animated: Bool) { + // Call original implementation + debugger_viewWillAppear(animated) + + // Log view controller lifecycle + Debug.shared.log(message: "[\(type(of: self))] viewWillAppear(animated: \(animated))", type: .info) + } + + /// Swizzled viewDidAppear method + @objc func debugger_viewDidAppear(_ animated: Bool) { + // Call original implementation + debugger_viewDidAppear(animated) + + // Log view controller lifecycle + Debug.shared.log(message: "[\(type(of: self))] viewDidAppear(animated: \(animated))", type: .info) + } + + /// Swizzled viewWillDisappear method + @objc func debugger_viewWillDisappear(_ animated: Bool) { + // Call original implementation + debugger_viewWillDisappear(animated) + + // Log view controller lifecycle + Debug.shared.log(message: "[\(type(of: self))] viewWillDisappear(animated: \(animated))", type: .info) + } + + /// Swizzled viewDidDisappear method + @objc func debugger_viewDidDisappear(_ animated: Bool) { + // Call original implementation + debugger_viewDidDisappear(animated) + + // Log view controller lifecycle + Debug.shared.log(message: "[\(type(of: self))] viewDidDisappear(animated: \(animated))", type: .info) + } + + // MARK: - Helper Methods + + /// Get the view hierarchy as a string + /// - Returns: String representation of the view hierarchy + func viewHierarchyDescription() -> String { + return viewHierarchyDescription(for: view, level: 0) + } + + /// Get the view hierarchy as a string for a specific view + /// - Parameters: + /// - view: The view + /// - level: Indentation level + /// - Returns: String representation of the view hierarchy + private func viewHierarchyDescription(for view: UIView, level: Int) -> String { + // Create indentation + let indent = String(repeating: " ", count: level) + + // Create description + var description = "\(indent)[\(type(of: view))] frame=\(view.frame), alpha=\(view.alpha), hidden=\(view.isHidden)\n" + + // Add subviews + for subview in view.subviews { + description += viewHierarchyDescription(for: subview, level: level + 1) + } + + return description + } + + /// Get the view controller hierarchy as a string + /// - Returns: String representation of the view controller hierarchy + func viewControllerHierarchyDescription() -> String { + return viewControllerHierarchyDescription(for: self, level: 0) + } + + /// Get the view controller hierarchy as a string for a specific view controller + /// - Parameters: + /// - viewController: The view controller + /// - level: Indentation level + /// - Returns: String representation of the view controller hierarchy + private func viewControllerHierarchyDescription(for viewController: UIViewController, level: Int) -> String { + // Create indentation + let indent = String(repeating: " ", count: level) + + // Create description + var description = "\(indent)[\(type(of: viewController))]\n" + + // Add child view controllers + for childViewController in viewController.children { + description += viewControllerHierarchyDescription(for: childViewController, level: level + 1) + } + + // Add presented view controller + if let presentedViewController = viewController.presentedViewController { + description += "\(indent) Presented:\n" + description += viewControllerHierarchyDescription(for: presentedViewController, level: level + 2) + } + + return description + } +} \ No newline at end of file diff --git a/iOS/Debugger/UI/DebuggerViewController.swift b/iOS/Debugger/UI/DebuggerViewController.swift index f49efb2..89c5bab 100644 --- a/iOS/Debugger/UI/DebuggerViewController.swift +++ b/iOS/Debugger/UI/DebuggerViewController.swift @@ -15,6 +15,9 @@ class DebuggerViewController: UIViewController { /// The debugger engine private let debuggerEngine = DebuggerEngine.shared + + /// FLEX integration + private let flexIntegration = FLEXIntegration.shared /// Logger instance private let logger = Debug.shared @@ -24,6 +27,9 @@ class DebuggerViewController: UIViewController { /// View controllers for each tab private var viewControllers: [UIViewController] = [] + + /// Object to focus on in variables tab + private var focusObject: Any? // MARK: - Lifecycle @@ -41,6 +47,12 @@ class DebuggerViewController: UIViewController { // Register as delegate for debugger engine debuggerEngine.delegate = self + + // If we have a focus object, show it in the variables tab + if let focusObject = focusObject { + showVariablesTab(withObject: focusObject) + self.focusObject = nil + } } override func viewWillDisappear(_ animated: Bool) { @@ -64,7 +76,17 @@ class DebuggerViewController: UIViewController { target: self, action: #selector(closeButtonTapped) ) - navigationItem.rightBarButtonItem = closeButton + + // Add FLEX mode button + let flexModeButton = UIBarButtonItem( + image: UIImage(systemName: "hammer.fill"), + style: .plain, + target: self, + action: #selector(flexModeButtonTapped) + ) + flexModeButton.tintColor = .systemBlue + + navigationItem.rightBarButtonItems = [closeButton, flexModeButton] // Add execution control buttons let pauseButton = UIBarButtonItem( @@ -134,6 +156,9 @@ class DebuggerViewController: UIViewController { let memoryVC = createMemoryViewController() let networkVC = createNetworkViewController() let performanceVC = createPerformanceViewController() + let fileBrowserVC = createFileBrowserViewController() + let systemLogVC = createSystemLogViewController() + let runtimeBrowserVC = createRuntimeBrowserViewController() // Set tab bar items consoleVC.tabBarItem = UITabBarItem(title: "Console", image: UIImage(systemName: "terminal"), tag: 0) @@ -146,6 +171,9 @@ class DebuggerViewController: UIViewController { memoryVC.tabBarItem = UITabBarItem(title: "Memory", image: UIImage(systemName: "memorychip"), tag: 3) networkVC.tabBarItem = UITabBarItem(title: "Network", image: UIImage(systemName: "network"), tag: 4) performanceVC.tabBarItem = UITabBarItem(title: "Performance", image: UIImage(systemName: "gauge"), tag: 5) + fileBrowserVC.tabBarItem = UITabBarItem(title: "Files", image: UIImage(systemName: "folder"), tag: 6) + systemLogVC.tabBarItem = UITabBarItem(title: "System Log", image: UIImage(systemName: "doc.text"), tag: 7) + runtimeBrowserVC.tabBarItem = UITabBarItem(title: "Runtime", image: UIImage(systemName: "hammer"), tag: 8) // Set view controllers viewControllers = [ @@ -155,6 +183,9 @@ class DebuggerViewController: UIViewController { UINavigationController(rootViewController: memoryVC), UINavigationController(rootViewController: networkVC), UINavigationController(rootViewController: performanceVC), + UINavigationController(rootViewController: fileBrowserVC), + UINavigationController(rootViewController: systemLogVC), + UINavigationController(rootViewController: runtimeBrowserVC) ] debugTabBarController.viewControllers = viewControllers @@ -186,12 +217,60 @@ class DebuggerViewController: UIViewController { private func createPerformanceViewController() -> UIViewController { return PerformanceViewController() } + + private func createFileBrowserViewController() -> UIViewController { + return FileBrowserViewController() + } + + private func createSystemLogViewController() -> UIViewController { + return SystemLogViewController() + } + + private func createRuntimeBrowserViewController() -> UIViewController { + return RuntimeBrowserViewController() + } + + // MARK: - Public Methods + + /// Show the variables tab and focus on the given object + /// - Parameter object: The object to focus on + func showVariablesTab(withObject object: Any) { + // If view is loaded, show the variables tab and set the object + if isViewLoaded { + debugTabBarController.selectedIndex = 2 + + // Get the variables view controller + if let navController = viewControllers[2] as? UINavigationController, + let variablesVC = navController.topViewController as? VariablesViewController { + variablesVC.focusOn(object: object) + } + } else { + // Store the object to focus on when the view loads + focusObject = object + } + } // MARK: - Actions @objc private func closeButtonTapped() { delegate?.debuggerViewControllerDidRequestDismissal(self) } + + @objc private func flexModeButtonTapped() { + // Switch to FLEX mode + NotificationCenter.default.post( + name: .switchDebugMode, + object: DebuggerManager.DebugMode.flex + ) + + // Dismiss this view controller + delegate?.debuggerViewControllerDidRequestDismissal(self) + + // Show FLEX explorer + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.flexIntegration.showExplorer() + } + } @objc private func pauseButtonTapped() { debuggerEngine.pause() diff --git a/iOS/Debugger/UI/FileBrowserViewController.swift b/iOS/Debugger/UI/FileBrowserViewController.swift new file mode 100644 index 0000000..1f61057 --- /dev/null +++ b/iOS/Debugger/UI/FileBrowserViewController.swift @@ -0,0 +1,354 @@ +import UIKit + +/// View controller for browsing the file system +class FileBrowserViewController: UIViewController { + // MARK: - Properties + + /// Table view for displaying files and directories + private let tableView = UITableView(frame: .zero, style: .plain) + + /// Current directory path + private var currentPath: URL + + /// Files and directories in the current path + private var items: [URL] = [] + + /// Date formatter for file dates + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + return formatter + }() + + /// File manager + private let fileManager = FileManager.default + + /// FLEX integration + private let flexIntegration = FLEXIntegration.shared + + /// Logger instance + private let logger = Debug.shared + + // MARK: - Initialization + + init() { + // Start at the app's document directory + currentPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + // Start at the app's document directory + currentPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + super.init(coder: coder) + } + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + setupUI() + loadItems() + } + + // MARK: - Setup + + private func setupUI() { + // Set title + title = "File Browser" + + // Set up table view + tableView.delegate = self + tableView.dataSource = self + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "FileCell") + + // Add table view to view hierarchy + view.addSubview(tableView) + tableView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + // Add navigation buttons + navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .refresh, + target: self, + action: #selector(refreshButtonTapped) + ) + + // Add "Go Up" button if not at root + updateNavigationButtons() + } + + // MARK: - Data Loading + + private func loadItems() { + do { + // Get contents of directory + let contents = try fileManager.contentsOfDirectory( + at: currentPath, + includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey, .creationDateKey], + options: [.skipsHiddenFiles] + ) + + // Sort directories first, then by name + items = contents.sorted { (url1, url2) -> Bool in + let isDirectory1 = (try? url1.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false + let isDirectory2 = (try? url2.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false + + if isDirectory1 && !isDirectory2 { + return true + } else if !isDirectory1 && isDirectory2 { + return false + } else { + return url1.lastPathComponent < url2.lastPathComponent + } + } + + // Reload table view + tableView.reloadData() + + // Update title with current directory name + title = currentPath.lastPathComponent + + // Update navigation buttons + updateNavigationButtons() + + } catch { + logger.log(message: "Error loading directory contents: \(error.localizedDescription)", type: .error) + } + } + + private func updateNavigationButtons() { + // Add "Go Up" button if not at root + if currentPath.path != "/" { + navigationItem.leftBarButtonItem = UIBarButtonItem( + title: "Up", + style: .plain, + target: self, + action: #selector(goUpButtonTapped) + ) + } else { + navigationItem.leftBarButtonItem = nil + } + } + + // MARK: - Actions + + @objc private func refreshButtonTapped() { + loadItems() + } + + @objc private func goUpButtonTapped() { + // Navigate to parent directory + if let parentURL = currentPath.deletingLastPathComponent() as URL? { + currentPath = parentURL + loadItems() + } + } + + // MARK: - File Operations + + private func showFileDetails(for url: URL) { + do { + // Get file attributes + let attributes = try fileManager.attributesOfItem(atPath: url.path) + let fileSize = attributes[.size] as? Int64 ?? 0 + let creationDate = attributes[.creationDate] as? Date ?? Date() + let modificationDate = attributes[.modificationDate] as? Date ?? Date() + let fileType = attributes[.type] as? String ?? "Unknown" + + // Format file size + let fileSizeString = ByteCountFormatter.string(fromByteCount: fileSize, countStyle: .file) + + // Create alert with file details + let alert = UIAlertController( + title: url.lastPathComponent, + message: """ + Size: \(fileSizeString) + Created: \(dateFormatter.string(from: creationDate)) + Modified: \(dateFormatter.string(from: modificationDate)) + Type: \(fileType) + Path: \(url.path) + """, + preferredStyle: .alert + ) + + // Add actions based on file type + let fileExtension = url.pathExtension.lowercased() + + // View action for text and image files + if ["txt", "json", "plist", "xml", "html", "css", "js", "swift", "m", "h", "log"].contains(fileExtension) { + alert.addAction(UIAlertAction(title: "View Contents", style: .default) { _ in + self.viewFileContents(url) + }) + } + + // Delete action + alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { _ in + self.deleteFile(url) + }) + + // Share action + alert.addAction(UIAlertAction(title: "Share", style: .default) { _ in + self.shareFile(url) + }) + + // Cancel action + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + + // Present alert + present(alert, animated: true) + + } catch { + logger.log(message: "Error getting file attributes: \(error.localizedDescription)", type: .error) + } + } + + private func viewFileContents(_ url: URL) { + do { + // Read file contents + let contents = try String(contentsOf: url, encoding: .utf8) + + // Create and present text view controller + let textViewController = UIViewController() + let textView = UITextView() + textView.text = contents + textView.isEditable = false + textView.font = UIFont.monospacedSystemFont(ofSize: 14, weight: .regular) + + textViewController.view = textView + textViewController.title = url.lastPathComponent + + navigationController?.pushViewController(textViewController, animated: true) + + } catch { + logger.log(message: "Error reading file contents: \(error.localizedDescription)", type: .error) + } + } + + private func deleteFile(_ url: URL) { + // Confirm deletion + let alert = UIAlertController( + title: "Confirm Deletion", + message: "Are you sure you want to delete \(url.lastPathComponent)?", + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { _ in + do { + try self.fileManager.removeItem(at: url) + self.loadItems() + } catch { + self.logger.log(message: "Error deleting file: \(error.localizedDescription)", type: .error) + } + }) + + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + + present(alert, animated: true) + } + + private func shareFile(_ url: URL) { + // Create activity view controller + let activityViewController = UIActivityViewController( + activityItems: [url], + applicationActivities: nil + ) + + // Present activity view controller + present(activityViewController, animated: true) + } +} + +// MARK: - UITableViewDataSource + +extension FileBrowserViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return items.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "FileCell", for: indexPath) + let url = items[indexPath.row] + + // Configure cell + cell.textLabel?.text = url.lastPathComponent + + do { + // Determine if item is a directory + let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey]) + let isDirectory = resourceValues.isDirectory ?? false + + // Set appropriate icon + if isDirectory { + cell.imageView?.image = UIImage(systemName: "folder") + cell.accessoryType = .disclosureIndicator + } else { + // Choose icon based on file extension + let fileExtension = url.pathExtension.lowercased() + + switch fileExtension { + case "txt", "log": + cell.imageView?.image = UIImage(systemName: "doc.text") + case "json", "plist", "xml": + cell.imageView?.image = UIImage(systemName: "doc.plaintext") + case "jpg", "jpeg", "png", "gif": + cell.imageView?.image = UIImage(systemName: "photo") + case "pdf": + cell.imageView?.image = UIImage(systemName: "doc.richtext") + case "swift", "m", "h": + cell.imageView?.image = UIImage(systemName: "chevron.left.forwardslash.chevron.right") + default: + cell.imageView?.image = UIImage(systemName: "doc") + } + + cell.accessoryType = .detailDisclosureButton + } + + } catch { + cell.imageView?.image = UIImage(systemName: "questionmark.circle") + cell.accessoryType = .none + } + + return cell + } +} + +// MARK: - UITableViewDelegate + +extension FileBrowserViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let url = items[indexPath.row] + + do { + // Check if item is a directory + let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey]) + let isDirectory = resourceValues.isDirectory ?? false + + if isDirectory { + // Navigate to directory + currentPath = url + loadItems() + } else { + // Show file details + showFileDetails(for: url) + } + + } catch { + logger.log(message: "Error determining item type: \(error.localizedDescription)", type: .error) + } + } + + func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { + let url = items[indexPath.row] + showFileDetails(for: url) + } +} \ No newline at end of file diff --git a/iOS/Debugger/UI/FloatingDebuggerButton.swift b/iOS/Debugger/UI/FloatingDebuggerButton.swift index 933413a..9520c73 100644 --- a/iOS/Debugger/UI/FloatingDebuggerButton.swift +++ b/iOS/Debugger/UI/FloatingDebuggerButton.swift @@ -9,6 +9,9 @@ class FloatingDebuggerButton: UIButton { // Pan gesture for dragging the button private var panGesture: UIPanGestureRecognizer! + + // Long press gesture for switching modes + private var longPressGesture: UILongPressGestureRecognizer! // Logger instance private let logger = Debug.shared @@ -16,6 +19,9 @@ class FloatingDebuggerButton: UIButton { // Keys for saving position private let positionXKey = "floating_debugger_button_x" private let positionYKey = "floating_debugger_button_y" + + // Current debug mode + private var currentMode: DebuggerManager.DebugMode = .standard override init(frame: CGRect) { super.init(frame: frame) @@ -61,6 +67,11 @@ class FloatingDebuggerButton: UIButton { panGesture.minimumNumberOfTouches = 1 panGesture.maximumNumberOfTouches = 1 addGestureRecognizer(panGesture) + + // Long press gesture for switching modes + longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) + longPressGesture.minimumPressDuration = 0.8 + addGestureRecognizer(longPressGesture) } @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { @@ -109,6 +120,57 @@ class FloatingDebuggerButton: UIButton { break } } + + @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) { + if gesture.state == .began { + // Show mode selection menu + showModeSelectionMenu() + } + } + + private func showModeSelectionMenu() { + // Create alert controller with mode options + let alertController = UIAlertController( + title: "Select Debug Mode", + message: "Choose which debugging interface to use", + preferredStyle: .actionSheet + ) + + // Add standard mode option + let standardAction = UIAlertAction(title: "Standard Debugger", style: .default) { _ in + NotificationCenter.default.post( + name: .switchDebugMode, + object: DebuggerManager.DebugMode.standard + ) + } + + // Add FLEX mode option + let flexAction = UIAlertAction(title: "FLEX Explorer", style: .default) { _ in + NotificationCenter.default.post( + name: .switchDebugMode, + object: DebuggerManager.DebugMode.flex + ) + } + + // Add cancel option + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + + // Add actions to alert controller + alertController.addAction(standardAction) + alertController.addAction(flexAction) + alertController.addAction(cancelAction) + + // Present the alert controller + if let rootViewController = UIApplication.shared.keyWindow?.rootViewController { + // For iPad, set the source view and rect + if let popoverController = alertController.popoverPresentationController { + popoverController.sourceView = self + popoverController.sourceRect = bounds + } + + rootViewController.present(alertController, animated: true, completion: nil) + } + } private func savePosition() { UserDefaults.standard.set(center.x, forKey: positionXKey) @@ -140,6 +202,39 @@ class FloatingDebuggerButton: UIButton { backgroundColor = UIColor.systemRed } } + + /// Update button appearance based on debug mode + /// - Parameter mode: The current debug mode + func updateAppearance(forMode mode: DebuggerManager.DebugMode) { + currentMode = mode + + // Update button appearance based on mode + switch mode { + case .standard: + // Standard mode - red button with bug emoji + setTitle("🐞", for: .normal) + titleLabel?.font = UIFont.systemFont(ofSize: 24) + + let interfaceStyle = UIScreen.main.traitCollection.userInterfaceStyle + if interfaceStyle == .dark { + backgroundColor = UIColor(red: 0.8, green: 0.2, blue: 0.2, alpha: 1.0) + } else { + backgroundColor = UIColor.systemRed + } + + case .flex: + // FLEX mode - blue button with hammer emoji + setTitle("🔧", for: .normal) + titleLabel?.font = UIFont.systemFont(ofSize: 24) + + let interfaceStyle = UIScreen.main.traitCollection.userInterfaceStyle + if interfaceStyle == .dark { + backgroundColor = UIColor(red: 0.0, green: 0.4, blue: 0.8, alpha: 1.0) + } else { + backgroundColor = UIColor.systemBlue + } + } + } @objc private func buttonTapped() { // Provide haptic feedback @@ -166,7 +261,7 @@ class FloatingDebuggerButton: UIButton { // Update appearance when theme changes if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - updateAppearance() + updateAppearance(forMode: currentMode) } } } @@ -176,4 +271,5 @@ extension Notification.Name { static let showDebugger = Notification.Name("showDebugger") static let showDebuggerButton = Notification.Name("showDebuggerButton") static let hideDebuggerButton = Notification.Name("hideDebuggerButton") + static let switchDebugMode = Notification.Name("switchDebugMode") } diff --git a/iOS/Debugger/UI/NetworkMonitorViewController.swift b/iOS/Debugger/UI/NetworkMonitorViewController.swift index 55c0f03..4f0c14f 100644 --- a/iOS/Debugger/UI/NetworkMonitorViewController.swift +++ b/iOS/Debugger/UI/NetworkMonitorViewController.swift @@ -7,9 +7,15 @@ import UIKit /// The debugger engine private let debuggerEngine = DebuggerEngine.shared + + /// FLEX integration + private let flexIntegration = FLEXIntegration.shared /// Logger instance private let logger = Debug.shared + + /// Toggle for using FLEX network monitoring + private var useFLEXMonitoring = true /// Table view for displaying network requests private let tableView: UITableView = { @@ -113,6 +119,14 @@ import UIKit // Set up search bar searchBar.delegate = self + + // Add FLEX button to navigation bar + navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "FLEX", + style: .plain, + target: self, + action: #selector(flexButtonTapped) + ) } private func setupActions() { @@ -124,23 +138,88 @@ import UIKit } private func setupNetworkMonitoring() { - // In a real implementation, this would set up URLProtocol swizzling - // to intercept and monitor network requests - - // For now, just log that network monitoring is set up - logger.log(message: "Network monitoring set up", type: .info) + if useFLEXMonitoring { + // Use FLEX's network monitoring capabilities + logger.log(message: "Using FLEX network monitoring", type: .info) + + // FLEX automatically sets up network monitoring when initialized + // We just need to make sure it's enabled + + // This is done through runtime invocation to avoid compile-time dependency + if let flexManager = flexIntegration.getFlexManager() { + // Check if network monitoring is enabled + if let isNetworkDebuggingEnabledSelector = NSSelectorFromString("isNetworkDebuggingEnabled"), + flexManager.responds(to: isNetworkDebuggingEnabledSelector) { + + // If not enabled, enable it + if let result = flexManager.perform(isNetworkDebuggingEnabledSelector)?.takeRetainedValue() as? Bool, + !result { + if let setNetworkDebuggingEnabledSelector = NSSelectorFromString("setNetworkDebuggingEnabled:"), + flexManager.responds(to: setNetworkDebuggingEnabledSelector) { + _ = flexManager.perform(setNetworkDebuggingEnabledSelector, with: true) + logger.log(message: "FLEX network monitoring enabled", type: .info) + } + } + } + } + } else { + // Use our custom network monitoring implementation + NetworkMonitor.shared.enable() + + // Register for network request notifications + NotificationCenter.default.addObserver( + self, + selector: #selector(handleNetworkRequestAdded), + name: .networkRequestAdded, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleNetworkRequestUpdated), + name: .networkRequestUpdated, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleNetworkRequestsCleared), + name: .networkRequestsCleared, + object: nil + ) + + logger.log(message: "Custom network monitoring set up", type: .info) + } } // MARK: - Actions @objc private func refreshNetworkRequests() { - // In a real implementation, this would refresh the network requests + if useFLEXMonitoring { + // Get network transactions from FLEX + fetchFLEXNetworkTransactions() + } else { + // Get network requests from our custom monitor + networkRequests = NetworkMonitor.shared.getNetworkRequests() + filteredRequests = networkRequests + + // Reload table view + tableView.reloadData() + } - // For now, just end refreshing + // End refreshing refreshControl.endRefreshing() } @objc private func clearButtonTapped() { + if useFLEXMonitoring { + // Clear FLEX network transactions + clearFLEXNetworkTransactions() + } else { + // Clear network requests from our custom monitor + NetworkMonitor.shared.clearNetworkRequests() + } + // Clear network requests networkRequests.removeAll() filteredRequests.removeAll() @@ -148,6 +227,155 @@ import UIKit // Reload table view tableView.reloadData() } + + @objc private func handleNetworkRequestAdded(_ notification: Notification) { + // Get the network request + guard let request = notification.object as? NetworkRequest else { return } + + // Add to network requests + networkRequests.append(request) + + // Apply filter + filterRequests() + + // Reload table view on main thread + DispatchQueue.main.async { + self.tableView.reloadData() + } + } + + @objc private func handleNetworkRequestUpdated(_ notification: Notification) { + // Get the network request + guard let updatedRequest = notification.object as? NetworkRequest else { return } + + // Find the request + if let index = networkRequests.firstIndex(where: { $0.url == updatedRequest.url }) { + // Update the request + networkRequests[index] = updatedRequest + + // Apply filter + filterRequests() + + // Reload table view on main thread + DispatchQueue.main.async { + self.tableView.reloadData() + } + } + } + + @objc private func handleNetworkRequestsCleared(_ notification: Notification) { + // Clear network requests + networkRequests.removeAll() + filteredRequests.removeAll() + + // Reload table view on main thread + DispatchQueue.main.async { + self.tableView.reloadData() + } + } + + @objc private func flexButtonTapped() { + // Show FLEX network monitor directly + if let flexManager = flexIntegration.getFlexManager() { + // Try to present the network history view controller + if let networkHistorySelector = NSSelectorFromString("showNetworkHistoryViewController"), + flexManager.responds(to: networkHistorySelector) { + _ = flexManager.perform(networkHistorySelector) + logger.log(message: "FLEX network history shown", type: .info) + } else { + // Fallback to showing the explorer + flexIntegration.showExplorer() + } + } else { + // Fallback to showing the explorer + flexIntegration.showExplorer() + } + } + + private func fetchFLEXNetworkTransactions() { + // Get network transactions from FLEX using runtime invocation + if let flexManager = flexIntegration.getFlexManager() { + // Try to get the network recorder + if let networkRecorderSelector = NSSelectorFromString("networkRecorder"), + flexManager.responds(to: networkRecorderSelector), + let networkRecorder = flexManager.perform(networkRecorderSelector)?.takeRetainedValue() { + + // Try to get the network transactions + if let transactionsSelector = NSSelectorFromString("transactions"), + networkRecorder.responds(to: transactionsSelector), + let transactions = networkRecorder.perform(transactionsSelector)?.takeRetainedValue() as? [AnyObject] { + + // Convert FLEX transactions to our model + networkRequests = convertFLEXTransactionsToNetworkRequests(transactions) + filteredRequests = networkRequests + + // Reload table view + tableView.reloadData() + + logger.log(message: "Fetched \(transactions.count) FLEX network transactions", type: .info) + } + } + } + } + + private func clearFLEXNetworkTransactions() { + // Clear FLEX network transactions using runtime invocation + if let flexManager = flexIntegration.getFlexManager() { + // Try to get the network recorder + if let networkRecorderSelector = NSSelectorFromString("networkRecorder"), + flexManager.responds(to: networkRecorderSelector), + let networkRecorder = flexManager.perform(networkRecorderSelector)?.takeRetainedValue() { + + // Try to clear the network transactions + if let clearRecorderSelector = NSSelectorFromString("clearRecordedActivity"), + networkRecorder.responds(to: clearRecorderSelector) { + _ = networkRecorder.perform(clearRecorderSelector) + logger.log(message: "Cleared FLEX network transactions", type: .info) + } + } + } + } + + private func convertFLEXTransactionsToNetworkRequests(_ transactions: [AnyObject]) -> [NetworkRequest] { + var requests: [NetworkRequest] = [] + + for transaction in transactions { + // Extract properties using KVC + let url = transaction.value(forKey: "request")?.value(forKey: "URL") as? URL ?? URL(string: "https://unknown.url")! + let method = transaction.value(forKey: "request")?.value(forKey: "HTTPMethod") as? String ?? "UNKNOWN" + + let requestHeaders = transaction.value(forKey: "request")?.value(forKey: "allHTTPHeaderFields") as? [String: String] ?? [:] + let requestBody = transaction.value(forKey: "request")?.value(forKey: "HTTPBody") as? Data + + let responseStatus = transaction.value(forKey: "response")?.value(forKey: "statusCode") as? Int ?? 0 + let responseHeaders = transaction.value(forKey: "response")?.value(forKey: "allHeaderFields") as? [String: String] ?? [:] + + let responseBodyData = transaction.value(forKey: "responseBody") as? Data + let responseBody = responseBodyData != nil ? String(data: responseBodyData!, encoding: .utf8) : nil + + let startTime = transaction.value(forKey: "startTime") as? TimeInterval ?? 0 + let duration = transaction.value(forKey: "duration") as? TimeInterval ?? 0 + + let timestamp = Date(timeIntervalSince1970: startTime) + + // Create network request + let request = NetworkRequest( + url: url, + method: method, + requestHeaders: requestHeaders, + requestBody: requestBody != nil ? String(data: requestBody!, encoding: .utf8) : nil, + responseStatus: responseStatus, + responseHeaders: responseHeaders, + responseBody: responseBody, + timestamp: timestamp, + duration: duration + ) + + requests.append(request) + } + + return requests + } // MARK: - Helper Methods diff --git a/iOS/Debugger/UI/RuntimeBrowserViewController.swift b/iOS/Debugger/UI/RuntimeBrowserViewController.swift new file mode 100644 index 0000000..f05d50b --- /dev/null +++ b/iOS/Debugger/UI/RuntimeBrowserViewController.swift @@ -0,0 +1,364 @@ +import UIKit + +/// View controller for browsing the runtime +class RuntimeBrowserViewController: UIViewController { + // MARK: - Properties + + /// Table view for displaying classes + private let tableView = UITableView(frame: .zero, style: .plain) + + /// Search bar for filtering classes + private let searchBar = UISearchBar() + + /// Runtime inspector + private let runtimeInspector = RuntimeInspector.shared + + /// FLEX integration + private let flexIntegration = FLEXIntegration.shared + + /// Logger instance + private let logger = Debug.shared + + /// All classes + private var allClasses: [String] = [] + + /// Filtered classes + private var filteredClasses: [String] = [] + + /// Current search text + private var searchText: String = "" + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + setupUI() + loadClasses() + } + + // MARK: - Setup + + private func setupUI() { + // Set title + title = "Runtime Browser" + + // Set up search bar + searchBar.placeholder = "Filter classes..." + searchBar.delegate = self + searchBar.translatesAutoresizingMaskIntoConstraints = false + + // Set up table view + tableView.delegate = self + tableView.dataSource = self + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "ClassCell") + tableView.translatesAutoresizingMaskIntoConstraints = false + + // Add subviews + view.addSubview(searchBar) + view.addSubview(tableView) + + // Set up constraints + NSLayoutConstraint.activate([ + searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + tableView.topAnchor.constraint(equalTo: searchBar.bottomAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + // Add FLEX button to navigation bar + navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "FLEX", + style: .plain, + target: self, + action: #selector(flexButtonTapped) + ) + } + + // MARK: - Data Loading + + private func loadClasses() { + // Get all loaded classes + allClasses = runtimeInspector.getLoadedClasses() + + // Apply filter + filterClasses() + } + + private func filterClasses() { + // Apply search filter + if searchText.isEmpty { + filteredClasses = allClasses + } else { + filteredClasses = allClasses.filter { $0.lowercased().contains(searchText.lowercased()) } + } + + // Reload table view + tableView.reloadData() + } + + // MARK: - Actions + + @objc private func flexButtonTapped() { + // Show FLEX explorer + flexIntegration.showExplorer() + } +} + +// MARK: - UITableViewDataSource + +extension RuntimeBrowserViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return filteredClasses.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "ClassCell", for: indexPath) + + // Configure cell + let className = filteredClasses[indexPath.row] + cell.textLabel?.text = className + cell.accessoryType = .disclosureIndicator + + return cell + } +} + +// MARK: - UITableViewDelegate + +extension RuntimeBrowserViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + // Get class name + let className = filteredClasses[indexPath.row] + + // Show class details + let classDetailsVC = ClassDetailsViewController(className: className) + navigationController?.pushViewController(classDetailsVC, animated: true) + } +} + +// MARK: - UISearchBarDelegate + +extension RuntimeBrowserViewController: UISearchBarDelegate { + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + // Update search text + self.searchText = searchText + + // Apply filter + filterClasses() + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + // Dismiss keyboard + searchBar.resignFirstResponder() + } +} + +// MARK: - ClassDetailsViewController + +class ClassDetailsViewController: UIViewController { + // MARK: - Properties + + /// Table view for displaying class details + private let tableView = UITableView(frame: .zero, style: .grouped) + + /// Runtime inspector + private let runtimeInspector = RuntimeInspector.shared + + /// FLEX integration + private let flexIntegration = FLEXIntegration.shared + + /// Logger instance + private let logger = Debug.shared + + /// Class name + private let className: String + + /// Methods + private var methods: [String] = [] + + /// Properties + private var properties: [String] = [] + + /// Ivars + private var ivars: [String] = [] + + /// Protocols + private var protocols: [String] = [] + + // MARK: - Initialization + + init(className: String) { + self.className = className + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + setupUI() + loadClassDetails() + } + + // MARK: - Setup + + private func setupUI() { + // Set title + title = className + + // Set up table view + tableView.delegate = self + tableView.dataSource = self + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "DetailCell") + tableView.translatesAutoresizingMaskIntoConstraints = false + + // Add subviews + view.addSubview(tableView) + + // Set up constraints + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + // Add FLEX button to navigation bar + navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "FLEX", + style: .plain, + target: self, + action: #selector(flexButtonTapped) + ) + } + + // MARK: - Data Loading + + private func loadClassDetails() { + // Get methods + methods = runtimeInspector.getMethods(forClass: className) + + // Get properties + properties = runtimeInspector.getProperties(forClass: className) + + // Get ivars + ivars = runtimeInspector.getIvars(forClass: className) + + // Get protocols + protocols = runtimeInspector.getProtocols(forClass: className) + + // Reload table view + tableView.reloadData() + } + + // MARK: - Actions + + @objc private func flexButtonTapped() { + // Show FLEX explorer + flexIntegration.showExplorer() + } +} + +// MARK: - UITableViewDataSource + +extension ClassDetailsViewController: UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + return 4 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch section { + case 0: + return properties.count + case 1: + return methods.count + case 2: + return ivars.count + case 3: + return protocols.count + default: + return 0 + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "DetailCell", for: indexPath) + + // Configure cell + switch indexPath.section { + case 0: + cell.textLabel?.text = properties[indexPath.row] + case 1: + cell.textLabel?.text = methods[indexPath.row] + case 2: + cell.textLabel?.text = ivars[indexPath.row] + case 3: + cell.textLabel?.text = protocols[indexPath.row] + default: + break + } + + return cell + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch section { + case 0: + return "Properties (\(properties.count))" + case 1: + return "Methods (\(methods.count))" + case 2: + return "Ivars (\(ivars.count))" + case 3: + return "Protocols (\(protocols.count))" + default: + return nil + } + } +} + +// MARK: - UITableViewDelegate + +extension ClassDetailsViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + // Get item + let item: String + switch indexPath.section { + case 0: + item = properties[indexPath.row] + case 1: + item = methods[indexPath.row] + case 2: + item = ivars[indexPath.row] + case 3: + item = protocols[indexPath.row] + default: + return + } + + // Show item details + let alert = UIAlertController( + title: item, + message: "Class: \(className)", + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: "OK", style: .default)) + + present(alert, animated: true) + } +} \ No newline at end of file diff --git a/iOS/Debugger/UI/SystemLogViewController.swift b/iOS/Debugger/UI/SystemLogViewController.swift new file mode 100644 index 0000000..490e1bd --- /dev/null +++ b/iOS/Debugger/UI/SystemLogViewController.swift @@ -0,0 +1,401 @@ +import UIKit +import OSLog + +/// View controller for displaying system logs +class SystemLogViewController: UIViewController { + // MARK: - Properties + + /// Text view for displaying logs + private let textView = UITextView() + + /// Toolbar for controls + private let toolbar = UIToolbar() + + /// Activity indicator for loading + private let activityIndicator = UIActivityIndicatorView(style: .medium) + + /// Logger instance + private let logger = Debug.shared + + /// Log level filter + private var logLevelFilter: LogType? + + /// Search text + private var searchText: String? + + /// Timer for auto-refresh + private var refreshTimer: Timer? + + /// Auto-refresh interval in seconds + private var refreshInterval: TimeInterval = 5.0 + + /// Flag indicating if auto-refresh is enabled + private var autoRefreshEnabled = false + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + setupUI() + loadLogs() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + // Stop refresh timer + refreshTimer?.invalidate() + refreshTimer = nil + } + + // MARK: - Setup + + private func setupUI() { + // Set title + title = "System Log" + + // Set up text view + textView.isEditable = false + textView.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular) + textView.translatesAutoresizingMaskIntoConstraints = false + + // Set up toolbar + toolbar.translatesAutoresizingMaskIntoConstraints = false + + // Add toolbar items + let refreshButton = UIBarButtonItem( + barButtonSystemItem: .refresh, + target: self, + action: #selector(refreshButtonTapped) + ) + + let filterButton = UIBarButtonItem( + image: UIImage(systemName: "line.horizontal.3.decrease.circle"), + style: .plain, + target: self, + action: #selector(filterButtonTapped) + ) + + let searchButton = UIBarButtonItem( + barButtonSystemItem: .search, + target: self, + action: #selector(searchButtonTapped) + ) + + let autoRefreshButton = UIBarButtonItem( + image: UIImage(systemName: "timer"), + style: .plain, + target: self, + action: #selector(autoRefreshButtonTapped) + ) + + let clearButton = UIBarButtonItem( + barButtonSystemItem: .trash, + target: self, + action: #selector(clearButtonTapped) + ) + + let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + + toolbar.items = [ + refreshButton, + flexibleSpace, + filterButton, + flexibleSpace, + searchButton, + flexibleSpace, + autoRefreshButton, + flexibleSpace, + clearButton + ] + + // Set up activity indicator + activityIndicator.hidesWhenStopped = true + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + + // Add subviews + view.addSubview(toolbar) + view.addSubview(textView) + view.addSubview(activityIndicator) + + // Set up constraints + NSLayoutConstraint.activate([ + toolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor), + toolbar.heightAnchor.constraint(equalToConstant: 44), + + textView.topAnchor.constraint(equalTo: toolbar.bottomAnchor), + textView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + textView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + textView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + + // Add navigation buttons + navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .action, + target: self, + action: #selector(shareButtonTapped) + ) + } + + // MARK: - Log Loading + + private func loadLogs() { + // Show activity indicator + activityIndicator.startAnimating() + + // Load logs asynchronously + DispatchQueue.global(qos: .userInitiated).async { + // Get app log file path + let logFilePath = self.getDocumentsDirectory().appendingPathComponent("logs.txt") + + do { + // Read log file contents + var logContents = try String(contentsOf: logFilePath, encoding: .utf8) + + // Apply filter if needed + if let filter = self.logLevelFilter { + logContents = self.filterLogs(logContents, byLevel: filter) + } + + // Apply search if needed + if let search = self.searchText, !search.isEmpty { + logContents = self.filterLogs(logContents, bySearchText: search) + } + + // Update UI on main thread + DispatchQueue.main.async { + self.textView.text = logContents + + // Scroll to bottom + if !logContents.isEmpty { + let bottom = NSRange(location: logContents.count - 1, length: 1) + self.textView.scrollRangeToVisible(bottom) + } + + self.activityIndicator.stopAnimating() + } + + } catch { + // Handle error + DispatchQueue.main.async { + self.textView.text = "Error loading logs: \(error.localizedDescription)" + self.activityIndicator.stopAnimating() + } + } + } + } + + private func filterLogs(_ logs: String, byLevel level: LogType) -> String { + // Get emoji for the log level + let emoji: String + switch level { + case .success: + emoji = "✅" + case .info: + emoji = "â„šī¸" + case .debug: + emoji = "🐛" + case .trace: + emoji = "🔍" + case .warning: + emoji = "âš ī¸" + case .error: + emoji = "❌" + case .critical, .fault: + emoji = "đŸ”Ĩ" + default: + emoji = "📝" + } + + // Filter logs by emoji + let lines = logs.components(separatedBy: .newlines) + let filteredLines = lines.filter { $0.contains(emoji) } + return filteredLines.joined(separator: "\n") + } + + private func filterLogs(_ logs: String, bySearchText searchText: String) -> String { + let lines = logs.components(separatedBy: .newlines) + let filteredLines = lines.filter { $0.lowercased().contains(searchText.lowercased()) } + return filteredLines.joined(separator: "\n") + } + + private func getDocumentsDirectory() -> URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + return paths[0] + } + + // MARK: - Actions + + @objc private func refreshButtonTapped() { + loadLogs() + } + + @objc private func filterButtonTapped() { + // Create alert controller with filter options + let alert = UIAlertController( + title: "Filter Logs", + message: "Select log level to filter by", + preferredStyle: .actionSheet + ) + + // Add filter options + alert.addAction(UIAlertAction(title: "All Logs", style: .default) { _ in + self.logLevelFilter = nil + self.loadLogs() + }) + + alert.addAction(UIAlertAction(title: "Info", style: .default) { _ in + self.logLevelFilter = .info + self.loadLogs() + }) + + alert.addAction(UIAlertAction(title: "Debug", style: .default) { _ in + self.logLevelFilter = .debug + self.loadLogs() + }) + + alert.addAction(UIAlertAction(title: "Warning", style: .default) { _ in + self.logLevelFilter = .warning + self.loadLogs() + }) + + alert.addAction(UIAlertAction(title: "Error", style: .default) { _ in + self.logLevelFilter = .error + self.loadLogs() + }) + + alert.addAction(UIAlertAction(title: "Critical", style: .default) { _ in + self.logLevelFilter = .critical + self.loadLogs() + }) + + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + + // For iPad, set the source view and rect + if let popoverController = alert.popoverPresentationController { + if let filterButton = toolbar.items?[2] { + popoverController.barButtonItem = filterButton + } else { + popoverController.sourceView = view + popoverController.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0) + } + } + + present(alert, animated: true) + } + + @objc private func searchButtonTapped() { + // Create alert controller for search + let alert = UIAlertController( + title: "Search Logs", + message: "Enter text to search for", + preferredStyle: .alert + ) + + // Add text field + alert.addTextField { textField in + textField.placeholder = "Search text" + textField.text = self.searchText + } + + // Add actions + alert.addAction(UIAlertAction(title: "Search", style: .default) { _ in + if let text = alert.textFields?.first?.text, !text.isEmpty { + self.searchText = text + self.loadLogs() + } + }) + + alert.addAction(UIAlertAction(title: "Clear Search", style: .destructive) { _ in + self.searchText = nil + self.loadLogs() + }) + + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + + present(alert, animated: true) + } + + @objc private func autoRefreshButtonTapped() { + // Toggle auto-refresh + autoRefreshEnabled = !autoRefreshEnabled + + if autoRefreshEnabled { + // Start refresh timer + refreshTimer = Timer.scheduledTimer( + timeInterval: refreshInterval, + target: self, + selector: #selector(refreshButtonTapped), + userInfo: nil, + repeats: true + ) + + // Update button appearance + if let autoRefreshButton = toolbar.items?[6] { + autoRefreshButton.image = UIImage(systemName: "timer.fill") + } + + logger.log(message: "Auto-refresh enabled", type: .info) + + } else { + // Stop refresh timer + refreshTimer?.invalidate() + refreshTimer = nil + + // Update button appearance + if let autoRefreshButton = toolbar.items?[6] { + autoRefreshButton.image = UIImage(systemName: "timer") + } + + logger.log(message: "Auto-refresh disabled", type: .info) + } + } + + @objc private func clearButtonTapped() { + // Confirm clear + let alert = UIAlertController( + title: "Clear Log View", + message: "This will clear the current log view but won't delete the log file.", + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: "Clear", style: .destructive) { _ in + self.textView.text = "" + }) + + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + + present(alert, animated: true) + } + + @objc private func shareButtonTapped() { + // Create temporary file with log contents + let tempFileURL = FileManager.default.temporaryDirectory.appendingPathComponent("app_logs.txt") + + do { + try textView.text.write(to: tempFileURL, atomically: true, encoding: .utf8) + + // Create activity view controller + let activityViewController = UIActivityViewController( + activityItems: [tempFileURL], + applicationActivities: nil + ) + + // Present activity view controller + if let popoverController = activityViewController.popoverPresentationController { + popoverController.barButtonItem = navigationItem.rightBarButtonItem + } + + present(activityViewController, animated: true) + + } catch { + logger.log(message: "Error creating log file for sharing: \(error.localizedDescription)", type: .error) + } + } +} \ No newline at end of file diff --git a/iOS/Debugger/UI/VariablesViewController.swift b/iOS/Debugger/UI/VariablesViewController.swift index 610c22d..24e1c5e 100644 --- a/iOS/Debugger/UI/VariablesViewController.swift +++ b/iOS/Debugger/UI/VariablesViewController.swift @@ -7,6 +7,9 @@ import UIKit /// The debugger engine private let debuggerEngine = DebuggerEngine.shared + + /// FLEX integration + private let flexIntegration = FLEXIntegration.shared /// Logger instance private let logger = Debug.shared @@ -42,6 +45,9 @@ import UIKit /// Current search text private var searchText: String = "" + + /// Object to focus on + private var focusedObject: Any? // MARK: - Lifecycle @@ -64,6 +70,88 @@ import UIKit // Reload variables when view appears reloadVariables() + + // If we have a focused object, show it + if let focusedObject = focusedObject { + showObjectDetails(focusedObject) + self.focusedObject = nil + } + } + + // MARK: - Public Methods + + /// Focus on a specific object + /// - Parameter object: The object to focus on + func focusOn(object: Any) { + focusedObject = object + + // If view is already loaded, show the object details immediately + if isViewLoaded && view.window != nil { + showObjectDetails(object) + focusedObject = nil + } + } + + /// Show details for an object + /// - Parameter object: The object to show details for + private func showObjectDetails(_ object: Any) { + // Create a variable representation of the object + let objectVariable = Variable( + name: String(describing: type(of: object)), + type: String(describing: type(of: object)), + value: String(describing: object), + summary: String(describing: object), + children: nil + ) + + // Show variable details alert + let alertController = UIAlertController( + title: objectVariable.name, + message: "Type: \(objectVariable.type)\nValue: \(objectVariable.value)\nSummary: \(objectVariable.summary)", + preferredStyle: .alert + ) + + // Add explore action + let exploreAction = UIAlertAction(title: "Explore with FLEX", style: .default) { [weak self] _ in + guard let self = self else { return } + + // Use FLEX to explore the object + DispatchQueue.main.async { + self.flexIntegration.presentObjectExplorer(object) + } + } + + // Add print action + let printAction = UIAlertAction(title: "Print Description", style: .default) { [weak self] _ in + guard let self = self else { return } + + // Show result + let resultAlert = UIAlertController( + title: "Object Description", + message: String(describing: object), + preferredStyle: .alert + ) + + // Add OK action + let okAction = UIAlertAction(title: "OK", style: .default) + + // Add actions + resultAlert.addAction(okAction) + + // Present alert + self.present(resultAlert, animated: true) + } + + // Add OK action + let okAction = UIAlertAction(title: "OK", style: .default) + + // Add actions + alertController.addAction(exploreAction) + alertController.addAction(printAction) + alertController.addAction(okAction) + + // Present alert + present(alertController, animated: true) } // MARK: - Setup