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()
for element in self {
BackgroundWork.globalSemaphore.wait()
BackgroundWork.uiDisplayQueue.async(group: group) {
BackgroundWork.mainQueueConcurrentWorkQueue.async(group: group) {
group.enter()
fn(element)
BackgroundWork.globalSemaphore.signal()
+11 -9
View File
@@ -50,7 +50,7 @@ class Application: NSObject {
func observeNewWindows() {
if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited {
retryUntilTimeout({ [weak self] in
retryAxCallUntilTimeout({ [weak self] in
guard let self = self else { return }
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)
@@ -92,15 +92,17 @@ class Application: NSObject {
private func observeEvents() {
guard let axObserver = axObserver else { return }
for notification in Application.notifications(runningApplication) {
retryUntilTimeout({ [weak self] in
retryAxCallUntilTimeout({ [weak self] in
guard let self = self else { return }
try self.axUiElement!.subscribeToNotification(axObserver, notification, { [weak self] in
guard let self = 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, and list their windows again at that point
if !self.isReallyFinishedLaunching {
self.isReallyFinishedLaunching = true
self.observeNewWindows()
try self.axUiElement!.subscribeToNotification(axObserver, notification, {
DispatchQueue.main.async { [weak self] in
guard let self = 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, and list their windows again at that point
if !self.isReallyFinishedLaunching {
self.isReallyFinishedLaunching = true
self.observeNewWindows()
}
}
}, 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
class BackgroundWork {
static let uiDisplayQueue = DispatchQueue.globalConcurrent("mainQueueConcurrentWork", .userInteractive)
static let accessibilityCommandsQueue = DispatchQueue.globalConcurrent("accessibilityCommands", .userInteractive)
static let accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEvents")
static let keyboardEventsThread = BackgroundThreadWithRunLoop("keyboardEvents")
static let mainQueueConcurrentWorkQueue = DispatchQueue.globalConcurrent("mainQueueConcurrentWorkQueue", .userInteractive)
static let accessibilityCommandsQueue = DispatchQueue.globalConcurrent("accessibilityCommandsQueue", .userInteractive)
static let axCallsQueue = DispatchQueue.globalConcurrent("axCallsQueue", .userInteractive)
static let accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread")
static let keyboardEventsThread = BackgroundThreadWithRunLoop("keyboardEventsThread")
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
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
static func start() {
@@ -22,12 +25,12 @@ extension DispatchQueue {
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 = {
fn()
BackgroundWork.globalSemaphore.signal()
semaphore.signal()
}
BackgroundWork.globalSemaphore.wait()
semaphore.wait()
if let deadline = deadline {
asyncAfter(deadline: deadline, execute: block)
} else {
+1 -1
View File
@@ -55,7 +55,7 @@ class Window {
AXObserverCreate(application.runningApplication.processIdentifier, axObserverCallback, &axObserver)
guard let axObserver = axObserver else { return }
for notification in Window.notifications {
retryUntilTimeout({ [weak self] in
retryAxCallUntilTimeout({ [weak self] in
guard let self = self else { return }
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 {
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()) {
DispatchQueue.global(qos: .userInteractive).async {
// 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
func retryAxCallUntilTimeout(_ fn: @escaping () throws -> Void, _ startTime: DispatchTime = DispatchTime.now()) {
BackgroundWork.axCallsQueue.asyncWithCap(semaphore: BackgroundWork.axCallsGlobalSemaphore) {
do {
try fn()
} catch {
let timePassedInSeconds = Double(DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000
if timePassedInSeconds < Double(AXUIElement.globalTimeoutInSeconds) {
DispatchQueue.global(qos: .userInteractive).asyncAfter(deadline: .now() + .milliseconds(10)) {
retryUntilTimeout(fn, startTime)
BackgroundWork.axCallsQueue.asyncWithCap(.now() + .milliseconds(10), semaphore: BackgroundWork.axCallsGlobalSemaphore) {
retryAxCallUntilTimeout(fn, startTime)
}
}
}