From 49816ab320cd2a6d1cc554c2ada0116c177cb3eb Mon Sep 17 00:00:00 2001 From: Louis Pontoise Date: Wed, 20 May 2020 17:29:35 +0900 Subject: [PATCH] fix: better handle apps that start as background processes some apps start with .activationPolicy == .prohibited and then transition to != .prohibited later on. We now observe that attribute evolution, instead of hardcoding exceptions for some apps. For the initial filtering of processes, we now takes every process that is not an XPC process, as all others could spawn windows down the line --- src/api-wrappers/AXUIElement.swift | 3 +- src/api-wrappers/PrivateApis.swift | 10 +++++++ src/logic/Application.swift | 39 +++++++++++--------------- src/logic/Applications.swift | 11 ++------ src/logic/events/WorkspaceEvents.swift | 29 ++++++++++--------- 5 files changed, 45 insertions(+), 47 deletions(-) diff --git a/src/api-wrappers/AXUIElement.swift b/src/api-wrappers/AXUIElement.swift index 43b93181..d6c70f16 100644 --- a/src/api-wrappers/AXUIElement.swift +++ b/src/api-wrappers/AXUIElement.swift @@ -28,6 +28,7 @@ extension AXUIElement { // All Steam windows have subrole = AXUnknown // some dropdown menus are not desirable; they have title == "", or sometimes role == nil when switching between menus quickly (bundleIdentifier == "com.valvesoftware.steam" && title() != "" && role() != nil)) && + // don't show floating windows isOnNormalLevel() } @@ -119,7 +120,7 @@ extension AXUIElement { func handleSubscriptionAttempt(_ result: AXError, _ axObserver: AXObserver, _ notification: String, _ pointer: UnsafeMutableRawPointer?, _ callback: (() -> Void)?, _ runningApplication: NSRunningApplication?, _ wid: CGWindowID?, _ startTime: DispatchTime) -> Void { if result == .success || result == .notificationAlreadyRegistered { - DispatchQueue.main.async { [weak self] () -> () in + DispatchQueue.main.async { () -> () in callback?() } } else if result != .notificationUnsupported && result != .notImplemented { diff --git a/src/api-wrappers/PrivateApis.swift b/src/api-wrappers/PrivateApis.swift index adaaa155..083a3488 100644 --- a/src/api-wrappers/PrivateApis.swift +++ b/src/api-wrappers/PrivateApis.swift @@ -160,6 +160,16 @@ let kAXFullscreenAttribute = "AXFullScreen" //@_silgen_name("GetProcessPID") //func GetProcessPID(_ psn: inout ProcessSerialNumber, _ pid: inout pid_t) -> Void // +//// returns info about a given psn +//// * macOS 10.9- +//@_silgen_name("GetProcessInformation") @discardableResult +//func GetProcessInformation(_ psn: inout ProcessSerialNumber, _ info: inout ProcessInfoRec) -> OSErr +// +//// returns the psn for a given pid +//// * macOS 10.9- +//@_silgen_name("GetProcessForPID") @discardableResult +//func GetProcessForPID(_ pid: pid_t, _ psn: inout ProcessSerialNumber) -> OSStatus +// //// crashed the app with SIGSEGV //// * macOS 10.10+ //@_silgen_name("CGSGetWindowType") @discardableResult diff --git a/src/logic/Application.swift b/src/logic/Application.swift index 0d143b17..3521cade 100644 --- a/src/logic/Application.swift +++ b/src/logic/Application.swift @@ -1,6 +1,8 @@ import Cocoa class Application: NSObject { + // kvObservers should be listed first, so it gets deinit'ed first; otherwise it can crash + var kvObservers: [NSKeyValueObservation]? var runningApplication: NSRunningApplication var axUiElement: AXUIElement? var axObserver: AXObserver? @@ -26,32 +28,29 @@ class Application: NSObject { init(_ runningApplication: NSRunningApplication) { self.runningApplication = runningApplication super.init() - if runningApplication.isFinishedLaunching { - addAndObserveWindows() - } else { - runningApplication.addObserver(self, forKeyPath: "isFinishedLaunching", options: [.new], context: nil) - } - } - - deinit { - // some apps never finish launching; observer should be removed to avoid leak - removeObserver() + addAndObserveWindows() + kvObservers = [ + runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in self?.addAndObserveWindows() }, + runningApplication.observe(\.activationPolicy, options: [.new]) { [weak self] _, _ in self?.addAndObserveWindows() }, + ] } func removeObserver() { runningApplication.safeRemoveObserver(self, "isFinishedLaunching") } - private func addAndObserveWindows() { - axUiElement = AXUIElementCreateApplication(runningApplication.processIdentifier) - AXObserverCreate(runningApplication.processIdentifier, axObserverCallback, &axObserver) - debugPrint("Adding app", runningApplication.processIdentifier, runningApplication.bundleIdentifier ?? "nil") - observeEvents() + func addAndObserveWindows() { + if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited { + axUiElement = AXUIElementCreateApplication(runningApplication.processIdentifier) + AXObserverCreate(runningApplication.processIdentifier, axObserverCallback, &axObserver) + debugPrint("Adding app", runningApplication.processIdentifier, runningApplication.bundleIdentifier ?? "nil") + observeEvents() + } } func observeNewWindows() { - if let windows = (axUiElement!.windows()? - .filter { $0.isActualWindow(runningApplication.bundleIdentifier) }) { + if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited, + let windows = (axUiElement!.windows()?.filter { $0.isActualWindow(runningApplication.bundleIdentifier) }) { // bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login) let actualWindows = Array(Set(windows.filter { Windows.list.firstIndexThatMatches($0) == nil })) if actualWindows.count > 0 { @@ -60,12 +59,6 @@ class Application: NSObject { } } - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - guard let isFinishedLaunching = change![.newKey], isFinishedLaunching as! Bool else { return } - removeObserver() - addAndObserveWindows() - } - private func addWindows(_ axWindows: [AXUIElement]) { let windows = axWindows.map { Window($0, self) } Windows.list.insertAndScaleRecycledPool(windows, at: 0) diff --git a/src/logic/Applications.swift b/src/logic/Applications.swift index 7b7c5432..fd9b9cdf 100644 --- a/src/logic/Applications.swift +++ b/src/logic/Applications.swift @@ -65,14 +65,7 @@ class Applications { } private static func isActualApplication(_ app: NSRunningApplication) -> Bool { - return (app.activationPolicy != .prohibited || - // Bug in CopyQ; see https://github.com/hluk/CopyQ/issues/1330 - app.bundleIdentifier == "io.github.hluk.CopyQ" || - // Bug in Parsec https://github.com/lwouis/alt-tab-macos/issues/206#issuecomment-609828033 - app.bundleIdentifier == "tv.parsec.www" || - // Bug in Octave.app; see https://github.com/octave-app/octave-app/issues/193#issuecomment-603648857 - app.localizedName == "octave-gui") && - // bug in Octave.app; see https://github.com/octave-app/octave-app/issues/193 - app.bundleIdentifier != "org.octave-app.Octave" + let bundlePackageType = app.bundleURL.flatMap { Bundle(url: $0) }.flatMap { $0.infoDictionary }.flatMap { $0["CFBundlePackageType"] as? String } + return bundlePackageType != "XPC!" } } diff --git a/src/logic/events/WorkspaceEvents.swift b/src/logic/events/WorkspaceEvents.swift index f4fe26db..911270d3 100644 --- a/src/logic/events/WorkspaceEvents.swift +++ b/src/logic/events/WorkspaceEvents.swift @@ -1,23 +1,24 @@ import Cocoa -class WorkspaceEvents: NSObject { - private static var appsObserver = WorkspaceEvents() +class WorkspaceEvents { + private static var appsObserver: NSKeyValueObservation! + private static var previousValueOfRunningApps: Set! static func observeRunningApplications() { - NSWorkspace.shared.addObserver(appsObserver, forKeyPath: "runningApplications", options: [.old, .new], context: nil) + previousValueOfRunningApps = Set(NSWorkspace.shared.runningApplications) + appsObserver = NSWorkspace.shared.observe(\.runningApplications, options: [.old, .new], changeHandler: observerCallback) } - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - let type = NSKeyValueChange(rawValue: change![.kindKey]! as! UInt) - if type == .insertion { - let apps = change![.newKey] as! [NSRunningApplication] - debugPrint("OS event", "apps launched", apps.map { ($0.processIdentifier, $0.bundleIdentifier) }) - Applications.addRunningApplications(apps) - } else if type == .removal { - let apps = change![.oldKey] as! [NSRunningApplication] - debugPrint("OS event", "apps quit", apps.map { ($0.processIdentifier, $0.bundleIdentifier) }) - Applications.removeRunningApplications(apps) - + static func observerCallback(_ application: NSWorkspace, _ change: NSKeyValueObservedChange) { + let workspaceApps = Set(NSWorkspace.shared.runningApplications) + let diff = Array(workspaceApps.symmetricDifference(previousValueOfRunningApps)) + if change.kind == .insertion { + debugPrint("OS event", "apps launched", diff.map { ($0.processIdentifier, $0.bundleIdentifier) }) + Applications.addRunningApplications(diff) + } else if change.kind == .removal { + debugPrint("OS event", "apps quit", diff.map { ($0.processIdentifier, $0.bundleIdentifier) }) + Applications.removeRunningApplications(diff) } + previousValueOfRunningApps = workspaceApps } }