fix: potential crash in very rare data-race scenario

This commit is contained in:
Louis Pontoise authored and lwouis committed 2020-07-08 20:30:46 +09:00
1 parent b9fef455b6
commit 4ff5d89429
5 files changed
+29 -23

No files matched your search

+1 -1
View File
@@ -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()
+11 -9
View File
@@ -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)
}) })
+10 -7
View File
@@ -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 {
+1 -1
View File
@@ -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)
}) })
+6 -5
View File
@@ -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)
} }
} }
} }