chore: handle NSExceptions from swift helper
This commit is contained in:
parent
6357709edf
commit
d07b6bf5c6
5 changed files with 177 additions and 63 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue