chore: handle NSExceptions from swift helper

This commit is contained in:
haritabh-z01 2026-01-08 15:44:34 +05:30
parent 6357709edf
commit d07b6bf5c6
5 changed files with 177 additions and 63 deletions

View file

@ -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")

View file

@ -0,0 +1,34 @@
#import "ObjCExceptionCatcher.h"
@interface ObjCExceptionInfo ()
@property (nonatomic, readwrite) NSString *name;
@property (nonatomic, readwrite) NSString *reason;
@property (nonatomic, readwrite, nullable) NSArray<NSString *> *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

View file

@ -0,0 +1,22 @@
#import <Foundation/Foundation.h>
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<NSString *> *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

View file

@ -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<T> {
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`<T>(_ block: @escaping () -> T?) -> ExceptionResult<T?> {
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)
}
}

View file

@ -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)
}