FocusCommand: fix 'is already unbound' crash when two focus commands race - #2228
Open
hernloh wants to merge 3 commits into
Open
FocusCommand: fix 'is already unbound' crash when two focus commands race#2228hernloh wants to merge 3 commits into
hernloh wants to merge 3 commits into
Conversation
TestWindow.getAxRect returns without ever suspending, while in production the AX read is dispatched to the app AX thread and the caller is resumed via a continuation. It means that tests can't reproduce anything that happens while a command is suspended in the middle of an AX read. Add a one-shot hook that turns a single getAxRect call into a controlled suspension point. Together with AwaitableOneTimeBroadcastLatch it lets a test pin two concurrent commands to an exact interleaving, without sleeps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…race nikitabobko#1311 makeFloatingWindowsSeenAsTiling iterates over workspace.floatingWindows and awaits window.getCenter before it calls window.unbindFromParent(). getCenter is a real suspension point, and light sessions are not serialized: every hotkey press starts its own unstructured task. So a second focus command can enter the very same loop while the first one is suspended, unbind the very same floating window, and leave the first one to unbind an already unbound node. Re-validate after each of the two suspension points of the loop that the window is still a child of floatingWindowsContainer, and skip it otherwise. Both checks are needed, and the regression tests pin them separately. The second check is not reached at all when there is no tiling window under the floating window center, which is exactly the case in the report, where all the windows are floating: that scenario still crashes with only the second check in place. And when the second check is reached, the window could have been unbound after the first check has already passed: that scenario still crashes with only the first check in place. The recorded "stacktrace where it was unbound" below is the same stack as the die site itself: the same makeFloatingWindowsSeenAsTiling async resume function at the same offset, under refreshSessionEvent: hotkeyBinding. In other words, the window was unbound by another instance of this very loop, and not by window closing or GC. ##### AeroSpace Runtime Error ##### Message: AppBundle.MacWindow is already unbound. The stacktrace where it was unbound: 0 AeroSpace 0x000000010500ed24 $s6Common19getStringStacktraceSSyF + 36 1 AeroSpace 0x0000000104e59820 $s9AppBundle8TreeNodeC13unbindIfBound33_C0BD111AB6D9D19A3A8697678A2C07EFLLAA11BindingDataVSgyF + 340 2 AeroSpace 0x0000000104e59a18 $s9AppBundle8TreeNodeC16unbindFromParentAA11BindingDataVyF + 28 3 AeroSpace 0x0000000104dc06d0 $s9AppBundle31makeFloatingWindowsSeenAsTiling33_5E41CC7E5F0BE51AD60C091A166A5AF8LL9workspaceSayAA0D10WindowDataACLLVGAA9WorkspaceC_tYaFTQ1_ + 884 4 AeroSpace 0x0000000104dbf6a9 $s9AppBundle12FocusCommandV3runy6Common14BinaryExitCodeOAA6CmdEnvV_AA0J2Io_ptYaFTQ2_ + 1 5 AeroSpace 0x0000000104dc1179 $s9AppBundle12FocusCommandVAA0D0A2aDP3runy1T_12ExitCodeTypeQZAA6CmdEnvV_AA0I2Io_ptYaFTWTQ0_ + 1 6 AeroSpace 0x0000000104e52849 $s9AppBundle5ShellOA2A7Command_pRszlE3runy6Common13Int32ExitCodeVAA6CmdEnvV_AA0J2Io_ptYaFTf4nen_nAA0jL4ImplC_Tg5TQ1_ + 1 7 AeroSpace 0x0000000104deb2c5 $s9AppBundle27activateMode_nonCancellableyySSSgYaFyycfU0_yyYaKcfU_yyYaYbKScMYcXEfU_TQ2_ + 1 8 AeroSpace 0x0000000104d9856d $s9AppBundle15runLightSession__4bodyx6Common07RefreshE5EventO_AA03RunE5GuardVxyYaYbKScMYcXEtYaKlFxyYaKXEfU_Sb_TG5TATQ0_ + 1 ... Version: 0.21.3-Beta Git hash: d56e163 refreshSessionEvent: hotkeyBinding Date: 2026-08-13 14:15:55 +0000 Is main thread: true axTaskLocalAppThreadToken: nil macOS version: Version 26.5.2 (Build 25F84) Coordinate: AppBundle/TreeNode.swift:122:32 unbindFromParent() cli: false die: true Monitor count: 3 Displays have separate spaces: true The crash reproduces on a workspace with a few floating windows within seconds: for i in {1..12}; do aerospace focus --boundaries all-monitors-outer-frame --boundaries-action wrap-around-all-monitors left & aerospace focus --boundaries all-monitors-outer-frame --boundaries-action wrap-around-all-monitors right & done; wait Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nikitabobko#1311 The tests force the poisonous schedule deterministically, without sleeps: the command 'a' is suspended inside the AX read of makeFloatingWindowsSeenAsTiling, the command 'b' runs the whole loop meanwhile and unbinds the same floating window, and only then 'a' is resumed. They differ in where 'a' is parked, and in what the workspace looks like: - The first one parks 'a' in the first getCenter, in a workspace that also has a tiling window. It crashes when both checks are dropped - The second one parks 'a' in the second getCenter, after the first check has already passed. It crashes when only the first check is in place - The third one is the all floating workspace from the report. There is no tiling window under the floating window center, so the second getCenter is never reached. It crashes when only the second check is in place All of them also assert that the tree is healthy in the end: every floating window is bound back to floatingWindowsContainer, none is lost, duplicated, or left behind in the tiling tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericreeves
added a commit
to ericreeves/AeroSpace
that referenced
this pull request
Aug 28, 2026
…abobko#2228 (focus race unbound crash) Both mergeable and validated locally: full assembly (2174 + 2232 + 2228 + 5 local patches) merges clean and swift build --target AppBundle is green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the "MacWindow is already unbound" crash in the
focuscommand over floating windows (#1311). Root cause, evidence, and a CLI reproducer are in the commit messages (and in more detail in my comment on #1311).Up-front disclosure: this PR — the patch, the tests, and this description — was written by an AI agent (Claude Code) that I operate; I'm the human in the loop. I root-caused the crash from a production runtime-error dump, the new regression tests crash unpatched sources and pass with the fix, and I've been running the patched build as my daily window manager — the reproducer no longer crashes it. If you'd rather not review AI-authored patches, feel free to close this without comment — the diagnosis on #1311 stands on its own, and whatever you decide to do with this is entirely your call.
PR checklist
./test.shexits with non-zero exit code.