fix: windows order was not updating correctly (closes #4754)
(regression introduced in v7.28.0)
This commit is contained in:
1 parent
27909f1824
commit
1316eb2769
7 files changed
+68
-46
No files matched your search
@@ -21,9 +21,19 @@ extension AXUIElement {
|
||||
AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), globalMessagingTimeoutInSeconds)
|
||||
}
|
||||
|
||||
enum AXCallType {
|
||||
case subscribeToAppNotification
|
||||
case subscribeToWindowNotification
|
||||
case subscribeToDockNotification
|
||||
case updateWindow
|
||||
case updateAppWindows
|
||||
case updateDockBadges
|
||||
case axEventEntrypoint
|
||||
}
|
||||
|
||||
/// if the window server is busy, it may not reply to AX calls. We retry right before the call times-out and returns a bogus value
|
||||
static func retryAxCallUntilTimeout(file: String = #file, function: String = #function, line: Int = #line, context: String = "", after: DispatchTime? = nil, debounceType: DebounceType? = nil, pid: pid_t? = nil, wid: CGWindowID? = nil, retriesQueue: Bool = false, startTimeInNanoseconds: UInt64 = DispatchTime.now().uptimeNanoseconds, block: @escaping () throws -> Void) {
|
||||
let closure = { retryAxCallUntilTimeout_(file: file, function: function, line: line, context: context, after: after, debounceType: debounceType, pid: pid, wid: wid, retriesQueue: retriesQueue, startTimeInNanoseconds: startTimeInNanoseconds, block: block) }
|
||||
static func retryAxCallUntilTimeout(file: String = #file, function: String = #function, line: Int = #line, context: String = "", after: DispatchTime? = nil, debounceType: DebounceType? = nil, pid: pid_t? = nil, wid: CGWindowID? = nil, retriesQueue: Bool = false, startTimeInNanoseconds: UInt64 = DispatchTime.now().uptimeNanoseconds, callType: AXCallType, block: @escaping () throws -> Void) {
|
||||
let closure = { retryAxCallUntilTimeout_(file: file, function: function, line: line, context: context, after: after, debounceType: debounceType, pid: pid, wid: wid, retriesQueue: retriesQueue, startTimeInNanoseconds: startTimeInNanoseconds, callType: callType, block: block) }
|
||||
let queue = retriesQueue ? BackgroundWork.axCallsRetriesQueue : BackgroundWork.axCallsFirstAttemptQueue
|
||||
if let after {
|
||||
queue!.addOperationAfter(deadline: after, block: closure)
|
||||
@@ -45,13 +55,14 @@ extension AXUIElement {
|
||||
}
|
||||
}
|
||||
|
||||
private static func retryAxCallUntilTimeout_(file: String, function: String, line: Int, context: String, after: DispatchTime?, debounceType: DebounceType?, pid: pid_t?, wid: CGWindowID?, retriesQueue: Bool, startTimeInNanoseconds: UInt64, block: @escaping () throws -> Void) {
|
||||
private static func retryAxCallUntilTimeout_(file: String, function: String, line: Int, context: String, after: DispatchTime?, debounceType: DebounceType?, pid: pid_t?, wid: CGWindowID?, retriesQueue: Bool, startTimeInNanoseconds: UInt64, callType: AXCallType, block: @escaping () throws -> Void) {
|
||||
// attempt the AX call
|
||||
if (try? block()) != nil {
|
||||
return
|
||||
}
|
||||
// do we already have ongoing retries for this pid?
|
||||
if let pid {
|
||||
// do we already have ongoing retries for this pid? The app is likely unresponsive
|
||||
// if their are common updates, we avoid congestion by only retrying the latest update
|
||||
if let pid, callType == .updateWindow || callType == .updateAppWindows {
|
||||
axCallsRetriesQueueUnresponsiveAppsMap.lock.lock()
|
||||
let time = axCallsRetriesQueueUnresponsiveAppsMap.map[pid]
|
||||
axCallsRetriesQueueUnresponsiveAppsMap.lock.unlock()
|
||||
@@ -81,7 +92,7 @@ extension AXUIElement {
|
||||
}
|
||||
// retry
|
||||
Logger.info(logFromContext(file, function, line, context))
|
||||
retryAxCallUntilTimeout(file: file, function: function, line: line, context: context, after: .now() + humanPerceptionDelay, debounceType: debounceType, pid: pid, wid: wid, retriesQueue: true, startTimeInNanoseconds: startTimeInNanoseconds, block: block)
|
||||
retryAxCallUntilTimeout(file: file, function: function, line: line, context: context, after: .now() + humanPerceptionDelay, debounceType: debounceType, pid: pid, wid: wid, retriesQueue: true, startTimeInNanoseconds: startTimeInNanoseconds, callType: callType, block: block)
|
||||
}
|
||||
|
||||
private static func logFromContext(_ file: String, _ function: String, _ line: Int, _ context: String) -> String {
|
||||
|
||||
+33
-26
@@ -21,6 +21,15 @@ class Application: NSObject {
|
||||
var alreadyRequestedToQuit = false
|
||||
var debugId: String { "(pid:\(String(describing: pid)) \(bundleIdentifier ?? bundleURL?.absoluteString ?? executableURL?.absoluteString ?? localizedName ?? "nil"))" }
|
||||
|
||||
static let notifications = [
|
||||
kAXApplicationActivatedNotification,
|
||||
kAXMainWindowChangedNotification,
|
||||
kAXFocusedWindowChangedNotification,
|
||||
kAXWindowCreatedNotification,
|
||||
kAXApplicationHiddenNotification,
|
||||
kAXApplicationShownNotification,
|
||||
]
|
||||
|
||||
init(_ runningApplication: NSRunningApplication) {
|
||||
self.runningApplication = runningApplication
|
||||
pid = runningApplication.processIdentifier
|
||||
@@ -61,15 +70,19 @@ class Application: NSObject {
|
||||
}
|
||||
|
||||
func observeEventsIfEligible() {
|
||||
if runningApplication.activationPolicy != .prohibited && axUiElement == nil {
|
||||
axUiElement = AXUIElementCreateApplication(pid)
|
||||
AXObserverCreate(pid, axObserverCallback, &axObserver)
|
||||
if runningApplication.activationPolicy != .prohibited && !isReallyFinishedLaunching {
|
||||
if axUiElement == nil {
|
||||
axUiElement = AXUIElementCreateApplication(pid)
|
||||
}
|
||||
if axObserver == nil {
|
||||
AXObserverCreate(pid, axObserverCallback, &axObserver)
|
||||
}
|
||||
observeEvents()
|
||||
}
|
||||
}
|
||||
|
||||
func manuallyUpdateWindows() {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid) { [weak self] in
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .updateAppWindows) { [weak self] in
|
||||
guard let self else { return }
|
||||
var atLeastOneActualWindow = false
|
||||
guard let axWindows = try self.axUiElement?.allWindows(self.pid) else { return }
|
||||
@@ -163,29 +176,23 @@ class Application: NSObject {
|
||||
|
||||
private func observeEvents() {
|
||||
guard let axObserver else { return }
|
||||
for notification in [
|
||||
kAXApplicationActivatedNotification,
|
||||
kAXMainWindowChangedNotification,
|
||||
kAXFocusedWindowChangedNotification,
|
||||
kAXWindowCreatedNotification,
|
||||
kAXApplicationHiddenNotification,
|
||||
kAXApplicationShownNotification,
|
||||
] {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid) { [weak self] in
|
||||
guard let self else { return }
|
||||
if try self.axUiElement!.subscribeToNotification(axObserver, notification) {
|
||||
if notification == kAXApplicationActivatedNotification {
|
||||
Logger.debug("Subscribed to app", self.debugId)
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: pid, callType: .subscribeToAppNotification) { [weak self] in
|
||||
guard let self else { return }
|
||||
if try self.axUiElement!.subscribeToNotification(axObserver, Application.notifications.first!) {
|
||||
Logger.debug("Subscribed to app", self.debugId)
|
||||
for notification in Application.notifications.dropFirst() {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.pid, callType: .subscribeToAppNotification) { [weak self] in
|
||||
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
|
||||
}
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
// 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
|
||||
if !self.isReallyFinishedLaunching {
|
||||
self.isReallyFinishedLaunching = true
|
||||
self.manuallyUpdateWindows()
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
// 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
|
||||
if !self.isReallyFinishedLaunching {
|
||||
self.isReallyFinishedLaunching = true
|
||||
self.manuallyUpdateWindows()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class Applications {
|
||||
|
||||
static func refreshBadgesAsync() {
|
||||
if !App.app.appIsBeingUsed || Preferences.hideAppBadges { return }
|
||||
AXUIElement.retryAxCallUntilTimeout {
|
||||
AXUIElement.retryAxCallUntilTimeout(callType: .updateDockBadges) {
|
||||
if let dockPid = (list.first { $0.bundleIdentifier == "com.apple.dock" }?.pid),
|
||||
let axList = (try AXUIElementCreateApplication(dockPid).children()?.first { try $0.role() == kAXListRole }),
|
||||
let axAppDockItem = (try axList.children()?.filter { try $0.subrole() == kAXApplicationDockItemSubrole && ($0.appIsRunning() ?? false) }) {
|
||||
|
||||
+11
-7
@@ -48,6 +48,9 @@ class Window {
|
||||
Window.globalCreationCounter += 1
|
||||
creationOrder = Window.globalCreationCounter
|
||||
application.removeWindowslessAppWindow()
|
||||
// the app may have timed out trying to subscribe to app notifications
|
||||
// It may be responsive now since it has a window; we attempt again
|
||||
application.observeEventsIfEligible()
|
||||
checkIfFocused(application, wid)
|
||||
Logger.debug(debugId)
|
||||
observeEvents()
|
||||
@@ -68,7 +71,7 @@ class Window {
|
||||
/// some apps will not trigger AXApplicationActivated, where we usually update application.focusedWindow
|
||||
/// workaround: we check and possibly do it here
|
||||
func checkIfFocused(_ application: Application, _ wid: CGWindowID) {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid) {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid, callType: .updateWindow) {
|
||||
let focusedWid = try application.axUiElement?.focusedWindow()?.cgWindowId()
|
||||
if wid == focusedWid {
|
||||
application.focusedWindow = self
|
||||
@@ -85,12 +88,13 @@ class Window {
|
||||
private func observeEvents() {
|
||||
AXObserverCreate(application.pid, axObserverCallback, &axObserver)
|
||||
guard let axObserver else { return }
|
||||
for notification in Window.notifications {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: debugId, pid: application.pid) { [weak self] in
|
||||
guard let self else { return }
|
||||
if try self.axUiElement!.subscribeToNotification(axObserver, notification) {
|
||||
if notification == kAXUIElementDestroyedNotification {
|
||||
Logger.debug("Subscribed to window", self.debugId)
|
||||
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)
|
||||
for notification in Window.notifications.dropFirst() {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: self.debugId, pid: self.application.pid, callType: .subscribeToWindowNotification) { [weak self] in
|
||||
try self?.axUiElement!.subscribeToNotification(axObserver, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class Windows {
|
||||
static func updateIsFullscreenOnCurrentSpace() {
|
||||
let windowsOnCurrentSpace = Windows.list.filter { !$0.isWindowlessApp }
|
||||
for window in windowsOnCurrentSpace {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: window.debugId, after: .now() + humanPerceptionDelay) { [weak window] in
|
||||
AXUIElement.retryAxCallUntilTimeout(context: window.debugId, after: .now() + humanPerceptionDelay, callType: .updateWindow) { [weak window] in
|
||||
guard let window else { return }
|
||||
try updateWindowSizeAndPositionAndFullscreen(window.axUiElement!, window.cgWindowId!, window)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import ApplicationServices.HIServices.AXNotificationConstants
|
||||
let axObserverCallback: AXObserverCallback = { _, element, notificationName, _ in
|
||||
let type = notificationName as String
|
||||
Logger.debug(type)
|
||||
AXUIElement.retryAxCallUntilTimeout { try handleEvent(type, element) }
|
||||
AXUIElement.retryAxCallUntilTimeout(callType: .axEventEntrypoint) { try handleEvent(type, element) }
|
||||
}
|
||||
|
||||
fileprivate func handleEvent(_ type: String, _ element: AXUIElement) throws {
|
||||
@@ -100,7 +100,7 @@ fileprivate func focusedWindowChanged(_ element: AXUIElement, _ pid: pid_t) thro
|
||||
if let windows = Windows.updateLastFocus(element, wid) {
|
||||
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
|
||||
} else {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "wid:\(wid) pid:\(pid)", pid: pid) {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "wid:\(wid) pid:\(pid)", pid: pid, callType: .updateWindow) {
|
||||
if let (title, role, subrole, isMinimized, isFullscreen) = try element.windowAttributes() {
|
||||
let position = try element.position()
|
||||
let size = try element.size()
|
||||
@@ -156,7 +156,7 @@ fileprivate func windowMiniaturizedOrDeminiaturized(_ element: AXUIElement, _ ty
|
||||
|
||||
fileprivate func windowTitleChanged(_ element: AXUIElement, _ pid: pid_t) throws {
|
||||
let wid = try element.cgWindowId()
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "\(wid)", debounceType: .windowTitleChanged, pid: pid, wid: wid) {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "\(wid)", debounceType: .windowTitleChanged, pid: pid, wid: wid, callType: .updateWindow) {
|
||||
if let (title, _, _, isMinimized, isFullscreen) = try element.windowAttributes() {
|
||||
DispatchQueue.main.async {
|
||||
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }), title != window.title {
|
||||
@@ -172,7 +172,7 @@ fileprivate func windowTitleChanged(_ element: AXUIElement, _ pid: pid_t) throws
|
||||
|
||||
fileprivate func windowResizedOrMoved(_ element: AXUIElement, _ pid: pid_t) throws {
|
||||
let wid = try element.cgWindowId()
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "\(wid)", debounceType: .windowResizedOrMoved, pid: pid, wid: wid) {
|
||||
AXUIElement.retryAxCallUntilTimeout(context: "\(wid)", debounceType: .windowResizedOrMoved, pid: pid, wid: wid, callType: .updateWindow) {
|
||||
try updateWindowSizeAndPositionAndFullscreen(element, wid, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ class DockEvents {
|
||||
AXObserverCreate(dockPid, handleEvent, &axObserver)
|
||||
// are we sure we always get a non-nil axObserver?
|
||||
for notification in MissionControlState.allCases {
|
||||
AXUIElement.retryAxCallUntilTimeout {
|
||||
AXUIElement.retryAxCallUntilTimeout(callType: .subscribeToDockNotification) {
|
||||
if try axUiElement!.subscribeToNotification(axObserver!, notification.rawValue, nil) {
|
||||
if notification == MissionControlState.showDesktop {
|
||||
Logger.debug("Subscribed to Dock")
|
||||
|
||||
Reference in new issue
Block a user