Skip to content

Commit

Permalink
fix: better handle apps that start as background processes
Browse files Browse the repository at this point in the history
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
  • Loading branch information
lwouis committed May 21, 2020
1 parent 86aad6d commit 49816ab
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 47 deletions.
3 changes: 2 additions & 1 deletion src/api-wrappers/AXUIElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions src/api-wrappers/PrivateApis.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 16 additions & 23 deletions src/logic/Application.swift
Original file line number Diff line number Diff line change
@@ -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?
Expand All @@ -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 {
Expand All @@ -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)
Expand Down
11 changes: 2 additions & 9 deletions src/logic/Applications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
}
}
29 changes: 15 additions & 14 deletions src/logic/events/WorkspaceEvents.swift
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit 49816ab

Please sign in to comment.