@@ -8,8 +8,8 @@ final class MacApp: AbstractApp {
88 /*conforms*/ let pid : Int32
99 /*conforms*/ let bundleId : String ?
1010 let nsApp : NSRunningApplication
11- private let axApp : ThreadGuardedValue < AXUIElement >
1211 let isZoom : Bool
12+ private let axApp : ThreadGuardedValue < AXUIElement >
1313 private let appAxSubscriptions : ThreadGuardedValue < [ AxSubscription ] > // keep subscriptions in memory
1414 private let windows : ThreadGuardedValue < [ UInt32 : AxWindow ] > = . init( [ : ] )
1515 private var thread : Thread ?
@@ -34,7 +34,8 @@ final class MacApp: AbstractApp {
3434 }
3535
3636 @MainActor
37- static func get( _ nsApp: NSRunningApplication ) async throws -> MacApp ? {
37+ @discardableResult
38+ static func getOrRegister( _ nsApp: NSRunningApplication ) async throws -> MacApp ? {
3839 // Don't perceive any of the lock screen windows as real windows
3940 // Otherwise, false positive ax notifications might trigger that lead to gcWindows
4041 if nsApp. bundleIdentifier == lockScreenAppBundleId {
@@ -236,68 +237,67 @@ final class MacApp: AbstractApp {
236237 } ?? " "
237238 }
238239
239- @MainActor func detectNewWindowsAndGetIds( ) async throws -> [ UInt32 ] {
240- try await thread? . runInLoop { [ axApp, windows, nsApp] job in
241- guard let newWindows = axApp. threadGuarded. get ( Ax . windowsAttr, signpostEvent: nsApp. idForDebug) else { return Array ( windows. threadGuarded. keys) }
242- var result : [ UInt32 ] = [ ]
243- for window in newWindows {
244- try job. checkCancellation ( )
245- if let windowId = windows. threadGuarded. getOrRegisterAxWindow ( window, nsApp) ? . windowId {
246- result. append ( windowId)
240+ @MainActor
241+ static func refreshAllAndGetAliveWindowIds( frontmostAppBundleId: String ? ) async throws -> [ MacApp : [ UInt32 ] ] {
242+ try await withThrowingTaskGroup ( of: Void . self) { group in
243+ // Register new apps
244+ for nsApp in NSWorkspace . shared. runningApplications where nsApp. activationPolicy == . regular {
245+ try group. addTaskOrCancelAll { @Sendable @MainActor in
246+ _ = try await getOrRegister ( nsApp)
247+ }
248+ }
249+ try await group. waitForAll ( )
250+ }
251+ return try await withThrowingTaskGroup ( of: ( pid_t, [ UInt32] ) . self, returning: [ MacApp : [ UInt32 ] ] . self) { group in
252+ // gc dead apps. refresh underlying windows
253+ for (_, app) in MacApp . allAppsMap {
254+ try group. addTaskOrCancelAll { @Sendable @MainActor in
255+ ( app. pid, try await app. refreshAndGetAliveWindowIds ( frontmostAppBundleId: frontmostAppBundleId) )
256+ }
257+ }
258+ var result : [ MacApp : [ UInt32 ] ] = [ : ]
259+ for try await (pid, windowIds) in group {
260+ if let app = allAppsMap [ pid] {
261+ result [ app] = windowIds
247262 }
248263 }
249264 return result
250- } ?? [ ]
265+ }
251266 }
252267
253268 @MainActor
254- func gcDeadWindowsAndGetAliveIds( frontmostAppBundleId: String ? ) async throws -> Set < UInt32 > {
255- try await thread? . runInLoop { [ nsApp, windows] ( job) -> Set < UInt32 > in
269+ private func refreshAndGetAliveWindowIds( frontmostAppBundleId: String ? ) async throws -> [ UInt32 ] {
270+ if nsApp. isTerminated {
271+ MacApp . allAppsMap. removeValue ( forKey: pid)
272+ thread? . runInLoopAsync { [ windows, appAxSubscriptions, axApp] job in
273+ axApp. destroy ( )
274+ appAxSubscriptions. destroy ( )
275+ windows. destroy ( )
276+ CFRunLoopStop ( CFRunLoopGetCurrent ( ) )
277+ }
278+ thread = nil // Disallow all future job submissions
279+ return [ ]
280+ }
281+ guard let thread else { return [ ] }
282+ return try await thread. runInLoop { [ nsApp, windows, axApp] ( job) -> [ UInt32 ] in
283+ var result : [ UInt32 : AxWindow ] = windows. threadGuarded
256284 // Second line of defence against lock screen. See the first line of defence: closedWindowsCache
257285 // Second and third lines of defence are technically needed only to avoid potential flickering
258- let _windows : [ UInt32 : AxWindow ] = windows . threadGuarded
259- if frontmostAppBundleId == lockScreenAppBundleId { return Set ( _windows . keys ) }
260- let toKeepAlive : [ UInt32 : AxWindow ] = try _windows . filter {
261- try job . checkCancellation ( )
262- return $0 . value . ax . containingWindowId ( signpostEvent : nsApp . idForDebug ) != nil
286+ if frontmostAppBundleId != lockScreenAppBundleId {
287+ result = try result . filter {
288+ try job . checkCancellation ( )
289+ return $0 . value . ax . containingWindowId ( signpostEvent : nsApp . idForDebug ) != nil
290+ }
263291 }
264- windows. threadGuarded = toKeepAlive
265- return Set ( toKeepAlive. keys)
266- } ?? [ ]
267- }
268292
269- @MainActor
270- func unregisterWindow( _ windowId: UInt32 ) {
271- thread? . runInLoopAsync { [ windows] job in
272- windows. threadGuarded. removeValue ( forKey: windowId)
273- }
274- }
275-
276- @MainActor
277- static func gcTerminatedApps( ) {
278- for app in allAppsMap. values where app. nsApp. isTerminated {
279- app. destroy ( skipClosedWindowsCache: true )
280- }
281- }
293+ for window in axApp. threadGuarded. get ( Ax . windowsAttr, signpostEvent: nsApp. idForDebug) ?? [ ] {
294+ try job. checkCancellation ( )
295+ result. getOrRegisterAxWindow ( window, nsApp)
296+ }
282297
283- @MainActor
284- func destroy( skipClosedWindowsCache: Bool ) {
285- MacApp . allAppsMap. removeValue ( forKey: nsApp. processIdentifier)
286- for (_, window) in MacWindow . allWindowsMap where window. app. pid == self . pid {
287- window. garbageCollect ( skipClosedWindowsCache: skipClosedWindowsCache, unregisterAxWindow: false )
298+ windows. threadGuarded = result
299+ return Array ( result. keys)
288300 }
289- thread? . runInLoopAsync { [ windows, appAxSubscriptions, axApp] job in
290- axApp. destroy ( )
291- appAxSubscriptions. destroy ( )
292- windows. destroy ( )
293- CFRunLoopStop ( CFRunLoopGetCurrent ( ) )
294- }
295- thread = nil // Disallow all future job submissions
296- }
297-
298- private func getThreadOrCancel( ) throws -> Thread { // todo convert untyped throws to throws across the whole app
299- if let thread { return thread }
300- throw CancellationError ( )
301301 }
302302
303303 @MainActor // todo swift is stupid
@@ -342,6 +342,7 @@ private class AxWindow {
342342}
343343
344344extension [ UInt32 : AxWindow ] {
345+ @discardableResult
345346 fileprivate mutating func getOrRegisterAxWindow( _ axWindow: AXUIElement , _ nsApp: NSRunningApplication ) -> AxWindow ? {
346347 guard let id = axWindow. containingWindowId ( ) else { return nil }
347348 if let existing = self [ id] {
@@ -429,7 +430,4 @@ extension NSRunningApplication {
429430 var idForDebug : String {
430431 " PID: \( processIdentifier) ID: \( bundleIdentifier ?? executableURL? . description ?? " " ) "
431432 }
432-
433- @MainActor
434- var macApp : MacApp ? { get async throws { try await MacApp . get ( self ) } }
435433}
0 commit comments