fix: observer leak would throw and crash the app sometimes

This commit is contained in:
louis.pontoise authored and lwouis committed 2020-03-10 14:00:37 +09:00
1 parent 9a8c83e39d
commit 9ca28eb2d8
3 files changed
+21 -7

No files matched your search

@@ -15,11 +15,11 @@ extension Optional {
// add throw-on-nil method on Optional // add throw-on-nil method on Optional
func orThrow() throws -> Wrapped { func orThrow() throws -> Wrapped {
switch self { switch self {
case .some(let value): case .some(let value):
return value return value
case .none: case .none:
Thread.callStackSymbols.forEach { print($0) } Thread.callStackSymbols.forEach { print($0) }
throw NSError.make(domain: "Optional", message: "Optional contained nil") throw NSError.make(domain: "Optional", message: "Optional contained nil")
} }
} }
} }
@@ -64,3 +64,11 @@ extension Collection {
return flatMap { ($0 as? [Any])?.joined() ?? [$0] } return flatMap { ($0 as? [Any])?.joined() ?? [$0] }
} }
} }
// removing an objc KVO observer if there is none throws an exception
extension NSObject {
func safeRemoveObserver(_ observer: NSObject, _ key: String) {
guard observationInfo != nil else { return }
removeObserver(observer, forKeyPath: key)
}
}
+5 -1
View File
@@ -16,6 +16,10 @@ class Application: NSObject {
} }
} }
func removeObserver() {
runningApplication.safeRemoveObserver(self, "isFinishedLaunching")
}
private func addAndObserveWindows() { private func addAndObserveWindows() {
axUiElement = AXUIElementCreateApplication(runningApplication.processIdentifier) axUiElement = AXUIElementCreateApplication(runningApplication.processIdentifier)
AXObserverCreate(runningApplication.processIdentifier, axObserverApplicationCallback, &axObserver) AXObserverCreate(runningApplication.processIdentifier, axObserverApplicationCallback, &axObserver)
@@ -44,7 +48,7 @@ class Application: NSObject {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard let isFinishedLaunching = change![.newKey], isFinishedLaunching as! Bool else { return } guard let isFinishedLaunching = change![.newKey], isFinishedLaunching as! Bool else { return }
runningApplication.removeObserver(self, forKeyPath: "isFinishedLaunching") removeObserver()
addAndObserveWindows() addAndObserveWindows()
} }
+3 -1
View File
@@ -30,13 +30,15 @@ class Applications {
var someAppsAreAlreadyTerminated = false var someAppsAreAlreadyTerminated = false
for runningApp in runningApps { for runningApp in runningApps {
guard runningApp.bundleIdentifier != nil else { someAppsAreAlreadyTerminated = true; continue } guard runningApp.bundleIdentifier != nil else { someAppsAreAlreadyTerminated = true; continue }
guard Applications.map[runningApp.processIdentifier] != nil else { continue } guard let app = Applications.map[runningApp.processIdentifier] else { continue }
var windowsToKeep = [Window]() var windowsToKeep = [Window]()
for window in Windows.listRecentlyUsedFirst { for window in Windows.listRecentlyUsedFirst {
guard window.application.runningApplication.processIdentifier != runningApp.processIdentifier else { continue } guard window.application.runningApplication.processIdentifier != runningApp.processIdentifier else { continue }
windowsToKeep.append(window) windowsToKeep.append(window)
} }
Windows.listRecentlyUsedFirst = windowsToKeep Windows.listRecentlyUsedFirst = windowsToKeep
// some apps never finish launching; the observer leaks for them without this
app.removeObserver()
Applications.map.removeValue(forKey: runningApp.processIdentifier) Applications.map.removeValue(forKey: runningApp.processIdentifier)
guard Windows.listRecentlyUsedFirst.count > 0 else { (App.shared as! App).hideUi(); return } guard Windows.listRecentlyUsedFirst.count > 0 else { (App.shared as! App).hideUi(); return }
// TODO: implement of more sophisticated way to decide which thumbnail gets focused on app quit // TODO: implement of more sophisticated way to decide which thumbnail gets focused on app quit