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
This commit is contained in:
Louis Pontoise committed 2020-05-21 12:24:37 +09:00
1 parent 86aad6d68b
commit 49816ab320
5 files changed
+45 -47

No files matched your search

+2 -1
View File
@@ -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 {
+10
View File
@@ -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
+16 -23
View File
@@ -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)
+2 -9
View File
@@ -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!"
}
}
+15 -14
View File
@@ -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<NSRunningApplication>!
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<A>(_ application: NSWorkspace, _ change: NSKeyValueObservedChange<A>) {
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
}
}