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