bug#72496: 31.0.50 macOS: freezes without beach ball

Eli Zaretskii <[email protected]> Mon, 03 Aug 2026 14:29:37 +0300
Newsgroups gmane.emacs.bugs
Message-ID <[email protected]>
> Date: Sun, 2 Aug 2026 15:39:41 +0100
> From: plug.rifling_5y--- via "Bug reports for GNU Emacs,
>  the Swiss army knife of text editors" <[email protected]>
> 
> I think I have found why the app-defined events get lost, and it can be
> reproduced on demand.
> 
> 'ns_send_appdefined' poststhe event with
> 
>     windowNumber: [[NSApp mainWindow] windowNumber]
> 
> While the frame is miniaturized -- or while key/main status is being handed
> over -- [NSApp mainWindow] is nil, so that argument is 0.  AppKit discards
> the event, and [NSApp run] never returns.
> 
> That explains why it is permanent rather than intermittent.  Nothing
> retries: 'send_appdefined' has already been set to NO by the same call, so
> every later EV_TRAILER re-post is suppressed, and 'timed_entry' has just
> been invalidated.  'ns_read_socket_1', unlike 'ns_select_1', never arms a
> 'timed_entry', so no timer can wake the loop either.  A single lost event
> wedges Emacs for good.
> 
> Evidence from a live hung process
> ---------------------------------
> 
> macOS 26.5.1, Emacs 30.2, built from source so lldb could attach.  Emacs
> had been hung for 59 minutes:
> 
>     [NSApp isRunning]         YES      -- so stop: was never called
>     [NSApp mainWindow]        nil
>     [NSApp keyWindow]         nil
>     send_appdefined           NO       -- latched; no re-post possible
>     timed_entry               NULL     -- no timer wake-up either
>     n_emacs_events_pending    99, and climbing
> 
> The stack was:
> 
>     wait_reading_process_output          process.c:5499
>       redisplay_preserve_echo_area (13)  xdisp.c:17757
>         flush_frame
>           ns_flush_display
>             ns_read_socket_1
>               -[EmacsApp run] -> [super run]   <- blocked here
> 
> n_emacs_events_pending climbing is the telling part: AppKit was delivering
> input normally and Emacs was queueing it.  Nothing was blocked or starved.
> The only thing missing was the event that ends [NSApp run].
> 
> A/B test on that same hung process
> ----------------------------------
> 
> 1. Running Emacs' own code inside the hung process --
>    send_appdefined = YES; ns_send_appdefined (-1); -- changed nothing.
>    mainWindow was still nil, so the window number was 0 again.
> 
> 2. Posting a byte-identical NSEventTypeApplicationDefined event that
>    differed only in carrying a real window number unfroze Emacs
>    immediately.
> 
> The window number is the entire difference.
> 
> Reproducer
> ----------
> 
> Miniaturizing makes both mainWindow and keyWindow nil, deterministically.
> Any subprocess status change then goes status_notify ->
> redisplay_preserve_echo_area (13) -> flush_frame -> ns_flush_display ->
> ns_read_socket_1.  So:
> 
>     (run-at-time 3 3 (lambda () (start-process "probe" nil "sleep" "1")))
> 
> then miniaturize the frame and wait.  Emacs is dead, and deminiaturizing
> does not bring it back.
> 
> This also matches the reports of freezes on app activation and on the green
> zoom button's tiling menu: those take main/key status away too.
> 
> Verification of the patch
> -------------------------
> 
> With the patch applied, in the same miniaturized state ([NSApp mainWindow]
> == nil), a breakpoint on -[NSApplication postEvent:atStart:] shows:
> 
>     type = 15 (NSEventTypeApplicationDefined)
>     windowNumber = 3516
>     data1 = -1
> 
> and 80 seconds of the reproducer above, with about 27 subprocess exits,
> produced no freeze.
> 
> Note that keyWindow is nil while miniaturized as well, so it is the scan
> over [NSApp windows] that actually does the work here; a fallback that only
> tried keyWindow would not fix anything.
> 
> The patch is against master (the function is unchanged from Emacs 30.2, and
> it applies to both).
> 
> I have not signed FSF copyright papers.  The change is about 12 lines of
> code plus a comment; I am happy to shorten it, or for someone to rewrite it
> on the strength of the diagnosis alone if that is easier.

Alan and Stéphane, any comments or suggestions?

> From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
> From: Anton Dergunov <[email protected]>
> Subject: [PATCH] Fix a permanent hang on macOS when no window is main
> 
> 'ns_send_appdefined' posted the event that ends [NSApp run] with
> windowNumber: [[NSApp mainWindow] windowNumber].  While the frame is
> miniaturized, or while key/main status is being handed over, [NSApp
> mainWindow] is nil and the window number is 0; AppKit discards such an
> event, so [NSApp run] never returns and Emacs never gets back to its
> command loop.
> 
> The hang is permanent rather than intermittent because nothing retries:
> 'send_appdefined' has already been cleared by this same call, so every
> later EV_TRAILER re-post is suppressed, and 'timed_entry' has just been
> invalidated.  Unlike 'ns_select_1', 'ns_read_socket_1' never arms a
> 'timed_entry', so no timer can wake the loop either.
> 
> Note that [NSApp keyWindow] is nil while miniaturized too, so the scan
> over [NSApp windows] is the fallback that actually applies.
> 
> * src/nsterm.m (ns_send_appdefined): Address the app-defined event to a
> window that exists.  (Bug#72496)
> ---
> --- a/src/nsterm.m
> +++ b/src/nsterm.m
> @@ -4686,6 +4686,7 @@
>    if (send_appdefined)
>      {
>        NSEvent *nxev;
> +      NSWindow *dest;
>  
>        /* We only need one NX_APPDEFINED event to stop NXApp from running.  */
>        send_appdefined = NO;
> @@ -4697,12 +4698,29 @@
>            [timed_entry release];
>            timed_entry = nil;
>          }
> +
> +      /* Address the event to a window that actually exists.  With no main
> +         window -- miniaturized, or mid handover of key/main status -- the
> +         window number would be 0 and AppKit would silently discard the
> +         event.  That is fatal here: send_appdefined has just been cleared
> +         and timed_entry invalidated, so nothing would ever end [NSApp run]
> +         again, and Emacs would hang forever with its UI unresponsive.  */
> +      dest = [NSApp mainWindow];
> +      if (dest == nil)
> +        dest = [NSApp keyWindow];
> +      if (dest == nil)
> +        for (NSWindow *cand in [NSApp windows])
> +          if ([cand windowNumber] > 0)
> +            {
> +              dest = cand;
> +              break;
> +            }
>  
>        nxev = [NSEvent otherEventWithType: NSEventTypeApplicationDefined
>                                  location: NSMakePoint (0, 0)
>                             modifierFlags: 0
>                                 timestamp: 0
> -                            windowNumber: [[NSApp mainWindow] windowNumber]
> +                            windowNumber: [dest windowNumber]
>                                   context: [NSApp context]
>                                   subtype: 0
>                                     data1: value
> -- 
> 2.39.5