@@ -6,10 +6,17 @@ import Foundation
66import Logger
77import Preferences
88import SuggestionModel
9+ import Toast
10+
11+ public extension Notification . Name {
12+ static let accessibilityAPIMalfunctioning = Notification . Name ( " accessibilityAPIMalfunctioning " )
13+ }
914
1015public final class XcodeInspector : ObservableObject {
1116 public static let shared = XcodeInspector ( )
1217
18+ private var toast : ToastController { ToastControllerDependencyKey . liveValue }
19+
1320 private var cancellable = Set < AnyCancellable > ( )
1421 private var activeXcodeObservations = Set < Task < Void , Error > > ( )
1522 private var appChangeObservations = Set < Task < Void , Never > > ( )
@@ -182,6 +189,36 @@ public final class XcodeInspector: ObservableObject {
182189 }
183190 }
184191 }
192+
193+ if UserDefaults . shared
194+ . value ( for: \. restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning)
195+ {
196+ group. addTask { [ weak self] in
197+ while true {
198+ guard let self else { return }
199+ if UserDefaults . shared. value (
200+ for: \. restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
201+ ) {
202+ return
203+ }
204+
205+ try await Task . sleep ( nanoseconds: 10_000_000_000 )
206+ await MainActor . run {
207+ self . checkForAccessibilityMalfunction ( " Timer " )
208+ }
209+ }
210+ }
211+ }
212+
213+ group. addTask { [ weak self] in // malfunctioning
214+ let sequence = NSWorkspace . shared. notificationCenter
215+ . notifications ( named: . accessibilityAPIMalfunctioning)
216+ for await notification in sequence {
217+ guard let self else { return }
218+ await self
219+ . recoverFromAccessibilityMalfunctioning ( notification. object as? String )
220+ }
221+ }
185222 }
186223 }
187224
@@ -207,7 +244,7 @@ public final class XcodeInspector: ObservableObject {
207244 activeWorkspaceURL = xcode. workspaceURL
208245 focusedWindow = xcode. focusedWindow
209246
210- let setFocusedElement = { [ weak self] in
247+ let setFocusedElement = { @ MainActor [ weak self] in
211248 guard let self else { return }
212249 focusedElement = xcode. appElement. focusedElement
213250 if let editorElement = focusedElement, editorElement. isSourceEditor {
@@ -218,27 +255,51 @@ public final class XcodeInspector: ObservableObject {
218255 } else if let element = focusedElement,
219256 let editorElement = element. firstParent ( where: \. isSourceEditor)
220257 {
258+ Logger . service. debug ( " Focused on child of source editor. " )
221259 focusedEditor = . init(
222260 runningApplication: xcode. runningApplication,
223261 element: editorElement
224262 )
225263 } else {
264+ Logger . service. debug ( " No source editor found. " )
226265 focusedEditor = nil
227266 }
228267 }
229268
230269 setFocusedElement ( )
231270 let focusedElementChanged = Task { @MainActor in
232271 for await notification in xcode. axNotifications {
233- guard notification. kind == . focusedUIElementChanged else { continue }
234- Logger . service. debug ( " Update focused element " )
235- try Task . checkCancellation ( )
236- setFocusedElement ( )
272+ if notification. kind == . focusedUIElementChanged {
273+ Logger . service. debug ( " Update focused element " )
274+ try Task . checkCancellation ( )
275+ setFocusedElement ( )
276+ }
237277 }
238278 }
239279
240280 activeXcodeObservations. insert ( focusedElementChanged)
241281
282+ if UserDefaults . shared
283+ . value ( for: \. restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning)
284+ {
285+ let malfunctionCheck = Task { @MainActor [ weak self] in
286+ if #available( macOS 13 . 0 , * ) {
287+ let notifications = xcode. axNotifications. filter {
288+ $0. kind == . uiElementDestroyed
289+ } . debounce ( for: . milliseconds( 1000 ) )
290+ for await _ in notifications {
291+ guard let self else { return }
292+ try Task . checkCancellation ( )
293+ self . checkForAccessibilityMalfunction ( " Element Destroyed " )
294+ }
295+ }
296+ }
297+
298+ activeXcodeObservations. insert ( malfunctionCheck)
299+
300+ checkForAccessibilityMalfunction ( " Reactivate Xcode " )
301+ }
302+
242303 xcode. $completionPanel. receive ( on: DispatchQueue . main) . sink { [ weak self] element in
243304 self ? . completionPanel = element
244305 } . store ( in: & activeXcodeCancellable)
@@ -259,5 +320,81 @@ public final class XcodeInspector: ObservableObject {
259320 self ? . focusedWindow = window
260321 } . store ( in: & activeXcodeCancellable)
261322 }
323+
324+ private var lastRecoveryFromAccessibilityMalfunctioningTimeStamp = Date ( )
325+
326+ @MainActor
327+ private func checkForAccessibilityMalfunction( _ source: String ) {
328+ guard Date ( ) . timeIntervalSince ( lastRecoveryFromAccessibilityMalfunctioningTimeStamp) > 5
329+ else { return }
330+
331+ Logger . service. debug ( """
332+ Check for Accessibility Malfunctioning:
333+ Source Editor: \( {
334+ if let editor = self . focusedEditor {
335+ return editor. element. description
336+ }
337+ return " Not Found "
338+ } ( ) )
339+ Focused Element: \( {
340+ if let element = self . focusedElement {
341+ return " \( element. description) , \( element. identifier) , \( element. role) "
342+ }
343+ return " Not Found "
344+ } ( ) )
345+
346+ Accessibility API Permission: \(
347+ AXIsProcessTrusted ( ) ? " Granted " :
348+ " Not Granted "
349+ )
350+ App: \(
351+ activeApplication? . runningApplication
352+ . bundleIdentifier ?? " "
353+ )
354+ Focused Element: \( {
355+ guard let element = self . activeApplication? . appElement
356+ . focusedElement
357+ else {
358+ return " Not Found "
359+ }
360+ return " \( element. description) , \( element. identifier) , \( element. role) "
361+ } ( ) )
362+ """ )
363+
364+ if let editor = focusedEditor, !editor. element. isSourceEditor {
365+ NSWorkspace . shared. notificationCenter. post (
366+ name: . accessibilityAPIMalfunctioning,
367+ object: " Source Editor Element Corrupted: \( source) "
368+ )
369+ } else if let element = activeXcode? . appElement. focusedElement {
370+ if element. description != focusedElement? . description ||
371+ element. role != focusedElement? . role
372+ {
373+ NSWorkspace . shared. notificationCenter. post (
374+ name: . accessibilityAPIMalfunctioning,
375+ object: " Element Inconsistency: \( source) "
376+ )
377+ }
378+ }
379+ }
380+
381+ @MainActor
382+ private func recoverFromAccessibilityMalfunctioning( _ source: String ? ) {
383+ if UserDefaults . shared. value ( for: \. toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted) {
384+ toast. toast (
385+ content: """
386+ Accessibility API malfunction detected: \
387+ \( source ?? " " ) .
388+ Resetting active Xcode.
389+ """ ,
390+ type: . warning
391+ )
392+ }
393+ if let activeXcode {
394+ lastRecoveryFromAccessibilityMalfunctioningTimeStamp = Date ( )
395+ setActiveXcode ( activeXcode)
396+ activeXcode. observeAXNotifications ( )
397+ }
398+ }
262399}
263400
0 commit comments