From 111013ddf189c3c8c0d244af3c139a7e8b8e77f8 Mon Sep 17 00:00:00 2001 From: Daneel Date: Fri, 27 Feb 2026 15:41:26 +0100 Subject: [PATCH] =?UTF-8?q?patches:=20fix=20VoiceOver=20deadlock=20?= =?UTF-8?q?=E2=80=94=20async=20AX=20notification=20posting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...oundsForRange-for-macOS-Zoom-cursor-.patch | 63 +++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch index 54cbf19..f35a647 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -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. @@ -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");