amical/packages/native-helpers/swift-helper/Sources/SwiftHelper/RpcHandler.swift
2026-01-08 15:44:34 +05:30

378 lines
18 KiB
Swift

import Foundation
import ObjCExceptionCatcher
class IOBridge: NSObject {
private let jsonEncoder: JSONEncoder
private let jsonDecoder: JSONDecoder
private let accessibilityService: AccessibilityService
private let audioService: AudioService
private let dateFormatter: DateFormatter
init(jsonEncoder: JSONEncoder, jsonDecoder: JSONDecoder) {
self.jsonEncoder = jsonEncoder
self.jsonDecoder = jsonDecoder
self.accessibilityService = AccessibilityService()
self.audioService = AudioService() // Audio preloaded here at startup
self.dateFormatter = DateFormatter()
self.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
super.init()
}
private func logToStderr(_ message: String) {
let timestamp = dateFormatter.string(from: Date())
let logMessage = "[\(timestamp)] \(message)\n"
FileHandle.standardError.write(logMessage.data(using: .utf8)!)
}
// Handles a single RPC Request
func handleRpcRequest(_ request: RPCRequestSchema) {
var rpcResponse: RPCResponseSchema
switch request.method {
case .getAccessibilityTreeDetails:
// Process accessibility tree requests on dedicated thread
AccessibilityQueue.shared.async { [weak self] in
guard let self = self else { return }
self.handleAccessibilityTreeDetails(request)
}
return
case .getAccessibilityContext:
// Process accessibility context requests on dedicated thread
AccessibilityQueue.shared.async { [weak self] in
guard let self = self else { return }
self.handleAccessibilityContext(request)
}
return
case .pasteText:
logToStderr("[IOBridge] Handling pasteText for ID: \(request.id)")
guard let paramsAnyCodable = request.params else {
let errPayload = Error(
code: -32602, data: nil, message: "Missing params for pasteText")
rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil)
sendRpcResponse(rpcResponse)
return
}
do {
let paramsData = try jsonEncoder.encode(paramsAnyCodable)
// Corrected to use generated Swift model name from models.swift
let pasteParams = try jsonDecoder.decode(
PasteTextParamsSchema.self, from: paramsData)
logToStderr("[IOBridge] Decoded pasteParams.transcript for ID: \(request.id)")
// Call the actual paste function (to be implemented in AccessibilityService or similar)
let success = accessibilityService.pasteText(transcript: pasteParams.transcript)
// Corrected to use generated Swift model name from models.swift
let resultPayload = PasteTextResultSchema(
message: success ? "Pasted successfully" : "Paste failed", success: success)
let resultData = try jsonEncoder.encode(resultPayload)
let resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultData)
rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny)
} catch {
logToStderr(
"[IOBridge] Error processing pasteText params or operation: \(error.localizedDescription) for ID: \(request.id)"
)
let errPayload = Error(
code: -32602, data: request.params,
message: "Invalid params or error during paste: \(error.localizedDescription)")
rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil)
}
case .muteSystemAudio:
logToStderr("[IOBridge] Handling muteSystemAudio for ID: \(request.id)")
audioService.playSound(named: "rec-start") { [weak self] in
guard let self = self else {
let timestamp = DateFormatter().string(from: Date())
let logMessage =
"[\(timestamp)] [IOBridge] self is nil in playSound completion for muteSystemAudio. ID: \(request.id)\n"
FileHandle.standardError.write(logMessage.data(using: .utf8)!)
return
}
self.logToStderr(
"[IOBridge] rec-start.mp3 finished playing successfully. Proceeding to mute system audio. ID: \(request.id)"
)
let success = self.accessibilityService.muteSystemAudio()
let resultPayload = MuteSystemAudioResultSchema(
message: success ? "Mute command sent" : "Failed to send mute command",
success: success)
var responseToSend: RPCResponseSchema
do {
let resultData = try self.jsonEncoder.encode(resultPayload)
let resultAsJsonAny = try self.jsonDecoder.decode(
JSONAny.self, from: resultData)
responseToSend = RPCResponseSchema(
error: nil, id: request.id, result: resultAsJsonAny)
} catch {
self.logToStderr(
"[IOBridge] Error encoding muteSystemAudio result: \(error.localizedDescription) for ID: \(request.id)"
)
let errPayload = Error(
code: -32603, data: nil,
message: "Error encoding result: \(error.localizedDescription)")
responseToSend = RPCResponseSchema(
error: errPayload, id: request.id, result: nil)
}
self.sendRpcResponse(responseToSend)
}
return
case .restoreSystemAudio:
logToStderr("[IOBridge] Handling restoreSystemAudio for ID: \(request.id)")
let success = accessibilityService.restoreSystemAudio()
if success { // Play sound only if restore was successful
audioService.playSound(named: "rec-stop")
}
let resultPayload = RestoreSystemAudioResultSchema(
message: success ? "Restore command sent" : "Failed to send restore command",
success: success)
do {
let resultData = try jsonEncoder.encode(resultPayload)
let resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultData)
rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny)
} catch {
logToStderr(
"[IOBridge] Error encoding pauseSystemAudio result: \(error.localizedDescription) for ID: \(request.id)"
)
let errPayload = Error(
code: -32603, data: nil,
message: "Error encoding result: \(error.localizedDescription)")
rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: nil)
}
case .setShortcuts:
logToStderr("[IOBridge] Handling setShortcuts for ID: \(request.id)")
guard let paramsAnyCodable = request.params else {
let errPayload = Error(
code: -32602, data: nil, message: "Missing params for setShortcuts")
rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil)
sendRpcResponse(rpcResponse)
return
}
do {
let paramsData = try jsonEncoder.encode(paramsAnyCodable)
let shortcutsParams = try jsonDecoder.decode(
SetShortcutsParamsSchema.self, from: paramsData)
// Update the ShortcutManager with the new shortcuts
ShortcutManager.shared.setShortcuts(
pushToTalk: shortcutsParams.pushToTalk,
toggleRecording: shortcutsParams.toggleRecording
)
let resultPayload = SetShortcutsResultSchema(success: true)
let resultData = try jsonEncoder.encode(resultPayload)
let resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultData)
rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny)
} catch {
logToStderr(
"[IOBridge] Error processing setShortcuts params: \(error.localizedDescription) for ID: \(request.id)"
)
let errPayload = Error(
code: -32602, data: request.params,
message: "Invalid params: \(error.localizedDescription)")
rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil)
}
default:
logToStderr("[IOBridge] Method not found: \(request.method) for ID: \(request.id)")
let errPayload = Error(
code: -32601, data: nil, message: "Method not found: \(request.method)")
rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil)
}
sendRpcResponse(rpcResponse)
}
private func sendRpcResponse(_ response: RPCResponseSchema) {
do {
let responseData = try jsonEncoder.encode(response)
if let responseString = String(data: responseData, encoding: .utf8) {
logToStderr("[Swift Biz Logic] FINAL JSON RESPONSE to stdout: \(responseString)")
print(responseString)
fflush(stdout)
}
} catch {
logToStderr("Error encoding RpcResponse: \(error.localizedDescription)")
}
}
// Main loop for processing RPC requests from stdin
func processRpcRequests() {
logToStderr("IOBridge: Starting RPC request processing loop.")
while let line = readLine(strippingNewline: true) {
guard !line.isEmpty, let data = line.data(using: .utf8) else {
logToStderr("Warning: Received empty or non-UTF8 line on stdin.")
continue
}
do {
let rpcRequest = try jsonDecoder.decode(RPCRequestSchema.self, from: data)
logToStderr(
"IOBridge: Received RPC Request ID \(rpcRequest.id), Method: \(rpcRequest.method)"
)
handleRpcRequest(rpcRequest)
} catch {
logToStderr(
"Error decoding RpcRequest from stdin: \(error.localizedDescription). Line: \(line)"
)
// Consider sending a parse error if ID can be extracted
}
}
logToStderr("IOBridge: RPC request processing loop finished (stdin closed).")
}
// MARK: - Async RPC Handlers
private func handleAccessibilityTreeDetails(_ request: RPCRequestSchema) {
var accessibilityParams: GetAccessibilityTreeDetailsParamsSchema? = nil
logToStderr("[IOBridge] Handling getAccessibilityTreeDetails for ID: \(request.id)")
if let paramsAnyCodable = request.params {
do {
let paramsData = try jsonEncoder.encode(paramsAnyCodable)
accessibilityParams = try jsonDecoder.decode(
GetAccessibilityTreeDetailsParamsSchema.self, from: paramsData)
logToStderr(
"[IOBridge] Decoded accessibilityParams.rootID: \(accessibilityParams?.rootID ?? "nil") for ID: \(request.id)"
)
} catch {
logToStderr(
"[IOBridge] Error decoding getAccessibilityTreeDetails params: \(error.localizedDescription)"
)
let errPayload = Error(
code: -32602, data: request.params,
message: "Invalid params: \(error.localizedDescription)")
let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil)
sendRpcResponse(rpcResponse)
return
}
}
// Fetch REAL accessibility tree data using the service
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)")
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("Error encoding result: \(error.localizedDescription)")
}
let rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny)
sendRpcResponse(rpcResponse)
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
}
let errPayload = Error(
code: -32603,
data: exceptionJsonAny,
message: "\(exception.name): \(exception.reason)"
)
let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil)
sendRpcResponse(rpcResponse)
}
}
private func handleAccessibilityContext(_ request: RPCRequestSchema) {
var contextParams: GetAccessibilityContextParamsSchema? = nil
logToStderr("[IOBridge] Handling getAccessibilityContext for ID: \(request.id)")
if let paramsAnyCodable = request.params {
do {
let paramsData = try jsonEncoder.encode(paramsAnyCodable)
contextParams = try jsonDecoder.decode(
GetAccessibilityContextParamsSchema.self, from: paramsData)
logToStderr(
"[IOBridge] Decoded contextParams.editableOnly: \(contextParams?.editableOnly ?? false) for ID: \(request.id)"
)
} catch {
logToStderr(
"[IOBridge] Error decoding getAccessibilityContext params: \(error.localizedDescription)"
)
let errPayload = Error(
code: -32602, data: request.params,
message: "Invalid params: \(error.localizedDescription)")
let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil)
sendRpcResponse(rpcResponse)
return
}
}
let editableOnly = contextParams?.editableOnly ?? false
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: exceptionJsonAny,
message: "\(exception.name): \(exception.reason)"
)
let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil)
sendRpcResponse(rpcResponse)
}
}
}