The fight continues
This commit is contained in:
parent
5cbe85e85a
commit
cb2d7055f0
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user