Add observe feature for cli and fix various json encoding issues
This commit is contained in:
parent
f5fb728228
commit
5cbe85e85a
@ -91,8 +91,130 @@ public class AXorcist {
|
||||
}
|
||||
return foundElement
|
||||
}
|
||||
|
||||
// MARK: - Observe Command Handler
|
||||
|
||||
@MainActor
|
||||
public func handleObserve(
|
||||
for appIdentifierOrNil: String?,
|
||||
notifications: [String], // These are AXNotification strings
|
||||
includeElementDetails: [String],
|
||||
watchChildren: Bool, // Parameter not used yet
|
||||
commandId: String,
|
||||
debugCLI: Bool
|
||||
) async -> Bool {
|
||||
let appIdentifier = appIdentifierOrNil ?? AXMiscConstants.focusedApplicationKey
|
||||
axInfoLog("[AXorcist.handleObserve][CmdID: \(commandId)] Starting observe for app: \(appIdentifier), notifications: \(notifications.joined(separator: ", ")), details: \(includeElementDetails.joined(separator: ", "))")
|
||||
|
||||
guard let appElement = applicationElement(for: appIdentifier) else {
|
||||
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Application not found: \(appIdentifier)")
|
||||
return false
|
||||
}
|
||||
|
||||
var subscriptionTokens: [AXObserverCenter.SubscriptionToken] = []
|
||||
|
||||
// This callback now captures necessary variables from the outer scope.
|
||||
// It matches the AXNotificationSubscriptionHandler signature.
|
||||
let observerCallback: AXNotificationSubscriptionHandler = {
|
||||
// Captured: commandId, includeElementDetails, appIdentifier, appElement
|
||||
obsPid, notificationNameString, rawObservedElement, nsUserInfo in
|
||||
|
||||
let observedElement = Element(rawObservedElement)
|
||||
// Ensure appElement is valid for path generation, otherwise path might be too long/incorrect
|
||||
let elementPath = observedElement.generatePathArray(upTo: appElement.pid() == obsPid ? appElement : nil)
|
||||
|
||||
let (attributes, _) = getElementAttributes(
|
||||
element: observedElement,
|
||||
attributes: includeElementDetails, // Captured
|
||||
outputFormat: .smart
|
||||
)
|
||||
|
||||
|
||||
// Build a raw element dictionary (sanitized) using plain Swift types
|
||||
var sanitizedElement: [String: Any] = [:]
|
||||
if !attributes.isEmpty {
|
||||
var sanitizedAttrs: [String: Any] = [:]
|
||||
for (k, v) in attributes {
|
||||
sanitizedAttrs[k] = sanitizeValue(v.value)
|
||||
}
|
||||
sanitizedElement["attributes"] = sanitizedAttrs
|
||||
}
|
||||
if !elementPath.isEmpty {
|
||||
sanitizedElement["path"] = elementPath
|
||||
}
|
||||
|
||||
// Build overall payload with primitive types after sanitization
|
||||
let payloadRaw: [String: Any] = [
|
||||
"timestamp": Date().timeIntervalSince1970,
|
||||
"commandId": commandId,
|
||||
"notification": notificationNameString.rawValue,
|
||||
"pid": obsPid,
|
||||
"application": appIdentifier,
|
||||
"element": sanitizedElement.mapValues { $0 }
|
||||
]
|
||||
|
||||
|
||||
let safePayload = makeJSONCompatible(payloadRaw) as! [String: Any]
|
||||
|
||||
if let data = try? JSONSerialization.data(withJSONObject: safePayload, options: []),
|
||||
let jsonStr = String(data: data, encoding: .utf8) {
|
||||
fputs("\(jsonStr)\n", stdout)
|
||||
fflush(stdout)
|
||||
} else {
|
||||
fputs("{\"error\": \"Unencodable payload\"}\n", stderr)
|
||||
fflush(stderr)
|
||||
}
|
||||
}
|
||||
|
||||
var allSubscriptionsSuccessful = true
|
||||
for notificationNameString in notifications {
|
||||
// Ensure axNotificationName is valid before using it
|
||||
guard let axNotificationName = AXNotification(rawValue: notificationNameString) else {
|
||||
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Invalid notification name string: \(notificationNameString). Skipping.")
|
||||
continue // Skip to the next notification string
|
||||
}
|
||||
|
||||
guard let targetPid = appElement.pid() else {
|
||||
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Could not get PID for appElement: \(appIdentifier)")
|
||||
allSubscriptionsSuccessful = false
|
||||
break
|
||||
}
|
||||
|
||||
let result = AXObserverCenter.shared.subscribe(
|
||||
pid: targetPid,
|
||||
element: appElement, // Observe the application element itself
|
||||
notification: axNotificationName, // Now safely unwrapped
|
||||
handler: observerCallback
|
||||
)
|
||||
|
||||
switch result {
|
||||
case .success(let token):
|
||||
subscriptionTokens.append(token)
|
||||
axDebugLog("[AXorcist.handleObserve][CmdID: \(commandId)] Subscribed to \(notificationNameString) for \(appIdentifier) (PID: \(targetPid))")
|
||||
case .failure(let error):
|
||||
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Error subscribing to \(notificationNameString) for \(appIdentifier): \(error.description)")
|
||||
allSubscriptionsSuccessful = false
|
||||
break
|
||||
}
|
||||
if !allSubscriptionsSuccessful { break }
|
||||
}
|
||||
|
||||
if !allSubscriptionsSuccessful || subscriptionTokens.isEmpty {
|
||||
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Failed to subscribe to one or more notifications for \(appIdentifier). Cleaning up...")
|
||||
for token in subscriptionTokens {
|
||||
do {
|
||||
try AXObserverCenter.shared.unsubscribe(token: token)
|
||||
axDebugLog("[AXorcist.handleObserve][CmdID: \(commandId)] Unsubscribed token \(token.id) during cleanup.")
|
||||
} catch {
|
||||
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Error unsubscribing token \(token.id) during cleanup: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
axInfoLog("[AXorcist.handleObserve][CmdID: \(commandId)] Successfully subscribed to \(subscriptionTokens.count) notifications for \(appIdentifier). Streaming output to stdout.")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: The global function `findElementViaPathAndCriteria` (likely in a different file)
|
||||
// still needs to be refactored to use GlobalAXLogger and remove its logging parameters.
|
||||
// The call above anticipates this change.
|
||||
// NOTE: The global function `findElementViaPathAndCriteria`
|
||||
@ -365,3 +365,4 @@ public enum AXMiscConstants {
|
||||
public static let maxPathSegments = 20 // Limit for path segment generation to avoid infinite loops
|
||||
// pathHintAttributeKey was for Element.swift's pathHint property, which is different from AXAttributeNames.kAXPathHintAttribute
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import CoreGraphics // Import for CGPoint, CGSize, CGRect
|
||||
import Accessibility // Import for AXTextMarker, AXTextMarkerRange
|
||||
import ApplicationServices // For AXUIElement
|
||||
// It's likely AXorcist module, where this file lives, already imports Accessibility or AppKit,
|
||||
// which would make AXTextMarker and AXTextMarkerRange available.
|
||||
// If not, a more dynamic type check might be needed, or this file needs to import them.
|
||||
@ -125,7 +126,22 @@ public struct AnyCodable: Codable, @unchecked Sendable {
|
||||
try container.encode(url.absoluteString)
|
||||
case let rect as CGRect:
|
||||
try container.encode(["x": rect.origin.x, "y": rect.origin.y, "width": rect.size.width, "height": rect.size.height])
|
||||
case let notif as AXNotification:
|
||||
// AXorcist: Handle AXNotification by encoding its raw string value.
|
||||
try container.encode(notif.rawValue)
|
||||
case let attrStr as NSAttributedString:
|
||||
// AXorcist: Handle NSAttributedString by encoding its string content.
|
||||
try container.encode(attrStr.string)
|
||||
case let element as Element:
|
||||
// AXorcist: Handle AXorcist 'Element' type by encoding its string description as a fallback.
|
||||
// Prefer specific serialization if a structured JSON representation is needed.
|
||||
try container.encode(String(describing: element))
|
||||
case let axEl as AXUIElement:
|
||||
// AXorcist: Handle CoreFoundation AXUIElement by encoding its string description as a fallback.
|
||||
// This avoids direct encoding of an opaque CFType.
|
||||
try container.encode(String(describing: axEl))
|
||||
case let val where String(describing: type(of: val)) == "__NSCFType":
|
||||
// Fallback for other __NSCFType instances (CoreFoundation types not explicitly handled).
|
||||
try container.encode(String(describing: val))
|
||||
case let array as [AnyCodable]:
|
||||
try container.encode(array)
|
||||
|
||||
@ -22,6 +22,11 @@ public struct CommandEnvelope: Codable {
|
||||
public let point: CGPoint? // Added for getElementAtPoint
|
||||
public let pid: Int? // Added for getElementAtPoint (optional specific PID)
|
||||
|
||||
// Parameters for 'observe' command
|
||||
public let notifications: [String]?
|
||||
public let includeElementDetails: [String]?
|
||||
public let watchChildren: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case commandId
|
||||
case command
|
||||
@ -39,6 +44,10 @@ public struct CommandEnvelope: Codable {
|
||||
case subCommands
|
||||
case point
|
||||
case pid
|
||||
// CodingKeys for observe parameters
|
||||
case notifications
|
||||
case includeElementDetails
|
||||
case watchChildren
|
||||
}
|
||||
|
||||
// Added a public initializer for convenience, matching fields.
|
||||
@ -57,7 +66,12 @@ public struct CommandEnvelope: Codable {
|
||||
actionValue: AnyCodable? = nil,
|
||||
subCommands: [CommandEnvelope]? = nil,
|
||||
point: CGPoint? = nil,
|
||||
pid: Int? = nil) {
|
||||
pid: Int? = nil,
|
||||
// Init parameters for observe
|
||||
notifications: [String]? = nil,
|
||||
includeElementDetails: [String]? = nil,
|
||||
watchChildren: Bool? = nil
|
||||
) {
|
||||
self.commandId = commandId
|
||||
self.command = command
|
||||
self.application = application
|
||||
@ -74,6 +88,10 @@ public struct CommandEnvelope: Codable {
|
||||
self.subCommands = subCommands
|
||||
self.point = point
|
||||
self.pid = pid
|
||||
// Assignments for observe parameters
|
||||
self.notifications = notifications
|
||||
self.includeElementDetails = includeElementDetails
|
||||
self.watchChildren = watchChildren
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -78,3 +78,4 @@ extension Element {
|
||||
return setBooleanAttribute(AXAttributeNames.kAXHiddenAttribute, value: false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,13 +13,16 @@ extension Element {
|
||||
|
||||
var childCollector = ChildCollector() // ChildCollector will use GlobalAXLogger internally
|
||||
|
||||
print("[PRINT Element.children] Before collectDirectChildren for: \(self.briefDescription(option: .default))")
|
||||
collectDirectChildren(collector: &childCollector)
|
||||
print("[PRINT Element.children] After collectDirectChildren, collector has: \(childCollector.collectedChildrenCount()) unique children.")
|
||||
|
||||
if !strict { // Only collect alternatives if not strict
|
||||
collectAlternativeChildren(collector: &childCollector)
|
||||
collectApplicationWindows(collector: &childCollector)
|
||||
collectAlternativeChildren(collector: &childCollector)
|
||||
collectApplicationWindows(collector: &childCollector)
|
||||
}
|
||||
|
||||
print("[PRINT Element.children] Before finalizeResults, collector has: \(childCollector.collectedChildrenCount()) unique children.")
|
||||
let result = childCollector.finalizeResults()
|
||||
axDebugLog("Final children count: \(result?.count ?? 0)")
|
||||
return result
|
||||
@ -44,19 +47,19 @@ extension Element {
|
||||
if let directChildrenUI = childrenCFArray as? [AXUIElement] {
|
||||
axDebugLog(
|
||||
"[\(selfDescForLog)]: Successfully fetched and cast " +
|
||||
"\(directChildrenUI.count) direct children."
|
||||
"\(directChildrenUI.count) direct children."
|
||||
)
|
||||
collector.addChildren(from: directChildrenUI)
|
||||
} else {
|
||||
axDebugLog(
|
||||
"[\(selfDescForLog)]: kAXChildrenAttribute was a CFArray but failed to cast " +
|
||||
"to [AXUIElement]. TypeID: \(CFGetTypeID(childrenCFArray))"
|
||||
"to [AXUIElement]. TypeID: \(CFGetTypeID(childrenCFArray))"
|
||||
)
|
||||
}
|
||||
} else if let nonArrayValue = value {
|
||||
axDebugLog(
|
||||
"[\(selfDescForLog)]: kAXChildrenAttribute was not a CFArray. " +
|
||||
"TypeID: \(CFGetTypeID(nonArrayValue)). Value: \(String(describing: nonArrayValue))"
|
||||
"TypeID: \(CFGetTypeID(nonArrayValue)). Value: \(String(describing: nonArrayValue))"
|
||||
)
|
||||
} else {
|
||||
axDebugLog("[\(selfDescForLog)]: kAXChildrenAttribute was nil despite .success error code.")
|
||||
@ -80,7 +83,7 @@ extension Element {
|
||||
]
|
||||
axDebugLog(
|
||||
"Using pruned attribute list (\(alternativeAttributes.count) items) " +
|
||||
"to avoid heavy payloads for alternative children."
|
||||
"to avoid heavy payloads for alternative children."
|
||||
)
|
||||
|
||||
for attrName in alternativeAttributes {
|
||||
@ -141,7 +144,7 @@ private struct ChildCollector {
|
||||
if !limitReached {
|
||||
axWarningLog(
|
||||
"ChildCollector: Reached maximum children limit (\(maxChildrenPerElement)). " +
|
||||
"No more children will be added for this element."
|
||||
"No more children will be added for this element."
|
||||
)
|
||||
limitReached = true
|
||||
}
|
||||
@ -156,6 +159,11 @@ private struct ChildCollector {
|
||||
}
|
||||
}
|
||||
|
||||
// New public method to get the count of unique children
|
||||
public func collectedChildrenCount() -> Int {
|
||||
return uniqueChildrenSet.count
|
||||
}
|
||||
|
||||
func finalizeResults() -> [Element]? { // Removed dLog param
|
||||
if collectedChildren.isEmpty {
|
||||
axDebugLog("ChildCollector: No children found after all collection methods.")
|
||||
|
||||
@ -22,5 +22,6 @@ public enum CommandType: String, Codable {
|
||||
case extractText
|
||||
case ping
|
||||
case getElementAtPoint
|
||||
case observe
|
||||
// Add future commands here, ensuring case matches JSON or provide explicit raw value
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ extension AXorcist {
|
||||
case .ping:
|
||||
return processPingCommand(subCmdID)
|
||||
|
||||
case .collectAll, .batch: // Recursive batch calls are not supported
|
||||
case .collectAll, .batch, .observe:
|
||||
return processUnsupportedCommand(subCommandEnvelope, subCmdID: subCmdID)
|
||||
|
||||
@unknown default:
|
||||
|
||||
@ -93,7 +93,7 @@ extension AXorcist {
|
||||
logCollectAllStart(params)
|
||||
|
||||
// Get app element
|
||||
guard let appElement = await applicationElement(for: params.appIdentifier) else {
|
||||
guard let appElement = applicationElement(for: params.appIdentifier) else {
|
||||
return await createErrorResponse(
|
||||
commandId: params.effectiveCommandId,
|
||||
appIdentifier: params.appIdentifier,
|
||||
@ -202,7 +202,7 @@ extension AXorcist {
|
||||
let pathHintString = hint.joined(separator: " -> ")
|
||||
axDebugLog("[CollectAll] Navigating to path hint: \(pathHintString)")
|
||||
|
||||
guard let navigatedElement = await navigateToElement(
|
||||
guard let navigatedElement = navigateToElement(
|
||||
from: appElement,
|
||||
pathHint: hint,
|
||||
maxDepth: AXMiscConstants.defaultMaxDepthSearch
|
||||
|
||||
@ -24,35 +24,35 @@ private func elementMatchesAllCriteria(
|
||||
for (key, expectedValue) in criteria {
|
||||
if key == "PID" { // Special handling for PID
|
||||
guard let actualPid_t = await element.pid() else { // Uses existing pid() -> pid_t?, await call
|
||||
axDebugLog("Element [\\(elementDescriptionForLog)] failed to provide PID (for path component [\\(pathComponentForLog)]). No match.", file: #file, function: #function, line: #line)
|
||||
axDebugLog("Element [\(elementDescriptionForLog)] failed to provide PID (for path component [\(pathComponentForLog)]). No match.", file: #file, function: #function, line: #line)
|
||||
return false
|
||||
}
|
||||
let actualPid = Int(actualPid_t) // Convert pid_t to Int for comparison
|
||||
guard let expectedPid = Int(expectedValue) else {
|
||||
axDebugLog("Element [\\(elementDescriptionForLog)] PID criteria '\\(expectedValue)' is not a valid Int (for path component [\\(pathComponentForLog)]). No match.", file: #file, function: #function, line: #line)
|
||||
axDebugLog("Element [\(elementDescriptionForLog)] PID criteria '\(expectedValue)' is not a valid Int (for path component [\(pathComponentForLog)]). No match.", file: #file, function: #function, line: #line)
|
||||
return false
|
||||
}
|
||||
if actualPid != expectedPid {
|
||||
axDebugLog("Element [\\(elementDescriptionForLog)] PID [\\(actualPid)] != expected [\\(expectedPid)] (for path component [\\(pathComponentForLog)]). No match.", file: #file, function: #function, line: #line)
|
||||
axDebugLog("Element [\(elementDescriptionForLog)] PID [\(actualPid)] != expected [\(expectedPid)] (for path component [\(pathComponentForLog)]). No match.", file: #file, function: #function, line: #line)
|
||||
return false
|
||||
}
|
||||
axDebugLog("Element [\\(elementDescriptionForLog)] PID [\\(actualPid)] == expected [\\(expectedPid)] (for path component [\\(pathComponentForLog)]). Criterion met.", file: #file, function: #function, line: #line)
|
||||
axDebugLog("Element [\(elementDescriptionForLog)] PID [\(actualPid)] == expected [\(expectedPid)] (for path component [\(pathComponentForLog)]). Criterion met.", file: #file, function: #function, line: #line)
|
||||
} else { // Handle other attributes as before
|
||||
let fetchedAttributeValue: String? = await element.attribute(Attribute(key)) // await call
|
||||
axDebugLog("PathNav/EMAC: For element [\\(elementDescriptionForLog)], component [\\(pathComponentForLog)], attr [\\(key)], fetched value is: [\\(String(describing: fetchedAttributeValue))]. About to check if nil.", file: #file, function: #function, line: #line) // NEW DETAILED LOG
|
||||
axDebugLog("PathNav/EMAC: For element [\(elementDescriptionForLog)], component [\(pathComponentForLog)], attr [\(key)], fetched value is: [\(String(describing: fetchedAttributeValue))]. About to check if nil.", file: #file, function: #function, line: #line) // NEW DETAILED LOG
|
||||
|
||||
guard let actualValue = fetchedAttributeValue else {
|
||||
axDebugLog("Element [\\(elementDescriptionForLog)] lacks attribute [\\(key)] (value was nil after fetch) for path component [\\(pathComponentForLog)]. No match.", file: #file, function: #function, line: #line) // Modified log
|
||||
axDebugLog("Element [\(elementDescriptionForLog)] lacks attribute [\(key)] (value was nil after fetch) for path component [\(pathComponentForLog)]. No match.", file: #file, function: #function, line: #line) // Modified log
|
||||
return false
|
||||
}
|
||||
if actualValue != expectedValue {
|
||||
axDebugLog("Element [\\(elementDescriptionForLog)] attribute [\\(key)] value [\\(actualValue)] != expected [\\(expectedValue)] (for path component [\\(pathComponentForLog)]). No match.", file: #file, function: #function, line: #line)
|
||||
axDebugLog("Element [\(elementDescriptionForLog)] attribute [\(key)] value [\(actualValue)] != expected [\(expectedValue)] (for path component [\(pathComponentForLog)]). No match.", file: #file, function: #function, line: #line)
|
||||
return false
|
||||
}
|
||||
axDebugLog("Element [\\(elementDescriptionForLog)] attribute [\\(key)] value [\\(actualValue)] == expected [\\(expectedValue)] (for path component [\\(pathComponentForLog)]). Criterion met.", file: #file, function: #function, line: #line)
|
||||
axDebugLog("Element [\(elementDescriptionForLog)] attribute [\(key)] value [\(actualValue)] == expected [\(expectedValue)] (for path component [\(pathComponentForLog)]). Criterion met.", file: #file, function: #function, line: #line)
|
||||
}
|
||||
}
|
||||
axDebugLog("Element [\\(elementDescriptionForLog)] matches ALL criteria for path component [\\(pathComponentForLog)]. Match!", file: #file, function: #function, line: #line)
|
||||
axDebugLog("Element [\(elementDescriptionForLog)] matches ALL criteria for path component [\(pathComponentForLog)]. Match!", file: #file, function: #function, line: #line)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -79,8 +79,8 @@ internal func navigateToElement(
|
||||
|
||||
if index >= maxDepth {
|
||||
axDebugLog(
|
||||
"Navigation aborted: Path hint index \\(index) reached maxDepth \\(maxDepth). " +
|
||||
"Path so far: \\(currentPathSegmentForLog)",
|
||||
"Navigation aborted: Path hint index \(index) reached maxDepth \(maxDepth). " +
|
||||
"Path so far: \(currentPathSegmentForLog)",
|
||||
file: #file,
|
||||
function: #function,
|
||||
line: #line
|
||||
@ -92,7 +92,7 @@ internal func navigateToElement(
|
||||
guard !criteriaToMatch.isEmpty else {
|
||||
axErrorLog(
|
||||
"CRITICAL_NAV_PARSE_FAILURE_MARKER: Empty or unparsable criteria from " +
|
||||
"pathComponentString '\\(pathComponentString)'",
|
||||
"pathComponentString '\(pathComponentString)'",
|
||||
file: #file,
|
||||
function: #function,
|
||||
line: #line
|
||||
@ -115,7 +115,7 @@ internal func navigateToElement(
|
||||
}
|
||||
|
||||
axDebugLog(
|
||||
"Navigation successful. Final element: \\(currentElement.briefDescription(option: .default))",
|
||||
"Navigation successful. Final element: \(currentElement.briefDescription(option: .default))",
|
||||
file: #file,
|
||||
function: #function,
|
||||
line: #line
|
||||
@ -131,34 +131,54 @@ private func processPathComponent(
|
||||
criteriaToMatch: [String: String],
|
||||
currentPathSegmentForLog: String // For logging
|
||||
) async -> Element? {
|
||||
let briefDesc = currentElement.briefDescription(option: .default)
|
||||
logPathComponentProcessing(pathComponentString: pathComponentString, briefDesc: briefDesc)
|
||||
// DIRECT LOGGING ATTEMPT
|
||||
await GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC_DIRECT_LOG: Entered for \(pathComponentString)", file: #file, function: #function, line: Int(#line)))
|
||||
|
||||
var stepCounter = 0
|
||||
axDebugLog("PathNav/PPC: Step \(stepCounter). Before briefDesc.")
|
||||
stepCounter += 1
|
||||
let briefDesc = currentElement.briefDescription(option: .default)
|
||||
axDebugLog("PathNav/PPC: Step \(stepCounter). Before logPathComponentProcessing. BriefDesc: \(briefDesc)")
|
||||
stepCounter += 1
|
||||
logPathComponentProcessing(pathComponentString: pathComponentString, briefDesc: briefDesc)
|
||||
axDebugLog("PathNav/PPC: Step \(stepCounter). After logPathComponentProcessing. Before PRE-CALL FMIC.")
|
||||
stepCounter += 1
|
||||
|
||||
axDebugLog("PathNav/PPC: PRE-CALL FMIC (SIMPLE)", file: #file, function: #function, line: #line)
|
||||
try? await Task.sleep(nanoseconds: 100_000_000) // Diagnostic delay
|
||||
|
||||
axDebugLog("PathNav/PPC: Step \(stepCounter). After PRE-CALL FMIC. Before findMatchingChild call.")
|
||||
stepCounter += 1
|
||||
|
||||
// Priority 1: Check children
|
||||
if let matchedChild = await findMatchingChild(
|
||||
currentElement: currentElement,
|
||||
criteriaToMatch: criteriaToMatch,
|
||||
pathComponentForLog: pathComponentString // Pass for logging inside elementMatchesAllCriteria
|
||||
) {
|
||||
axDebugLog("PathNav/PPC: Step \(stepCounter). findMatchingChild returned non-nil.")
|
||||
return matchedChild
|
||||
}
|
||||
axDebugLog("PathNav/PPC: Step \(stepCounter). findMatchingChild returned nil. Before elementMatchesAllCriteria.")
|
||||
stepCounter += 1
|
||||
|
||||
// Priority 2: If no child matched, check current element itself
|
||||
if await elementMatchesAllCriteria(currentElement, criteria: criteriaToMatch, forPathComponent: pathComponentString) {
|
||||
axDebugLog(
|
||||
"Current element \\(briefDesc) itself matches component '\\(pathComponentString)'. " +
|
||||
"Current element \(briefDesc) itself matches component '\(pathComponentString)'. " +
|
||||
"Retaining current element for this step.",
|
||||
file: #file, function: #function, line: #line
|
||||
)
|
||||
axDebugLog("PathNav/PPC: Step \(stepCounter). elementMatchesAllCriteria on currentElement was true.")
|
||||
return currentElement
|
||||
}
|
||||
axDebugLog("PathNav/PPC: Step \(stepCounter). elementMatchesAllCriteria on currentElement was false. Before logNoMatchFound.")
|
||||
stepCounter += 1
|
||||
|
||||
// No match found
|
||||
logNoMatchFound(
|
||||
briefDesc: briefDesc,
|
||||
pathComponentString: pathComponentString,
|
||||
currentPathSegmentForLog: currentPathSegmentForLog
|
||||
)
|
||||
axDebugLog("PathNav/PPC: Step \(stepCounter). After logNoMatchFound. Returning nil.")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -166,8 +186,8 @@ private func processPathComponent(
|
||||
@MainActor
|
||||
private func logPathComponentProcessing(pathComponentString: String, briefDesc: String) {
|
||||
axDebugLog(
|
||||
"Navigating: Processing path component '\\(pathComponentString)' " +
|
||||
"from current element: \\(briefDesc)",
|
||||
"Navigating: Processing path component '\(pathComponentString)' " +
|
||||
"from current element: \(briefDesc)",
|
||||
file: #file,
|
||||
function: #function,
|
||||
line: #line
|
||||
@ -182,9 +202,9 @@ private func logNoMatchFound(
|
||||
currentPathSegmentForLog: String
|
||||
) {
|
||||
axDebugLog(
|
||||
"Neither current element \\(briefDesc) nor its children (after all checks) " +
|
||||
"matched criteria for path component '\\(pathComponentString)'. " +
|
||||
"Path: \\(currentPathSegmentForLog) // CHILD_MATCH_FAILURE_MARKER",
|
||||
"Neither current element \(briefDesc) nor its children (after all checks) " +
|
||||
"matched criteria for path component '\(pathComponentString)'. " +
|
||||
"Path: \(currentPathSegmentForLog) // CHILD_MATCH_FAILURE_MARKER",
|
||||
file: #file,
|
||||
function: #function,
|
||||
line: #line
|
||||
@ -198,17 +218,33 @@ private func findMatchingChild(
|
||||
criteriaToMatch: [String: String],
|
||||
pathComponentForLog: String // Pass for logging inside elementMatchesAllCriteria
|
||||
) async -> Element? {
|
||||
axDebugLog("PathNav/FMIC: ABSOLUTE ENTRY (SIMPLE)", file: #file, function: #function, line: #line)
|
||||
try? await Task.sleep(nanoseconds: 100_000_000) // Diagnostic delay
|
||||
|
||||
axDebugLog("PathNav/FMIC: Entered function for component [\(pathComponentForLog)]. Criteria: \(criteriaToMatch)", file: #file, function: #function, line: #line)
|
||||
|
||||
guard let children = await getChildrenFromElement(currentElement) else {
|
||||
return nil // Logged in getChildrenFromElement if no children
|
||||
return nil
|
||||
}
|
||||
|
||||
logChildCount(count: children.count) // Optional: log how many children are being checked
|
||||
if children.isEmpty {
|
||||
axDebugLog("PathNav/FMIC: Children array IS EMPTY for component [\(pathComponentForLog)]. No children to iterate.", file: #file, function: #function, line: #line)
|
||||
return nil
|
||||
}
|
||||
|
||||
return await findMatchInChildren(
|
||||
children: children,
|
||||
criteriaToMatch: criteriaToMatch,
|
||||
pathComponentForLog: pathComponentForLog
|
||||
)
|
||||
axDebugLog("PathNav/FMIC: Iterating \(children.count) children for component [\(pathComponentForLog)]. Criteria to match: \(criteriaToMatch)", file: #file, function: #function, line: #line)
|
||||
for (childIndex, child) in children.enumerated() {
|
||||
let childDescriptionForLog = child.briefDescription(option: .default)
|
||||
axDebugLog("PathNav/FMIC: Child [\(childIndex)]/[\(children.count - 1)]: [\(childDescriptionForLog)]. About to call EMAC for component [\(pathComponentForLog)].", file: #file, function: #function, line: #line)
|
||||
// Re-enable this check
|
||||
if await elementMatchesAllCriteria(child, criteria: criteriaToMatch, forPathComponent: pathComponentForLog) {
|
||||
axDebugLog("Matched component [\(pathComponentForLog)] to child: [\(childDescriptionForLog)]",
|
||||
file: #file, function: #function, line: #line)
|
||||
return child
|
||||
}
|
||||
}
|
||||
axDebugLog("PathNav/FMIC: Loop finished or no match from EMAC. Returning nil.", file: #file, function: #function, line: #line)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper to get children from element
|
||||
@ -217,7 +253,7 @@ private func getChildrenFromElement(_ element: Element) async -> [Element]? {
|
||||
guard let children = await element.children() else {
|
||||
let currentElementDescForLog = element.briefDescription(option: .default)
|
||||
axDebugLog(
|
||||
"Current element [\\(currentElementDescForLog)] has no children from Element.children() " +
|
||||
"Current element [\(currentElementDescForLog)] has no children from Element.children() " +
|
||||
"or children array was nil.",
|
||||
file: #file,
|
||||
function: #function,
|
||||
@ -232,35 +268,13 @@ private func getChildrenFromElement(_ element: Element) async -> [Element]? {
|
||||
@MainActor
|
||||
private func logChildCount(count: Int) {
|
||||
axDebugLog(
|
||||
"Child count from Element.children(): \\(count)",
|
||||
"Child count from Element.children(): \(count)",
|
||||
file: #file,
|
||||
function: #function,
|
||||
line: #line
|
||||
)
|
||||
}
|
||||
|
||||
// Helper to find a match in children array
|
||||
@MainActor
|
||||
private func findMatchInChildren(
|
||||
children: [Element],
|
||||
criteriaToMatch: [String: String],
|
||||
pathComponentForLog: String // Pass for logging inside elementMatchesAllCriteria
|
||||
) async -> Element? {
|
||||
axDebugLog("PathNav/FMIC: Iterating \(children.count) children for component [\(pathComponentForLog)]. Criteria to match: \(criteriaToMatch)", file: #file, function: #function, line: #line)
|
||||
for (childIndex, child) in children.enumerated() {
|
||||
let childDescriptionForLog = child.briefDescription(option: .default)
|
||||
axDebugLog("PathNav/FMIC: Child [\(childIndex)]/[\(children.count - 1)]: [\(childDescriptionForLog)]. About to call EMAC for component [\(pathComponentForLog)].", file: #file, function: #function, line: #line)
|
||||
if await elementMatchesAllCriteria(child, criteria: criteriaToMatch, forPathComponent: pathComponentForLog) {
|
||||
// Matched child! Log before returning.
|
||||
axDebugLog("Matched component [\\(pathComponentForLog)] to child: [\\(childDescriptionForLog)]",
|
||||
file: #file, function: #function, line: #line)
|
||||
return child
|
||||
}
|
||||
}
|
||||
// No child matched all criteria for this component
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Index-based Navigation (If still needed, would need careful review)
|
||||
// The PathUtils.parseRichPathComponent currently does not produce index-based hints.
|
||||
// If "@index" style hints are required, parseRichPathComponent and the matching logic
|
||||
@ -274,10 +288,10 @@ private func findMatchInChildren(
|
||||
if let indexStr = criteriaToMatch["@index"], let index = Int(indexStr) {
|
||||
if let children = getChildrenFromElement(currentElement), index >= 0, index < children.count {
|
||||
let indexedChild = children[index]
|
||||
axDebugLog("Path component '\\(pathComponentString)' resolved to child at index \\(index): \\(indexedChild.briefDescription())")
|
||||
axDebugLog("Path component '\(pathComponentString)' resolved to child at index \(index): \(indexedChild.briefDescription())")
|
||||
return indexedChild
|
||||
} else {
|
||||
axDebugLog("Path component '\\(pathComponentString)' (index \\(index)) out of bounds for \\(currentElement.briefDescription()) with \\(getChildrenFromElement(currentElement)?.count ?? 0) children.")
|
||||
axDebugLog("Path component '\(pathComponentString)' (index \(index)) out of bounds for \(currentElement.briefDescription()) with \(getChildrenFromElement(currentElement)?.count ?? 0) children.")
|
||||
// logNoMatchFound would have been called if attribute matching failed before this.
|
||||
// If ONLY index was provided and it failed, this is the failure point.
|
||||
return nil
|
||||
@ -349,7 +363,7 @@ private func original_logChildCheck( // Marked as original
|
||||
) {
|
||||
let matchStatus = (actualValue == expectedValue) ? "==" : "!="
|
||||
axDebugLog(
|
||||
"Checking child: \\(childDesc) | Attribute: \\(attributeName) | Actual: '\\(actualValue)' \\(matchStatus) Expected: '\\(expectedValue)'",
|
||||
"Checking child: \(childDesc) | Attribute: \(attributeName) | Actual: '\(actualValue)' \(matchStatus) Expected: '\(expectedValue)'",
|
||||
file: #file,
|
||||
function: #function,
|
||||
line: #line
|
||||
@ -363,7 +377,7 @@ private func original_logChildMatch( // Marked as original
|
||||
expectedValue: String
|
||||
) {
|
||||
axDebugLog(
|
||||
"MATCHED child: \\(childDesc) for \\(attributeName):\\(expectedValue)",
|
||||
"MATCHED child: \(childDesc) for \(attributeName):\(expectedValue)",
|
||||
file: #file,
|
||||
function: #function,
|
||||
line: #line
|
||||
|
||||
68
Sources/AXorcist/Utils/SerializationUtils.swift
Normal file
68
Sources/AXorcist/Utils/SerializationUtils.swift
Normal file
@ -0,0 +1,68 @@
|
||||
import Foundation
|
||||
import CoreGraphics // For CGRect, CGPoint, CGSize
|
||||
import ApplicationServices // For AXUIElement, AXNotification (if used directly)
|
||||
|
||||
// Consider if AXNotification needs to be accessible here, or if its rawValue is sufficient.
|
||||
// If AXorcist/Models/DataModels.swift defines AXNotification, that might be a better import.
|
||||
|
||||
// Recursively sanitize value into JSON-encodable form
|
||||
internal func sanitizeValue(_ val: Any) -> Any {
|
||||
switch val {
|
||||
case let notif as AXNotification: // Assuming AXNotification is accessible
|
||||
return notif.rawValue
|
||||
case is AXUIElement:
|
||||
return "<AXUIElement>" // Placeholder for opaque AXUIElementRef
|
||||
case let elem as Element: // Assuming Element is accessible
|
||||
return String(describing: elem) // Or a more specific brief description if safe
|
||||
case let attrStr as NSAttributedString:
|
||||
return attrStr.string
|
||||
case let rect as CGRect:
|
||||
return ["x": rect.origin.x, "y": rect.origin.y, "width": rect.size.width, "height": rect.size.height]
|
||||
case let point as CGPoint:
|
||||
return ["x": point.x, "y": point.y]
|
||||
case let size as CGSize:
|
||||
return ["width": size.width, "height": size.height]
|
||||
case let dict as [String: Any]:
|
||||
var newDict: [String: Any] = [:]
|
||||
for (k, v) in dict { newDict[k] = sanitizeValue(v) }
|
||||
return newDict
|
||||
case let arr as [Any]:
|
||||
return arr.map { sanitizeValue($0) }
|
||||
// Consider adding cases for other common non-JSON-friendly types like URL, Date etc.
|
||||
// For Date, you might convert to ISO8601 string or epoch timestamp.
|
||||
default:
|
||||
// If it's a simple value type (Int, Double, Bool, String), it's already fine.
|
||||
// For anything else, converting to String is a safe fallback.
|
||||
// However, be mindful that this might not be the desired representation.
|
||||
if val is String || val is Int || val is Double || val is Bool || val is NSNull {
|
||||
return val
|
||||
}
|
||||
// Fallback for unknown complex types
|
||||
return String(describing: val)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all nested values are JSON-serialisable (NSString/NSNumber/NSNull/Array/Dict)
|
||||
// This function is crucial for preparing the payload for JSONSerialization.
|
||||
internal func makeJSONCompatible(_ value: Any) -> Any {
|
||||
switch value {
|
||||
case let str as String:
|
||||
return str // Already a JSON primitive
|
||||
case let num as NSNumber: // Handles Int, Double, Bool bridged from Objective-C
|
||||
return num // Already a JSON primitive (or convertible)
|
||||
case is NSNull:
|
||||
return value // Already a JSON primitive
|
||||
case let dict as [String: Any]:
|
||||
var newDict = [String: Any]()
|
||||
for (k,v) in dict { newDict[k] = makeJSONCompatible(v) }
|
||||
return newDict // Recurse for dictionary values
|
||||
case let arr as [Any]:
|
||||
return arr.map { makeJSONCompatible($0) } // Recurse for array elements
|
||||
default:
|
||||
// If it's not one of the above, it's likely not directly JSON serializable.
|
||||
// Convert to a string representation as a fallback.
|
||||
// This ensures that JSONSerialization.data(withJSONObject:) doesn't throw an error
|
||||
// due to an invalid top-level or nested type.
|
||||
return String(describing: value)
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
import ArgumentParser
|
||||
import AXorcist // For AXorcist instance
|
||||
import Foundation
|
||||
import CoreFoundation
|
||||
|
||||
@main
|
||||
struct AXORCCommand: AsyncParsableCommand {
|
||||
@ -28,6 +29,67 @@ struct AXORCCommand: AsyncParsableCommand {
|
||||
)
|
||||
var directPayload: String?
|
||||
|
||||
// Helper function to process and execute a CommandEnvelope
|
||||
private func processAndExecuteCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) async {
|
||||
if debugCLI {
|
||||
axDebugLog("Successfully parsed command: \(command.command) (ID: \(command.commandId))")
|
||||
}
|
||||
|
||||
let resultJsonString = await CommandExecutor.execute(
|
||||
command: command,
|
||||
axorcist: axorcist,
|
||||
debugCLI: debugCLI
|
||||
)
|
||||
print(resultJsonString)
|
||||
fflush(stdout)
|
||||
|
||||
if command.command == .observe {
|
||||
var observerSetupSucceeded = false
|
||||
if let resultData = resultJsonString.data(using: .utf8) {
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: resultData, options: []) as? [String: Any],
|
||||
let success = json["success"] as? Bool,
|
||||
let status = json["status"] as? String {
|
||||
axInfoLog("AXORCMain: Parsed initial response for observe: success=\(success), status=\(status)")
|
||||
if success && status == "observer_started" {
|
||||
observerSetupSucceeded = true
|
||||
axInfoLog("AXORCMain: Observer setup deemed SUCCEEDED for observe command.")
|
||||
} else {
|
||||
axInfoLog("AXORCMain: Observer setup deemed FAILED for observe command (success=\(success), status=\(status)).")
|
||||
}
|
||||
} else {
|
||||
axErrorLog("AXORCMain: Failed to parse expected fields (success, status) from observe setup JSON.")
|
||||
}
|
||||
} catch {
|
||||
axErrorLog("AXORCMain: Could not parse result JSON from observe setup to check for success: \(error.localizedDescription)")
|
||||
}
|
||||
} else {
|
||||
axErrorLog("AXORCMain: Could not convert result JSON string to data for observe setup check.")
|
||||
}
|
||||
|
||||
if observerSetupSucceeded {
|
||||
axInfoLog("AXORCMain: Observer setup successful. Process will remain alive.")
|
||||
#if DEBUG
|
||||
axInfoLog("AXORCMain: DEBUG mode - launching dedicated run-loop thread for observer.")
|
||||
Thread.detachNewThread { CFRunLoopRun() }
|
||||
axInfoLog("AXORCMain: DEBUG mode - main task entering infinite sleep loop.")
|
||||
while true {
|
||||
do { try await Task.sleep(nanoseconds: 3_600_000_000_000) }
|
||||
catch { axInfoLog("AXORCMain: Main task sleep interrupted."); break }
|
||||
}
|
||||
#else
|
||||
fputs("{\"error\": \"The 'observe' command is intended for DEBUG builds or specific use cases and will not run indefinitely in this release build. Exiting.\"}\n", stderr)
|
||||
fflush(stderr)
|
||||
exit(1)
|
||||
#endif
|
||||
} else {
|
||||
axErrorLog("AXORCMain: Observe command setup reported failure or result was not a success status. Exiting.")
|
||||
}
|
||||
} else {
|
||||
await axClearLogs() // Clear logs for non-observe commands after execution
|
||||
}
|
||||
}
|
||||
|
||||
mutating func run() async throws {
|
||||
// Configure GlobalAXLogger based on debug flag
|
||||
await GlobalAXLogger.shared.setLoggingEnabled(debug)
|
||||
@ -60,7 +122,7 @@ struct AXORCCommand: AsyncParsableCommand {
|
||||
return
|
||||
}
|
||||
|
||||
guard let jsonString = inputResult.jsonString else {
|
||||
guard var jsonStringFromInput = inputResult.jsonString else {
|
||||
let collectedLogs = debug ? await GlobalAXLogger.shared.getLogsAsStrings(format: .text, includeTimestamps: true, includeLevels: true, includeDetails: true) : nil
|
||||
|
||||
let errorResponse = ErrorResponse(
|
||||
@ -77,9 +139,37 @@ struct AXORCCommand: AsyncParsableCommand {
|
||||
}
|
||||
return
|
||||
}
|
||||
axDebugLog("AXORCMain: jsonStringFromInput (from InputHandler): [\(jsonStringFromInput)] (length: \(jsonStringFromInput.count))")
|
||||
|
||||
// Ensure we are working with a "concrete" String instance to avoid Substring/StringProtocol ambiguities
|
||||
var jsonString = String(jsonStringFromInput)
|
||||
axDebugLog("AXORCMain: jsonString (after String(jsonStringFromInput)): [\(jsonString)] (length: \(jsonString.count))")
|
||||
|
||||
// Log first/last chars of the concrete jsonString
|
||||
if !jsonString.isEmpty {
|
||||
axDebugLog("AXORCMain: First char of concrete jsonString: \(jsonString.first!) (ASCII: \(jsonString.first!.asciiValue ?? 0)), Last char: \(jsonString.last!) (ASCII: \(jsonString.last!.asciiValue ?? 0))")
|
||||
}
|
||||
|
||||
// Parse JSON command
|
||||
guard let jsonData = jsonString.data(using: .utf8) else {
|
||||
var dataToDecode = jsonString.data(using: .utf8) // Default to using the concrete jsonString
|
||||
var didAttemptUnwrap = false
|
||||
|
||||
if jsonString.hasPrefix("[") && jsonString.hasSuffix("]") && jsonString.count > 2 { // Use concrete jsonString for checks
|
||||
let innerContentString = String(jsonString.dropFirst().dropLast())
|
||||
axDebugLog("AXORCMain: Original concrete jsonString appeared to be an array. Attempting to use its inner content: [\(innerContentString)]")
|
||||
if let innerData = innerContentString.data(using: .utf8) {
|
||||
dataToDecode = innerData
|
||||
didAttemptUnwrap = true
|
||||
} else {
|
||||
axDebugLog("AXORCMain: Failed to convert innerContentString to data. Will use original concrete jsonString data.")
|
||||
}
|
||||
} else {
|
||||
axDebugLog("AXORCMain: Original concrete jsonString does not appear to be a simple array wrapper. Proceeding with it for data conversion.")
|
||||
}
|
||||
|
||||
// axDebugLog("AXORCMain: effectiveJsonString after unwrap attempt: [\(effectiveJsonString)]") // Old log, dataToDecode is now key
|
||||
|
||||
guard let jsonData = dataToDecode else {
|
||||
// Clear logs after error
|
||||
await axClearLogs()
|
||||
print("{\"error\": \"Failed to convert JSON string to data\"}")
|
||||
@ -87,61 +177,42 @@ struct AXORCCommand: AsyncParsableCommand {
|
||||
}
|
||||
|
||||
if debug {
|
||||
axDebugLog("AXORCMain: jsonString before decode: [\(jsonString)]")
|
||||
axDebugLog("AXORCMain: jsonData.count before decode: \(jsonData.count)")
|
||||
axDebugLog("AXORCMain: jsonData.count before decode (this is from effective/unwrapped data if unwrap occurred): \(jsonData.count)")
|
||||
}
|
||||
|
||||
let axorcist = AXorcist() // Initialize once, outside the do-catch for broader scope
|
||||
|
||||
do {
|
||||
// This is the primary attempt, using `jsonData` (derived from `dataToDecode`,
|
||||
// which is from `jsonString` after faulty unwrap attempt due to string issues)
|
||||
let command = try JSONDecoder().decode(CommandEnvelope.self, from: jsonData)
|
||||
|
||||
if debug {
|
||||
axDebugLog("Successfully parsed command: \(command.command)")
|
||||
}
|
||||
|
||||
// Execute command using CommandExecutor
|
||||
let axorcist = AXorcist()
|
||||
let result = await CommandExecutor.execute(
|
||||
command: command,
|
||||
axorcist: axorcist,
|
||||
debugCLI: debug
|
||||
)
|
||||
|
||||
print(result) // CommandExecutor.execute should return a string (JSON response)
|
||||
|
||||
// Stop collecting logs after successful execution
|
||||
// Clear logs after error
|
||||
await axClearLogs()
|
||||
|
||||
} catch {
|
||||
axErrorLog("DECODE_ERROR_DEBUG: Original jsonString that led to this error: [\(jsonString)]")
|
||||
axErrorLog("DECODE_ERROR_DEBUG: jsonData.count that led to this error: \(jsonData.count)")
|
||||
axErrorLog("DECODE_ERROR_DEBUG: Raw error.localizedDescription: \(error.localizedDescription)")
|
||||
axErrorLog("DECODE_ERROR_DEBUG: Full error object: \(error)")
|
||||
|
||||
let errorMessage = "Failed to parse JSON command. Raw Error: \(error.localizedDescription). JSON Input (first 100 chars): \(jsonString.prefix(100))..."
|
||||
|
||||
// For decode errors, always collect logs
|
||||
if !debug {
|
||||
await GlobalAXLogger.shared.setLoggingEnabled(true)
|
||||
await GlobalAXLogger.shared.setDetailLevel(.verbose)
|
||||
}
|
||||
let collectedLogs = await GlobalAXLogger.shared.getLogsAsStrings(format: .text, includeTimestamps: true, includeLevels: true, includeDetails: true)
|
||||
await axClearLogs()
|
||||
|
||||
let errorResponse = ErrorResponse(
|
||||
commandId: "decode_error",
|
||||
error: errorMessage,
|
||||
debugLogs: collectedLogs
|
||||
)
|
||||
|
||||
if let responseData = try? JSONEncoder().encode(errorResponse),
|
||||
let responseStr = String(data: responseData, encoding: .utf8) {
|
||||
print(responseStr)
|
||||
axDebugLog("AXORCMain: Decode attempt 1 (from jsonData derived from potentially pre-unwrapped jsonString) successful.")
|
||||
await processAndExecuteCommand(command: command, axorcist: axorcist, debugCLI: debug)
|
||||
} catch let error1 {
|
||||
axDebugLog("AXORCMain: Decode attempt 1 (from jsonData) FAILED. Error: \(error1). jsonStringFromInput (raw from InputHandler) was: [\(jsonStringFromInput)]")
|
||||
// Fallback: Assume jsonStringFromInput is "[{...}]" because InputHandler (via ArgumentParser) seems to yield this.
|
||||
// Try to extract "{...}" and decode that as a single CommandEnvelope.
|
||||
if jsonStringFromInput.count > 2 { // Basic check for "[]" at least
|
||||
let potentiallyInnerJsonString = String(jsonStringFromInput.dropFirst().dropLast())
|
||||
axDebugLog("AXORCMain: Fallback: Extracted potentiallyInnerJsonString from jsonStringFromInput: [\(potentiallyInnerJsonString)]")
|
||||
if let innerData = potentiallyInnerJsonString.data(using: .utf8) {
|
||||
do {
|
||||
let command = try JSONDecoder().decode(CommandEnvelope.self, from: innerData)
|
||||
axDebugLog("AXORCMain: Decode attempt 2 (from inner content of jsonStringFromInput) SUCCESSFUL.")
|
||||
await processAndExecuteCommand(command: command, axorcist: axorcist, debugCLI: debug)
|
||||
} catch let error2 {
|
||||
axDebugLog("AXORCMain: Decode attempt 2 (from inner content of jsonStringFromInput) FAILED. Error: \(error2). Will rethrow original error from attempt 1.")
|
||||
throw error1 // Rethrow original error from attempt 1
|
||||
}
|
||||
} else {
|
||||
axDebugLog("AXORCMain: Fallback: Failed to convert potentiallyInnerJsonString to data. Will rethrow original error from attempt 1.")
|
||||
throw error1 // Rethrow original error from attempt 1
|
||||
}
|
||||
} else {
|
||||
// Fallback if even error encoding fails
|
||||
let fallbackErrorMsg = "{\"error\": \"Failed to encode error response. Original error for decode: \(error.localizedDescription). Input was: \(jsonString)\"}"
|
||||
print(fallbackErrorMsg)
|
||||
axDebugLog("AXORCMain: Fallback: jsonStringFromInput too short to be '[{...}]'. Will rethrow original error from attempt 1.")
|
||||
throw error1 // Rethrow original error from attempt 1
|
||||
}
|
||||
}
|
||||
// Removed the final generic catch to ensure errors propagate to ArgumentParser if not handled by the specific fallback.
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +113,10 @@ struct CommandExecutor {
|
||||
case .getElementAtPoint:
|
||||
// Pass debugCLI to handler
|
||||
return await handleNotImplementedCommand(command: command, message: "getElementAtPoint command not yet implemented", debugCLI: debugCLI)
|
||||
|
||||
case .observe:
|
||||
// Pass debugCLI to handler
|
||||
return await handleObserveCommand(command: command, axorcist: axorcist, debugCLI: debugCLI)
|
||||
}
|
||||
}
|
||||
|
||||
@ -445,6 +449,81 @@ struct CommandExecutor {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder for handleObserveCommand
|
||||
private static func handleObserveCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) async -> String {
|
||||
axDebugLog("Observe command received by CommandExecutor. debugCLI: \(debugCLI)")
|
||||
|
||||
guard let notifications = command.notifications, !notifications.isEmpty else {
|
||||
let errorMsg = "Missing or empty 'notifications' array for observe command."
|
||||
axErrorLog(errorMsg)
|
||||
return await finalizeAndEncodeResponse(
|
||||
commandId: command.commandId,
|
||||
commandType: command.command.rawValue,
|
||||
handlerResponse: HandlerResponse(data: nil, error: errorMsg),
|
||||
debugCLI: debugCLI
|
||||
)
|
||||
}
|
||||
|
||||
let includeDetails = command.includeElementDetails ?? []
|
||||
let watchChildren = command.watchChildren ?? false
|
||||
|
||||
let observerSetupSuccess = await axorcist.handleObserve(
|
||||
for: command.application,
|
||||
notifications: notifications,
|
||||
includeElementDetails: includeDetails,
|
||||
watchChildren: watchChildren,
|
||||
commandId: command.commandId,
|
||||
debugCLI: debugCLI
|
||||
)
|
||||
|
||||
if observerSetupSuccess {
|
||||
// Observer started successfully. Print initial success message to stdout.
|
||||
// Further notification data will be streamed directly to stdout by AXorcist.handleObserve's callback.
|
||||
|
||||
let successResponsePayload: [String: AnyCodable] = [
|
||||
"commandId": AnyCodable(command.commandId),
|
||||
"command": AnyCodable(command.command.rawValue),
|
||||
"status": AnyCodable("observer_started"),
|
||||
"success": AnyCodable(true) // Indicate successful setup
|
||||
]
|
||||
// let logs = await GlobalAXLogger.shared.getLogsAsStringsIfEnabled(format: .text)
|
||||
// No logs are added to this initial success message for observe,
|
||||
// as the primary output is the stream of notifications.
|
||||
|
||||
do {
|
||||
let jsonData = try JSONEncoder().encode(successResponsePayload)
|
||||
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||
// This will be the only JSON output from CommandExecutor for a successful observe setup.
|
||||
// AXORCMain will need to ensure the process stays alive.
|
||||
return jsonString
|
||||
} else {
|
||||
let errorMsg = "{\"error\": \"Failed to encode initial success response for observe command.\"}"
|
||||
fputs("\(errorMsg)\n", stderr)
|
||||
fflush(stderr)
|
||||
return errorMsg // Return error string
|
||||
}
|
||||
} catch {
|
||||
let errorMsg = "{\"error\": \"Exception encoding initial success response: \(error.localizedDescription)\"}"
|
||||
fputs("\(errorMsg)\n", stderr)
|
||||
fflush(stderr)
|
||||
return errorMsg // Return error string
|
||||
}
|
||||
|
||||
// DO NOT CALL RunLoop.current.run() here.
|
||||
// AXORCMain will handle keeping the process alive.
|
||||
} else {
|
||||
// Failed to start observer
|
||||
let errorMsg = "Failed to start observer for application: \(command.application ?? "focused")"
|
||||
axErrorLog(errorMsg)
|
||||
return await finalizeAndEncodeResponse(
|
||||
commandId: command.commandId,
|
||||
commandType: command.command.rawValue,
|
||||
handlerResponse: HandlerResponse(data: nil, error: errorMsg),
|
||||
debugCLI: debugCLI
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to GlobalAXLogger for convenience
|
||||
|
||||
@ -219,7 +219,7 @@ func testQueryForTextEditTextArea() async throws {
|
||||
"Debug logs should indicate query execution."
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Describe TextEdit Text Area")
|
||||
@MainActor
|
||||
@ -293,7 +293,7 @@ func testDescribeTextEditTextArea() async throws {
|
||||
"Debug logs should indicate describeElement execution."
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user