Skip to content

Commit 60b75c4

Browse files
committed
[hotfix] Fix logical data-race in list-windows/expandFormatVar
#1306 Precondtion: Use `list-windows` command in `exec-on-workspace-change` callback The race is between `list-windows` and `MacWindow.garbageCollect` This hotfix drops all possible `await` supension points from the `list-windows` command & `expandFormatVar` function Crash example: ##### AeroSpace Runtime Error ##### Please report to: https://github.com/nikitabobko/AeroSpace/discussions/categories/potential-bugs Please describe what you did to trigger this error Message: Windows must always have a parent. The Window was unbound at: 0 AeroSpace 0x000000010236d110 $s6Common19getStringStacktraceSSyF + 44 1 AeroSpace 0x0000000102200fb4 $s9AppBundle8TreeNodeC13unbindIfBound33_C0BD111AB6D9D19A3A8697678A2C07EFLLAA11BindingDataVSgyF + 684 2 AeroSpace 0x0000000102201260 $s9AppBundle8TreeNodeC16unbindFromParentAA11BindingDataVyF + 28 3 AeroSpace 0x00000001021fa778 $s9AppBundle9MacWindowC14garbageCollect22skipClosedWindowsCacheySb_tF + 240 4 AeroSpace 0x00000001021d4604 $s9AppBundle7refresh33_9D4CAE07A16011BB7304F8F5007BD739LLyyYaKFTY2_ + 292 5 AeroSpace 0x00000001021cff7d $s9AppBundle25runRefreshSessionBlocking_16layoutWorkspaces023optimisticallyPreLayoutH0y6Common0dE5EventO_S2btYaKFyyYaKXEfU_yyYaKXEfU_TQ6_ + 1 6 AeroSpace 0x000000010215c9b1 $s9AppBundle17runRefreshSession_26screenIsDefinitelyUnlocked33optimisticallyPreLayoutWorkspacesy6Common0dE5EventO_S2btFyyYaKcfU_TATQ0_ + 1 7 libswift_Concurrency.dylib 0x000000027085c461 $ss9TaskLocalC13withValueImpl_9operation9isolation4file4lineqd__xn_qd__yYaKXEScA_pSgYiSSSutYaKlFTQ1_ + 1 8 libswift_Concurrency.dylib 0x000000027085c109 $ss9TaskLocalC9withValue_9operation9isolation4file4lineqd__x_qd__yYaKXEScA_pSgYiSSSutYaKlFTQ1_ + 1 9 AeroSpace 0x00000001021cf251 $s9AppBundle25runRefreshSessionBlocking_16layoutWorkspaces023optimisticallyPreLayoutH0y6Common0dE5EventO_S2btYaKFyyYaKXEfU_TQ1_ + 1 10 AeroSpace 0x000000010215c9b1 $s9AppBundle17runRefreshSession_26screenIsDefinitelyUnlocked33optimisticallyPreLayoutWorkspacesy6Common0dE5EventO_S2btFyyYaKcfU_TATQ0_ + 1 11 libswift_Concurrency.dylib 0x000000027085c461 $ss9TaskLocalC13withValueImpl_9operation9isolation4file4lineqd__xn_qd__yYaKXEScA_pSgYiSSSutYaKlFTQ1_ + 1 12 libswift_Concurrency.dylib 0x000000027085c109 $ss9TaskLocalC9withValue_9operation9isolation4file4lineqd__x_qd__yYaKXEScA_pSgYiSSSutYaKlFTQ1_ + 1 13 AeroSpace 0x00000001021ce7b9 $s9AppBundle25runRefreshSessionBlocking_16layoutWorkspaces023optimisticallyPreLayoutH0y6Common0dE5EventO_S2btYaKFTQ1_ + 1 14 AeroSpace 0x00000001021cef71 $s9AppBundle17runRefreshSession_26screenIsDefinitelyUnlocked33optimisticallyPreLayoutWorkspacesy6Common0dE5EventO_S2btFyyYaKcfU_TQ1_ + 1 15 AeroSpace 0x000000010215c9b1 $s9AppBundle17runRefreshSession_26screenIsDefinitelyUnlocked33optimisticallyPreLayoutWorkspacesy6Common0dE5EventO_S2btFyyYaKcfU_TATQ0_ + 1 16 libswift_Concurrency.dylib 0x000000027089083d _ZL23completeTaskWithClosurePN5swift12AsyncContextEPNS_10SwiftErrorE + 1 Version: 0.18.3-Beta Git hash: 3af07d9 refreshSessionEvent: socketServer Date: 2025-04-26 19:13:28 +0000 Thread name: Is main thread: true axTaskLocalAppThreadToken: nil macOS version: Version 15.4 (Build 24E248) Coordinate: AppBundle/Window.swift:8:29 parent recursionDetectorDuringTermination: false cli: false Monitor count: 2 Displays have separate spaces: false Stacktrace: 0 AeroSpace 0x000000010236c73c $s6Common4dieT_4file4line6column8functionxSS_SSS2iSStlF + 2256 1 AeroSpace 0x0000000102203888 $s9AppBundle6WindowC6parentAA21NonLeafTreeNodeObject_pvg + 260 2 AeroSpace 0x00000001022028d4 $s9AppBundle8TreeNodeC11nodeMonitorAA0F0_pSgvg + 68 3 AeroSpace 0x000000010216a4b0 $sSS9AppBundleE15expandFormatVar3objs6ResultOyAA9PrimitiveOS2Ss5Error6CommonyHCg_GAA7AeroObjO_tYaKFTY0_ + 1356 4 AeroSpace 0x00000001021691c1 $sSa9AppBundleAA7AeroObjORszlE6formatys6ResultOySaySSGS2Ss5Error6CommonyHCg_GSayAI16StringInterTokenOGYaKFTQ1_ + 1 5 AeroSpace 0x000000010218f0d5 $s9AppBundle18ListWindowsCommandV3runySbAA6CmdEnvV_AA0G2IoCtYaKFTf4dnn_nTQ5_ + 1 6 AeroSpace 0x000000010218afe1 $s9AppBundle18ListWindowsCommandVAA0E0A2aDP3runySbAA6CmdEnvV_AA0G2IoCtYaKFTWTQ0_ + 1 7 AeroSpace 0x0000000102164431 $sSa9AppBundleAA7Command_pRszlE9runCmdSeqySbAA0E3EnvV_AA0E2IoCntYaKFTQ1_ + 1 8 AeroSpace 0x0000000102163d79 $s9AppBundle7CommandPAAE3runyAA9CmdResultVAA0E3EnvV_AA0E5StdinCtYaKFTQ2_ + 1 9 AeroSpace 0x00000001021e49dd $s9AppBundle13newConnection33_8AA123C1AC8B76A52844CE5C668569DDLLyy6SocketADCYaF6Common12ServerAnswerVyYaKcfU0_AHyYaYbKScMYcXEfU_TQ1_ + 1 10 AeroSpace 0x000000010215c3f5 $s9AppBundle14GlobalObserverC7onNotif33_F1205CFDC13498282252A689C79131F6LLyy10Foundation12NotificationVFZyyYaYbScMYccfU_TATQ0_ + 1 11 AeroSpace 0x00000001021c8899 $s9AppBundle10runSession__4bodyx6Common07RefreshD5EventO_AA03RunD5GuardVxyYaYbKScMYcXEtYaKlFxyYaKXEfU_xyYaKXEfU_AD12ServerAnswerV_Tg5TQ7_ + 1 12 AeroSpace 0x000000010215c9b1 $s9AppBundle17runRefreshSession_26screenIsDefinitelyUnlocked33optimisticallyPreLayoutWorkspacesy6Common0dE5EventO_S2btFyyYaKcfU_TATQ0_ + 1 13 libswift_Concurrency.dylib 0x000000027085c461 $ss9TaskLocalC13withValueImpl_9operation9isolation4file4lineqd__xn_qd__yYaKXEScA_pSgYiSSSutYaKlFTQ1_ + 1 14 libswift_Concurrency.dylib 0x000000027085c109 $ss9TaskLocalC9withValue_9operation9isolation4file4lineqd__x_qd__yYaKXEScA_pSgYiSSSutYaKlFTQ1_ + 1 15 AeroSpace 0x00000001021c5f1d $s9AppBundle10runSession__4bodyx6Common07RefreshD5EventO_AA03RunD5GuardVxyYaYbKScMYcXEtYaKlFxyYaKXEfU_AD12ServerAnswerV_Tg5TQ1_ + 1 16 AeroSpace 0x000000010215c9b1 $s9AppBundle17runRefreshSession_26screenIsDefinitelyUnlocked33optimisticallyPreLayoutWorkspacesy6Common0dE5EventO_S2btFyyYaKcfU_TATQ0_ + 1 17 libswift_Concurrency.dylib 0x000000027085c461 $ss9TaskLocalC13withValueImpl_9operation9isolation4file4lineqd__xn_qd__yYaKXEScA_pSgYiSSSutYaKlFTQ1_ + 1 18 libswift_Concurrency.dylib 0x000000027085c109 $ss9TaskLocalC9withValue_9operation9isolation4file4lineqd__x_qd__yYaKXEScA_pSgYiSSSutYaKlFTQ1_ + 1 19 AeroSpace 0x00000001021c327d $s9AppBundle10runSession__4bodyx6Common07RefreshD5EventO_AA03RunD5GuardVxyYaYbKScMYcXEtYaKlFAD12ServerAnswerV_Tg5TQ1_ + 1 20 AeroSpace 0x00000001021c13d5 $s9AppBundle13newConnection33_8AA123C1AC8B76A52844CE5C668569DDLLyy6SocketADCYaF6Common12ServerAnswerVyYaKcfU0_TQ1_ + 1 21 AeroSpace 0x000000010215c9b1 $s9AppBundle17runRefreshSession_26screenIsDefinitelyUnlocked33optimisticallyPreLayoutWorkspacesy6Common0dE5EventO_S2btFyyYaKcfU_TATQ0_ + 1 22 libswift_Concurrency.dylib 0x000000027089083d _ZL23completeTaskWithClosurePN5swift12AsyncContextEPNS_10SwiftErrorE + 1
1 parent 3af07d9 commit 60b75c4

