Skip to content

Commit eeff068

Browse files
committed
Add accessibility api malfunction check
1 parent 535cbba commit eeff068

File tree

7 files changed

+146
-97
lines changed

7 files changed

+146
-97
lines changed

Core/Sources/HostApp/DebugView.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ final class DebugSettings: ObservableObject {
2121
var disableFileContentManipulationByCheatsheet
2222
@AppStorage(\.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning)
2323
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning
24+
@AppStorage(\.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer)
25+
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
26+
@AppStorage(\.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted)
27+
var toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted
2428
init() {}
2529
}
2630

@@ -76,9 +80,30 @@ struct DebugSettingsView: View {
7680
Toggle(isOn: $settings.disableFileContentManipulationByCheatsheet) {
7781
Text("Disable file content manipulation by cheatsheet")
7882
}
79-
80-
Toggle(isOn: $settings.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning) {
81-
Text("Re-activate Xcode Inspector when Accessibility API malfunctioning detected")
83+
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+
}
82107
}
83108

84109
Button("Reset migration version to 0") {

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 0 additions & 2 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(

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))

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: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,9 +572,23 @@ public extension UserDefaultPreferenceKeys {
572572

573573
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning: FeatureFlag {
574574
.init(
575-
defaultValue: false,
575+
defaultValue: true,
576576
key: "FeatureFlag-RestartXcodeInspectorIfAccessibilityAPIIsMalfunctioning"
577577
)
578578
}
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+
}
579593
}
580594

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: 98 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -196,99 +196,42 @@ public final class XcodeInspector: ObservableObject {
196196
group.addTask { [weak self] in
197197
while true {
198198
guard let self else { return }
199-
try await Task.sleep(nanoseconds: 10_000_000_000)
200-
Logger.service.debug("""
201-
Check for Accessibility Malfunctioning:
202-
Source Editor: \({
203-
if let editor = self.focusedEditor {
204-
return editor.element.description
205-
}
206-
return "Not Found"
207-
}())
208-
Focused Element: \({
209-
if let element = self.focusedElement {
210-
return "\(element.description), \(element.identifier), \(element.role)"
211-
}
212-
return "Not Found"
213-
}())
214-
215-
Accessibility API Permission: \(
216-
AXIsProcessTrusted() ? "Granted" :
217-
"Not Granted"
218-
)
219-
App: \(
220-
self.activeApplication?.runningApplication
221-
.bundleIdentifier ?? ""
222-
)
223-
Focused Element: \({
224-
guard let element = self.activeApplication?.appElement
225-
.focusedElement
226-
else {
227-
return "Not Found"
228-
}
229-
return "\(element.description), \(element.identifier), \(element.role)"
230-
}())
231-
First Source Editor: \({
232-
guard let element = self.activeApplication?.appElement
233-
.firstChild(where: \.isSourceEditor)
234-
else {
235-
return "Not Found"
236-
}
237-
return "\(element.description), \(element.identifier), \(element.role)"
238-
}())
239-
""")
240-
241-
if let editor = self.focusedEditor, !editor.element.isSourceEditor {
242-
NSWorkspace.shared.notificationCenter.post(
243-
name: .accessibilityAPIMalfunctioning,
244-
object: "Source Editor Element Corrupted"
245-
)
246-
} else if let element = self.activeXcode?.appElement.focusedElement {
247-
if element.description != self.focusedElement?.description ||
248-
element.identifier != self.focusedElement?.role
249-
{
250-
NSWorkspace.shared.notificationCenter.post(
251-
name: .accessibilityAPIMalfunctioning,
252-
object: "Element Inconsistency"
253-
)
254-
}
199+
if UserDefaults.shared.value(
200+
for: \.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
201+
) {
202+
return
255203
}
256-
}
257-
}
258-
259-
group.addTask {
260-
let sequence = DistributedNotificationCenter.default()
261-
.notifications(named: .init("com.apple.accessibility.api"))
262-
for await notification in sequence {
263-
if AXIsProcessTrusted() {
264-
Logger.service.debug("Accessibility API Permission Granted")
265-
} else {
266-
Logger.service.debug("Accessibility API Permission Not Granted")
267-
NSWorkspace.shared.notificationCenter.post(
268-
name: .accessibilityAPIMalfunctioning,
269-
object: "Accessibility API Permission Check"
270-
)
204+
205+
try await Task.sleep(nanoseconds: 10_000_000_000)
206+
await MainActor.run {
207+
self.checkForAccessibilityMalfunction("Timer")
271208
}
272209
}
273210
}
274211
}
275-
276-
277212

278213
group.addTask { [weak self] in // malfunctioning
279214
let sequence = NSWorkspace.shared.notificationCenter
280215
.notifications(named: .accessibilityAPIMalfunctioning)
281216
for await notification in sequence {
282217
guard let self else { return }
283218
let toast = self.toast
284-
toast.toast(
285-
content: "Accessibility API malfunction detected: \(notification.object as? String ?? "")",
286-
type: .warning
287-
)
219+
if UserDefaults.shared
220+
.value(for: \.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted)
221+
{
222+
toast.toast(
223+
content: """
224+
Accessibility API malfunction detected: \
225+
\(notification.object as? String ?? "").
226+
Resetting active Xcode.
227+
""",
228+
type: .warning
229+
)
230+
}
288231
if let activeXcode {
289-
toast.toast(content: "Resetting active Xcode", type: .warning)
290232
await MainActor.run {
291233
self.setActiveXcode(activeXcode)
234+
activeXcode.observeAXNotifications()
292235
}
293236
}
294237
}
@@ -318,12 +261,10 @@ public final class XcodeInspector: ObservableObject {
318261
activeWorkspaceURL = xcode.workspaceURL
319262
focusedWindow = xcode.focusedWindow
320263

321-
let setFocusedElement = { [weak self] in
264+
let setFocusedElement = { @MainActor [weak self] in
322265
guard let self else { return }
323266
focusedElement = xcode.appElement.focusedElement
324-
Logger.service.debug("Update focused element.")
325267
if let editorElement = focusedElement, editorElement.isSourceEditor {
326-
Logger.service.debug("Focused on source editor.")
327268
focusedEditor = .init(
328269
runningApplication: xcode.runningApplication,
329270
element: editorElement
@@ -345,15 +286,35 @@ public final class XcodeInspector: ObservableObject {
345286
setFocusedElement()
346287
let focusedElementChanged = Task { @MainActor in
347288
for await notification in xcode.axNotifications {
348-
guard notification.kind == .focusedUIElementChanged else { continue }
349-
Logger.service.debug("Update focused element")
350-
try Task.checkCancellation()
351-
setFocusedElement()
289+
if notification.kind == .focusedUIElementChanged {
290+
Logger.service.debug("Update focused element")
291+
try Task.checkCancellation()
292+
setFocusedElement()
293+
}
352294
}
353295
}
354296

355297
activeXcodeObservations.insert(focusedElementChanged)
356298

299+
if UserDefaults.shared
300+
.value(for: \.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning)
301+
{
302+
let malfunctionCheck = Task { @MainActor [weak self] in
303+
if #available(macOS 13.0, *) {
304+
let notifications = xcode.axNotifications.filter {
305+
$0.kind == .uiElementDestroyed
306+
}.debounce(for: .milliseconds(500))
307+
for await _ in notifications {
308+
guard let self else { return }
309+
try Task.checkCancellation()
310+
self.checkForAccessibilityMalfunction("Element Destroyed")
311+
}
312+
}
313+
}
314+
315+
activeXcodeObservations.insert(malfunctionCheck)
316+
}
317+
357318
xcode.$completionPanel.receive(on: DispatchQueue.main).sink { [weak self] element in
358319
self?.completionPanel = element
359320
}.store(in: &activeXcodeCancellable)
@@ -374,5 +335,57 @@ public final class XcodeInspector: ObservableObject {
374335
self?.focusedWindow = window
375336
}.store(in: &activeXcodeCancellable)
376337
}
338+
339+
@MainActor
340+
private func checkForAccessibilityMalfunction(_ source: String) {
341+
Logger.service.debug("""
342+
Check for Accessibility Malfunctioning:
343+
Source Editor: \({
344+
if let editor = self.focusedEditor {
345+
return editor.element.description
346+
}
347+
return "Not Found"
348+
}())
349+
Focused Element: \({
350+
if let element = self.focusedElement {
351+
return "\(element.description), \(element.identifier), \(element.role)"
352+
}
353+
return "Not Found"
354+
}())
355+
356+
Accessibility API Permission: \(
357+
AXIsProcessTrusted() ? "Granted" :
358+
"Not Granted"
359+
)
360+
App: \(
361+
activeApplication?.runningApplication
362+
.bundleIdentifier ?? ""
363+
)
364+
Focused Element: \({
365+
guard let element = self.activeApplication?.appElement
366+
.focusedElement
367+
else {
368+
return "Not Found"
369+
}
370+
return "\(element.description), \(element.identifier), \(element.role)"
371+
}())
372+
""")
373+
374+
if let editor = focusedEditor, !editor.element.isSourceEditor {
375+
NSWorkspace.shared.notificationCenter.post(
376+
name: .accessibilityAPIMalfunctioning,
377+
object: "Source Editor Element Corrupted: \(source)"
378+
)
379+
} else if let element = activeXcode?.appElement.focusedElement {
380+
if element.description != focusedElement?.description ||
381+
element.role != focusedElement?.role
382+
{
383+
NSWorkspace.shared.notificationCenter.post(
384+
name: .accessibilityAPIMalfunctioning,
385+
object: "Element Inconsistency: \(source)"
386+
)
387+
}
388+
}
389+
}
377390
}
378391

0 commit comments

Comments
 (0)