Skip to content

Commit d1fc4d6

Browse files
committed
Merge branch 'feature/accessibility-api-malfunction-recovery' into develop
2 parents 8419b80 + 1c2ee31 commit d1fc4d6

File tree

10 files changed

+206
-21
lines changed

10 files changed

+206
-21
lines changed

Core/Sources/HostApp/DebugView.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ final class DebugSettings: ObservableObject {
1919
@AppStorage(\.disableGitIgnoreCheck) var disableGitIgnoreCheck
2020
@AppStorage(\.disableFileContentManipulationByCheatsheet)
2121
var disableFileContentManipulationByCheatsheet
22+
@AppStorage(\.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning)
23+
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning
24+
@AppStorage(\.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer)
25+
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
26+
@AppStorage(\.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted)
27+
var toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted
2228
init() {}
2329
}
2430

@@ -75,6 +81,31 @@ struct DebugSettingsView: View {
7581
Text("Disable file content manipulation by cheatsheet")
7682
}
7783

84+
Group {
85+
Toggle(
86+
isOn: $settings
87+
.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning
88+
) {
89+
Text(
90+
"Re-activate Xcode Inspector when Accessibility API malfunctioning detected"
91+
)
92+
}
93+
94+
Toggle(
95+
isOn: $settings
96+
.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
97+
) {
98+
Text("Trigger malfunctioning detection only with events")
99+
}
100+
101+
Toggle(
102+
isOn: $settings
103+
.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted
104+
) {
105+
Text("Toast for the reason of re-activation of Xcode Inspector")
106+
}
107+
}
108+
78109
Button("Reset migration version to 0") {
79110
UserDefaults.shared.set(nil, forKey: "OldMigrationVersion")
80111
}

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,10 @@ public actor RealtimeSuggestionController {
7171

7272
switch notification.kind {
7373
case .valueChanged:
74-
Logger.service.debug("Receive valueChanged from editor")
7574
await cancelInFlightTasks()
7675
await self.triggerPrefetchDebounced()
7776
await self.notifyEditingFileChange(editor: sourceEditor.element)
7877
case .selectedTextChanged:
79-
Logger.service.debug("Receive selectedTextChanged from editor")
8078
guard let fileURL = XcodeInspector.shared.activeDocumentURL
8179
else { break }
8280
await PseudoCommandHandler().invalidateRealtimeSuggestionsIfNeeded(
@@ -96,7 +94,7 @@ public actor RealtimeSuggestionController {
9694
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL)
9795

9896
if filespace.codeMetadata.uti == nil {
99-
Logger.service.info("Generate cache for file.")
97+
Logger.service.info("Generate cache for file.")
10098
// avoid the command get called twice
10199
filespace.codeMetadata.uti = ""
102100
do {

Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,6 @@ public struct WidgetFeature: ReducerProtocol {
356356
await send(.panel(.switchToAnotherEditorAndUpdateContent))
357357

358358
for await notification in notifications {
359-
Logger.service.debug("Receive \(notification.kind) from Xcode")
360-
361359
try Task.checkCancellation()
362360

363361
// Hide the widgets before switching to another window/editor
@@ -402,11 +400,10 @@ public struct WidgetFeature: ReducerProtocol {
402400

403401
return .run { send in
404402
if #available(macOS 13.0, *) {
405-
for await notification in merge(
403+
for await _ in merge(
406404
selectionRangeChange.debounce(for: Duration.milliseconds(500)),
407405
scroll
408406
) {
409-
Logger.service.debug("Receive \(notification.kind) from editor")
410407
guard xcodeInspector.latestActiveXcode != nil else { return }
411408
try Task.checkCancellation()
412409
await send(.updateWindowLocation(animated: false))

ExtensionService/AppDelegate+Menu.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,15 @@ extension AppDelegate: NSMenuDelegate {
126126
.append(.text("Active Workspace: \(inspector.activeWorkspaceURL?.path ?? "N/A")"))
127127
menu.items
128128
.append(.text("Active Document: \(inspector.activeDocumentURL?.path ?? "N/A")"))
129-
129+
130130
if let focusedWindow = inspector.focusedWindow {
131131
menu.items.append(.text(
132132
"Active Window: \(focusedWindow.uiElement.identifier)"
133133
))
134134
} else {
135135
menu.items.append(.text("Active Window: N/A"))
136136
}
137-
137+
138138
if let focusedElement = inspector.focusedElement {
139139
menu.items.append(.text(
140140
"Focused Element: \(focusedElement.description)"
@@ -144,9 +144,9 @@ extension AppDelegate: NSMenuDelegate {
144144
}
145145

146146
if let sourceEditor = inspector.focusedEditor {
147-
menu.items.append(.text(
148-
"Active Source Editor: \(sourceEditor.element.isSourceEditor ? "Found" : "Error")"
149-
))
147+
let label = sourceEditor.element.description
148+
menu.items
149+
.append(.text("Active Source Editor: \(label.isEmpty ? "Unknown" : label)"))
150150
} else {
151151
menu.items.append(.text("Active Source Editor: N/A"))
152152
}

Pro

Submodule Pro updated from 31e43f1 to 58572b0

Tool/Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ let package = Package(
168168
"SuggestionModel",
169169
"AXNotificationStream",
170170
"Logger",
171+
"Toast",
172+
"Preferences",
171173
.product(name: "AsyncExtensions", package: "AsyncExtensions"),
172174
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
173175
]

Tool/Sources/Preferences/Keys.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,5 +569,26 @@ public extension UserDefaultPreferenceKeys {
569569
key: "FeatureFlag-DisableEnhancedWorkspace"
570570
)
571571
}
572+
573+
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning: FeatureFlag {
574+
.init(
575+
defaultValue: true,
576+
key: "FeatureFlag-RestartXcodeInspectorIfAccessibilityAPIIsMalfunctioning"
577+
)
578+
}
579+
580+
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer: FeatureFlag {
581+
.init(
582+
defaultValue: true,
583+
key: "FeatureFlag-RestartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer"
584+
)
585+
}
586+
587+
var toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted: FeatureFlag {
588+
.init(
589+
defaultValue: false,
590+
key: "FeatureFlag-ToastForTheReasonWhyXcodeInspectorNeedsToBeRestarted"
591+
)
592+
}
572593
}
573594

Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
135135
observeAXNotifications()
136136

137137
try await Task.sleep(nanoseconds: 3_000_000_000)
138-
// Sometimes the focused window may not be rea?dy on app launch.
138+
// Sometimes the focused window may not be ready on app launch.
139139
if !(focusedWindow is WorkspaceXcodeWindowInspector) {
140140
observeFocusedWindow()
141141
}
@@ -200,7 +200,7 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
200200
}
201201

202202
@MainActor
203-
private func observeAXNotifications() {
203+
func observeAXNotifications() {
204204
longRunningTasks.forEach { $0.cancel() }
205205
longRunningTasks = []
206206

Tool/Sources/XcodeInspector/XcodeInspector.swift

Lines changed: 142 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@ import Foundation
66
import Logger
77
import Preferences
88
import SuggestionModel
9+
import Toast
10+
11+
public extension Notification.Name {
12+
static let accessibilityAPIMalfunctioning = Notification.Name("accessibilityAPIMalfunctioning")
13+
}
914

1015
public 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

Tool/Sources/XcodeInspector/XcodeWindowInspector.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector {
5454
for await notification in axNotifications {
5555
guard notification.kind == .focusedUIElementChanged else { continue }
5656
guard let self else { return }
57-
Logger.service.debug("Workspace refresh")
5857
try Task.checkCancellation()
5958
await self.updateURLs()
6059
}

0 commit comments

Comments
 (0)