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