fix: app could sometimes crash in the background

closes #5260

appcenter showed that access to debugId() from main and non-main threa could crash the app. Making debugId a string that's set once at init simplifies everything and allows concurrent access from threads
This commit is contained in:
lwouis committed 2026-01-31 18:42:42 +01:00
1 parent 42c0a03ddc
commit ab704dc4be
7 files changed
+26 -29

No files matched your search

+8 -10
View File
@@ -19,10 +19,7 @@ class Application: NSObject {
var dockLabel: String?
var focusedWindow: Window? = nil
var alreadyRequestedToQuit = false
func debugId() -> String {
return "(pid:\(pid) \(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName))"
}
var debugId: String
static let notifications = [
kAXApplicationActivatedNotification,
@@ -81,8 +78,9 @@ class Application: NSObject {
bundleIdentifier = runningApplication.bundleIdentifier
bundleURL = runningApplication.bundleURL
executableURL = runningApplication.executableURL
debugId = "(pid:\(pid) \(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName))"
super.init()
Logger.info { self.debugId() }
Logger.info { self.debugId }
observeEventsIfEligible()
kvObservers = [
runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in
@@ -100,7 +98,7 @@ class Application: NSObject {
}
deinit {
Logger.info { self.debugId() }
Logger.info { self.debugId }
}
@@ -118,17 +116,17 @@ class Application: NSObject {
private func observeEvents() {
guard let axObserver else { return }
AXUIElement.retryAxCallUntilTimeout(context: debugId(), pid: pid, callType: .subscribeToAppNotification) { [weak self] in
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .subscribeToAppNotification) { [weak self] in
guard let self, !self.isReallyFinishedLaunching else { return }
if try self.axUiElement!.subscribeToNotification(axObserver, Application.notifications.first!) {
Logger.debug { "Subscribed to app: \(self.debugId())" }
Logger.debug { "Subscribed to app: \(self.debugId)" }
if !self.isReallyFinishedLaunching {
// some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete
// we consider them ready when the first subscription succeeds
// windows opened before that point won't send a notification, so check those windows manually here
self.isReallyFinishedLaunching = true
for notification in Application.notifications.dropFirst() {
AXUIElement.retryAxCallUntilTimeout(context: self.debugId(), pid: self.pid, callType: .subscribeToAppNotification) { [weak self] in
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.pid, callType: .subscribeToAppNotification) { [weak self] in
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
}
}
@@ -142,7 +140,7 @@ class Application: NSObject {
}
func manuallyUpdateWindows() {
AXUIElement.retryAxCallUntilTimeout(context: debugId(), pid: pid, callType: .updateAppWindows) { [weak self] in
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .updateAppWindows) { [weak self] in
guard let self, let axUiElement = self.axUiElement else { return }
let axWindows = try axUiElement.allWindows(self.pid)
guard !axWindows.isEmpty else {
+1 -1
View File
@@ -47,7 +47,7 @@ class Applications {
let zombies = believedAlive.subtracting(confirmedAlive)
for window in Windows.list.reversed() {
if let wid = window.cgWindowId, zombies.contains(wid) {
Logger.debug { window.debugId() }
Logger.debug { window.debugId }
Windows.removeWindows([window], true)
}
}
+11 -12
View File
@@ -11,7 +11,7 @@ class Window {
]
private static var globalCreationCounter = Int.zero
var id: String!
var id: String
var cgWindowId: CGWindowID?
var lastFocusOrder = Int.zero
var creationOrder = Int.zero
@@ -35,10 +35,7 @@ class Window {
var application: Application
var axObserver: AXObserver?
var rowIndex: Int?
func debugId() -> String {
return "\(application.debugId()) (wid:\(cgWindowId) title:\(title))"
}
var debugId: String!
init(_ axUiElement: AXUIElement, _ application: Application, _ wid: CGWindowID, _ title: String?, _ isFullscreen: Bool?, _ isMinimized: Bool?, _ position: CGPoint?, _ size: CGSize?) {
id = "\(wid)"
@@ -47,6 +44,7 @@ class Window {
cgWindowId = wid
self.updateSpacesAndScreen()
updateFromAxAttributes(title, size, position, isFullscreen, isMinimized)
debugId = "\(self.application.debugId) (wid:\(cgWindowId) title:\(self.title))"
Window.globalCreationCounter += 1
creationOrder = Window.globalCreationCounter
application.removeWindowlessAppWindow()
@@ -54,7 +52,7 @@ class Window {
// It may be responsive now since it has a window; we attempt again
application.observeEventsIfEligible()
checkIfFocused()
Logger.info { self.debugId() }
Logger.info { self.debugId }
observeEvents()
}
@@ -64,11 +62,12 @@ class Window {
title = bestEffortTitle(nil)
Window.globalCreationCounter += 1
creationOrder = Window.globalCreationCounter
Logger.debug { self.debugId() }
debugId = "\(application.debugId) (title:\(title))"
Logger.debug { self.debugId }
}
deinit {
Logger.info { self.debugId() }
Logger.info { self.debugId }
}
func updateFromAxAttributes(_ title: String?, _ size: CGSize?, _ position: CGPoint?, _ isFullscreen: Bool?, _ isMinimized: Bool?) {
@@ -88,12 +87,12 @@ class Window {
private func observeEvents() {
AXObserverCreate(application.pid, AccessibilityEvents.axObserverCallback, &axObserver)
guard let axObserver else { return }
AXUIElement.retryAxCallUntilTimeout(context: debugId(), pid: application.pid, callType: .subscribeToWindowNotification) { [weak self] in
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .subscribeToWindowNotification) { [weak self] in
guard let self else { return }
if try self.axUiElement!.subscribeToNotification(axObserver, Window.notifications.first!) {
Logger.debug { "Subscribed to window: \(self.debugId())" }
Logger.debug { "Subscribed to window: \(self.debugId)" }
for notification in Window.notifications.dropFirst() {
AXUIElement.retryAxCallUntilTimeout(context: self.debugId(), pid: self.application.pid, callType: .subscribeToWindowNotification) { [weak self] in
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.application.pid, callType: .subscribeToWindowNotification) { [weak self] in
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
}
}
@@ -316,7 +315,7 @@ class Window {
/// some apps will not trigger AXApplicationActivated, where we usually update application.focusedWindow
/// workaround: we check and possibly do it here
private func checkIfFocused() {
AXUIElement.retryAxCallUntilTimeout(context: debugId(), pid: application.pid, callType: .updateWindow) { [weak self] in
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .updateWindow) { [weak self] in
guard let self,
let focusedWindow = try self.application.axUiElement?.attributes([kAXFocusedWindowAttribute]).focusedWindow,
let window = try (Windows.list.first { $0.isEqualRobust(focusedWindow, (try focusedWindow.cgWindowId())) }) else { return }
+1 -1
View File
@@ -10,7 +10,7 @@ class Windows {
static func updateIsFullscreenOnCurrentSpace() {
let windowsOnCurrentSpace = list.filter { !$0.isWindowlessApp }
for window in windowsOnCurrentSpace {
AXUIElement.retryAxCallUntilTimeout(context: window.debugId(), after: .now() + humanPerceptionDelay, callType: .updateWindow) { [weak window] in
AXUIElement.retryAxCallUntilTimeout(context: window.debugId, after: .now() + humanPerceptionDelay, callType: .updateWindow) { [weak window] in
guard let window else { return }
// we reuse existing code, to update .isFullscreen, as if there was a kAXWindowResizedNotification
try AccessibilityEvents.handleEventWindow(kAXWindowResizedNotification, window.cgWindowId!, window.application.pid, window.axUiElement!)
+3 -3
View File
@@ -25,7 +25,7 @@ class AccessibilityEvents {
let appFocusedWindow = try element.attributes([kAXFocusedWindowAttribute]).focusedWindow
DispatchQueue.main.async {
guard let app = Applications.findOrCreate(pid) else { return }
Logger.info { "\(type) app:\(app.debugId())" }
Logger.info { "\(type) app:\(app.debugId)" }
if type == kAXApplicationActivatedNotification {
applicationActivated(app, pid, type, appFocusedWindow)
} else if type == kAXApplicationHiddenNotification || type == kAXApplicationShownNotification {
@@ -84,7 +84,7 @@ class AccessibilityEvents {
let a = try element.attributes([kAXTitleAttribute, kAXSubroleAttribute, kAXRoleAttribute, kAXSizeAttribute, kAXPositionAttribute, kAXFullscreenAttribute, kAXMinimizedAttribute])
DispatchQueue.main.async {
guard let app = Applications.findOrCreate(pid) else { return }
Logger.info { "\(type) wid:\(wid) app:\(app.debugId())" }
Logger.info { "\(type) wid:\(wid) app:\(app.debugId)" }
if type == kAXUIElementDestroyedNotification {
windowDestroyed(element, pid, wid)
return
@@ -96,7 +96,7 @@ class AccessibilityEvents {
}
return
}
Logger.debug { "\(type) win:\(window.debugId())" }
Logger.debug { "\(type) win:\(window.debugId)" }
if type == kAXMainWindowChangedNotification || type == kAXFocusedWindowChangedNotification {
focusedWindowChanged(window)
} else if type == kAXWindowResizedNotification || type == kAXWindowMovedNotification {
+1 -1
View File
@@ -62,7 +62,7 @@ class WindowCaptureScreenshots {
ActiveWindowCaptures.increment()
SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) { cgImage, error in
ActiveWindowCaptures.decrement()
guard let cgImage, error == nil else { Logger.error { "\(window.debugId()) \(cgImage == nil) \(error)" }; return }
guard let cgImage, error == nil else { Logger.error { "\(window.debugId) \(cgImage == nil) \(error)" }; return }
guard source != .refreshOnlyThumbnailsAfterShowUi || App.app.appIsBeingUsed else { return }
DispatchQueue.main.async {
guard source != .refreshOnlyThumbnailsAfterShowUi || App.app.appIsBeingUsed else { return }
+1 -1
View File
@@ -117,7 +117,7 @@ class App: AppCenterApplication {
func focusTarget() {
guard appIsBeingUsed else { return } // already hidden
let selectedWindow = Windows.selectedWindow()
Logger.info { selectedWindow?.debugId() }
Logger.info { selectedWindow?.debugId }
focusSelectedWindow(selectedWindow)
}