bug#72496: 31.0.50 macOS: freezes without beach ball

Eli Zaretskii <[email protected]>
Newsgroups gmane.emacs.bugs
Message-ID <[email protected]>
[Please always use Reply to All to reply, to keep everyone CC'ed.]

> From: [email protected]
> Date: Sat, 15 Aug 2026 20:29:19 +0100
> 
> 
> Thanks for attention to this issue!
> 
> I just also wanted to share a follow up on this from my side:
> 
> I have also recently found a different case for the same symptom, and I have made a separate patch for that issue as well. Please see both of these patches attached.
> After these two fixes I no longer experience this Emacs freezing problem on my side.
> 
> Thanks a lot!
> 
> 
> Recover when the appdefined event that ends [NSApp run] is dropped.
> 
> Companion to emacs-30-ns-appdefined-windownumber.patch, which makes the event
> carry a window number that exists.  That is necessary but not sufficient: a
> freeze reproduced on 2026-08-03 wedged an Emacs built *with* that patch, with
> two visible frames whose window numbers were both valid.
> 
> `ns_read_socket_1' posts one application-defined event and then calls
> [NSApp run], which returns only when that event comes back through
> -[EmacsApp sendEvent:].  AppKit can still fail to deliver it while key/main
> status is being handed over -- app activation, a second frame taking focus, a
> frame being miniaturized.  In the observed hang the last thing logged before
> the stall was a focus change, and afterwards [NSApp mainWindow] and
> [NSApp keyWindow] were both nil and stayed nil: even activating the app and
> calling makeKeyAndOrderFront: left AppKit refusing to give either window key
> status, and -[EmacsApp sendEvent:] was never reached again.
> 
> What makes that fatal is the missing recovery path, not the drop itself.  By
> the time the event is posted `send_appdefined' is already NO, so every later
> EV_TRAILER re-post is suppressed, and `timed_entry' has just been invalidated,
> so no timer can wake the loop.  One dropped event hangs Emacs forever: the UI
> keeps redrawing but never accepts input again, and only Force Quit ends it.
> 
> So keep re-posting until [NSApp run] actually exits.  The timer is scheduled
> only around the ns_read_socket_1 call, which merely drains events that are
> already queued and so returns in microseconds; if it is still running half a
> second later, something has gone wrong and another event costs nothing.
> ns_select's much longer [NSApp run] waits are deliberately left alone, so idle
> Emacs is not woken up any more often than before.
> 
> Diagnosed on macOS 26.5.1 with Emacs 30.2 (emacs-plus@30) by sampling and then
> attaching lldb to the hung process: main thread parked in
> ns_flush_display -> ns_read_socket_1 -> [NSApp run] -> _DPSNextEvent, all other
> threads idle, send_appdefined == NO, timed_entry == NULL, and the event queue
> empty for every mask.
> 
> --- a/src/nsterm.m
> +++ b/src/nsterm.m
> @@ -4804,12 +4804,41 @@
>          }
>        else
>          {
> +          NSTimer *retry;
> +
>            /* Run and wait for events.  We must always send one NX_APPDEFINED event
>               to ourself, otherwise [NXApp run] will never exit.  */
>            send_appdefined = YES;
>            ns_send_appdefined (-1);
> +
> +          /* That one event is the only thing that ends [NSApp run], and AppKit
> +             does drop it: while key/main status is being handed over -- app
> +             activation, a second frame taking focus, a frame being
> +             miniaturized -- an application-defined event can fail to come
> +             back.  By this point `send_appdefined' is NO, so every later
> +             EV_TRAILER re-post is suppressed, and `timed_entry' has just been
> +             invalidated, so no timer can wake the loop either.  A single
> +             dropped event therefore hangs Emacs permanently, with its UI
> +             drawn but dead.
>  
> +             Re-post until the loop really exits.  This branch only drains
> +             events that are already queued, so it normally returns in
> +             microseconds; if it is still running when the timer fires,
> +             something has gone wrong and another event costs nothing.  */
> +          retry = [[NSTimer scheduledTimerWithTimeInterval: 0.5
> +                                                   repeats: YES
> +                                                     block: ^(NSTimer *timer) {
> +                if ([NSApp isRunning])
> +                  {
> +                    send_appdefined = YES;
> +                    ns_send_appdefined (-1);
> +                  }
> +              }] retain];
> +
>            [NSApp run];
> +
> +          [retry invalidate];
> +          [retry release];
>          }
>  
>        nevents = n_emacs_events_pending;
> 
> Fix a permanent macOS UI freeze when no window is main.
> 
> `ns_read_socket_1' enters [NSApp run], which returns only when the
> application-defined event posted by `ns_send_appdefined' comes back through
> -[EmacsApp sendEvent:] and triggers [self stop: self].
> 
> That event is posted with `windowNumber: [[NSApp mainWindow] windowNumber]'.
> While the frame is miniaturized, or while key/main status is being handed
> over (app activation, the window tiling menu, a child frame being closed),
> [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 -- it keeps queueing input it can never process.
> 
> There is no recovery path: `send_appdefined' has already been cleared, so
> every later EV_TRAILER re-post is suppressed, and `timed_entry' has just
> been invalidated, so no timer can wake the loop either.  A single dropped
> event wedges Emacs permanently.
> 
> Diagnosed on macOS 26.5.1 with Emacs 30.2 by attaching lldb to a hung
> process: [NSApp isRunning] == YES (so stop: never ran), [NSApp mainWindow]
> == nil, [NSApp keyWindow] == nil, send_appdefined == NO, timed_entry ==
> NULL, n_emacs_events_pending == 99 and climbing.  Re-running
> ns_send_appdefined inside the hung process changed nothing; posting a
> byte-identical event that differed only in carrying a real window number
> unfroze it instantly.
> 
> Prefer the main window, then the key window, then any window with a valid
> number, so the event is always addressed to a window that exists.
> 
> --- 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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.