fix(get_window_state): 30s timeout + 2000-node cap for heavy webview apps#1609
fix(get_window_state): 30s timeout + 2000-node cap for heavy webview apps#1609hippoley wants to merge 1 commit into
Conversation
…apps Fixes trycua#1537. Addresses trycua#1564. get_window_state hangs forever on Arc, Safari with many tabs, and Electron apps because AXUIElementCopyAttributeValue blocks indefinitely via XPC when the app's accessibility tree is pathologically large. Changes (Rust — get_window_state.rs + ax/tree.rs): - Wrap spawn_blocking AX walk in tokio::time::timeout(30s). Returns a descriptive error suggesting capture_mode=vision or a query filter. - Add MAX_ELEMENTS=2000 cap to walk_element. When hit, the walk stops early and TreeWalkResult.truncated=true is set. The partial tree is still returned with a warning line appended to tree_markdown. Changes (Swift — AppState.swift): - Wrap renderTree in withThrowingTaskGroup with a 30s deadline task. Throws AppStateError.axWalkTimedOut(pid:) on expiry. - Add maxElements=2000 cap to renderTree via visitedCount parameter. When hit, appends a warning line to the markdown output. - renderTree is now nonisolated so it can run on the detached task. Both implementations mirror each other: 30s timeout + 2000-node cap with a user-visible warning rather than a silent hang or crash.
|
@nishantpurohit04 is attempting to deploy a commit to the Cua Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a 30-second timeout and per-walk node-count cap (2000 nodes) to AX tree traversals in both Rust and Swift layers to prevent indefinite hangs on large accessibility trees in webview-heavy applications like Arc and Safari with many tabs. ChangesAX Tree Walk Timeout and Node-Count Bounds
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@libs/cua-driver-rs/crates/platform-macos/src/ax/tree.rs`:
- Around line 127-145: Replace the current truncation check that computes
truncated = visited_count >= MAX_ELEMENTS with an explicit boolean flag that is
set only when walk_element's guard actually stops further traversal; add a
mutable truncated variable (e.g., truncated: bool) that walk_element sets to
true when it refuses to descend/queue more nodes, propagate that flag out of the
traversal and use it when constructing TreeWalkResult { tree_markdown, nodes,
truncated } and when appending the warning to tree_markdown instead of relying
on visited_count and MAX_ELEMENTS; update any references to visited_count for
display but do not use it to decide truncation.
In `@libs/cua-driver-rs/crates/platform-macos/src/tools/get_window_state.rs`:
- Around line 97-103: The error message returned by ToolResult::error in
get_window_state.rs should not recommend using "query" as a timeout workaround
because query only filters tree_markdown after the walk completes; update the
string literal passed to ToolResult::error to remove any suggestion to retry
with `query` and instead only recommend alternatives that reduce traversal work
(for example, "switch to capture_mode=vision for pixel-click workflows" or "use
capture_mode=ax with a traversal-limiting/scan-scoped filter or depth limit
applied during the AX tree walk"). Ensure you edit the exact string in the
ToolResult::error call so the misleading "query" recommendation is removed.
- Around line 90-106: The timeout currently only drops the JoinHandle but does
not stop the blocking AX walk (spawn_blocking + crate::ax::tree::walk_tree), so
repeated timeouts leak blocking threads; modify the approach so the blocking
work can be cooperatively cancelled: change walk_tree to accept a cancellation
token/flag (e.g., tokio_util::sync::CancellationToken or an Arc<AtomicBool>) and
have walk_tree periodically check it and return early, then when
tokio::time::timeout fires set/trigger that token before returning the timeout
ToolResult; update the call site (the walk_future spawn_blocking closure and its
caller) to create and pass the token, signal it on Err(_elapsed), and
await/handle the task completion or a bounded grace period to ensure the
blocking thread finishes cleanly.
In `@libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift`:
- Around line 321-326: Replace the current truncation check that uses
snapshotVisited >= AppStateEngine.maxElements with an explicit boolean flag
(e.g., didTruncate) that is set only when the traversal actually stops early
because the cap was reached (i.e., when a recursive/iterative step is skipped in
AppStateEngine's walk logic). Update the code that appends the warning to check
this didTruncate flag instead of snapshotVisited, and ensure the flag is
initialized false before traversal and flipped true at the exact point where the
traversal abandons further nodes; reference snapshotVisited,
AppStateEngine.maxElements, and the markdown append site to locate the changes.
- Around line 283-316: The snapshot task-group race is unsafe because renderTree
is synchronous/noncancellable and marked nonisolated while calling
actor-isolated helpers, and truncation uses >= causing false positives; fix by
moving the AX walk off the actor onto an unstructured background worker/thread
(e.g., create a Task.detached or DispatchThread to run renderTree) so the walk
can be abandoned independently of withThrowingTaskGroup and the timeout (replace
the current group.addTask that calls renderTree), remove the nonisolated
annotation from renderTree or make all helpers called by renderTree
(attributeString, attributeBool, topLevelChildren, children, isMenuOpen,
actionNames, cleanActionName, windows) nonisolated so actor isolation is
satisfied, and change the truncation check from if snapshotVisited >=
AppStateEngine.maxElements to if snapshotVisited > AppStateEngine.maxElements to
only truncate when the limit is exceeded; keep AppStateError.axWalkTimedOut
thrown by the timeout worker.
- Around line 560-568: renderTree is declared nonisolated but calls
actor-isolated helpers (topLevelChildren, children, isMenuOpen, attributeString,
attributeBool, actionNames); fix by crossing the actor boundary correctly:
either mark those six helper methods nonisolated if they are safe to run
off-actor, or make renderTree async (remove nonisolated) and call each helper
with await (e.g., await topLevelChildren(...)) so the calls respect actor
isolation, or alternatively move the helper implementations out of the actor
into a non-actor helper type and call those from renderTree.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c9e14423-2b0f-42fc-b589-7852179a63fd
📒 Files selected for processing (3)
libs/cua-driver-rs/crates/platform-macos/src/ax/tree.rslibs/cua-driver-rs/crates/platform-macos/src/tools/get_window_state.rslibs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift
| let truncated = visited_count >= MAX_ELEMENTS; | ||
| let raw_markdown = render_lines(&lines); | ||
| let tree_markdown = if let Some(q) = query { | ||
| let mut tree_markdown = if let Some(q) = query { | ||
| filter_tree(&raw_markdown, q) | ||
| } else { | ||
| raw_markdown | ||
| }; | ||
|
|
||
| TreeWalkResult { tree_markdown, nodes } | ||
| if truncated { | ||
| tree_markdown.push_str(&format!( | ||
| "\n⚠️ AX tree truncated at {MAX_ELEMENTS} nodes \ | ||
| (app has a very large accessibility tree — Arc, Electron, or similar). \ | ||
| Element indices above are still valid. Use pixel clicks for elements \ | ||
| not visible in this partial tree." | ||
| )); | ||
| } | ||
|
|
||
| TreeWalkResult { tree_markdown, nodes, truncated } | ||
| } |
There was a problem hiding this comment.
Use an explicit truncation flag instead of visited_count >= MAX_ELEMENTS.
A walk that naturally ends on the 2,000th visited node will still be marked truncated and get the warning even though no recursion was cut short. Track a separate truncated flag when the guard in walk_element actually rejects more work, and use that here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@libs/cua-driver-rs/crates/platform-macos/src/ax/tree.rs` around lines 127 -
145, Replace the current truncation check that computes truncated =
visited_count >= MAX_ELEMENTS with an explicit boolean flag that is set only
when walk_element's guard actually stops further traversal; add a mutable
truncated variable (e.g., truncated: bool) that walk_element sets to true when
it refuses to descend/queue more nodes, propagate that flag out of the traversal
and use it when constructing TreeWalkResult { tree_markdown, nodes, truncated }
and when appending the warning to tree_markdown instead of relying on
visited_count and MAX_ELEMENTS; update any references to visited_count for
display but do not use it to decide truncation.
| let walk_future = tokio::task::spawn_blocking(move || { | ||
| crate::ax::tree::walk_tree(pid, Some(window_id), q.as_deref()) | ||
| }).await; | ||
| match result { | ||
| Ok(r) => Some(r), | ||
| Err(e) => return ToolResult::error(format!("AX tree walk failed: {e}")), | ||
| }); | ||
| match tokio::time::timeout(std::time::Duration::from_secs(30), walk_future).await { | ||
| Ok(Ok(r)) => Some(r), | ||
| Ok(Err(e)) => return ToolResult::error(format!("AX tree walk failed: {e}")), | ||
| Err(_elapsed) => { | ||
| return ToolResult::error(format!( | ||
| "AX tree walk for pid={pid} timed out after 30 s. \ | ||
| The app (likely Arc, Electron, or Safari with many tabs) has a \ | ||
| pathologically large accessibility tree. \ | ||
| Workarounds: switch to capture_mode=vision for pixel-click \ | ||
| workflows, or use capture_mode=ax with a query filter to \ | ||
| limit the walk scope." | ||
| )); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Tokio, if tokio::time::timeoutexpires while awaiting aspawn_blocking JoinHandle, does the underlying blocking task stop, or does it continue running after the handle is dropped?
💡 Result:
If tokio::time::timeout expires while you’re awaiting a JoinHandle returned by tokio::task::spawn_blocking, the underlying blocking closure does not get stopped; it continues running in the background after the JoinHandle is dropped. Tokio’s docs state that spawn_blocking closures “cannot be aborted because they are not async” and that even calling abort() on a spawn_blocking task “will not have any effect” (once it has started). It also explains that when a Tokio runtime is shut down it “will attempt to abort all tasks including spawn_blocking tasks,” but blocking tasks “cannot be aborted once they start running,” and shutdown may still wait for them to finish. [1][2] Separately, Tokio’s JoinHandle behavior is that dropping the handle “detaches the associated task,” and “the task continues running in the background and its return value is lost.” [3][4] So, in the specific scenario you described: - timeout(...).await timing out typically means the await is dropped (i.e., the JoinHandle variable goes out of scope / is dropped). - Dropping the JoinHandle detaches the task, so the blocking closure keeps running. - Additionally, even if you were to try to abort the handle, spawn_blocking tasks can’t be aborted once they’ve started. [1][3]
Citations:
- 1: https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html
- 2: https://docs.rs/tokio/latest/tokio/task/fn.spawn%5Fblocking.html
- 3: https://docs.rs/tokio/latest/%20tokio/task/struct.JoinHandle.html
- 4: https://docs.rs/tokio_wasi/latest/tokio/task/struct.JoinHandle.html
Timeout does not stop the blocking task—the AX walk continues running in the background.
When tokio::time::timeout expires on the spawn_blocking JoinHandle, the underlying blocking closure is not aborted; it continues consuming a blocking thread in the pool. Repeated timeouts can accumulate idle threads and starve the blocking pool for future work.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@libs/cua-driver-rs/crates/platform-macos/src/tools/get_window_state.rs`
around lines 90 - 106, The timeout currently only drops the JoinHandle but does
not stop the blocking AX walk (spawn_blocking + crate::ax::tree::walk_tree), so
repeated timeouts leak blocking threads; modify the approach so the blocking
work can be cooperatively cancelled: change walk_tree to accept a cancellation
token/flag (e.g., tokio_util::sync::CancellationToken or an Arc<AtomicBool>) and
have walk_tree periodically check it and return early, then when
tokio::time::timeout fires set/trigger that token before returning the timeout
ToolResult; update the call site (the walk_future spawn_blocking closure and its
caller) to create and pass the token, signal it on Err(_elapsed), and
await/handle the task completion or a bounded grace period to ensure the
blocking thread finishes cleanly.
| return ToolResult::error(format!( | ||
| "AX tree walk for pid={pid} timed out after 30 s. \ | ||
| The app (likely Arc, Electron, or Safari with many tabs) has a \ | ||
| pathologically large accessibility tree. \ | ||
| Workarounds: switch to capture_mode=vision for pixel-click \ | ||
| workflows, or use capture_mode=ax with a query filter to \ | ||
| limit the walk scope." |
There was a problem hiding this comment.
Don't recommend query as a timeout workaround here.
This file's own tool description says query only filters tree_markdown after the walk completes. Users who retry with query will hit the same timeout because it does not shrink the traversal.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@libs/cua-driver-rs/crates/platform-macos/src/tools/get_window_state.rs`
around lines 97 - 103, The error message returned by ToolResult::error in
get_window_state.rs should not recommend using "query" as a timeout workaround
because query only filters tree_markdown after the walk completes; update the
string literal passed to ToolResult::error to remove any suggestion to retry
with `query` and instead only recommend alternatives that reduce traversal work
(for example, "switch to capture_mode=vision for pixel-click workflows" or "use
capture_mode=ax with a traversal-limiting/scan-scoped filter or depth limit
applied during the AX tree walk"). Ensure you edit the exact string in the
ToolResult::error call so the misleading "query" recommendation is removed.
| let (snapshotMarkdown, snapshotElements, snapshotVisited) = try await withThrowingTaskGroup( | ||
| of: (String, [Int: AXUIElement], Int).self | ||
| ) { group in | ||
| group.addTask { | ||
| let root = AXUIElementCreateApplication(pid) | ||
| var els: [Int: AXUIElement] = [:] | ||
| var idx = 0 | ||
| var md = "" | ||
| var visited = 0 | ||
| let layer0WindowIds: Set<CGWindowID> = Set( | ||
| WindowEnumerator.allWindows() | ||
| .filter { $0.layer == 0 } | ||
| .map { CGWindowID($0.id) } | ||
| ) | ||
| self.renderTree( | ||
| root, | ||
| depth: 0, | ||
| targetWindowId: windowId, | ||
| layer0WindowIds: layer0WindowIds, | ||
| elements: &els, | ||
| nextIndex: &idx, | ||
| output: &md, | ||
| visitedCount: &visited | ||
| ) | ||
| return (md, els, visited) | ||
| } | ||
| group.addTask { | ||
| try await Task.sleep(nanoseconds: 30_000_000_000) | ||
| throw AppStateError.axWalkTimedOut(pid: pid) | ||
| } | ||
| let result = try await group.next()! | ||
| group.cancelAll() | ||
| return result | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does Swift withThrowingTaskGroup wait for canceled child tasks to finish before the group scope exits, and can it enforce a timeout around synchronous work that never checks cancellation?
💡 Result:
Yes, Swift's withThrowingTaskGroup always waits for all of its child tasks to complete, including any that were canceled, before the group's scope exits [1][2]. This behavior is a fundamental guarantee of Swift's structured concurrency, ensuring that child tasks do not become "orphaned" or continue running unexpectedly after the group has finished [2][3]. Regarding your second question, withThrowingTaskGroup cannot enforce a timeout on synchronous work that does not check for cancellation [4]. Swift's concurrency model relies on cooperative cancellation [5][4]. When you cancel a task group (e.g., via cancelAll), it merely sets a cancellation flag on the child tasks [2][5]. It is the responsibility of the code within those tasks to periodically check this flag (using Task.isCancelled or try Task.checkCancellation) and exit voluntarily [2][5][6]. If a child task is performing long-running synchronous work or blocking operations that do not include such checks, it will ignore the cancellation signal and continue executing until the work is finished [4]. Consequently, even if you implement a timeout mechanism—typically by racing your work against a sleeping task in a group—the group will remain suspended, waiting for that non-cooperative task to complete before it can finally return [1][4]. To ensure a timeout works as expected, you must manually ensure that the long-running operation is designed to respond to cancellation [4][6].
Citations:
- 1: https://developer.apple.com/documentation/swift/withthrowingtaskgroup(of:returning:isolation:body:)
- 2: https://developer.apple.com/documentation/swift/taskgroup
- 3: https://anasaman-p.medium.com/understanding-swifts-withthrowingtaskgroup-ee9646b5d4e8
- 4: https://www.donnywals.com/implementing-task-timeout-with-swift-concurrency/
- 5: https://www.hackingwithswift.com/quick-start/concurrency/how-to-cancel-a-task-group
- 6: https://stackoverflow.com/questions/77279252/timeout-for-swift-functions
🏁 Script executed:
#!/bin/bash
# Check the renderTree method signature and verify nonisolated marking
rg -A 20 "func renderTree" libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swiftRepository: trycua/cua
Length of output: 952
🏁 Script executed:
#!/bin/bash
# Check for truncation warning logic
rg "truncation|maxElements|AppStateEngine" libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swiftRepository: trycua/cua
Length of output: 409
🏁 Script executed:
#!/bin/bash
# Verify the file is part of an actor and check class/actor declaration
head -100 libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift | grep -E "^(class|actor|struct)"Repository: trycua/cua
Length of output: 36
🏁 Script executed:
#!/bin/bash
# Search for the declaration of attributeString and attributeBool
rg -B 2 "func attributeString|func attributeBool" libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift | head -40Repository: trycua/cua
Length of output: 249
🏁 Script executed:
#!/bin/bash
# Also check for topLevelChildren, children, isMenuOpen, actionNames, cleanActionName, windows
rg "func topLevelChildren|func children|func isMenuOpen|func actionNames|func cleanActionName|func windows" libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift | head -20Repository: trycua/cua
Length of output: 458
This task-group race does not guarantee a 30-second return.
If the sleeper throws first, withThrowingTaskGroup still has to unwind the render child before the scope can exit. renderTree is synchronous and the AX calls inside it do not observe cancellation, so a blocked AX walk can still keep snapshot() hung past the deadline. This misses the bounded-time objective unless the walk is moved behind an unstructured worker/thread boundary that can be abandoned independently.
Additionally, renderTree is marked private nonisolated but calls actor-isolated instance methods (attributeString, attributeBool, topLevelChildren, children, isMenuOpen, actionNames, cleanActionName, windows), which violates Swift actor isolation rules and will cause a compile error. The method must either be non-nonisolated or all helper calls must be nonisolated.
The truncation logic checks if snapshotVisited >= AppStateEngine.maxElements, replicating the exact-cap false positive from the Rust implementation—truncation occurs at exactly 2_000 nodes rather than exceeding the limit.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift` around lines
283 - 316, The snapshot task-group race is unsafe because renderTree is
synchronous/noncancellable and marked nonisolated while calling actor-isolated
helpers, and truncation uses >= causing false positives; fix by moving the AX
walk off the actor onto an unstructured background worker/thread (e.g., create a
Task.detached or DispatchThread to run renderTree) so the walk can be abandoned
independently of withThrowingTaskGroup and the timeout (replace the current
group.addTask that calls renderTree), remove the nonisolated annotation from
renderTree or make all helpers called by renderTree (attributeString,
attributeBool, topLevelChildren, children, isMenuOpen, actionNames,
cleanActionName, windows) nonisolated so actor isolation is satisfied, and
change the truncation check from if snapshotVisited >=
AppStateEngine.maxElements to if snapshotVisited > AppStateEngine.maxElements to
only truncate when the limit is exceeded; keep AppStateError.axWalkTimedOut
thrown by the timeout worker.
| if snapshotVisited >= AppStateEngine.maxElements { | ||
| markdown += "\n⚠️ AX tree truncated at \(AppStateEngine.maxElements) nodes" | ||
| + " (app has a very large accessibility tree — Arc, Electron, or similar)." | ||
| + " Element indices above are still valid. Use pixel clicks for elements" | ||
| + " not visible in this partial tree." | ||
| } |
There was a problem hiding this comment.
Use an explicit truncation flag instead of snapshotVisited >= maxElements.
A walk that finishes naturally on exactly element 2,000 will still append the truncation warning. Set the warning off a flag that is flipped only when a recursive step is skipped because the cap was already reached.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift` around lines
321 - 326, Replace the current truncation check that uses snapshotVisited >=
AppStateEngine.maxElements with an explicit boolean flag (e.g., didTruncate)
that is set only when the traversal actually stops early because the cap was
reached (i.e., when a recursive/iterative step is skipped in AppStateEngine's
walk logic). Update the code that appends the warning to check this didTruncate
flag instead of snapshotVisited, and ensure the flag is initialized false before
traversal and flipped true at the exact point where the traversal abandons
further nodes; reference snapshotVisited, AppStateEngine.maxElements, and the
markdown append site to locate the changes.
| private nonisolated func renderTree( | ||
| _ element: AXUIElement, | ||
| depth: Int, | ||
| targetWindowId: UInt32?, | ||
| layer0WindowIds: Set<CGWindowID> = [], | ||
| elements: inout [Int: AXUIElement], | ||
| nextIndex: inout Int, | ||
| output: inout String | ||
| output: inout String, | ||
| visitedCount: inout Int |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Swift actors, can a nonisolatedinstance method synchronously call actor-isolated instance methods, or must those helpers also be markednonisolated/static?
💡 Result:
In Swift, a nonisolated instance method cannot synchronously call an actor-isolated instance method [1][2]. Actor-isolated methods are protected by the actor’s serial executor, ensuring that they can only be accessed sequentially from within the actor or asynchronously via await from outside [3][4][2]. Because a nonisolated method is explicitly opted out of this isolation—meaning it does not have safe access to the actor's mutable state—calling an isolated method synchronously from a nonisolated context would violate the actor's thread-safety guarantees [1][3][2]. To call an actor-isolated method from a nonisolated context, you must use the await keyword [3][4]. This introduces a potential suspension point, allowing the system to safely hop onto the actor's executor to perform the operation [1][2]. If you find yourself needing to perform such calls frequently, these alternatives are commonly used: 1. Asynchronous Calls: Use await to bridge the isolation boundary [3][2]. 2. Isolated Parameters: If you are in a context where you have access to the actor, you can use an isolated parameter on a function to run synchronously on that actor's executor without needing await [5][4][6]. 3. Architecture Refactoring: If the "helper" logic does not actually require access to the actor’s mutable state, you can mark the helper method itself as nonisolated [3][4]. If it does require access, it must remain isolated and be called as an asynchronous operation [1][4].
Citations:
- 1: https://stackoverflow.com/questions/78435902/call-to-actor-isolated-instance-method-in-a-synchronous-actor-isolated-context
- 2: https://hackernoon.com/swift-concurrency-actors-executors-and-their-interaction-with-threads
- 3: https://byby.dev/swift-actor-isolation
- 4: https://www.avanderlee.com/swift/nonisolated-isolated/
- 5: How to use isolated and nonisolated in Swift 6 concurrency onmyway133/blog#1039
- 6: https://github.com/apple/swift-evolution/blob/main/proposals/0313-actor-isolation-control.md
🏁 Script executed:
#!/bin/bash
# Find the AppState.swift file and examine the renderTree method
find . -name "AppState.swift" -path "*CuaDriverCore*" | head -5Repository: trycua/cua
Length of output: 118
🏁 Script executed:
#!/bin/bash
# Check the file size and read the relevant section
wc -l "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift"Repository: trycua/cua
Length of output: 120
🏁 Script executed:
#!/bin/bash
# Read the renderTree method (lines 560-620 to see the full implementation)
sed -n '560,620p' "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | cat -nRepository: trycua/cua
Length of output: 3350
🏁 Script executed:
#!/bin/bash
# Search for the helper methods to check their isolation status
rg -n "(topLevelChildren|children|isMenuOpen|attributeString|attributeBool|actionNames)\s*\(" "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" -A 3 | head -60Repository: trycua/cua
Length of output: 3049
🏁 Script executed:
#!/bin/bash
# Find the class declaration for AppState - search backwards from line 560
sed -n '1,560p' "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | grep -E "^(actor|class|struct)" | tail -3Repository: trycua/cua
Length of output: 36
🏁 Script executed:
#!/bin/bash
# Get more context around the class declaration
head -50 "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | cat -nRepository: trycua/cua
Length of output: 2955
🏁 Script executed:
#!/bin/bash
# Search for the actor declaration containing these methods
rg -n "^(actor|class|struct)\s+AppState" "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | head -5Repository: trycua/cua
Length of output: 36
🏁 Script executed:
#!/bin/bash
# Search more broadly for actor keyword in this file
rg -n "^actor\s" "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift"Repository: trycua/cua
Length of output: 36
🏁 Script executed:
#!/bin/bash
# Search for AppState declaration with different patterns
rg -n "class AppState|actor AppState|struct AppState" "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift"Repository: trycua/cua
Length of output: 143
🏁 Script executed:
#!/bin/bash
# Look for the main type definition in the file
sed -n '50,150p' "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | cat -nRepository: trycua/cua
Length of output: 6038
🏁 Script executed:
#!/bin/bash
# Verify the helper methods are NOT marked nonisolated
sed -n '680,790p' "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | grep -E "private (nonisolated )?func (topLevelChildren|children|isMenuOpen|attributeString|attributeBool|actionNames)"Repository: trycua/cua
Length of output: 507
Mark helper methods nonisolated or refactor to bridge the actor isolation boundary.
The renderTree method is marked nonisolated but calls actor-isolated instance methods (topLevelChildren, children, isMenuOpen, attributeString, attributeBool, actionNames) synchronously. Swift actors prohibit this—a nonisolated method cannot directly call actor-isolated methods without await. Either mark all six helper methods as nonisolated, make renderTree async and use await for these calls, or move the helpers outside the actor.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift` around lines
560 - 568, renderTree is declared nonisolated but calls actor-isolated helpers
(topLevelChildren, children, isMenuOpen, attributeString, attributeBool,
actionNames); fix by crossing the actor boundary correctly: either mark those
six helper methods nonisolated if they are safe to run off-actor, or make
renderTree async (remove nonisolated) and call each helper with await (e.g.,
await topLevelChildren(...)) so the calls respect actor isolation, or
alternatively move the helper implementations out of the actor into a non-actor
helper type and call those from renderTree.
Problem
get_window_statehangs forever on Arc, Safari with many tabs, and Electron apps becauseAXUIElementCopyAttributeValueblocks indefinitely via XPC when the app's accessibility tree is pathologically large.Fix
Rust (
get_window_state.rs+ax/tree.rs):spawn_blockingAX walk intokio::time::timeout(30s). Returns a descriptive error suggestingcapture_mode=visionor a query filter.MAX_ELEMENTS=2000cap towalk_element. When hit, the walk stops early andTreeWalkResult.truncated=trueis set. The partial tree is returned with a warning line appended totree_markdown.Swift (
AppState.swift):renderTreeinwithThrowingTaskGroupwith a 30s deadline task. ThrowsAppStateError.axWalkTimedOut(pid:)on expiry.maxElements=2000cap viavisitedCountparameter. When hit, appends a warning line to the markdown output.renderTreeis nownonisolatedso it can run on the detached task.Both implementations mirror each other: 30s timeout + 2000-node cap with a user-visible warning rather than a silent hang.
Fixes #1537. Addresses #1564.
Summary by CodeRabbit
Release Notes