@@ -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 {
@@ -295,9 +296,67 @@ final class MacApp: AbstractApp {
295296 thread = nil // Disallow all future job submissions
296297 }
297298
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 ( )
299+ @MainActor
300+ private static func refreshAndGetAliveWindowIds( frontmostAppBundleId: String ? ) async throws -> [ UInt32 ] {
301+ try await withThrowingTaskGroup ( of: Void . self) { group in
302+ // Register new apps
303+ for nsApp in NSWorkspace . shared. runningApplications where nsApp. activationPolicy == . regular {
304+ let succ = group. addTaskUnlessCancelled { @Sendable @MainActor in
305+ _ = try await getOrRegister ( nsApp)
306+ }
307+ if !succ { throw CancellationError ( ) }
308+ }
309+ try await group. waitForAll ( )
310+ }
311+ return try await withThrowingTaskGroup ( of: [ UInt32 ] . self, returning: [ UInt32 ] . self) { group in
312+ var result : [ UInt32 ] = [ ]
313+ // gc dead apps. refresh underlying windows
314+ for (_, app) in MacApp . allAppsMap {
315+ let succ = group. addTaskUnlessCancelled { @Sendable @MainActor in
316+ try await app. refreshAndGetAliveWindowIds ( frontmostAppBundleId: frontmostAppBundleId)
317+ }
318+ if !succ { throw CancellationError ( ) }
319+ }
320+ for try await child in group {
321+ result. append ( contentsOf: child)
322+ }
323+ return result
324+ }
325+ }
326+
327+ @MainActor
328+ private func refreshAndGetAliveWindowIds( frontmostAppBundleId: String ? ) async throws -> [ UInt32 ] {
329+ if nsApp. isTerminated {
330+ MacApp . allAppsMap. removeValue ( forKey: pid)
331+ thread? . runInLoopAsync { [ windows, appAxSubscriptions, axApp] job in
332+ axApp. destroy ( )
333+ appAxSubscriptions. destroy ( )
334+ windows. destroy ( )
335+ CFRunLoopStop ( CFRunLoopGetCurrent ( ) )
336+ }
337+ thread = nil // Disallow all future job submissions
338+ return [ ]
339+ }
340+ guard let thread else { return [ ] }
341+ return try await thread. runInLoop { [ nsApp, windows, axApp] ( job) -> [ UInt32 ] in
342+ var result : [ UInt32 : AxWindow ] = windows. threadGuarded
343+ // Second line of defence against lock screen. See the first line of defence: closedWindowsCache
344+ // Second and third lines of defence are technically needed only to avoid potential flickering
345+ if frontmostAppBundleId != lockScreenAppBundleId {
346+ result = try result. filter {
347+ try job. checkCancellation ( )
348+ return $0. value. ax. containingWindowId ( signpostEvent: nsApp. idForDebug) != nil
349+ }
350+ }
351+
352+ for window in axApp. threadGuarded. get ( Ax . windowsAttr, signpostEvent: nsApp. idForDebug) ?? [ ] {
353+ try job. checkCancellation ( )
354+ result. getOrRegisterAxWindow ( window, nsApp)
355+ }
356+
357+ windows. threadGuarded = result
358+ return Array ( result. keys)
359+ }
301360 }
302361
303362 @MainActor // todo swift is stupid
@@ -342,6 +401,7 @@ private class AxWindow {
342401}
343402
344403extension [ UInt32 : AxWindow ] {
404+ @discardableResult
345405 fileprivate mutating func getOrRegisterAxWindow( _ axWindow: AXUIElement , _ nsApp: NSRunningApplication ) -> AxWindow ? {
346406 guard let id = axWindow. containingWindowId ( ) else { return nil }
347407 if let existing = self [ id] {
@@ -442,7 +502,4 @@ extension NSRunningApplication {
442502 var idForDebug : String {
443503 " PID: \( processIdentifier) ID: \( bundleIdentifier ?? executableURL? . description ?? " " ) "
444504 }
445-
446- @MainActor
447- var macApp : MacApp ? { get async throws { try await MacApp . get ( self ) } }
448505}
0 commit comments