File tree

7 files changed

+34
-32
lines changed

7 files changed

+34
-32
lines changed

Sources/AppBundle/command/format.swift

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Common
22

33
enum AeroObj {
4-
case window(Window)
4+
case window(window: Window, title: String)
55
case workspace(Workspace)
66
case app(any AbstractApp)
77
case monitor(Monitor)
@@ -28,7 +28,7 @@ enum PlainInterVar: String, CaseIterable {
2828

2929
extension [AeroObj] {
3030
@MainActor
31-
func format(_ format: [StringInterToken]) async throws -> Result<[String], String> {
31+
func format(_ format: [StringInterToken]) -> Result<[String], String> {
3232
var cellTable: [[Cell<String>]] = []
3333
for obj in self {
3434
var line: [Cell<String>] = []
@@ -42,7 +42,7 @@ extension [AeroObj] {
4242
case .literal(let literal):
4343
curCell += literal
4444
case .interVar(let value):
45-
switch try await value.expandFormatVar(obj: obj) {
45+
switch value.expandFormatVar(obj: obj) {
4646
case .success(let expanded): curCell += expanded.toString()
4747
case .failure(let error): errors.append(error)
4848
}
@@ -156,32 +156,32 @@ private struct Cell<T> {
156156

157157
extension String {
158158
@MainActor
159-
func expandFormatVar(obj: AeroObj) async throws -> Result<Primitive, String> {
159+
func expandFormatVar(obj: AeroObj) -> Result<Primitive, String> {
160160
let formatVar = self.toFormatVar()
161161
switch (obj, formatVar) {
162162
case (_, .none): break
163163

164-
case (.window(let w), .workspace):
165-
return try await w.nodeWorkspace.flatMap(AeroObj.workspace).mapAsyncMainActor(expandFormatVar) ?? .success(.string("NULL-WOKRSPACE"))
166-
case (.window(let w), .monitor):
167-
return try await w.nodeMonitor.flatMap(AeroObj.monitor).mapAsyncMainActor(expandFormatVar) ?? .success(.string("NULL-MONITOR"))
168-
case (.window(let w), .app):
169-
return try await expandFormatVar(obj: .app(w.app))
170-
case (.window(_), .window): break
164+
case (.window(let w, _), .workspace):
165+
return w.nodeWorkspace.flatMap(AeroObj.workspace).map(expandFormatVar) ?? .success(.string("NULL-WOKRSPACE"))
166+
case (.window(let w, _), .monitor):
167+
return w.nodeMonitor.flatMap(AeroObj.monitor).map(expandFormatVar) ?? .success(.string("NULL-MONITOR"))
168+
case (.window(let w, _), .app):
169+
return expandFormatVar(obj: .app(w.app))
170+
case (.window(_, _), .window): break
171171

172172
case (.workspace(let ws), .monitor):
173-
return try await expandFormatVar(obj: AeroObj.monitor(ws.workspaceMonitor))
173+
return expandFormatVar(obj: AeroObj.monitor(ws.workspaceMonitor))
174174
case (.workspace, _): break
175175

176176
case (.app(_), _): break
177177
case (.monitor(_), _): break
178178
}
179179
switch (obj, formatVar) {
180-
case (.window(let w), .window(let f)):
180+
case (.window(let w, let title), .window(let f)):
181181
return switch f {
182182
case .windowId: .success(.uint32(w.windowId))
183183
case .windowIsFullscreen: .success(.bool(w.isFullscreen))
184-
case .windowTitle: .success(.string(try await w.title))
184+
case .windowTitle: .success(.string(title))
185185
}
186186
case (.workspace(let w), .workspace(let f)):
187187
return switch f {

Sources/AppBundle/command/formatToJson.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Foundation
33

44
extension [AeroObj] {
55
@MainActor
6-
func formatToJson(_ format: [StringInterToken], ignoreRightPaddingVar: Bool) async throws -> Result<String, String> {
6+
func formatToJson(_ format: [StringInterToken], ignoreRightPaddingVar: Bool) -> Result<String, String> {
77
var list: [[String: Primitive]] = []
88
for richObj in self {
99
var rawObj: [String: Primitive] = [:]
@@ -14,7 +14,7 @@ extension [AeroObj] {
1414
case .literal:
1515
break // should be spaces
1616
case .interVar(let varName):
17-
switch try await varName.expandFormatVar(obj: richObj) {
17+
switch varName.expandFormatVar(obj: richObj) {
1818
case .success(let expanded): rawObj[varName] = expanded
1919
case .failure(let error): return .failure(error)
2020
}

Sources/AppBundle/command/impl/ListAppsCommand.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Common
44
struct ListAppsCommand: Command {
55
let args: ListAppsCmdArgs
66

7-
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
7+
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
88
var result = Array(MacApp.allAppsMap.values)
99
if let hidden = args.macosHidden {
1010
result = result.filter { $0.nsApp.isHidden == hidden }
@@ -15,12 +15,12 @@ struct ListAppsCommand: Command {
1515
} else {
1616
let list = result.map { AeroObj.app($0) }
1717
if args.json {
18-
return switch try await list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
18+
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
1919
case .success(let json): io.out(json)
2020
case .failure(let msg): io.err(msg)
2121
}
2222
} else {
23-
return switch try await list.format(args.format) {
23+
return switch list.format(args.format) {
2424
case .success(let lines): io.out(lines)
2525
case .failure(let msg): io.err(msg)
2626
}

Sources/AppBundle/command/impl/ListMonitorsCommand.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Common
44
struct ListMonitorsCommand: Command {
55
let args: ListMonitorsCmdArgs
66

7-
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
7+
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
88
let focus = focus
99
var result = sortedMonitors
1010
if let focused = args.focused {
@@ -20,12 +20,12 @@ struct ListMonitorsCommand: Command {
2020
} else {
2121
let list = result.map { AeroObj.monitor($0) }
2222
if args.json {
23-
return switch try await list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
23+
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
2424
case .success(let json): io.out(json)
2525
case .failure(let msg): io.err(msg)
2626
}
2727
} else {
28-
return switch try await list.format(args.format) {
28+
return switch list.format(args.format) {
2929
case .success(let lines): io.out(lines)
3030
case .failure(let msg): io.err(msg)
3131
}

Sources/AppBundle/command/impl/ListWindowsCommand.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,21 @@ struct ListWindowsCommand: Command {
4343
if args.outputOnlyCount {
4444
return io.out("\(windows.count)")
4545
} else {
46-
var _windows: [(window: Window, title: String)] = [] // todo cleanup
46+
var _list: [(window: Window, title: String)] = [] // todo cleanup
4747
for window in windows {
48-
_windows.append((window, try await window.title))
48+
_list.append((window, try await window.title))
4949
}
50-
windows = _windows.sortedBy([{ $0.window.app.name ?? "" }, \.title]).map { $0.window }
50+
_list = _list.filter { $0.window.isBound }
51+
_list = _list.sortedBy([{ $0.window.app.name ?? "" }, \.title])
5152

52-
let list = windows.map { AeroObj.window($0) }
53+
let list = _list.map { AeroObj.window(window: $0.window, title: $0.title) }
5354
if args.json {
54-
return switch try await list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
55+
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
5556
case .success(let json): io.out(json)
5657
case .failure(let msg): io.err(msg)
5758
}
5859
} else {
59-
return switch try await list.format(args.format) {
60+
return switch list.format(args.format) {
6061
case .success(let lines): io.out(lines)
6162
case .failure(let msg): io.err(msg)
6263
}

Sources/AppBundle/command/impl/ListWorkspacesCommand.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Common
44
struct ListWorkspacesCommand: Command {
55
let args: ListWorkspacesCmdArgs
66

7-
func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
7+
func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
88
var result: [Workspace] = Workspace.all
99
if let visible = args.filteringOptions.visible {
1010
result = result.filter { $0.isVisible == visible }
@@ -23,12 +23,12 @@ struct ListWorkspacesCommand: Command {
2323
} else {
2424
let list = result.map { AeroObj.workspace($0) }
2525
if args.json {
26-
return switch try await list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
26+
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
2727
case .success(let json): io.out(json)
2828
case .failure(let msg): io.err(msg)
2929
}
3030
} else {
31-
return switch try await list.format(args.format) {
31+
return switch list.format(args.format) {
3232
case .success(let lines): io.out(lines)
3333
case .failure(let msg): io.err(msg)
3434
}

Sources/AppBundle/tree/TreeNode.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class TreeNode: Equatable {
1717
// - drag window with mouse
1818
// - move-mouse command
1919
var lastAppliedLayoutPhysicalRect: Rect? = nil // with real inner gaps
20-
var unboundStacktrace: String? = nil
20+
final var unboundStacktrace: String? = nil
21+
var isBound: Bool { unboundStacktrace == nil } // todo drop, once https://github.com/nikitabobko/AeroSpace/issues/1215 is fixed
2122

2223
@MainActor
2324
init(parent: NonLeafTreeNodeObject, adaptiveWeight: CGFloat, index: Int) {

0 commit comments

Comments
 (0)