The fight continues

This commit is contained in:
Peter Steinberger 2025-05-25 22:15:54 +02:00
parent 5cbe85e85a
commit cb2d7055f0
18 changed files with 1131 additions and 1281 deletions

View File

@ -20,11 +20,9 @@ let package = Package(
.target(
name: "AXorcist",
dependencies: [],
path: "Sources",
exclude: ["axorc"],
sources: [
"AXorcist"
]
path: "Sources/AXorcist", // Be very direct about the source path
exclude: [], // Explicitly no excludes
sources: nil // Explicitly let SPM find all sources in the path
),
.executableTarget(
name: "axorc", // Executable target name

View File

@ -345,6 +345,7 @@ public enum AXMiscConstants {
public static let defaultMaxDepthSearch = 10
public static let defaultMaxDepthPathResolution = 10
public static let defaultMaxDepthDescribe = 3
public static let defaultMaxElementsToCollect = 1000 // New constant for element collection limit
public static let defaultTimeoutPerElementCollectAll: TimeInterval = 2.0 // seconds
// String Constants (for default/fallback values)

View File

@ -10,7 +10,7 @@ public struct CommandEnvelope: Codable {
public let application: String?
public let attributes: [String]?
public let payload: [String: String]? // For ping compatibility
public let debugLogging: Bool?
public let debugLogging: Bool
public let locator: Locator? // Locator from this file
public let pathHint: [String]?
public let maxElements: Int?
@ -27,6 +27,9 @@ public struct CommandEnvelope: Codable {
public let includeElementDetails: [String]?
public let watchChildren: Bool?
// New field for collectAll filtering
public let filterCriteria: [String: String]?
enum CodingKeys: String, CodingKey {
case commandId
case command
@ -48,6 +51,8 @@ public struct CommandEnvelope: Codable {
case notifications
case includeElementDetails
case watchChildren
// CodingKey for new field
case filterCriteria
}
// Added a public initializer for convenience, matching fields.
@ -56,7 +61,7 @@ public struct CommandEnvelope: Codable {
application: String? = nil,
attributes: [String]? = nil,
payload: [String: String]? = nil,
debugLogging: Bool? = nil,
debugLogging: Bool = false,
locator: Locator? = nil,
pathHint: [String]? = nil,
maxElements: Int? = nil,
@ -70,7 +75,9 @@ public struct CommandEnvelope: Codable {
// Init parameters for observe
notifications: [String]? = nil,
includeElementDetails: [String]? = nil,
watchChildren: Bool? = nil
watchChildren: Bool? = nil,
// Init parameter for new field
filterCriteria: [String: String]? = nil
) {
self.commandId = commandId
self.command = command
@ -92,6 +99,8 @@ public struct CommandEnvelope: Codable {
self.notifications = notifications
self.includeElementDetails = includeElementDetails
self.watchChildren = watchChildren
// Assignment for new field
self.filterCriteria = filterCriteria
}
}
@ -100,6 +109,7 @@ public struct Locator: Codable {
public var matchAll: Bool?
public var criteria: [String: String]
public var rootElementPathHint: [String]?
public var descendantCriteria: [String: String]?
public var requireAction: String?
public var computedNameContains: String?
@ -107,6 +117,7 @@ public struct Locator: Codable {
case matchAll
case criteria
case rootElementPathHint
case descendantCriteria
case requireAction
case computedNameContains
}
@ -115,12 +126,14 @@ public struct Locator: Codable {
matchAll: Bool? = nil,
criteria: [String: String] = [:],
rootElementPathHint: [String]? = nil,
descendantCriteria: [String: String]? = nil,
requireAction: String? = nil,
computedNameContains: String? = nil
) {
self.matchAll = matchAll
self.criteria = criteria
self.rootElementPathHint = rootElementPathHint
self.descendantCriteria = descendantCriteria
self.requireAction = requireAction
self.computedNameContains = computedNameContains
}

View File

@ -13,16 +13,16 @@ extension Element {
var childCollector = ChildCollector() // ChildCollector will use GlobalAXLogger internally
print("[PRINT Element.children] Before collectDirectChildren for: \(self.briefDescription(option: .default))")
// 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.")
// 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)
}
print("[PRINT Element.children] Before finalizeResults, collector has: \(childCollector.collectedChildrenCount()) unique children.")
// 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

View File

@ -23,5 +23,6 @@ public enum CommandType: String, Codable {
case ping
case getElementAtPoint
case observe
case setFocusedValue // New: sets a value on the currently focused element
// Add future commands here, ensuring case matches JSON or provide explicit raw value
}

View File

@ -266,7 +266,7 @@ public struct CollectAllOutput: Codable {
public let commandId: String
public let success: Bool
public let command: String
public let collectedElements: [AXElement]
public let collectedElements: [AXElementData]
public let appBundleId: String?
public let debugLogs: [String]?
public let errorMessage: String?

View File

@ -101,66 +101,61 @@ extension AXorcist {
locator: Locator,
actionName: String,
actionValue: ActionValueCodable? = nil,
pathHint: [PathHintComponent]? = nil,
maxDepth: Int? = nil
) async -> HandlerResponse {
let logMessage2 = "handlePerformAction: App=\(application ?? AXMiscConstants.focusedApplicationKey), Locator=\(locator), Action=\(actionName), Value=\(String(describing: actionValue))"
axInfoLog(logMessage2)
// Determine search depth
let searchMaxDepth = maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
// Call the global findTargetElement which returns Result<Element, HandlerErrorInfo>
let findResult = await findTargetElement(
for: application,
locator: locator,
pathHint: pathHint?.compactMap { $0.originalSegment }, // Use .originalSegment
maxDepthForSearch: searchMaxDepth
)
let targetElement: Element
// appElement is not directly returned by the new findTargetElement, handle if necessary
// For now, we primarily need the targetElement or error.
if let targetElement = findResult.element {
axDebugLog("handlePerformAction: Element found: \(targetElement.briefDescription())")
// Proceed with targetElement
let axStatus: AXError
var actionErrorString: String?
switch findResult {
case .success(let foundEl):
targetElement = foundEl
case .failure(let errorInfo):
let errorMessage = "handlePerformAction: Error finding element: \(errorInfo.message)"
let standardActions = [
AXActionNames.kAXIncrementAction,
AXActionNames.kAXDecrementAction,
AXActionNames.kAXConfirmAction,
AXActionNames.kAXCancelAction,
AXActionNames.kAXShowMenuAction,
AXActionNames.kAXPickAction,
AXActionNames.kAXPressAction,
AXActionNames.kAXRaiseAction
]
if standardActions.contains(actionName) {
axStatus = executeStandardAccessibilityAction(actionName as CFString, on: targetElement, actionNameForLog: actionName)
} else {
let setResult = executeSetAttributeValueAction(attributeName: actionName, value: actionValue, on: targetElement)
axStatus = setResult.axStatus
actionErrorString = setResult.errorMessage
}
if axStatus == .success {
axDebugLog("Action '\(actionName)' performed successfully on \(targetElement.briefDescription()).")
return HandlerResponse(data: AnyCodable(PerformResponse(commandId: "", success: true)))
} else {
let finalErrorMessage = actionErrorString ?? "Action '\(actionName)' failed on \(targetElement.briefDescription()) with status: \(axErrorToString(axStatus))"
axErrorLog(finalErrorMessage)
return HandlerResponse(error: finalErrorMessage)
}
} else if let errorMsg = findResult.error {
let errorMessage = "handlePerformAction: Error finding element: \(errorMsg)"
axErrorLog(errorMessage)
return HandlerResponse(error: "Error finding element: \(errorInfo.message)")
}
let axStatus: AXError
var actionErrorString: String? // To capture specific error from set attribute
let standardActions = [
AXActionNames.kAXIncrementAction,
AXActionNames.kAXDecrementAction,
AXActionNames.kAXConfirmAction,
AXActionNames.kAXCancelAction,
AXActionNames.kAXShowMenuAction,
AXActionNames.kAXPickAction,
AXActionNames.kAXPressAction,
AXActionNames.kAXRaiseAction
]
if standardActions.contains(actionName) {
axStatus = executeStandardAccessibilityAction(actionName as CFString, on: targetElement, actionNameForLog: actionName)
return HandlerResponse(error: errorMessage)
} else {
let setResult = executeSetAttributeValueAction(attributeName: actionName, value: actionValue, on: targetElement)
axStatus = setResult.axStatus
actionErrorString = setResult.errorMessage
}
if axStatus == .success {
axDebugLog("Action '\(actionName)' performed successfully on \(targetElement.briefDescription()).")
// Assuming PerformResponse is a valid Codable struct for the data part
return HandlerResponse(data: AnyCodable(PerformResponse(commandId: "", success: true)))
} else {
let finalErrorMessage = actionErrorString ?? "Action '\(actionName)' failed on \(targetElement.briefDescription()) with status: \(axErrorToString(axStatus))"
axErrorLog(finalErrorMessage)
return HandlerResponse(error: finalErrorMessage)
// Should not happen if findTargetElement always returns either element or error
let errorMessage = "handlePerformAction: Unknown error finding element."
axErrorLog(errorMessage)
return HandlerResponse(error: errorMessage)
}
}
@ -168,62 +163,55 @@ extension AXorcist {
public func handleExtractText(
for application: String?,
locator: Locator,
pathHint: [PathHintComponent]? = nil,
maxDepth: Int? = nil
) async -> HandlerResponse {
let logMessage3 = "handleExtractText: App=\(application ?? AXMiscConstants.focusedApplicationKey), Locator=\(locator)"
axInfoLog(logMessage3)
// Determine search depth
let searchMaxDepth = maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
// Call the global findTargetElement
let findResult = await findTargetElement(
for: application,
locator: locator,
pathHint: pathHint?.compactMap { $0.originalSegment }, // Use .originalSegment
maxDepthForSearch: searchMaxDepth
)
let targetElement: Element
// We might need appElement for path generation later, let's try to get it
let appElementInstance = applicationElement(for: application ?? AXMiscConstants.focusedApplicationKey)
switch findResult {
case .success(let foundEl):
targetElement = foundEl
case .failure(let errorInfo):
let errorMessage = "handleExtractText: Error finding element: \(errorInfo.message)"
if let targetElement = findResult.element {
axDebugLog("handleExtractText: Element found: \(targetElement.briefDescription())")
// Proceed with targetElement
guard appElementInstance != nil else {
let appNameToLog = application ?? AXMiscConstants.focusedApplicationKey
let errorMsg = "Could not get application element for path generation in handleExtractText for appKey: \(appNameToLog)."
axErrorLog(errorMsg)
return HandlerResponse(data: AnyCodable(TextExtractionResponse(textContent: nil)), error: errorMsg)
}
var allTextValues: [String] = []
if let title: String = targetElement.attribute(.title) { allTextValues.append(title) }
if let desc: String = targetElement.attribute(.description) { allTextValues.append(desc) }
if let valStr: String = targetElement.attribute(Attribute<String>(AXAttributeNames.kAXValueAttribute)) { allTextValues.append(valStr) }
if let selectedText: String = targetElement.attribute(.selectedText) { allTextValues.append(selectedText) }
if let placeholder: String = targetElement.attribute(.placeholderValue) { allTextValues.append(placeholder) }
let combinedText = allTextValues.joined(separator: " ").lowercased()
if combinedText.isEmpty {
axDebugLog("No textual content found for element: \(targetElement.briefDescription())")
return HandlerResponse(data: AnyCodable(TextExtractionResponse(textContent: nil)), error: "No textual content found")
} else {
axDebugLog("Extracted text: '\(combinedText)' from element: \(targetElement.briefDescription())")
return HandlerResponse(data: AnyCodable(TextExtractionResponse(textContent: combinedText)))
}
} else if let errorMsg = findResult.error {
let errorMessage = "handleExtractText: Error finding element: \(errorMsg)"
axErrorLog(errorMessage)
return HandlerResponse(error: "Error finding element: \(errorInfo.message)")
}
guard appElementInstance != nil else {
let appNameToLog = application ?? AXMiscConstants.focusedApplicationKey
let errorMsg = "Could not get application element for path generation in handleExtractText for appKey: \(appNameToLog)."
axErrorLog(errorMsg)
// Return nil for textContent as part of TextExtractionResponse, not in HandlerResponse.error
return HandlerResponse(data: AnyCodable(TextExtractionResponse(textContent: nil)), error: errorMsg)
}
// Text extraction logic
var allTextValues: [String] = []
if let title: String = targetElement.attribute(.title) { allTextValues.append(title) }
if let desc: String = targetElement.attribute(.description) { allTextValues.append(desc) }
if let valStr: String = targetElement.attribute(Attribute<String>(AXAttributeNames.kAXValueAttribute)) { allTextValues.append(valStr) }
if let selectedText: String = targetElement.attribute(.selectedText) { allTextValues.append(selectedText) }
if let placeholder: String = targetElement.attribute(.placeholderValue) { allTextValues.append(placeholder) }
let combinedText = allTextValues.joined(separator: " ").lowercased()
if combinedText.isEmpty {
axDebugLog("No textual content found for element: \(targetElement.briefDescription())")
// Return nil for textContent as part of TextExtractionResponse
return HandlerResponse(data: AnyCodable(TextExtractionResponse(textContent: nil)), error: "No textual content found")
return HandlerResponse(error: errorMessage)
} else {
axDebugLog("Extracted text: '\(combinedText)' from element: \(targetElement.briefDescription())")
// Return extracted text
return HandlerResponse(data: AnyCodable(TextExtractionResponse(textContent: combinedText)))
let errorMessage = "handleExtractText: Unknown error finding element."
axErrorLog(errorMessage)
return HandlerResponse(error: errorMessage)
}
}
}

View File

@ -10,6 +10,34 @@ import Foundation
// MARK: - Batch Processing Handler Extension
extension AXorcist {
@MainActor
private func prepareLocator(for subCommandEnvelope: CommandEnvelope, existingLocator: Locator?) -> Locator? {
guard var newLocator = existingLocator else {
// If there's a pathHint on the envelope but no locator, this is problematic.
// For now, if no base locator, we can't effectively use the pathHint from the envelope alone
// unless we construct a new locator, but that might miss criteria.
// This case should ideally be handled by validation upstream or clearer contract.
// If pathHint is the ONLY way to locate, the CommandEnvelope should reflect that.
// For now, just return the locator as is (which might be nil).
if subCommandEnvelope.pathHint != nil && !subCommandEnvelope.pathHint!.isEmpty {
axWarningLog("SubCommand \(subCommandEnvelope.commandId) has a pathHint but no base locator. PathHint will not be used unless locator also has criteria.")
// Optionally, create a locator with only pathHint if that's a valid use case:
// return Locator(criteria: [:], rootElementPathHint: subCommandEnvelope.pathHint)
}
return existingLocator
}
// If CommandEnvelope.pathHint is provided, and locator.rootElementPathHint is not,
// transfer the pathHint to the locator.
if let topLevelPathHint = subCommandEnvelope.pathHint,
!topLevelPathHint.isEmpty,
newLocator.rootElementPathHint == nil || newLocator.rootElementPathHint!.isEmpty {
axDebugLog("AXorcist+BatchHandler: Populating locator.rootElementPathHint from CommandEnvelope.pathHint for sub-command \(subCommandEnvelope.commandId).")
newLocator.rootElementPathHint = topLevelPathHint
}
return newLocator
}
@MainActor
public func handleBatchCommands(
batchCommandID: String, // The ID of the overall batch command
@ -56,6 +84,10 @@ extension AXorcist {
case .performAction:
return await processPerformActionCommand(subCommandEnvelope, subCmdID: subCmdID)
case .setFocusedValue:
axWarningLog("Command 'setFocusedValue' found in batch. Current batch handler does not specifically process it. Returning as unsupported for now.")
return processUnsupportedCommand(subCommandEnvelope, subCmdID: subCmdID)
case .extractText:
return await processExtractTextCommand(subCommandEnvelope, subCmdID: subCmdID)
@ -83,16 +115,17 @@ extension AXorcist {
@MainActor
private func processGetAttributesCommand(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let locator = subCommandEnvelope.locator else {
guard let originalLocator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for getAttributes in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let finalLocator = prepareLocator(for: subCommandEnvelope, existingLocator: originalLocator)
return await self.handleGetAttributes(
for: subCommandEnvelope.application,
locator: locator,
locator: finalLocator!, // Safe to force unwrap as we checked originalLocator
requestedAttributes: subCommandEnvelope.attributes,
pathHint: subCommandEnvelope.pathHint,
maxDepth: subCommandEnvelope.maxElements, // maxElements often used as maxDepth for search in handlers
outputFormat: subCommandEnvelope.outputFormat
)
@ -100,15 +133,16 @@ extension AXorcist {
@MainActor
private func processQueryCommand(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let locator = subCommandEnvelope.locator else {
guard let originalLocator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for query in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let finalLocator = prepareLocator(for: subCommandEnvelope, existingLocator: originalLocator)
return await self.handleQuery(
for: subCommandEnvelope.application,
locator: locator,
pathHint: subCommandEnvelope.pathHint,
locator: finalLocator!, // Safe to force unwrap
maxDepth: subCommandEnvelope.maxElements,
requestedAttributes: subCommandEnvelope.attributes,
outputFormat: subCommandEnvelope.outputFormat
@ -117,15 +151,16 @@ extension AXorcist {
@MainActor
private func processDescribeElementCommand(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let locator = subCommandEnvelope.locator else {
guard let originalLocator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for describeElement in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let finalLocator = prepareLocator(for: subCommandEnvelope, existingLocator: originalLocator)
return await self.handleDescribeElement(
for: subCommandEnvelope.application,
locator: locator,
pathHint: subCommandEnvelope.pathHint,
locator: finalLocator!, // Safe to force unwrap
maxDepth: subCommandEnvelope.maxDepth, // Use maxDepth for describeElement
requestedAttributes: subCommandEnvelope.attributes,
outputFormat: subCommandEnvelope.outputFormat
@ -134,7 +169,7 @@ extension AXorcist {
@MainActor
private func processPerformActionCommand(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let locator = subCommandEnvelope.locator else {
guard let originalLocator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for performAction in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
@ -144,29 +179,29 @@ extension AXorcist {
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let pathHintComponents = subCommandEnvelope.pathHint?.compactMap { PathHintComponent(pathSegment: $0) }
let finalLocator = prepareLocator(for: subCommandEnvelope, existingLocator: originalLocator)
return await self.handlePerformAction(
for: subCommandEnvelope.application,
locator: locator, // Safely unwrapped above
locator: finalLocator!, // Safe to force unwrap
actionName: actionName,
actionValue: subCommandEnvelope.actionValue,
pathHint: pathHintComponents,
maxDepth: subCommandEnvelope.maxElements // maxElements often used as maxDepth for search in handlers
)
}
@MainActor
private func processExtractTextCommand(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let locator = subCommandEnvelope.locator else {
guard let originalLocator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for extractText in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let pathHintComponents = subCommandEnvelope.pathHint?.compactMap { PathHintComponent(pathSegment: $0) }
let finalLocator = prepareLocator(for: subCommandEnvelope, existingLocator: originalLocator)
return await self.handleExtractText(
for: subCommandEnvelope.application,
locator: locator, // Safely unwrapped above
pathHint: pathHintComponents,
locator: finalLocator!, // Safe to force unwrap
maxDepth: subCommandEnvelope.maxElements // maxElements often used as maxDepth for search in handlers
)
}

View File

@ -56,10 +56,12 @@ extension AXorcist {
return String(data: jsonData, encoding: .utf8) ?? "{\"error\":\"Failed to encode CollectAllOutput to string (fallback)\"}"
} catch {
axErrorLog("Exception encoding CollectAllOutput: \(error.localizedDescription)")
let cmdId = output.commandId // Assuming these are direct properties
let cmdType = output.command
let errorJson = """
{"command_id":"\(output.commandId)", \
{"command_id":"\(cmdId)", \
"success":false, \
"command":"\(output.command)", \
"command":"\(cmdType)", \
"error_message":"Catastrophic JSON encoding failure for CollectAllOutput. Original error logged.", \
"collected_elements":[], \
"debug_logs":["Catastrophic JSON encoding failure as well."]}
@ -72,22 +74,22 @@ extension AXorcist {
public func handleCollectAll(
for appIdentifierOrNil: String?,
locator: Locator?,
pathHint: [String]?,
maxDepth: Int?,
requestedAttributes: [String]?,
outputFormat: OutputFormat?,
commandId: String?,
debugCLI: Bool
debugCLI: Bool,
filterCriteria: [String: String]? = nil
) async -> String {
let params = CollectAllParameters(
appIdentifierOrNil: appIdentifierOrNil,
locator: locator,
pathHint: pathHint,
maxDepth: maxDepth,
requestedAttributes: requestedAttributes,
outputFormat: outputFormat,
commandId: commandId,
focusedAppKey: AXMiscConstants.focusedApplicationKey
focusedAppKey: AXMiscConstants.focusedApplicationKey,
filterCriteria: filterCriteria
)
logCollectAllStart(params)
@ -102,10 +104,9 @@ extension AXorcist {
)
}
// Determine start element
let startElementResult = await determineStartElement(
// Determine start element using locator.rootElementPathHint
let startElementResult = await determineStartElementForCollectAll(
appElement: appElement,
pathHint: pathHint,
locator: locator,
params: params
)
@ -114,7 +115,7 @@ extension AXorcist {
return await createErrorResponse(
commandId: params.effectiveCommandId,
appIdentifier: params.appIdentifier,
error: startElementResult.error ?? "Failed to determine start element",
error: startElementResult.error ?? "Failed to determine start element for collectAll",
debugCLI: debugCLI
)
}
@ -142,17 +143,17 @@ extension AXorcist {
let attributesToFetch: [String]
let effectiveOutputFormat: OutputFormat
let locator: Locator?
let pathHint: [String]?
let filterCriteria: [String: String]?
init(
appIdentifierOrNil: String?,
locator: Locator?,
pathHint: [String]?,
maxDepth: Int?,
requestedAttributes: [String]?,
outputFormat: OutputFormat?,
commandId: String?,
focusedAppKey: String
focusedAppKey: String,
filterCriteria: [String: String]?
) {
self.effectiveCommandId = commandId ?? "collectAll_internal_id_\(UUID().uuidString.prefix(8))"
self.appIdentifier = appIdentifierOrNil ?? focusedAppKey
@ -162,105 +163,70 @@ extension AXorcist {
self.attributesToFetch = requestedAttributes ?? AXorcist.defaultAttributesToFetch
self.effectiveOutputFormat = outputFormat ?? .smart
self.locator = locator
self.pathHint = pathHint
self.filterCriteria = filterCriteria
}
}
@MainActor
private func logCollectAllStart(_ params: CollectAllParameters) {
let appNameForLog = params.appIdentifier
let locatorDesc = params.locator != nil ? String(describing: params.locator!.criteria) : "nil"
let pathHintDesc = String(describing: params.pathHint)
let locatorCriteriaDesc = params.locator?.criteria.isEmpty == false ? String(describing: params.locator!.criteria) : "nil"
let locatorPathHintDesc = params.locator?.rootElementPathHint?.joined(separator: "->") ?? "nil"
let maxDepthDesc = String(describing: params.recursionDepthLimit)
axInfoLog(
"[AXorcist.handleCollectAll] Starting. App: \(appNameForLog), " +
"Locator: \(locatorDesc), PathHint: \(pathHintDesc), MaxDepth: \(maxDepthDesc)"
"LocatorCriteria: \(locatorCriteriaDesc), LocatorPathHint: \(locatorPathHintDesc), MaxDepth: \(maxDepthDesc)"
)
axDebugLog(
"Effective recursionDepthLimit: \(params.recursionDepthLimit), " +
"attributesToFetch: \(params.attributesToFetch.count) items, " +
"effectiveOutputFormat: \(params.effectiveOutputFormat.rawValue)"
"attributesToFetch: \(params.attributesToFetch.count) items, " +
"effectiveOutputFormat: \(params.effectiveOutputFormat.rawValue)"
)
axDebugLog("Using app identifier: \(params.appIdentifier)")
}
@MainActor
private func determineStartElement(
private func determineStartElementForCollectAll(
appElement: Element,
pathHint: [String]?,
locator: Locator?,
params: CollectAllParameters
) async -> (element: Element?, error: String?) {
var startElement = appElement
var pathNavigated = false
// Navigate to path hint if provided
if let hint = pathHint, !hint.isEmpty {
let pathHintString = hint.joined(separator: " -> ")
axDebugLog("[CollectAll] Navigating to path hint: \(pathHintString)")
guard let navigatedElement = navigateToElement(
from: appElement,
pathHint: hint,
maxDepth: AXMiscConstants.defaultMaxDepthSearch
) else {
return (nil, "Failed to navigate to path: \(pathHintString)")
// If locator.rootElementPathHint is provided, use it to find the start element.
if let pathHintStrings = locator?.rootElementPathHint, !pathHintStrings.isEmpty {
let pathHintComponents = pathHintStrings.compactMap { PathHintComponent(pathSegment: $0) }
if pathHintComponents.count != pathHintStrings.count {
let errorMsg = "[CollectAll] Invalid path hint components in locator for collectAll."
axWarningLog(errorMsg)
return (nil, errorMsg)
}
if pathHintComponents.isEmpty {
axDebugLog("[CollectAll] Locator provided with empty or unparsable rootElementPathHint. Starting from app root.")
return (appElement, nil)
}
startElement = navigatedElement
pathNavigated = true
axDebugLog("[CollectAll] Path navigation successful. Current startElement: \(startElement.briefDescription())")
} else {
axDebugLog("[CollectAll] No pathHint provided. Current startElement: \(startElement.briefDescription()) (app root)")
}
if !pathNavigated, let loc = locator, !loc.criteria.isEmpty {
axDebugLog("[CollectAll] Path navigation did not occur. Trying locator.criteria from startElement: \(startElement.briefDescription())")
if let locatedElement = findElementByLocator(
startElement: startElement,
locator: loc
let pathHintString = pathHintStrings.joined(separator: " -> ")
axDebugLog("[CollectAll] Navigating for start element using locator.rootElementPathHint: \(pathHintString)")
if let navigatedElement = navigateToElementByPathHint(
pathHint: pathHintComponents, // Assuming this is already defined, if not, use global one
initialSearchElement: appElement,
pathHintMaxDepth: pathHintComponents.count - 1
) {
axDebugLog(
"[CollectAll] Locator (criteria-only) found element: \(locatedElement.briefDescription()). " +
"This will be the root for collectAll recursion."
)
startElement = locatedElement
axDebugLog("[CollectAll] Path navigation successful. Start element for collectAll: \(navigatedElement.briefDescription())")
return (navigatedElement, nil)
} else {
let locatorDescription = String(describing: loc.criteria)
let currentStartDesc = startElement.briefDescription()
axWarningLog(
"[CollectAll] Locator (criteria-only) provided but no element found for: \(locatorDescription) from \(currentStartDesc). " +
"CollectAll will proceed from \(currentStartDesc)."
)
let errorMsg = "[CollectAll] Failed to navigate to start element using locator.rootElementPathHint: \(pathHintString)"
axWarningLog(errorMsg)
return (nil, errorMsg)
}
} else if pathNavigated {
axDebugLog("[CollectAll] Path navigation occurred. Using element from path as definitive root: \(startElement.briefDescription()). Locator.criteria (if any) will not be used to further refine this root.")
} else if let loc = locator, loc.criteria.isEmpty {
axDebugLog("[CollectAll] Locator provided with empty criteria and no path hint. Using current startElement: \(startElement.briefDescription()) as root.")
} else {
// No rootElementPathHint in locator, or locator is nil. Start from the application element.
axDebugLog("[CollectAll] No rootElementPathHint in locator or locator is nil. Starting collectAll from app root: \(appElement.briefDescription())")
return (appElement, nil)
}
return (startElement, nil)
}
@MainActor
private func findElementByLocator(
startElement: Element,
locator: Locator
) -> Element? {
var treeTraverser = TreeTraverser()
let searchVisitor = SearchVisitor(locator: locator, requireAction: locator.requireAction)
var traversalState = TraversalState(
maxDepth: AXMiscConstants.defaultMaxDepthSearch,
startElement: startElement
)
return treeTraverser.traverse(
from: startElement,
visitor: searchVisitor,
state: &traversalState
)
}
@MainActor
@ -268,34 +234,29 @@ extension AXorcist {
startElement: Element,
appElement: Element,
params: CollectAllParameters
) async -> [AXElement] {
var traverser = TreeTraverser()
) async -> [AXElementData] {
axDebugLog(
"[CollectAll.performCollectionTraversal] Starting traversal from: \(startElement.briefDescription()), " +
"MaxDepth: \(params.recursionDepthLimit)"
)
let visitor = CollectAllVisitor(
attributesToFetch: params.attributesToFetch,
outputFormat: params.effectiveOutputFormat,
appElement: appElement
appElement: appElement,
valueFormatOption: .default,
filterCriteria: params.filterCriteria
)
var traversalState = TraversalState(
/* ElementSearch. */collectAll(
appElement: appElement,
locator: params.locator ?? Locator(criteria: [:]),
currentElement: startElement,
depth: 0,
maxDepth: params.recursionDepthLimit,
startElement: startElement,
strictChildren: true
maxElements: AXMiscConstants.defaultMaxElementsToCollect,
visitor: visitor
)
axDebugLog("[Pre-Traverse PCT] Handler: validStartElement is: \(startElement.briefDescription(option: .default)) with strictChildren=true")
_ = traverser.traverse(from: startElement, visitor: visitor, state: &traversalState)
let collectedElementsData = visitor.collectedElements
let collectedElementsOutput = collectedElementsData.map { data in
AXElement(attributes: data.attributes, path: data.path)
}
axDebugLog("Traversal complete. Collected \(collectedElementsOutput.count) elements.")
if collectedElementsOutput.isEmpty {
axInfoLog("No elements collected, but traversal itself was successful.")
}
return collectedElementsOutput
axDebugLog("[CollectAll.performCollectionTraversal] Traversal complete. Collected \(visitor.collectedElements.count) elements.")
return visitor.collectedElements
}
@MainActor
@ -305,34 +266,34 @@ extension AXorcist {
error: String,
debugCLI: Bool
) async -> String {
axErrorLog(error)
// Conditionally fetch logs based on debugCLI
axErrorLog("[CollectAll] Error for app \(appIdentifier): \(error)")
let logs = debugCLI ? await GlobalAXLogger.shared.getLogsAsStrings(format: .text) : nil
return encode(CollectAllOutput(
let output = CollectAllOutput(
commandId: commandId,
success: false,
command: "collectAll",
collectedElements: [],
collectedElements: [], // Empty for error response
appBundleId: appIdentifier,
debugLogs: logs,
errorMessage: error
))
)
return encode(output)
}
@MainActor
private func createSuccessResponse(
commandId: String,
appIdentifier: String,
collectedElements: [AXElement],
collectedElements collectedElementsData: [AXElementData],
debugCLI: Bool
) async -> String {
// Conditionally fetch logs based on debugCLI
axInfoLog("[CollectAll] Successfully collected \(collectedElementsData.count) elements for app \(appIdentifier).")
let logs = debugCLI ? await GlobalAXLogger.shared.getLogsAsStrings(format: .text) : nil
let output = CollectAllOutput(
commandId: commandId,
success: true,
command: "collectAll",
collectedElements: collectedElements,
collectedElements: collectedElementsData, // Pass the data directly
appBundleId: appIdentifier,
debugLogs: logs,
errorMessage: nil
@ -340,3 +301,39 @@ extension AXorcist {
return encode(output)
}
}
// Assuming CollectAllOutput is defined something like this:
// struct CollectAllOutput: Codable {
// var commandId: String
// var success: Bool
// var command: String // e.g., "collectAll"
// var errorMessage: String?
// var collectedElements: [AXElementData]
// var appBundleId: String
// var debugLogs: [String]?
//
// enum CodingKeys: String, CodingKey {
// case commandId = "command_id"
// case success
// case command
// case errorMessage = "error_message" // Ensure consistency if CommandEnvelope uses error_message
// case collectedElements = "collected_elements"
// case appBundleId = "app_bundle_id"
// case debugLogs = "debug_logs"
// }
// }
// Make sure AXElementData is defined, probably in DataModels.swift or similar
// public struct AXElementData: Codable { ... }
// Ensure navigateToElementByPathHint is accessible. It is private in ElementSearch.swift.
// For this refactor, we'll assume it's made internal or public, or we use findTargetElement.
// The call `AXorcist.collectAll` in `performCollectionTraversal` refers to the global func in ElementSearch.swift
// It needs to be `ElementSearch.collectAll` or just `collectAll` if in the same module and accessible.
// For the edit, I will assume `collectAll` is callable as a static/global function.
// And `navigateToElementByPathHint` is also made accessible for `determineStartElementForCollectAll`.
// To make `navigateToElementByPathHint` and `collectAll` (from ElementSearch) accessible here,
// they should be marked `internal` or `public` in ElementSearch.swift if AXorcist+CollectAllHandler.swift
// is in a different file but same module, or `public` if different modules.
// For simplicity of this step, I'm writing the logic as if they are callable.

File diff suppressed because it is too large Load Diff

View File

@ -89,6 +89,27 @@ extension GlobalAXLogger {
public func getLogsAsJSON() async throws -> String {
return try await getLogEntriesAsJSON()
}
public func getLogsAsStringsIfEnabled(
format: AXLogOutputFormat,
includeTimestamps: Bool = true,
includeLevels: Bool = true,
includeDetails: Bool = false,
includeAppName: Bool = false,
includeCommandID: Bool = false
) async -> [String]? {
if await self.isLoggingEnabled() {
return await self.getLogsAsStrings(
format: format,
includeTimestamps: includeTimestamps,
includeLevels: includeLevels,
includeDetails: includeDetails,
includeAppName: includeAppName,
includeCommandID: includeCommandID
)
}
return nil
}
}
// MARK: - Log Detail Level

View File

@ -8,6 +8,12 @@ public actor GlobalAXLogger {
public static let shared = GlobalAXLogger()
private var logEntries: [AXLogEntry] = []
// For duplicate suppression
private var lastCondensedMessage: String? = nil
private var duplicateCount: Int = 0
private let duplicateSummaryThreshold: Int = 5
// Maximum characters to keep in a log message before truncating (for readability)
private let maxMessageLength: Int = 300
// private var subscribers: [UUID: @MainActor (AXLogEntry) -> Void] = [:] // REMOVED
// Publicly accessible for direct checks if needed, though usually consumers use subscription.
@ -26,12 +32,67 @@ public actor GlobalAXLogger {
// This method is called by the global ax...Log functions.
// It's actor-isolated, so access to logEntries is serialized.
func log(_ entry: AXLogEntry) {
logEntries.append(entry)
// Condense the message to avoid overly verbose output
let condensedMessage: String = {
if entry.message.count > maxMessageLength {
let prefix = entry.message.prefix(maxMessageLength)
return "\(prefix)… (\(entry.message.count) chars)"
} else {
return entry.message
}
}()
// Suppress consecutive duplicate messages, but emit a summary every duplicateSummaryThreshold repeats
if let last = lastCondensedMessage, last == condensedMessage {
duplicateCount += 1
if duplicateCount % duplicateSummaryThreshold != 0 {
return // Skip storing/logging this duplicate
} else {
let summaryEntry = AXLogEntry(
level: .debug,
message: "⟳ Previous message repeated \(duplicateSummaryThreshold) more times",
file: entry.file,
function: entry.function,
line: entry.line,
details: nil
)
logEntries.append(summaryEntry)
// Fall through to log the duplicate after summary emission
}
} else {
// If a series of duplicates ended, optionally summarise the total count if it exceeds threshold
if duplicateCount >= duplicateSummaryThreshold && lastCondensedMessage != nil {
let summaryEntry = AXLogEntry(
level: .debug,
message: "⟳ Previous message repeated \(duplicateCount) times in total",
file: entry.file,
function: entry.function,
line: entry.line,
details: nil
)
logEntries.append(summaryEntry)
}
// Reset duplicate tracking
lastCondensedMessage = condensedMessage
duplicateCount = 0
}
// Store the (potentially condensed) entry
let processedEntry = AXLogEntry(
level: entry.level,
message: condensedMessage,
file: entry.file,
function: entry.function,
line: entry.line,
details: entry.details
)
logEntries.append(processedEntry)
// JSON logging to stderr if enabled
if isJSONLoggingEnabled {
do {
let jsonData = try JSONEncoder().encode(entry)
let jsonData = try JSONEncoder().encode(processedEntry)
if let jsonString = String(data: jsonData, encoding: .utf8) {
fputs(jsonString + "\\n", stderr) // Output JSON string to stderr
}

View File

@ -6,36 +6,146 @@ import Foundation
// PathHintComponent and criteriaMatch are now in SearchCriteriaUtils.swift
// MARK: - Main Search Logic (findElementViaPathAndCriteria and its helpers)
// MARK: - Main Element Finding Orchestration
/**
Unified function to find a target element based on application, locator (criteria and/or path hint).
This is the primary entry point for handlers.
*/
@MainActor
public func findTargetElement(
for appIdentifierOrNil: String?,
locator: Locator,
maxDepthForSearch: Int
) async -> (element: Element?, error: String?) { // Changed return type to match old handlers
let appIdentifier = appIdentifierOrNil ?? AXMiscConstants.focusedApplicationKey
axDebugLog(
"[findTargetElement ENTRY] App=\(appIdentifier), Locator: criteria=\(locator.criteria), " +
"pathHint=\(locator.rootElementPathHint?.joined(separator: "->") ?? "nil")"
)
guard let appElement = applicationElement(for: appIdentifier) else {
let msg = "Application not found: \(appIdentifier)"
axErrorLog(msg)
return (nil, msg)
}
let pathHintStrings = locator.rootElementPathHint
let criteria = locator.criteria
// Scenario 1: Only pathHint is provided (or criteria are app-specific)
let appSpecificCriteriaKeys = ["bundleId", "application", "pid", "path"]
let hasOnlyAppSpecificCriteria = criteria.isEmpty || criteria.keys.allSatisfy { appSpecificCriteriaKeys.contains($0) }
if let hintStrings = pathHintStrings, !hintStrings.isEmpty, hasOnlyAppSpecificCriteria {
axDebugLog("findTargetElement: Using pathHint primarily as criteria are app-specific or empty.")
let pathComponents = hintStrings.compactMap { PathHintComponent(pathSegment: $0) }
if pathComponents.count != hintStrings.count {
let msg = "Invalid path hint components provided."
axWarningLog(msg)
// Fall through to regular search if path hint is malformed but criteria exist
if criteria.isEmpty { return (nil, msg) }
} else if !pathComponents.isEmpty {
if let elementFromPath = /* private */ navigateToElementByPathHint(
pathHint: pathComponents,
initialSearchElement: appElement,
pathHintMaxDepth: pathComponents.count - 1 // Navigate full path
) {
axInfoLog("findTargetElement: Found element directly via pathHint: \(elementFromPath.briefDescription())")
// If caller specified descendantCriteria, search within the located element.
if let descCrit = locator.descendantCriteria, !descCrit.isEmpty {
axDebugLog("findTargetElement: Performing descendantCriteria search within located element. Descendant criteria: \(descCrit)")
var descLocator = Locator(criteria: descCrit)
if let descendant = traverseAndSearch(currentElement: elementFromPath,
locator: descLocator,
effectiveMaxDepth: maxDepthForSearch) {
return (descendant, nil)
} else {
return (nil, "Descendant element not found matching descendantCriteria: \(descCrit)")
}
}
return (elementFromPath, nil)
} else {
let msg = "Element not found via pathHint: \(hintStrings.joined(separator: " -> "))"
axWarningLog(msg)
return (nil, msg) // Path hint was specified but failed
}
}
}
// Scenario 2: Criteria are present (potentially with a pathHint to narrow down search root)
// findElementViaPathAndCriteria will use pathHint from locator to find searchStartElement,
// then apply criteria.
axDebugLog("findTargetElement: Proceeding with criteria-based search (pathHint may refine start).")
if let foundElement = findElementViaPathAndCriteria(
application: appElement,
locator: locator, // This locator contains both criteria and potentially rootElementPathHint
maxDepth: maxDepthForSearch
) {
axInfoLog("findTargetElement: Found element via criteria (and/or path): \(foundElement.briefDescription())")
var baseElement = foundElement
// Apply descendantCriteria if present
if let descCrit = locator.descendantCriteria, !descCrit.isEmpty {
axDebugLog("findTargetElement: Performing descendantCriteria search within base element. Descendant criteria: \(descCrit)")
let descLoc = Locator(criteria: descCrit)
if let descendant = traverseAndSearch(currentElement: baseElement,
locator: descLoc,
effectiveMaxDepth: maxDepthForSearch) {
baseElement = descendant
} else {
let msg = "Descendant element not found matching descendantCriteria: \(descCrit)"
axWarningLog(msg)
return (nil, msg)
}
}
return (baseElement, nil)
} else {
let msg = "Element not found matching criteria: \(locator.criteria)"
if let hint = locator.rootElementPathHint, !hint.isEmpty {
axWarningLog("\(msg) (path hint was: \(hint.joined(separator: " -> ")))")
} else {
axWarningLog(msg)
}
return (nil, msg)
}
}
// MARK: - Core Search Logic (findElementViaPathAndCriteria and its helpers)
@MainActor
private func navigateToElementByPathHint(
/* private -> internal */ internal func navigateToElementByPathHint(
pathHint: [PathHintComponent],
initialSearchElement: Element,
pathHintMaxDepth: Int
pathHintMaxDepth: Int // Max depth for THIS path navigation segment
) -> Element? {
var currentElementInPath = initialSearchElement
axDebugLog(
"PathHintNav: Starting with \(pathHint.count) components from " +
"\(initialSearchElement.briefDescription())"
"\(initialSearchElement.briefDescription()), maxNavDepth: \(pathHintMaxDepth)"
)
for (index, pathComponent) in pathHint.enumerated() {
let currentNavigationDepth = index
if index > pathHintMaxDepth { // Respect max depth for this navigation
axDebugLog("PathHintNav: Max navigation depth (\(pathHintMaxDepth)) reached at component #\(index).")
return currentElementInPath // Return what we have so far
}
let criteriaDesc = pathComponent.criteria.map { "\($0.key):\($0.value)" }.joined(separator: ", ")
axDebugLog(
"PathHintNav: Visiting comp #\(index), Depth:\(currentNavigationDepth), " +
"PathHintNav: Visiting comp #\(index), Depth:\(index), " +
"Elem:\(currentElementInPath.briefDescription(option: .short)), " +
"Crit:\(criteriaDesc), MaxD:\(pathHintMaxDepth)"
"Crit:\(criteriaDesc))"
)
// Check if the current element in path matches the current path component
// This logic was a bit off. The component should match the *current* element, not its children.
if !pathComponent.matches(element: currentElementInPath) {
axDebugLog(
"PathHintNav: No match for comp #\(index), " +
"Elem:\(currentElementInPath.briefDescription(option: .short)), " +
"Crit:\(criteriaDesc))"
"PathHintNav: Current element \(currentElementInPath.briefDescription(option: .short)) " +
"does NOT match comp #\(index) Crit:\(criteriaDesc))"
)
return nil
return nil // Path broken
}
axDebugLog(
@ -44,35 +154,39 @@ private func navigateToElementByPathHint(
"Crit:\(criteriaDesc))"
)
// If this is the last component, we've successfully navigated the path
if index == pathHint.count - 1 {
return currentElementInPath // Reached end of path hint and matched
return currentElementInPath
}
let nextPathComponentCriteria = pathHint[index + 1].criteria
var foundNextChild: Element?
if let children = currentElementInPath.children() {
for child in children {
let tempPathComponent = PathHintComponent(criteria: nextPathComponentCriteria)
if tempPathComponent.matches(element: child) {
currentElementInPath = child
foundNextChild = child
break
}
// Not the last component, so we need to find a child that matches the *next* component
guard let children = currentElementInPath.children() else {
axDebugLog("PathHintNav: Current element \(currentElementInPath.briefDescription(option: .short)) has no children. Cannot proceed to next component.")
return nil // Path broken, cannot find next step
}
let nextPathComponent = pathHint[index + 1]
var foundNextChildInPath: Element? = nil
for child in children {
if nextPathComponent.matches(element: child) {
currentElementInPath = child // Advance current element
foundNextChildInPath = child
break
}
}
if foundNextChild == nil {
let nextCriteriaDesc = nextPathComponentCriteria
.map { "\($0.key):\($0.value)" }.joined(separator: ", ")
if foundNextChildInPath == nil {
let nextCriteriaDesc = nextPathComponent.criteria.map { "\($0.key):\($0.value)" }.joined(separator: ", ")
axDebugLog(
"PathHintNav: Could not find child for next comp #\(index + 1), " +
"Under Elem:\(currentElementInPath.briefDescription(option: .short)), " +
"NextCrit:\(nextCriteriaDesc))"
"PathHintNav: Could not find child matching next comp #\(index + 1) " +
"(Crit: \(nextCriteriaDesc)) under Elem:\(currentElementInPath.briefDescription(option: .short))"
)
return nil
return nil // Path broken, cannot find next step
}
}
return currentElementInPath
// Should have returned from within the loop if path was fully matched or broken
// If loop finishes it means pathHint was empty or logic error
return pathHint.isEmpty ? initialSearchElement : nil
}
@MainActor
@ -81,8 +195,16 @@ private func traverseAndSearch(
locator: Locator,
effectiveMaxDepth: Int
) -> Element? {
// Ensure criteria exist if we are in traverseAndSearch.
// If only path hint was used, findTargetElement should have returned earlier.
if locator.criteria.isEmpty {
axDebugLog("traverseAndSearch: Called with empty criteria. This usually means element should have been found by path hint alone. Returning current element: \(currentElement.briefDescription())")
// This might be the element found by path hint if criteria were indeed empty.
return currentElement
}
var traverser = TreeTraverser()
let visitor = SearchVisitor(locator: locator)
let visitor = SearchVisitor(locator: locator) // SearchVisitor uses locator.criteria
var traversalState = TraversalState(maxDepth: effectiveMaxDepth, startElement: currentElement)
let result = traverser.traverse(from: currentElement, visitor: visitor, state: &traversalState)
return result
@ -94,7 +216,7 @@ private func processPathHintAndDetermineStartElement(
locator: Locator
) -> Element {
guard let pathHintStrings = locator.rootElementPathHint, !pathHintStrings.isEmpty else {
axDebugLog("No path hint provided. Searching from application root.")
axDebugLog("processPathHint: No rootElementPathHint provided in locator. Searching from application root.")
return application
}
@ -102,51 +224,73 @@ private func processPathHintAndDetermineStartElement(
guard !pathHintComponents.isEmpty && pathHintComponents.count == pathHintStrings.count else {
axDebugLog(
"Path hint strings provided but failed to parse into components or " +
"processPathHint: rootElementPathHint strings provided but failed to parse into components or " +
"some were invalid. Full search from app root."
)
return application
}
axDebugLog("Starting path hint navigation. Number of components: \(pathHintComponents.count)")
axDebugLog("processPathHint: Starting path hint navigation for search root. Number of components: \(pathHintComponents.count)")
if let elementFromPathHint = navigateToElementByPathHint(
if let elementFromPathHint = /* private -> internal */ navigateToElementByPathHint(
pathHint: pathHintComponents,
initialSearchElement: application,
pathHintMaxDepth: pathHintComponents.count - 1
pathHintMaxDepth: pathHintComponents.count - 1 // Navigate the full path to find the start element
) {
axDebugLog(
"Path hint navigation successful. New start: " +
"\(elementFromPathHint.briefDescription()). Starting criteria search."
"processPathHint: Path hint navigation successful. New search start: " +
"\(elementFromPathHint.briefDescription())."
)
return elementFromPathHint
} else {
axDebugLog("Path hint navigation failed. Full search from app root.")
axWarningLog("processPathHint: Path hint navigation failed. Full search will be from app root. Path: \(pathHintStrings.joined(separator: " -> "))")
return application
}
}
/**
This function is the core for criteria-based search, potentially starting from an element
determined by a path hint (via locator.rootElementPathHint).
*/
@MainActor
func findElementViaPathAndCriteria(
/* internal -> func */ func findElementViaPathAndCriteria(
application: Element,
locator: Locator,
maxDepth: Int?
) -> Element? {
let pathHintDebug = locator.rootElementPathHint?.joined(separator: " -> ") ?? "nil"
let criteriaDebug = locator.criteria
axDebugLog(
"[findElementViaPathAndCriteria ENTRY] locator.criteria: \(locator.criteria), " +
"locator.rootElementPathHint: \(pathHintDebug) from app PID \(application.pid() ?? -1)"
"[findElementViaPathAndCriteria ENTRY] AppPID: \(application.pid() ?? -1), Locator.criteria: \(criteriaDebug), " +
"Locator.rootElementPathHint: \(pathHintDebug)"
)
// Determine the actual starting element for the criteria search.
// If locator.rootElementPathHint is present, navigate to it. Otherwise, start from app root.
let searchStartElement = processPathHintAndDetermineStartElement(
application: application,
locator: locator
)
// If criteria are empty at this point, it means the path hint (if any) was the sole specifier.
// The searchStartElement is our target.
if locator.criteria.isEmpty {
if locator.rootElementPathHint != nil && !locator.rootElementPathHint!.isEmpty {
axInfoLog("[findElementViaPathAndCriteria] Criteria are empty, path hint was primary. Returning element from path: \(searchStartElement.briefDescription())")
return searchStartElement // Element found by path hint is the target
} else {
axWarningLog("[findElementViaPathAndCriteria] Criteria are empty and no path hint. Returning application root by default.")
return application // Or nil if this case isn't desired
}
}
axDebugLog("[findElementViaPathAndCriteria] Search start element: \(searchStartElement.briefDescription()). Now applying criteria: \(locator.criteria)")
let resolvedMaxDepth = maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
return traverseAndSearch(
currentElement: searchStartElement,
locator: locator,
locator: locator, // Locator contains criteria
effectiveMaxDepth: resolvedMaxDepth
)
}
@ -165,12 +309,13 @@ internal func evaluateElementAgainstCriteria(
depth: Int // Depth might still be useful for logical purposes, not for logging state
) -> ElementMatchStatus {
if locator.rootElementPathHint != nil, !locator.rootElementPathHint!.isEmpty {
axDebugLog(
"evaluateElement: Path hint was present in locator, assuming pre-navigated. " +
"Element: \(element.briefDescription())"
)
}
// Path hint check here might be less relevant if pre-navigation is robust
// if locator.rootElementPathHint != nil, !locator.rootElementPathHint!.isEmpty {
// axDebugLog(
// "evaluateElement: Path hint was present in locator, assuming pre-navigated. " +
// "Element: \(element.briefDescription())"
// )
// }
if !criteriaMatch(element: element, criteria: locator.criteria) {
return .noMatch
@ -182,7 +327,7 @@ internal func evaluateElementAgainstCriteria(
"Element \(element.briefDescription()) matches criteria but is " +
"missing required action '\(actionName)'."
)
return .noMatch
return .noMatch // Changed from partialMatchActionMissing to noMatch for stricter interpretation
}
axDebugLog("Element \(element.briefDescription()) matches criteria AND has required action '\(actionName)'.")
} else {
@ -229,7 +374,7 @@ public func search(element: Element,
*/
@MainActor
public func collectAll(
/* public -> internal */ internal func collectAll(
appElement: Element, // Root element of the application, for path context
locator: Locator,
// Criteria for matching elements (though CollectAllVisitor doesn't use it for filtering currently)
@ -247,10 +392,11 @@ public func collectAll(
var traverser = TreeTraverser()
var state = TraversalState(maxDepth: maxDepth, startElement: currentElement)
// The traverse method in TreeTraverser doesn't directly use maxElements from TraversalState to stop.
// The CollectAllVisitor's visit method should implement the maxElements check.
_ = traverser.traverse(from: currentElement, visitor: visitor, state: &state)
axDebugLog("collectAll: Traversal complete. Visitor collected \(visitor.collectedElements.count) elements.")
// Result of traverse is Element? (the first one found), but for collectAll we rely on visitor's side effects.
axDebugLog("collectAll: Traversal complete. Collected \(visitor.collectedElements.count) elements.")
}
// Remaining functions in this file (like path navigation helpers if any outside findElementViaPathAndCriteria)

View File

@ -13,46 +13,53 @@ private func elementMatchesAllCriteria(
forPathComponent pathComponentForLog: String // For logging
) async -> Bool {
let elementDescriptionForLog = element.briefDescription(option: .short)
// Explicitly log the element being checked and the criteria count
axDebugLog("PathNav/EMAC: Checking element [\(elementDescriptionForLog)] against criteria for component [\(pathComponentForLog)]. Criteria count: \(criteria.count). Criteria: \(criteria)", file: #file, function: #function, line: #line)
axDebugLog("PathNav/EMAC: Checking element [\(elementDescriptionForLog)] against criteria for component [\(pathComponentForLog)]. Criteria count: \(criteria.count). Criteria: \(criteria)")
guard !criteria.isEmpty else {
axWarningLog("PathNav/EMAC: Criteria IS EMPTY for path component [\(pathComponentForLog)] on element [\(elementDescriptionForLog)]. Bailing out.", file: #file, function: #function, line: #line)
return false
axWarningLog("PathNav/EMAC: Criteria IS EMPTY for path component [\(pathComponentForLog)] on element [\(elementDescriptionForLog)]. Returning false as no criteria to match.")
return false // If criteria is empty, technically nothing to match against.
}
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)
// If the element being checked IS the application (by its role),
// and we're checking its PID criterion from a path hint component,
// assume the PID matches because the app context is already established.
if await element.role() == AXRoleNames.kAXApplicationRole {
axDebugLog("Element [\(elementDescriptionForLog)] is AXApplication (role check). PID criterion '\(expectedValue)' from path component '\(pathComponentForLog)' considered met by context.")
continue // Skip further PID checks for the application element itself
}
guard let actualPid_t = await element.pid() else {
axDebugLog("Element [\(elementDescriptionForLog)] failed to provide PID (for path component [\(pathComponentForLog)]). No match.")
return false
}
let actualPid = Int(actualPid_t) // Convert pid_t to Int for comparison
let actualPid = Int(actualPid_t)
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.")
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.")
return false
}
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("Element [\(elementDescriptionForLog)] PID [\(actualPid)] == expected [\(expectedPid)] (for path component [\(pathComponentForLog)]). Criterion met.")
} else { // Handle other attributes
let fetchedAttributeValue: String? = await element.attribute(Attribute(key))
axDebugLog("PathNav/EMAC: For element [\(elementDescriptionForLog)], component [\(pathComponentForLog)], attr [\(key)], fetched value is: [\(String(describing: fetchedAttributeValue))].")
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
guard let actualValue = fetchedAttributeValue else {
axDebugLog("Element [\(elementDescriptionForLog)] lacks attribute [\(key)] (value was nil after fetch) for path component [\(pathComponentForLog)]. No match.")
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.")
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.")
}
}
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!")
return true
}
@ -144,8 +151,7 @@ private func processPathComponent(
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: PRE-CALL FMIC", file: #file, function: #function, line: #line)
axDebugLog("PathNav/PPC: Step \(stepCounter). After PRE-CALL FMIC. Before findMatchingChild call.")
stepCounter += 1
@ -218,8 +224,7 @@ 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: ABSOLUTE ENTRY", file: #file, function: #function, line: #line)
axDebugLog("PathNav/FMIC: Entered function for component [\(pathComponentForLog)]. Criteria: \(criteriaToMatch)", file: #file, function: #function, line: #line)

View File

@ -10,27 +10,58 @@ public struct PathHintComponent {
public let criteria: [String: String]
public let originalSegment: String // Added to store the original segment
// Refactored initializer
/// Aliases mapping human-readable keys (as produced by Accessibility Inspector) to actual AX attribute names.
private static let attributeAliases: [String: String] = [
// Common role/title identifiers that use ':' delimiter in Inspector output
"Role": AXAttributeNames.kAXRoleAttribute,
"Title": AXAttributeNames.kAXTitleAttribute,
"Subrole": AXAttributeNames.kAXSubroleAttribute,
"Identifier": AXAttributeNames.kAXIdentifierAttribute,
"DOMId": AXAttributeNames.kAXDOMIdentifierAttribute,
// PID is handled specially elsewhere, keep as-is
"PID": "PID"
]
public init?(pathSegment: String) {
self.originalSegment = pathSegment // Store original segment
var parsedCriteria: [String: String] = [:]
let pairs = pathSegment.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
for pair in pairs {
let keyValue = pair.split(separator: "=", maxSplits: 1)
self.originalSegment = pathSegment
// First, try to parse with PathUtils.parseRichPathComponent which supports ':' delimiters
var parsedCriteria = PathUtils.parseRichPathComponent(pathSegment)
// Fallback older format that uses '=' as delimiter
if parsedCriteria.isEmpty {
let fallbackPairs = pathSegment
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
if keyValue.count == 2 {
parsedCriteria[String(keyValue[0])] = String(keyValue[1])
} else {
axDebugLog("PathHintComponent: Invalid key-value pair: \(pair)")
for pair in fallbackPairs {
let keyValue = pair.split(separator: "=", maxSplits: 1)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
if keyValue.count == 2 {
parsedCriteria[String(keyValue[0])] = String(keyValue[1])
}
}
}
if parsedCriteria.isEmpty && !pathSegment.isEmpty {
axDebugLog("PathHintComponent: Path segment \"\(pathSegment)\" parsed into empty criteria.")
// Apply alias mapping so that keys line up with real AX attribute names expected by the matcher.
var mappedCriteria: [String: String] = [:]
for (rawKey, value) in parsedCriteria {
if let mappedKey = Self.attributeAliases[rawKey] {
mappedCriteria[mappedKey] = value
} else {
mappedCriteria[rawKey] = value
}
}
self.criteria = parsedCriteria
let criteriaForLog = self.criteria
let segmentForLog = pathSegment
axDebugLog("PathHintComponent initialized with criteria: \(criteriaForLog) from segment: \(segmentForLog)")
// If still empty after parsing/mapping, return nil so that the component is ignored by caller.
if mappedCriteria.isEmpty {
axWarningLog("PathHintComponent: Path segment '\(pathSegment)' produced no usable criteria after parsing.")
return nil
}
self.criteria = mappedCriteria
let critDesc = mappedCriteria
axDebugLog("PathHintComponent initialized. Segment: '\(pathSegment)' => criteria: \(critDesc)")
}
// Convenience initializer if criteria is already a dictionary
@ -47,57 +78,119 @@ public struct PathHintComponent {
// MARK: - Criteria Matching Helper
@MainActor
func criteriaMatch(element: Element, criteria: [String: String]?) -> Bool {
public func criteriaMatch(
element: Element,
criteria: [String: String]?,
matchAll: Bool? = true,
appProcessId: pid_t? = nil
) -> Bool {
guard let criteria = criteria, !criteria.isEmpty else {
return true // No criteria means an automatic match
}
for (key, expectedValue) in criteria {
if key == AXAttributeNames.kAXRoleAttribute && expectedValue == "*" { continue } // Wildcard for role
let elementDescriptionForLog = element.briefDescription(option: .short)
axDebugLog("criteriaMatch: Checking element [\(elementDescriptionForLog)] against criteria. Criteria count: \(criteria.count). Criteria: \(criteria)")
if key == "IsClickable" { // Computed property
for (key, expectedValue) in criteria {
if key == AXAttributeNames.kAXRoleAttribute && expectedValue == "*" {
axDebugLog("criteriaMatch: Wildcard for role attribute matched.")
continue // Wildcard for role
}
// Special handling for PID, similar to elementMatchesAllCriteria
if key == "PID" {
if element.role() == AXRoleNames.kAXApplicationRole {
axDebugLog("Element [\(elementDescriptionForLog)] is AXApplication. PID criterion '\(expectedValue)' considered met by context.")
continue
}
guard let actualPid_t = element.pid() else {
axDebugLog("Element [\(elementDescriptionForLog)] failed to provide PID. No match for key 'PID'.")
return false
}
let actualPid = Int(actualPid_t)
guard let expectedPid = Int(expectedValue) else {
axDebugLog("Element [\(elementDescriptionForLog)] PID criteria '\(expectedValue)' is not a valid Int. No match for key 'PID'.")
return false
}
if actualPid != expectedPid {
axDebugLog("Element [\(elementDescriptionForLog)] PID [\(actualPid)] != expected [\(expectedPid)]. No match for key 'PID'.")
return false
}
axDebugLog("Element [\(elementDescriptionForLog)] PID [\(actualPid)] == expected [\(expectedPid)]. Criterion met for key 'PID'.")
continue // PID matched, move to next criterion
}
// Handle "IsClickable" as a computed property
if key == "IsClickable" {
let supportsPress = element.isActionSupported(AXActionNames.kAXPressAction)
let expectedBoolValue = (expectedValue.lowercased() == "true")
if supportsPress == expectedBoolValue {
axDebugLog(
"Computed criteria 'IsClickable' (via AXPress support) matched: " +
"Expected '\(expectedValue)', Got '\(supportsPress)'."
)
axDebugLog("Computed criteria 'IsClickable' (via AXPress support) matched: Expected '\(expectedValue)', Got '\(supportsPress)'.")
continue
} else {
axDebugLog(
"Computed criteria 'IsClickable' (via AXPress support) mismatch: " +
"Expected '\(expectedValue)', Got '\(supportsPress)'. " +
"Element: \(element.briefDescription(option: .default)). No match."
)
axDebugLog("Computed criteria 'IsClickable' (via AXPress support) mismatch: Expected '\(expectedValue)', Got '\(supportsPress)'. Element: \(elementDescriptionForLog). No match.")
return false
}
}
// Removed unused variable: var attributeValueCFType: CFTypeRef?
let rawValue = element.rawAttributeValue(named: key)
// For other attributes, fetch as String and perform exact match
let fetchedAttributeValue: String? = element.attribute(Attribute(key))
axDebugLog("criteriaMatch: For element [\(elementDescriptionForLog)], attr [\(key)], fetched value is: [\(String(describing: fetchedAttributeValue))]. Expected: [\(expectedValue)]")
guard let actualValueCF = rawValue else {
axDebugLog(
"Attribute \(key) not found or error on element " +
"\(element.briefDescription(option: .default)). No match."
)
guard let actualValue = fetchedAttributeValue else {
// If attribute is not present, it's a mismatch unless expectedValue indicates absence (e.g., "~nil" or "~empty")
// or if a regex is used that could potentially match an empty string (though attribute must exist).
if expectedValue.lowercased() == "~nil" {
axDebugLog("Element [\(elementDescriptionForLog)] lacks attribute [\(key)]. Expected '~nil'. Criterion met.")
continue
}
// If expecting a regex match, the attribute must exist.
if expectedValue.starts(with: "~regex:") {
axDebugLog("Element [\(elementDescriptionForLog)] lacks attribute [\(key)] (value was nil after fetch). Expected regex match for '\(expectedValue)'. No match.")
return false
}
axDebugLog("Element [\(elementDescriptionForLog)] lacks attribute [\(key)] (value was nil after fetch). Expected '\(expectedValue)'. No match.")
return false
}
let actualValueSwift: Any? = ValueUnwrapper.unwrap(actualValueCF)
let actualValueString = String(describing: actualValueSwift ?? "nil_after_unwrap")
// Handle ~empty explicitly, before regex, as regex could also match empty.
if expectedValue.lowercased() == "~empty" {
if actualValue.isEmpty {
axDebugLog("Element [\(elementDescriptionForLog)] attribute [\(key)] is empty. Expected '~empty'. Criterion met.")
continue
} else {
axDebugLog("Element [\(elementDescriptionForLog)] attribute [\(key)] value [\(actualValue)] is not empty. Expected '~empty'. No match.")
return false
}
}
if !(actualValueString.localizedCaseInsensitiveContains(expectedValue) || actualValueString == expectedValue) {
axDebugLog(
"Attribute '\(key)' mismatch: Expected '\(expectedValue)', " +
"Got '\(actualValueString)'. " +
"Element: \(element.briefDescription(option: .default)). No match."
)
// Regular Expression Matching
if expectedValue.starts(with: "~regex:") {
let pattern = String(expectedValue.dropFirst("~regex:".count))
do {
// Default to case-insensitive matching for UI elements, which is generally more useful.
let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let range = NSRange(actualValue.startIndex..<actualValue.endIndex, in: actualValue)
if regex.firstMatch(in: actualValue, options: [], range: range) != nil {
axDebugLog("Element [\(elementDescriptionForLog)] attribute [\(key)] value [\(actualValue)] MATCHED regex [\(pattern)]. Criterion met.")
continue // Matched
} else {
axDebugLog("Element [\(elementDescriptionForLog)] attribute [\(key)] value [\(actualValue)] did NOT match regex [\(pattern)]. No match.")
return false // Did not match
}
} catch {
axErrorLog("Invalid regex pattern [\(pattern)] for key [\(key)]: \(error.localizedDescription). Treating as no match.")
return false // Invalid regex pattern
}
}
// Exact, case-sensitive match (fallback if not a special prefix)
else if actualValue != expectedValue {
axDebugLog("Element [\(elementDescriptionForLog)] attribute [\(key)] value [\(actualValue)] != expected [\(expectedValue)] (exact match). No match.")
return false
}
axDebugLog("Attribute '\(key)' matched: Expected '\(expectedValue)', Got '\(actualValueString)'.")
axDebugLog("Element [\(elementDescriptionForLog)] attribute [\(key)] value [\(actualValue)] == expected [\(expectedValue)] (or matched regex). Criterion met.")
}
axDebugLog("All criteria matched for element: \(element.briefDescription(option: .default)).")
axDebugLog("Element [\(elementDescriptionForLog)] matches ALL criteria. Match!")
return true
}

View File

@ -108,15 +108,35 @@ public class CollectAllVisitor: TreeVisitor {
public var collectedElements: [AXElementData] = []
// Default valueFormatOption for CollectAllVisitor if not specified otherwise
private let valueFormatOption: ValueFormatOption
private let filterCriteria: [String: String]?
public init(attributesToFetch: [String], outputFormat: OutputFormat, appElement: Element, valueFormatOption: ValueFormatOption = .default) {
public init(attributesToFetch: [String], outputFormat: OutputFormat, appElement: Element, valueFormatOption: ValueFormatOption = .default, filterCriteria: [String: String]? = nil) {
self.attributesToFetch = attributesToFetch
self.outputFormat = outputFormat
self.appElement = appElement
self.valueFormatOption = valueFormatOption
self.filterCriteria = filterCriteria
}
public func visit(element: Element, depth: Int, state: inout TraversalState) -> TraversalAction {
// Check against filterCriteria if provided
if let criteria = self.filterCriteria, !criteria.isEmpty {
// Assuming SearchCriteriaUtils.criteriaMatch is accessible here.
// If not, this logic needs to be adapted or the function made available.
// Defaulting matchAll to true, as filters usually imply all conditions must pass.
let matchesFilter = SearchCriteriaUtils.criteriaMatch(
element: element,
criteria: criteria,
matchAll: true,
appProcessId: element.pid() // Pass PID for context if needed by criteriaMatch
)
if !matchesFilter {
axDebugLog("[CollectAllVisitor] Element \(element.briefDescription()) did NOT match filterCriteria. Skipping.")
return .continueTraversal // Skip this element, but continue traversal for its children
}
axDebugLog("[CollectAllVisitor] Element \(element.briefDescription()) MATCHED filterCriteria.")
}
// getElementAttributes is now a global function
let (fetchedAttrs, _) = getElementAttributes(
element: element,

View File

@ -106,15 +106,8 @@ struct AXORCCommand: AsyncParsableCommand {
// Handle input errors
if let error = inputResult.error {
let collectedLogs = debug ? await GlobalAXLogger.shared.getLogsAsStrings(format: .text, includeTimestamps: true, includeLevels: true, includeDetails: true) : nil
let errorResponse = ErrorResponse(
commandId: "input_error",
error: error,
debugLogs: collectedLogs
)
if let jsonData = try? JSONEncoder().encode(errorResponse),
let jsonString = String(data: jsonData, encoding: .utf8) {
let errorResponse = ErrorResponse(commandId: "input_error", error: error, debugLogs: collectedLogs)
if let jsonData = try? JSONEncoder().encode(errorResponse), let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
} else {
print("{\"error\": \"Failed to encode error response\"}")
@ -122,97 +115,50 @@ struct AXORCCommand: AsyncParsableCommand {
return
}
guard var jsonStringFromInput = inputResult.jsonString else {
guard let jsonStringFromInput = inputResult.jsonString else {
let collectedLogs = debug ? await GlobalAXLogger.shared.getLogsAsStrings(format: .text, includeTimestamps: true, includeLevels: true, includeDetails: true) : nil
let errorResponse = ErrorResponse(
commandId: "no_input",
error: "No valid JSON input received",
debugLogs: collectedLogs
)
if let jsonData = try? JSONEncoder().encode(errorResponse),
let jsonStr = String(data: jsonData, encoding: .utf8) {
let errorResponse = ErrorResponse(commandId: "no_input", error: "No valid JSON input received", debugLogs: collectedLogs)
if let jsonData = try? JSONEncoder().encode(errorResponse), let jsonStr = String(data: jsonData, encoding: .utf8) {
print(jsonStr)
} else {
print("{\"error\": \"Failed to encode error response\"}")
}
return
}
axDebugLog("AXORCMain: jsonStringFromInput (from InputHandler): [\(jsonStringFromInput)] (length: \(jsonStringFromInput.count))")
axDebugLog("AXORCMain Test: Received jsonStringFromInput: [\(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
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.")
if let data = jsonStringFromInput.data(using: .utf8) {
let axorcist = AXorcist()
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
// Attempt 1: Decode as [CommandEnvelope]
let commands = try decoder.decode([CommandEnvelope].self, from: data)
if let command = commands.first {
axDebugLog("AXORCMain Test: Decode attempt 1: Successfully decoded [CommandEnvelope] and got first command.")
await processAndExecuteCommand(command: command, axorcist: axorcist, debugCLI: debug)
} else {
axDebugLog("AXORCMain Test: Decode attempt 1: Decoded [CommandEnvelope] but array was empty.")
// Create a generic error to throw if this path is problematic
let anError = NSError(domain: "AXORCErrorDomain", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Decoded empty command array from [CommandEnvelope] attempt."])
throw anError
}
} catch let arrayDecodeError {
axDebugLog("AXORCMain Test: Decode attempt 1 (as [CommandEnvelope]) FAILED. Error: \(arrayDecodeError). Will try as single CommandEnvelope.")
// Attempt 2: Decode as single CommandEnvelope
do {
let command = try decoder.decode(CommandEnvelope.self, from: data) // data is still from jsonStringFromInput
axDebugLog("AXORCMain Test: Decode attempt 2: Successfully decoded as SINGLE CommandEnvelope.")
await processAndExecuteCommand(command: command, axorcist: axorcist, debugCLI: debug)
} catch let singleDecodeError {
axDebugLog("AXORCMain Test: Decode attempt 2 (as single CommandEnvelope) ALSO FAILED. Error: \(singleDecodeError). Original array decode error was: \(arrayDecodeError)")
throw singleDecodeError // Throw the error from the single decode attempt as it's the most direct if input was not an array
}
}
} else {
axDebugLog("AXORCMain: Original concrete jsonString does not appear to be a simple array wrapper. Proceeding with it for data conversion.")
axDebugLog("AXORCMain Test: Failed to convert jsonStringFromInput to data.")
let anError = NSError(domain: "AXORCErrorDomain", code: 1002, userInfo: [NSLocalizedDescriptionKey: "Failed to convert jsonStringFromInput to data."])
throw anError
}
// 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\"}")
return
}
if debug {
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)
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 {
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.
}
}

View File

@ -47,10 +47,17 @@ struct CommandExecutor {
}
private static func setupLogging(for command: CommandEnvelope) async -> (Bool, AXLogDetailLevel) {
// DIAGNOSTIC LOG: Print the received value of command.debugLogging
fputs("CommandExecutor.setupLogging: Received command.debugLogging = \(command.debugLogging)\n", stderr)
fflush(stderr) // Ensure it prints immediately for CLI debugging
// Also log it via axDebugLog so it becomes part of the collected logs
axDebugLog("[CommandExecutor.setupLogging] Received command.debugLogging = \(command.debugLogging)")
let initialLoggingEnabled = await GlobalAXLogger.shared.isLoggingEnabled()
let initialDetailLevel = await GlobalAXLogger.shared.getDetailLevel()
if let cmdDebug = command.debugLogging, cmdDebug {
if command.debugLogging {
await GlobalAXLogger.shared.setLoggingEnabled(true)
await GlobalAXLogger.shared.setDetailLevel(.verbose)
}
@ -94,12 +101,12 @@ struct CommandExecutor {
return await axorcist.handleCollectAll(
for: command.application,
locator: command.locator,
pathHint: command.pathHint, // from CommandEnvelope
maxDepth: command.maxDepth,
requestedAttributes: command.attributes,
outputFormat: command.outputFormat,
commandId: command.commandId,
debugCLI: debugCLI // Pass the flag
debugCLI: debugCLI, // Pass the flag
filterCriteria: command.filterCriteria // ADDED
)
case .batch:
@ -117,6 +124,9 @@ struct CommandExecutor {
case .observe:
// Pass debugCLI to handler
return await handleObserveCommand(command: command, axorcist: axorcist, debugCLI: debugCLI)
case .setFocusedValue:
return await handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeSetFocusedValue)
}
}
@ -125,19 +135,14 @@ struct CommandExecutor {
let error = "Missing actionName for performAction"
axErrorLog(error) // Log error
// Conditionally include logs in error response based on debugCLI
let logsToInclude = debugCLI ? await GlobalAXLogger.shared.getLogsAsStringsIfEnabled(format: .text, includeTimestamps: false, includeLevels: false) : nil
let queryResponse = QueryResponse(
success: false,
let errorResponse = HandlerResponse(data: nil, error: error)
return await finalizeAndEncodeResponse(
commandId: command.commandId,
command: command.command.rawValue,
error: error, // This is a String, QueryResponse legacy init handles String for error
debugLogs: logsToInclude
commandType: command.command.rawValue,
handlerResponse: errorResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
let jsonString = encodeToJson(queryResponse)
let fallbackJson = """
{"error": "Encoding error response failed"}
"""
return jsonString ?? fallbackJson
}
let handlerResponse = await executePerformAction(
@ -150,7 +155,8 @@ struct CommandExecutor {
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debugCLI: debugCLI
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@ -161,12 +167,13 @@ struct CommandExecutor {
executor: (CommandEnvelope, AXorcist) async -> HandlerResponse
) async -> String {
let handlerResponse = await executor(command, axorcist)
// Pass debugCLI to finalizeAndEncodeResponse
// Pass debugCLI and command.debugLogging to finalizeAndEncodeResponse
return await finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debugCLI: debugCLI
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@ -199,12 +206,13 @@ struct CommandExecutor {
data: nil,
error: nil
)
// Pass debugCLI to finalizeAndEncodeResponse
// Pass debugCLI and command.debugLogging to finalizeAndEncodeResponse
return await finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: pingHandlerResponse,
debugCLI: debugCLI
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@ -213,12 +221,13 @@ struct CommandExecutor {
data: nil,
error: message
)
// Pass debugCLI to finalizeAndEncodeResponse
// Pass debugCLI and command.debugLogging to finalizeAndEncodeResponse
return await finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: notImplementedResponse,
debugCLI: debugCLI
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@ -229,29 +238,33 @@ struct CommandExecutor {
axorcist: AXorcist,
actionName: String
) async -> HandlerResponse {
guard let locator = command.locator else {
let error = "Missing locator for performAction"
var locator: Locator? = command.locator
// If pathHint is valid and locator is nil, create a default empty locator
if let pathHint = command.pathHint, !pathHint.isEmpty, locator == nil {
locator = Locator(criteria: [:])
axDebugLog("CommandExecutor: Created default empty locator because pathHint was provided but locator was nil.")
}
// If locator is still nil (no pathHint provided and no locator in command), return error
guard var validLocator = locator else {
let error = "Missing locator or pathHint for performAction"
axErrorLog(error)
return HandlerResponse(data: nil, error: error)
}
// Convert path_hint from [String] to [PathHintComponent] if needed
var pathHintComponents: [PathHintComponent]?
if let pathHints = command.pathHint {
pathHintComponents = []
for hint in pathHints {
if let component = await PathHintComponent(pathSegment: hint) {
pathHintComponents?.append(component)
}
}
// If CommandEnvelope.pathHint is provided, and locator.rootElementPathHint is not,
// transfer the pathHint to the locator.
if let topLevelPathHint = command.pathHint, !topLevelPathHint.isEmpty, validLocator.rootElementPathHint == nil {
axDebugLog("CommandExecutor: Populating locator.rootElementPathHint from CommandEnvelope.pathHint.")
validLocator.rootElementPathHint = topLevelPathHint
}
return await axorcist.handlePerformAction( // This handler uses GlobalAXLogger
return await axorcist.handlePerformAction(
for: command.application,
locator: locator,
locator: validLocator,
actionName: actionName,
actionValue: command.actionValue,
pathHint: pathHintComponents,
maxDepth: command.maxElements
)
}
@ -260,7 +273,7 @@ struct CommandExecutor {
command: CommandEnvelope,
axorcist: AXorcist
) async -> HandlerResponse {
return await axorcist.handleGetFocusedElement( // This handler uses GlobalAXLogger
return await axorcist.handleGetFocusedElement(
for: command.application,
requestedAttributes: command.attributes
)
@ -270,17 +283,22 @@ struct CommandExecutor {
command: CommandEnvelope,
axorcist: AXorcist
) async -> HandlerResponse {
guard let locator = command.locator else {
let error = "Missing locator for getAttributes"
axErrorLog(error)
return HandlerResponse(data: nil, error: error)
guard var locator = command.locator else {
axErrorLog("Missing locator for getAttributes")
return HandlerResponse(data: nil, error: "Missing locator for getAttributes")
}
return await axorcist.handleGetAttributes( // This handler uses GlobalAXLogger
// If CommandEnvelope.pathHint is provided, and locator.rootElementPathHint is not,
// transfer the pathHint to the locator.
if let topLevelPathHint = command.pathHint, !topLevelPathHint.isEmpty, locator.rootElementPathHint == nil {
axDebugLog("CommandExecutor: Populating locator.rootElementPathHint from CommandEnvelope.pathHint for getAttributes.")
locator.rootElementPathHint = topLevelPathHint
}
return await axorcist.handleGetAttributes(
for: command.application,
locator: locator,
requestedAttributes: command.attributes,
pathHint: command.pathHint,
maxDepth: command.maxElements,
maxDepth: command.maxDepth,
outputFormat: command.outputFormat
)
}
@ -289,16 +307,21 @@ struct CommandExecutor {
command: CommandEnvelope,
axorcist: AXorcist
) async -> HandlerResponse {
guard let locator = command.locator else {
let error = "Missing locator for query"
axErrorLog(error)
return HandlerResponse(data: nil, error: error)
guard var locator = command.locator else {
axErrorLog("Missing locator for query")
return HandlerResponse(data: nil, error: "Missing locator for query")
}
return await axorcist.handleQuery( // This handler uses GlobalAXLogger
// If CommandEnvelope.pathHint is provided, and locator.rootElementPathHint is not,
// transfer the pathHint to the locator.
if let topLevelPathHint = command.pathHint, !topLevelPathHint.isEmpty, locator.rootElementPathHint == nil {
axDebugLog("CommandExecutor: Populating locator.rootElementPathHint from CommandEnvelope.pathHint for query.")
locator.rootElementPathHint = topLevelPathHint
}
return await axorcist.handleQuery(
for: command.application,
locator: locator,
pathHint: command.pathHint,
maxDepth: command.maxElements,
maxDepth: command.maxDepth,
requestedAttributes: command.attributes,
outputFormat: command.outputFormat
)
@ -308,16 +331,21 @@ struct CommandExecutor {
command: CommandEnvelope,
axorcist: AXorcist
) async -> HandlerResponse {
guard let locator = command.locator else {
let error = "Missing locator for describeElement"
axErrorLog(error)
return HandlerResponse(data: nil, error: error)
guard var locator = command.locator else {
axErrorLog("Missing locator for describeElement")
return HandlerResponse(data: nil, error: "Missing locator for describeElement")
}
return await axorcist.handleDescribeElement( // This handler uses GlobalAXLogger
// If CommandEnvelope.pathHint is provided, and locator.rootElementPathHint is not,
// transfer the pathHint to the locator.
if let topLevelPathHint = command.pathHint, !topLevelPathHint.isEmpty, locator.rootElementPathHint == nil {
axDebugLog("CommandExecutor: Populating locator.rootElementPathHint from CommandEnvelope.pathHint for describeElement.")
locator.rootElementPathHint = topLevelPathHint
}
return await axorcist.handleDescribeElement(
for: command.application,
locator: locator,
pathHint: command.pathHint,
maxDepth: command.maxElements,
maxDepth: command.maxDepth,
requestedAttributes: command.attributes,
outputFormat: command.outputFormat
)
@ -327,26 +355,21 @@ struct CommandExecutor {
command: CommandEnvelope,
axorcist: AXorcist
) async -> HandlerResponse {
guard let locator = command.locator else {
let error = "Missing locator for extractText"
axErrorLog(error)
return HandlerResponse(data: nil, error: error)
guard var locator = command.locator else {
axErrorLog("Missing locator for extractText")
return HandlerResponse(data: nil, error: "Missing locator for extractText")
}
// Convert path_hint from [String] to [PathHintComponent] if needed
var pathHintComponents: [PathHintComponent]?
if let pathHints = command.pathHint {
pathHintComponents = []
for hint in pathHints {
if let component = await PathHintComponent(pathSegment: hint) {
pathHintComponents?.append(component)
}
}
// If CommandEnvelope.pathHint is provided, and locator.rootElementPathHint is not,
// transfer the pathHint to the locator.
if let topLevelPathHint = command.pathHint, !topLevelPathHint.isEmpty, locator.rootElementPathHint == nil {
axDebugLog("CommandExecutor: Populating locator.rootElementPathHint from CommandEnvelope.pathHint for extractText.")
locator.rootElementPathHint = topLevelPathHint
}
return await axorcist.handleExtractText( // This handler uses GlobalAXLogger
return await axorcist.handleExtractText(
for: command.application,
locator: locator,
pathHint: pathHintComponents
maxDepth: command.maxDepth
)
}
@ -408,15 +431,101 @@ struct CommandExecutor {
)
}
// MARK: - NEW COMMAND: setFocusedValue
private static func executeSetFocusedValue(
command: CommandEnvelope,
axorcist: AXorcist
) async -> HandlerResponse {
// 1. Retrieve the currently-focused element in the target application
let focusedResp = await axorcist.handleGetFocusedElement(for: command.application, requestedAttributes: nil)
if let err = focusedResp.error {
return HandlerResponse(data: nil, error: "Failed to fetch focused element: \(err)")
}
guard let raw = focusedResp.data?.value as? Element else {
return HandlerResponse(data: nil, error: "Focused element missing from response or not the correct Element type")
}
// 2. Determine the operation
let actionName = command.actionName ?? "AXSetValue"
// We handle the most common cases directly to avoid another brittle lookup
// Standard press-style actions
let standardActions: Set<String> = [
AXActionNames.kAXPressAction,
AXActionNames.kAXPickAction,
AXActionNames.kAXConfirmAction,
AXActionNames.kAXCancelAction,
AXActionNames.kAXIncrementAction,
AXActionNames.kAXDecrementAction,
AXActionNames.kAXShowMenuAction,
AXActionNames.kAXRaiseAction
]
if standardActions.contains(actionName) {
let status = AXUIElementPerformAction(raw.underlyingElement, actionName as CFString)
if status == .success {
return HandlerResponse(data: AnyCodable(PerformResponse(commandId: command.commandId, success: true)))
} else {
return HandlerResponse(error: "AX action \(actionName) failed: \(axErrorToString(status))")
}
} else {
// Treat actionName as an attribute to be set (e.g., AXSetValue / AXValue)
let attrName = (actionName == "AXSetValue") ? AXAttributeNames.kAXValueAttribute : actionName
guard let val = command.actionValue?.value else {
return HandlerResponse(error: "No actionValue provided for \(actionName)")
}
// Bridge Swift value CFTypeRef where possible
var cf: CFTypeRef?
if let s = val as? String { cf = s as CFString }
else if let b = val as? Bool { cf = (b ? kCFBooleanTrue : kCFBooleanFalse) }
else if let n = val as? NSNumber { cf = n }
else { return HandlerResponse(error: "Unsupported value type \(type(of: val)) for \(attrName)") }
let status = AXUIElementSetAttributeValue(raw.underlyingElement, attrName as CFString, cf!)
if status == .success {
return HandlerResponse(data: AnyCodable(PerformResponse(commandId: command.commandId, success: true)))
} else {
return HandlerResponse(error: "Failed to set \(attrName): \(axErrorToString(status))")
}
}
}
// MARK: - Helper Functions
private static func finalizeAndEncodeResponse(
commandId: String,
commandType: String,
handlerResponse: HandlerResponse, // This is from AXorcist library
debugCLI: Bool // Added debugCLI
debugCLI: Bool, // Added debugCLI
commandDebugLogging: Bool // MODIFIED: Now non-optional Bool
) async -> String {
let logsToInclude = debugCLI ? await GlobalAXLogger.shared.getLogsAsStringsIfEnabled(format: .text, includeTimestamps: false, includeLevels: false) : nil
let shouldIncludeLogs = debugCLI || commandDebugLogging
fputs("[FEAR] shouldIncludeLogs: \(shouldIncludeLogs), debugCLI: \(debugCLI), cmdDebugLogging: \(commandDebugLogging)\n", stderr) // fputs DIAGNOSTIC
axDebugLog("[finalizeAndEncodeResponse] shouldIncludeLogs: \(shouldIncludeLogs), debugCLI: \(debugCLI), cmdDebugLogging: \(commandDebugLogging)") // DIAGNOSTIC
let logsToInclude: [String]?
if shouldIncludeLogs {
fputs("[FEAR] Attempting to fetch logs...\n", stderr) // fputs DIAGNOSTIC
axDebugLog("[finalizeAndEncodeResponse] Attempting to fetch logs...") // DIAGNOSTIC
logsToInclude = await GlobalAXLogger.shared.getLogsAsStrings(
format: .text,
includeTimestamps: false,
includeLevels: false,
includeDetails: false,
includeAppName: false,
includeCommandID: false
)
fputs("[FEAR] Fetched logs. Count: \(logsToInclude?.count ?? -1)\n", stderr) // fputs DIAGNOSTIC
axDebugLog("[finalizeAndEncodeResponse] Fetched logs. Count: \(logsToInclude?.count ?? -1)") // DIAGNOSTIC
} else {
fputs("[FEAR] Not fetching logs.\n", stderr) // fputs DIAGNOSTIC
logsToInclude = nil
axDebugLog("[finalizeAndEncodeResponse] Not fetching logs.") // DIAGNOSTIC
}
fflush(stderr) // Ensure all fputs are flushed
// Use the specialized QueryResponse initializer that takes a HandlerResponse
let response = QueryResponse(
@ -461,7 +570,8 @@ struct CommandExecutor {
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: HandlerResponse(data: nil, error: errorMsg),
debugCLI: debugCLI
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@ -520,32 +630,9 @@ struct CommandExecutor {
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: HandlerResponse(data: nil, error: errorMsg),
debugCLI: debugCLI
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
}
}
// Extension to GlobalAXLogger for convenience
extension GlobalAXLogger {
func getLogsAsStringsIfEnabled(
format: AXLogOutputFormat,
includeTimestamps: Bool = true,
includeLevels: Bool = true,
includeDetails: Bool = false,
includeAppName: Bool = false,
includeCommandID: Bool = false
) async -> [String]? {
if await self.isLoggingEnabled() {
return await self.getLogsAsStrings(
format: format,
includeTimestamps: includeTimestamps,
includeLevels: includeLevels,
includeDetails: includeDetails,
includeAppName: includeAppName,
includeCommandID: includeCommandID
)
}
return nil
}
}