diff --git a/packages/native-helpers/swift-helper/Package.swift b/packages/native-helpers/swift-helper/Package.swift index 748be46..94829b9 100644 --- a/packages/native-helpers/swift-helper/Package.swift +++ b/packages/native-helpers/swift-helper/Package.swift @@ -13,11 +13,14 @@ let package = Package( // .package(url: /* package url */, from: "1.0.0"), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( - name: "SwiftHelper", + name: "ObjCExceptionCatcher", dependencies: [], + publicHeadersPath: "include" + ), + .executableTarget( + name: "SwiftHelper", + dependencies: ["ObjCExceptionCatcher"], resources: [ .embedInCode("Resources/rec-start.mp3"), .embedInCode("Resources/rec-stop.mp3") diff --git a/packages/native-helpers/swift-helper/Sources/ObjCExceptionCatcher/ObjCExceptionCatcher.m b/packages/native-helpers/swift-helper/Sources/ObjCExceptionCatcher/ObjCExceptionCatcher.m new file mode 100644 index 0000000..942547c --- /dev/null +++ b/packages/native-helpers/swift-helper/Sources/ObjCExceptionCatcher/ObjCExceptionCatcher.m @@ -0,0 +1,34 @@ +#import "ObjCExceptionCatcher.h" + +@interface ObjCExceptionInfo () +@property (nonatomic, readwrite) NSString *name; +@property (nonatomic, readwrite) NSString *reason; +@property (nonatomic, readwrite, nullable) NSArray *callStackSymbols; +@end + +@implementation ObjCExceptionInfo +@end + +@implementation ObjCExceptionCatcher + ++ (nullable id)catchException:(id _Nullable (^)(void))block + exceptionInfo:(ObjCExceptionInfo * _Nullable * _Nullable)exceptionInfo { + @try { + return block(); + } + @catch (NSException *exception) { + NSLog(@"[ObjCExceptionCatcher] Caught NSException: %@ - %@", + exception.name, exception.reason); + + if (exceptionInfo) { + ObjCExceptionInfo *info = [[ObjCExceptionInfo alloc] init]; + info.name = exception.name ?: @"Unknown"; + info.reason = exception.reason ?: @"Unknown reason"; + info.callStackSymbols = exception.callStackSymbols; + *exceptionInfo = info; + } + return nil; + } +} + +@end diff --git a/packages/native-helpers/swift-helper/Sources/ObjCExceptionCatcher/include/ObjCExceptionCatcher.h b/packages/native-helpers/swift-helper/Sources/ObjCExceptionCatcher/include/ObjCExceptionCatcher.h new file mode 100644 index 0000000..9beee33 --- /dev/null +++ b/packages/native-helpers/swift-helper/Sources/ObjCExceptionCatcher/include/ObjCExceptionCatcher.h @@ -0,0 +1,22 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Information about a caught NSException +@interface ObjCExceptionInfo : NSObject +@property (nonatomic, readonly) NSString *name; +@property (nonatomic, readonly) NSString *reason; +@property (nonatomic, readonly, nullable) NSArray *callStackSymbols; +@end + +/// Catches Objective-C NSExceptions and converts them to Swift-friendly errors +@interface ObjCExceptionCatcher : NSObject + +/// Executes a block and catches any NSException +/// Returns the block result on success, or nil + exception info on failure ++ (nullable id)catchException:(id _Nullable (^)(void))block + exceptionInfo:(ObjCExceptionInfo * _Nullable * _Nullable)exceptionInfo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/native-helpers/swift-helper/Sources/SwiftHelper/ExceptionCatcher.swift b/packages/native-helpers/swift-helper/Sources/SwiftHelper/ExceptionCatcher.swift new file mode 100644 index 0000000..eeffab7 --- /dev/null +++ b/packages/native-helpers/swift-helper/Sources/SwiftHelper/ExceptionCatcher.swift @@ -0,0 +1,41 @@ +import Foundation +import ObjCExceptionCatcher + +/// Details about a caught NSException +struct CaughtException { + let name: String + let reason: String + let callStack: [String] +} + +/// Result type for exception-catching operations +enum ExceptionResult { + case success(T) + case exception(CaughtException) +} + +/// Swift-friendly wrapper for catching NSExceptions +enum ExceptionCatcher { + /// Executes a closure and catches any NSException + /// Returns .success with result, or .exception with details + static func `try`(_ block: @escaping () -> T?) -> ExceptionResult { + var exceptionInfo: ObjCExceptionInfo? + let result = ObjCExceptionCatcher.catchException({ + return block() as Any? + }, exceptionInfo: &exceptionInfo) + + if let info = exceptionInfo { + let exception = CaughtException( + name: info.name, + reason: info.reason, + callStack: info.callStackSymbols ?? [] + ) + FileHandle.standardError.write( + "[ExceptionCatcher] NSException caught: \(info.name) - \(info.reason)\n".data(using: .utf8)! + ) + return .exception(exception) + } + + return .success(result as? T) + } +} diff --git a/packages/native-helpers/swift-helper/Sources/SwiftHelper/RpcHandler.swift b/packages/native-helpers/swift-helper/Sources/SwiftHelper/RpcHandler.swift index 64264a0..a198a5d 100644 --- a/packages/native-helpers/swift-helper/Sources/SwiftHelper/RpcHandler.swift +++ b/packages/native-helpers/swift-helper/Sources/SwiftHelper/RpcHandler.swift @@ -1,4 +1,5 @@ import Foundation +import ObjCExceptionCatcher class IOBridge: NSObject { private let jsonEncoder: JSONEncoder @@ -258,59 +259,53 @@ class IOBridge: NSObject { } // Fetch REAL accessibility tree data using the service - let actualTreeData: AccessibilityElementNode? = - accessibilityService.fetchFullAccessibilityTree(rootId: accessibilityParams?.rootID) + switch ExceptionCatcher.try({ + self.accessibilityService.fetchFullAccessibilityTree(rootId: accessibilityParams?.rootID) + }) { + case .success(let actualTreeData): + logToStderr("[IOBridge] Fetched actualTreeData. Is nil? \(actualTreeData == nil). For ID: \(request.id)") - logToStderr( - "[IOBridge] Fetched actualTreeData from AccessibilityService. Is nil? \(actualTreeData == nil). For ID: \(request.id)" - ) - - var treeAsJsonAny: JSONAny? = nil - if let dataToEncode = actualTreeData { - do { - let encodedData = try jsonEncoder.encode(dataToEncode) - treeAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: encodedData) - if let treeDataForLog = try? jsonEncoder.encode(treeAsJsonAny), - let treeStringForLog = String(data: treeDataForLog, encoding: .utf8) - { - logToStderr( - "[IOBridge] treeAsJsonAny (after encoding actualTreeData): \(treeStringForLog) for ID: \(request.id)" - ) + var treeAsJsonAny: JSONAny? = nil + if let dataToEncode = actualTreeData { + do { + let encodedData = try jsonEncoder.encode(dataToEncode) + treeAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: encodedData) + } catch { + logToStderr("[IOBridge] Error encoding actualTreeData: \(error.localizedDescription)") } + } + + let resultPayload = GetAccessibilityTreeDetailsResultSchema(tree: treeAsJsonAny) + var resultAsJsonAny: JSONAny? = nil + do { + let resultPayloadData = try jsonEncoder.encode(resultPayload) + resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultPayloadData) } catch { - logToStderr( - "[IOBridge] Error encoding actualTreeData to JSONAny: \(error.localizedDescription) for ID: \(request.id)" - ) + logToStderr("Error encoding result: \(error.localizedDescription)") } - } + let rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny) + sendRpcResponse(rpcResponse) - let resultPayload = GetAccessibilityTreeDetailsResultSchema(tree: treeAsJsonAny) - do { - let resultPayloadForLogData = try jsonEncoder.encode(resultPayload) - if let resultPayloadStringForLog = String( - data: resultPayloadForLogData, encoding: .utf8) - { - logToStderr( - "[IOBridge] GetAccessibilityTreeDetailsResultSchema (resultPayload) before final encoding: \(resultPayloadStringForLog) for ID: \(request.id)" - ) + case .exception(let exception): + logToStderr("[IOBridge] NSException in fetchFullAccessibilityTree: \(exception.name) - \(exception.reason)") + let exceptionData: [String: Any] = [ + "name": exception.name, + "reason": exception.reason, + "callStack": exception.callStack.prefix(10).joined(separator: "\n") + ] + var exceptionJsonAny: JSONAny? = nil + if let jsonData = try? JSONSerialization.data(withJSONObject: exceptionData), + let decoded = try? jsonDecoder.decode(JSONAny.self, from: jsonData) { + exceptionJsonAny = decoded } - } catch { - logToStderr( - "[IOBridge] Error encoding resultPayload for logging: \(error.localizedDescription) for ID: \(request.id)" + let errPayload = Error( + code: -32603, + data: exceptionJsonAny, + message: "\(exception.name): \(exception.reason)" ) + let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) + sendRpcResponse(rpcResponse) } - - var resultAsJsonAny: JSONAny? = nil - do { - let resultPayloadData = try jsonEncoder.encode(resultPayload) - resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultPayloadData) - } catch { - logToStderr( - "Error encoding GetAccessibilityTreeDetailsResultSchema to JSONAny: \(error.localizedDescription)" - ) - } - let rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny) - sendRpcResponse(rpcResponse) } private func handleAccessibilityContext(_ request: RPCRequestSchema) { @@ -339,23 +334,42 @@ class IOBridge: NSObject { } let editableOnly = contextParams?.editableOnly ?? false - let context = AccessibilityContextService.getAccessibilityContext( - editableOnly: editableOnly) - logToStderr("[IOBridge] Retrieved context for ID: \(request.id)") - let resultPayload = GetAccessibilityContextResultSchema(context: context) - do { - let resultData = try jsonEncoder.encode(resultPayload) - let resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultData) - let rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny) - sendRpcResponse(rpcResponse) - } catch { - logToStderr( - "[IOBridge] Error encoding getAccessibilityContext result: \(error.localizedDescription) for ID: \(request.id)" - ) + switch ExceptionCatcher.try({ + AccessibilityContextService.getAccessibilityContext(editableOnly: editableOnly) + }) { + case .success(let context): + logToStderr("[IOBridge] Retrieved context for ID: \(request.id)") + let resultPayload = GetAccessibilityContextResultSchema(context: context) + do { + let resultData = try jsonEncoder.encode(resultPayload) + let resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultData) + let rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny) + sendRpcResponse(rpcResponse) + } catch { + logToStderr("[IOBridge] Error encoding result: \(error.localizedDescription) for ID: \(request.id)") + let errPayload = Error(code: -32603, data: nil, message: "Error encoding result: \(error.localizedDescription)") + let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) + sendRpcResponse(rpcResponse) + } + + case .exception(let exception): + logToStderr("[IOBridge] NSException in getAccessibilityContext: \(exception.name) - \(exception.reason)") + let exceptionData: [String: Any] = [ + "name": exception.name, + "reason": exception.reason, + "callStack": exception.callStack.prefix(10).joined(separator: "\n") + ] + var exceptionJsonAny: JSONAny? = nil + if let jsonData = try? JSONSerialization.data(withJSONObject: exceptionData), + let decoded = try? jsonDecoder.decode(JSONAny.self, from: jsonData) { + exceptionJsonAny = decoded + } let errPayload = Error( - code: -32603, data: nil, - message: "Error encoding result: \(error.localizedDescription)") + code: -32603, + data: exceptionJsonAny, + message: "\(exception.name): \(exception.reason)" + ) let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) sendRpcResponse(rpcResponse) }