Various fixes to improve our drilling

This commit is contained in:
Peter Steinberger 2025-05-25 15:52:08 +02:00
parent ff3f16a125
commit b5ba7c66c9
9 changed files with 94 additions and 60 deletions

View File

@ -92,7 +92,4 @@ extension AXorcist {
return HandlerResponse(data: AnyCodable(axElement), error: nil)
}
// TODO: Add remaining command handler methods here...
// This is a placeholder file to demonstrate the refactoring approach
// The complete implementation would include all handle* methods
}

View File

@ -1,4 +1,9 @@
import Foundation
import CoreGraphics // Import for CGPoint, CGSize, CGRect
import Accessibility // Import for AXTextMarker, AXTextMarkerRange
// It's likely AXorcist module, where this file lives, already imports Accessibility or AppKit,
// which would make AXTextMarker and AXTextMarkerRange available.
// If not, a more dynamic type check might be needed, or this file needs to import them.
// For encoding/decoding 'Any' type in JSON, especially for element attributes.
// Note: @unchecked Sendable is used because 'Any' cannot guarantee thread safety
@ -112,6 +117,16 @@ public struct AnyCodable: Codable, @unchecked Sendable {
try container.encode(float)
case let string as String:
try container.encode(string)
case let point as CGPoint:
try container.encode(["x": point.x, "y": point.y])
case let size as CGSize:
try container.encode(["width": size.width, "height": size.height])
case let url as NSURL:
try container.encode(url.absoluteString)
case let rect as CGRect:
try container.encode(["x": rect.origin.x, "y": rect.origin.y, "width": rect.size.width, "height": rect.size.height])
case let val where String(describing: type(of: val)) == "__NSCFType":
try container.encode(String(describing: val))
case let array as [AnyCodable]:
try container.encode(array)
case let array as [Any?]:
@ -121,6 +136,8 @@ public struct AnyCodable: Codable, @unchecked Sendable {
case let dictionary as [String: Any?]:
try container.encode(dictionary.mapValues { AnyCodable($0) })
default:
// DEBUG: Print the type of unhandled values
print("AnyCodable unhandled type: \(String(describing: type(of: value))), value: \(String(describing: value))")
return false
}
return true

View File

@ -6,16 +6,19 @@ import Foundation
extension Element {
@MainActor
public func children() -> [Element]? { // Removed logging params
public func children(strict: Bool = false) -> [Element]? { // Added strict parameter
// Logging for this top-level call
// self.briefDescription() is assumed to be refactored and available
axDebugLog("Getting children for element: \(self.briefDescription(option: .default))")
axDebugLog("Getting children for element: \(self.briefDescription(option: .default)), strict: \(strict)")
var childCollector = ChildCollector() // ChildCollector will use GlobalAXLogger internally
collectDirectChildren(collector: &childCollector)
collectAlternativeChildren(collector: &childCollector)
collectApplicationWindows(collector: &childCollector)
if !strict { // Only collect alternatives if not strict
collectAlternativeChildren(collector: &childCollector)
collectApplicationWindows(collector: &childCollector)
}
let result = childCollector.finalizeResults()
axDebugLog("Final children count: \(result?.count ?? 0)")

View File

@ -77,7 +77,7 @@ extension AXorcist {
requestedAttributes: [String]?,
outputFormat: OutputFormat?,
commandId: String?
) -> String {
) async -> String {
SearchVisitor.resetGlobalVisitCount()
let params = CollectAllParameters(
@ -95,7 +95,7 @@ extension AXorcist {
// Get app element
guard let appElement = applicationElement(for: params.appIdentifier) else {
return createErrorResponse(
return await createErrorResponse(
commandId: params.effectiveCommandId,
appIdentifier: params.appIdentifier,
error: "Failed to get app element for identifier: \(params.appIdentifier)"
@ -103,7 +103,7 @@ extension AXorcist {
}
// Determine start element
let startElementResult = determineStartElement(
let startElementResult = await determineStartElement(
appElement: appElement,
pathHint: pathHint,
locator: locator,
@ -111,7 +111,7 @@ extension AXorcist {
)
guard let startElement = startElementResult.element else {
return createErrorResponse(
return await createErrorResponse(
commandId: params.effectiveCommandId,
appIdentifier: params.appIdentifier,
error: startElementResult.error ?? "Failed to determine start element"
@ -119,13 +119,13 @@ extension AXorcist {
}
// Perform collection
let collectedElements = performCollectionTraversal(
let collectedElements = await performCollectionTraversal(
startElement: startElement,
appElement: appElement,
params: params
)
return createSuccessResponse(
return await createSuccessResponse(
commandId: params.effectiveCommandId,
appIdentifier: params.appIdentifier,
collectedElements: collectedElements
@ -191,45 +191,52 @@ extension AXorcist {
pathHint: [String]?,
locator: Locator?,
params: CollectAllParameters
) -> (element: Element?, error: String?) {
) 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("Navigating to path hint: \(pathHintString)")
axDebugLog("[CollectAll] Navigating to path hint: \(pathHintString)")
guard let navigatedElement = navigateToElement(
from: appElement,
pathHint: hint,
maxDepth: AXMiscConstants.defaultMaxDepthSearch
maxDepth: AXMiscConstants.defaultMaxDepthSearch
) else {
return (nil, "Failed to navigate to path: \(pathHintString)")
}
startElement = navigatedElement
pathNavigated = true
axDebugLog("[CollectAll] Path navigation successful. Current startElement: \(startElement.briefDescription())")
} else {
axDebugLog("Using app element as start element")
axDebugLog("[CollectAll] No pathHint provided. Current startElement: \(startElement.briefDescription()) (app root)")
}
// Apply locator if provided
if let locator = locator {
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: locator
locator: loc
) {
axDebugLog(
"Locator found element: \(locatedElement.briefDescription()). " +
"[CollectAll] Locator (criteria-only) found element: \(locatedElement.briefDescription()). " +
"This will be the root for collectAll recursion."
)
startElement = locatedElement
} else {
let locatorDescription = String(describing: locator.criteria)
let elementDescription = startElement.briefDescription()
let locatorDescription = String(describing: loc.criteria)
let currentStartDesc = startElement.briefDescription()
axWarningLog(
"Locator provided but no element found for: \(locatorDescription). " +
"CollectAll will proceed from the current start element: \(elementDescription)."
"[CollectAll] Locator (criteria-only) provided but no element found for: \(locatorDescription) from \(currentStartDesc). " +
"CollectAll will proceed from \(currentStartDesc)."
)
}
} 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.")
}
return (startElement, nil)
@ -259,7 +266,7 @@ extension AXorcist {
startElement: Element,
appElement: Element,
params: CollectAllParameters
) -> [AXElement] {
) async -> [AXElement] {
var traverser = TreeTraverser()
let visitor = CollectAllVisitor(
attributesToFetch: params.attributesToFetch,
@ -269,10 +276,11 @@ extension AXorcist {
var traversalState = TraversalState(
maxDepth: params.recursionDepthLimit,
startElement: startElement
startElement: startElement,
strictChildren: true
)
axDebugLog("Starting unified tree traversal from start element: \(startElement.briefDescription())")
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
@ -293,15 +301,17 @@ extension AXorcist {
commandId: String,
appIdentifier: String,
error: String
) -> String {
) async -> String {
axErrorLog(error)
// Fetch logs
let logs = await GlobalAXLogger.shared.getLogsAsStrings(format: .text)
return encode(CollectAllOutput(
commandId: commandId,
success: false,
command: "collectAll",
collectedElements: [],
appBundleId: appIdentifier,
debugLogs: [],
debugLogs: logs,
errorMessage: error
))
}
@ -311,14 +321,16 @@ extension AXorcist {
commandId: String,
appIdentifier: String,
collectedElements: [AXElement]
) -> String {
) async -> String {
// Fetch logs
let logs = await GlobalAXLogger.shared.getLogsAsStrings(format: .text)
let output = CollectAllOutput(
commandId: commandId,
success: true,
command: "collectAll",
collectedElements: collectedElements,
appBundleId: appIdentifier,
debugLogs: [],
debugLogs: logs,
errorMessage: nil
)
return encode(output)
@ -350,11 +362,11 @@ extension AXorcist {
var traversalState = TraversalState(
maxDepth: recursionDepthLimit,
startElement: startElement
startElement: startElement,
strictChildren: true
)
axDebugLog("Beginning tree traversal for collectAll (CommandEnvelope) from: \(startElement.briefDescription())")
axDebugLog("[Pre-Traverse CGHEH] Handler: validStartElement is: \(startElement.briefDescription(option: .default)) with strictChildren=true")
_ = traverser.traverse(from: startElement, visitor: visitor, state: &traversalState)
let collectedElementsData = visitor.collectedElements

View File

@ -0,0 +1 @@

View File

@ -16,11 +16,13 @@ public enum TraversalAction {
public struct TraversalState {
public let maxDepth: Int
public let startElement: Element // Could be useful for context if GlobalAXLogger needs it indirectly
public let strictChildren: Bool // New flag
// Add other non-logging state if needed by visitors, e.g., a shared Set for specific tracking.
public init(maxDepth: Int, startElement: Element) {
public init(maxDepth: Int, startElement: Element, strictChildren: Bool = false) { // Default to false
self.maxDepth = maxDepth
self.startElement = startElement
self.strictChildren = strictChildren
}
}
@ -45,12 +47,19 @@ public struct TreeTraverser {
public init() {}
public mutating func traverse(from element: Element, visitor: TreeVisitor, state: inout TraversalState) -> Element? {
public mutating func traverse(from startNode: Element, visitor: TreeVisitor, state: inout TraversalState) -> Element? {
let startNodeDesc = startNode.briefDescription(option: .default)
let logMaxDepth = state.maxDepth // Capture value for logging
let logStrictChildren = state.strictChildren // Capture value for logging
axDebugLog("[Traverse Entry] TreeTraverser.traverse starting from: \(startNodeDesc). MaxDepth: \(logMaxDepth), StrictChildren: \(logStrictChildren)")
visitedElements.removeAll()
return _traverse(currentElement: element, depth: 0, visitor: visitor, state: &state)
return _traverse(currentElement: startNode, depth: 0, visitor: visitor, state: &state)
}
private mutating func _traverse(currentElement: Element, depth: Int, visitor: TreeVisitor, state: inout TraversalState) -> Element? {
let currentDesc = currentElement.briefDescription(option: .default) // Corrected label
axDebugLog("[_Traverse Entry] Visiting \(currentDesc) at depth \(depth)") // MODIFIED LOG
if depth > state.maxDepth {
let maxDepth = state.maxDepth
axDebugLog("Max depth (\(maxDepth)) reached at \(currentElement.briefDescription())")
@ -75,7 +84,7 @@ public struct TreeTraverser {
break
}
guard let children = currentElement.children() else {
guard let children = currentElement.children(strict: state.strictChildren) else {
axDebugLog("No children for \(currentElement.briefDescription()) or error fetching them.")
return nil
}

View File

@ -81,7 +81,7 @@ struct AXORCCommand: AsyncParsableCommand {
// Parse JSON command
guard let jsonData = jsonString.data(using: .utf8) else {
// Clear logs after error
await GlobalAXLogger.shared.clearLogs()
await axClearLogs()
print("{\"error\": \"Failed to convert JSON string to data\"}")
return
}
@ -109,7 +109,7 @@ struct AXORCCommand: AsyncParsableCommand {
// Stop collecting logs after successful execution
// Clear logs after error
await GlobalAXLogger.shared.clearLogs()
await axClearLogs()
} catch {
axErrorLog("DECODE_ERROR_DEBUG: Original jsonString that led to this error: [\(jsonString)]")
@ -125,7 +125,7 @@ struct AXORCCommand: AsyncParsableCommand {
await GlobalAXLogger.shared.setDetailLevel(.verbose)
}
let collectedLogs = await GlobalAXLogger.shared.getLogsAsStrings(format: .text, includeTimestamps: true, includeLevels: true, includeDetails: true)
await GlobalAXLogger.shared.clearLogs()
await axClearLogs()
let errorResponse = ErrorResponse(
commandId: "decode_error",

View File

@ -38,7 +38,7 @@ struct CommandExecutor {
// Clear logs for this specific operation after processing,
// so next command in a batch (if any) or next CLI call starts fresh for its specific logs.
await GlobalAXLogger.shared.clearLogs()
await axClearLogs()
await GlobalAXLogger.shared.updateOperationDetails(commandID: nil, appName: nil) // Reset context
return responseString
@ -77,7 +77,16 @@ struct CommandExecutor {
return await handleSimpleCommand(command: command, axorcist: axorcist, executor: executeExtractText)
case .collectAll:
return await handleCollectAllCommand(command: command, axorcist: axorcist)
// Directly await the call to the now async axorcist.handleCollectAll
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
)
case .batch:
return await handleBatchCommand(command: command, axorcist: axorcist)
@ -128,20 +137,6 @@ struct CommandExecutor {
)
}
private static func handleCollectAllCommand(command: CommandEnvelope, axorcist: AXorcist) async -> String {
// handleCollectAll itself was refactored to use GlobalAXLogger.
// It returns a JSON string directly.
return await axorcist.handleCollectAll(
for: command.application,
locator: command.locator,
pathHint: command.pathHint,
maxDepth: command.maxDepth,
requestedAttributes: command.attributes,
outputFormat: command.outputFormat,
commandId: command.commandId
)
}
private static func handleBatchCommand(command: CommandEnvelope, axorcist: AXorcist) async -> String {
let batchResponse = await executeBatch(
command: command,

View File

@ -36,9 +36,9 @@ struct InputHandler {
if trimmedJsonString.isEmpty {
let error = "Error: --json argument was provided but the string is empty or only whitespace."
axErrorLog(error)
return (nil, "--json argument", error)
return ParseResult(jsonString: nil, sourceDescription: "--json argument", error: error)
}
return (trimmedJsonString, "--json argument", nil)
return ParseResult(jsonString: trimmedJsonString, sourceDescription: "--json argument", error: nil)
} else if positionalPayloadProvided {
return handleDirectPayload(directPayload: directPayload)
} else {