patches: fix VoiceOver deadlock — async AX notification posting

NSAccessibilityPostNotification may synchronously invoke VoiceOver
callbacks from a background AX server thread.  Those callbacks call
dispatch_sync(main_queue) to read buffer state.  If the main thread
is still inside the notification-posting method (postAccessibilityUpdates,
windowDidBecomeKey, or postAccessibilityNotificationsForFrame), the
dispatch_sync deadlocks.

Symptom: Emacs hangs on C-x o after M-x list-buffers from Completions
buffer, but only with VoiceOver enabled.

Fix: introduce ns_ax_post_notification() and
ns_ax_post_notification_with_info() wrappers that defer notification
posting via dispatch_async(main_queue).  This lets the current method
return and frees the main queue for VoiceOver's dispatch_sync calls.

All 14 notification-posting sites now use the async wrappers.
This commit is contained in:
2026-02-27 15:41:26 +01:00
parent fa3ee7cc88
commit 111013ddf1

View File

@@ -310,7 +310,7 @@ index 932d209f..ea2de6f2 100644
ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -6849,218 +6891,2387 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
@@ -6849,218 +6891,2414 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
/* ==========================================================================
@@ -939,6 +939,33 @@ index 932d209f..ea2de6f2 100644
+ return @"";
}
+/* Post AX notifications asynchronously to prevent deadlock.
+ NSAccessibilityPostNotification may synchronously invoke VoiceOver
+ callbacks that dispatch_sync back to the main queue. If we are
+ already on the main queue (e.g., inside postAccessibilityUpdates
+ called from ns_update_end), that dispatch_sync deadlocks.
+ Deferring via dispatch_async lets the current method return first,
+ freeing the main queue for VoiceOver's dispatch_sync calls. */
+
+static inline void
+ns_ax_post_notification (id element,
+ NSAccessibilityNotificationName name)
+{
+ dispatch_async (dispatch_get_main_queue (), ^{
+ NSAccessibilityPostNotification (element, name);
+ });
+}
+
+static inline void
+ns_ax_post_notification_with_info (id element,
+ NSAccessibilityNotificationName name,
+ NSDictionary *info)
+{
+ dispatch_async (dispatch_get_main_queue (), ^{
+ NSAccessibilityPostNotificationWithUserInfo (element, name, info);
+ });
+}
+
+/* Scan visible range of window W for interactive spans.
+ Returns NSArray<EmacsAccessibilityInteractiveSpan *>.
@@ -1758,7 +1785,7 @@ index 932d209f..ea2de6f2 100644
+ @(ns_ax_text_state_change_selection_move),
+ @"AXTextChangeElement": self
+ };
+ NSAccessibilityPostNotificationWithUserInfo (
+ ns_ax_post_notification_with_info (
+ self, NSAccessibilitySelectedTextChangedNotification, info);
+}
+
@@ -2135,7 +2162,7 @@ index 932d209f..ea2de6f2 100644
+ @"AXTextChangeValues": @[change],
+ @"AXTextChangeElement": self
+ };
+ NSAccessibilityPostNotificationWithUserInfo (
+ ns_ax_post_notification_with_info (
+ self, NSAccessibilityValueChangedNotification, userInfo);
+ }
+
@@ -2244,7 +2271,7 @@ index 932d209f..ea2de6f2 100644
+ if (!isCharMove)
+ moveInfo[@"AXTextSelectionGranularity"] = @(granularity);
+
+ NSAccessibilityPostNotificationWithUserInfo (
+ ns_ax_post_notification_with_info (
+ self,
+ NSAccessibilitySelectedTextChangedNotification,
+ moveInfo);
@@ -2275,7 +2302,7 @@ index 932d209f..ea2de6f2 100644
+ NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityHigh)
+ };
+ NSAccessibilityPostNotificationWithUserInfo (
+ ns_ax_post_notification_with_info (
+ NSApp,
+ NSAccessibilityAnnouncementRequestedNotification,
+ annInfo);
@@ -2332,7 +2359,7 @@ index 932d209f..ea2de6f2 100644
+ NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityHigh)
+ };
+ NSAccessibilityPostNotificationWithUserInfo (
+ ns_ax_post_notification_with_info (
+ NSApp,
+ NSAccessibilityAnnouncementRequestedNotification,
+ annInfo);
@@ -2495,7 +2522,7 @@ index 932d209f..ea2de6f2 100644
+ NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityHigh)
+ };
+ NSAccessibilityPostNotificationWithUserInfo (
+ ns_ax_post_notification_with_info (
+ NSApp,
+ NSAccessibilityAnnouncementRequestedNotification,
+ annInfo);
@@ -2846,7 +2873,7 @@ index 932d209f..ea2de6f2 100644
int code;
unsigned fnKeysym = 0;
static NSMutableArray *nsEvArray;
@@ -8237,6 +10448,31 @@ - (void)windowDidBecomeKey /* for direct calls */
@@ -8237,6 +10475,31 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop
@@ -2860,25 +2887,25 @@ index 932d209f..ea2de6f2 100644
+ if (focused
+ && [focused isKindOfClass:[EmacsAccessibilityBuffer class]])
+ {
+ NSAccessibilityPostNotification (focused,
+ ns_ax_post_notification (focused,
+ NSAccessibilityFocusedUIElementChangedNotification);
+ NSDictionary *info = @{
+ @"AXTextStateChangeType":
+ @(ns_ax_text_state_change_selection_move),
+ @"AXTextChangeElement": focused
+ };
+ NSAccessibilityPostNotificationWithUserInfo (focused,
+ ns_ax_post_notification_with_info (focused,
+ NSAccessibilitySelectedTextChangedNotification, info);
+ }
+ else if (focused)
+ NSAccessibilityPostNotification (focused,
+ ns_ax_post_notification (focused,
+ NSAccessibilityFocusedUIElementChangedNotification);
+ }
+#endif
}
@@ -9474,6 +11710,332 @@ - (int) fullscreenState
@@ -9474,6 +11737,332 @@ - (int) fullscreenState
return fs_state;
}
@@ -3062,13 +3089,13 @@ index 932d209f..ea2de6f2 100644
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
+ if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]])
+ [(EmacsAccessibilityBuffer *) elem invalidateInteractiveSpans];
+ NSAccessibilityPostNotification (self,
+ ns_ax_post_notification (self,
+ NSAccessibilityLayoutChangedNotification);
+
+ /* Post focus change so VoiceOver picks up the new tree. */
+ id focused = [self accessibilityFocusedUIElement];
+ if (focused && focused != self)
+ NSAccessibilityPostNotification (focused,
+ ns_ax_post_notification (focused,
+ NSAccessibilityFocusedUIElementChangedNotification);
+
+ lastSelectedWindow = emacsframe->selected_window;
@@ -3101,18 +3128,18 @@ index 932d209f..ea2de6f2 100644
+ if (focused && focused != self
+ && [focused isKindOfClass:[EmacsAccessibilityBuffer class]])
+ {
+ NSAccessibilityPostNotification (focused,
+ ns_ax_post_notification (focused,
+ NSAccessibilityFocusedUIElementChangedNotification);
+ NSDictionary *info = @{
+ @"AXTextStateChangeType":
+ @(ns_ax_text_state_change_selection_move),
+ @"AXTextChangeElement": focused
+ };
+ NSAccessibilityPostNotificationWithUserInfo (focused,
+ ns_ax_post_notification_with_info (focused,
+ NSAccessibilitySelectedTextChangedNotification, info);
+ }
+ else if (focused && focused != self)
+ NSAccessibilityPostNotification (focused,
+ ns_ax_post_notification (focused,
+ NSAccessibilityFocusedUIElementChangedNotification);
+ }
+
@@ -3211,7 +3238,7 @@ index 932d209f..ea2de6f2 100644
@end /* EmacsView */
@@ -11303,6 +13865,18 @@ Convert an X font name (XLFD) to an NS font name.
@@ -11303,6 +13892,18 @@ Convert an X font name (XLFD) to an NS font name.
DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic");
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");