fix: potential crash in very rare data-race scenario
This commit is contained in:
1 parent
b9fef455b6
commit
4ff5d89429
5 files changed
+29
-23
No files matched your search
@@ -47,7 +47,7 @@ extension Array {
|
|||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
for element in self {
|
for element in self {
|
||||||
BackgroundWork.globalSemaphore.wait()
|
BackgroundWork.globalSemaphore.wait()
|
||||||
BackgroundWork.uiDisplayQueue.async(group: group) {
|
BackgroundWork.mainQueueConcurrentWorkQueue.async(group: group) {
|
||||||
group.enter()
|
group.enter()
|
||||||
fn(element)
|
fn(element)
|
||||||
BackgroundWork.globalSemaphore.signal()
|
BackgroundWork.globalSemaphore.signal()
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class Application: NSObject {
|
|||||||
|
|
||||||
func observeNewWindows() {
|
func observeNewWindows() {
|
||||||
if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited {
|
if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited {
|
||||||
retryUntilTimeout({ [weak self] in
|
retryAxCallUntilTimeout({ [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if let windows_ = try self.axUiElement!.windows(), windows_.count > 0 {
|
if let windows_ = try self.axUiElement!.windows(), windows_.count > 0 {
|
||||||
// bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login)
|
// bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login)
|
||||||
@@ -92,15 +92,17 @@ class Application: NSObject {
|
|||||||
private func observeEvents() {
|
private func observeEvents() {
|
||||||
guard let axObserver = axObserver else { return }
|
guard let axObserver = axObserver else { return }
|
||||||
for notification in Application.notifications(runningApplication) {
|
for notification in Application.notifications(runningApplication) {
|
||||||
retryUntilTimeout({ [weak self] in
|
retryAxCallUntilTimeout({ [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
try self.axUiElement!.subscribeToNotification(axObserver, notification, { [weak self] in
|
try self.axUiElement!.subscribeToNotification(axObserver, notification, {
|
||||||
guard let self = self else { return }
|
DispatchQueue.main.async { [weak self] in
|
||||||
// some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete
|
guard let self = self else { return }
|
||||||
// we consider them ready when the first subscription succeeds, and list their windows again at that point
|
// some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete
|
||||||
if !self.isReallyFinishedLaunching {
|
// we consider them ready when the first subscription succeeds, and list their windows again at that point
|
||||||
self.isReallyFinishedLaunching = true
|
if !self.isReallyFinishedLaunching {
|
||||||
self.observeNewWindows()
|
self.isReallyFinishedLaunching = true
|
||||||
|
self.observeNewWindows()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, self.runningApplication)
|
}, self.runningApplication)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import Foundation
|
|||||||
|
|
||||||
// queues and dedicated threads to observe background events such as keyboard inputs, or accessibility events
|
// queues and dedicated threads to observe background events such as keyboard inputs, or accessibility events
|
||||||
class BackgroundWork {
|
class BackgroundWork {
|
||||||
static let uiDisplayQueue = DispatchQueue.globalConcurrent("mainQueueConcurrentWork", .userInteractive)
|
static let mainQueueConcurrentWorkQueue = DispatchQueue.globalConcurrent("mainQueueConcurrentWorkQueue", .userInteractive)
|
||||||
static let accessibilityCommandsQueue = DispatchQueue.globalConcurrent("accessibilityCommands", .userInteractive)
|
static let accessibilityCommandsQueue = DispatchQueue.globalConcurrent("accessibilityCommandsQueue", .userInteractive)
|
||||||
static let accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEvents")
|
static let axCallsQueue = DispatchQueue.globalConcurrent("axCallsQueue", .userInteractive)
|
||||||
static let keyboardEventsThread = BackgroundThreadWithRunLoop("keyboardEvents")
|
static let accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread")
|
||||||
|
static let keyboardEventsThread = BackgroundThreadWithRunLoop("keyboardEventsThread")
|
||||||
|
|
||||||
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
|
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
|
||||||
static let globalSemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.processorCount)
|
static let globalSemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.processorCount)
|
||||||
|
// AX calls may block for a long time. We use a big semaphore to avoid blocking the main thread in case the window server is busy
|
||||||
|
static let axCallsGlobalSemaphore = DispatchSemaphore(value: 100)
|
||||||
|
|
||||||
// swift static variables are lazy; we artificially force the threads to init
|
// swift static variables are lazy; we artificially force the threads to init
|
||||||
static func start() {
|
static func start() {
|
||||||
@@ -22,12 +25,12 @@ extension DispatchQueue {
|
|||||||
return DispatchQueue(label: label, target: .global(qos: qos.qosClass))
|
return DispatchQueue(label: label, target: .global(qos: qos.qosClass))
|
||||||
}
|
}
|
||||||
|
|
||||||
func asyncWithCap(_ deadline: DispatchTime? = nil, _ fn: @escaping () -> Void) {
|
func asyncWithCap(_ deadline: DispatchTime? = nil, semaphore: DispatchSemaphore = BackgroundWork.globalSemaphore, _ fn: @escaping () -> Void) {
|
||||||
let block = {
|
let block = {
|
||||||
fn()
|
fn()
|
||||||
BackgroundWork.globalSemaphore.signal()
|
semaphore.signal()
|
||||||
}
|
}
|
||||||
BackgroundWork.globalSemaphore.wait()
|
semaphore.wait()
|
||||||
if let deadline = deadline {
|
if let deadline = deadline {
|
||||||
asyncAfter(deadline: deadline, execute: block)
|
asyncAfter(deadline: deadline, execute: block)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class Window {
|
|||||||
AXObserverCreate(application.runningApplication.processIdentifier, axObserverCallback, &axObserver)
|
AXObserverCreate(application.runningApplication.processIdentifier, axObserverCallback, &axObserver)
|
||||||
guard let axObserver = axObserver else { return }
|
guard let axObserver = axObserver else { return }
|
||||||
for notification in Window.notifications {
|
for notification in Window.notifications {
|
||||||
retryUntilTimeout({ [weak self] in
|
retryAxCallUntilTimeout({ [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
try self.axUiElement.subscribeToNotification(axObserver, notification, nil, nil, self.cgWindowId)
|
try self.axUiElement.subscribeToNotification(axObserver, notification, nil, nil, self.cgWindowId)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ import Cocoa
|
|||||||
|
|
||||||
func axObserverCallback(observer: AXObserver, element: AXUIElement, notificationName: CFString, _: UnsafeMutableRawPointer?) -> Void {
|
func axObserverCallback(observer: AXObserver, element: AXUIElement, notificationName: CFString, _: UnsafeMutableRawPointer?) -> Void {
|
||||||
let type = notificationName as String
|
let type = notificationName as String
|
||||||
retryUntilTimeout({ try handleEvent(type, element) })
|
retryAxCallUntilTimeout({ try handleEvent(type, element) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func retryUntilTimeout(_ fn: @escaping () throws -> Void, _ startTime: DispatchTime = DispatchTime.now()) {
|
// 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
|
||||||
DispatchQueue.global(qos: .userInteractive).async {
|
func retryAxCallUntilTimeout(_ fn: @escaping () throws -> Void, _ startTime: DispatchTime = DispatchTime.now()) {
|
||||||
|
BackgroundWork.axCallsQueue.asyncWithCap(semaphore: BackgroundWork.axCallsGlobalSemaphore) {
|
||||||
do {
|
do {
|
||||||
try fn()
|
try fn()
|
||||||
} catch {
|
} catch {
|
||||||
let timePassedInSeconds = Double(DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000
|
let timePassedInSeconds = Double(DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000
|
||||||
if timePassedInSeconds < Double(AXUIElement.globalTimeoutInSeconds) {
|
if timePassedInSeconds < Double(AXUIElement.globalTimeoutInSeconds) {
|
||||||
DispatchQueue.global(qos: .userInteractive).asyncAfter(deadline: .now() + .milliseconds(10)) {
|
BackgroundWork.axCallsQueue.asyncWithCap(.now() + .milliseconds(10), semaphore: BackgroundWork.axCallsGlobalSemaphore) {
|
||||||
retryUntilTimeout(fn, startTime)
|
retryAxCallUntilTimeout(fn, startTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user