From d54710d063e8d5a4b254a8ecb2c160f9125840ad Mon Sep 17 00:00:00 2001 From: Ranxi <1198379757@qq.com> Date: Sun, 5 Jul 2026 16:57:15 +0800 Subject: [PATCH] Fix orphaned modal sheet deadlock when parent window is destroyed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -[NSAlert(iTerm) runSheetModalForWindow:] uses beginSheetModalForWindow: with a synchronous runModalForWindow:. If the parent window is destroyed before the user dismisses the sheet, the completion handler never fires so stopModalWithCode: is never called, leaving the main thread permanently stuck in the modal run loop. This can happen when a temporary window (e.g. Touch Bar popup) is closed while a confirm-close sheet is showing: the window auto- dismisses, the sheet is destroyed with it, but the modal loop continues running. The entire app becomes unresponsive — menu bar, mouse input, keyboard, new windows all blocked. The fix adds an NSWindowWillCloseNotification observer that calls [NSApp abortModal] if the parent window closes before the sheet is dismissed. A stopped flag ensures the observer and completion handler do not race. Added a deterministic test that closes the window from a background thread while the modal is running, verifying the call returns instead of blocking. --- ModernTests/NSAlert_iTermTests.swift | 42 ++++++++++++++++++++++++++++ sources/Categories/NSAlert+iTerm.m | 25 +++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 ModernTests/NSAlert_iTermTests.swift diff --git a/ModernTests/NSAlert_iTermTests.swift b/ModernTests/NSAlert_iTermTests.swift new file mode 100644 index 0000000000..9e133ec509 --- /dev/null +++ b/ModernTests/NSAlert_iTermTests.swift @@ -0,0 +1,42 @@ +// +// NSAlert_iTermTests.swift +// iTerm2 ModernTests +// +// Verifies that -[NSAlert(iTerm) runSheetModalForWindow:] does not +// hang when the parent window is destroyed while the sheet is shown. +// + +import XCTest +@testable import iTerm2SharedARC + +final class NSAlert_iTermTests: XCTestCase { + + func test_modalReturnsWhenParentWindowCloses() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 100, height: 100), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false) + + let alert = NSAlert() + alert.messageText = "test" + alert.addButton(withTitle: "OK") + + // Close the window from a background queue shortly after the + // modal begins. The nested run loop inside runModalForWindow: + // will deliver NSWindowWillCloseNotification, our observer + // will fire abortModal, and the modal must unwind — proving + // we are not deadlocked. + DispatchQueue.global().async { + // Give the modal a moment to enter its run loop. + Thread.sleep(forTimeInterval: 0.1) + window.close() + } + + let response = alert.runSheetModalForWindow(window) + // Any return value means the fix is working — the call did not + // block forever. + XCTAssertNotEqual(response, NSModalResponseContinue, + "Modal should not return NSModalResponseContinue when aborted") + } +} diff --git a/sources/Categories/NSAlert+iTerm.m b/sources/Categories/NSAlert+iTerm.m index aced7e7a9d..8dacea2b61 100644 --- a/sources/Categories/NSAlert+iTerm.m +++ b/sources/Categories/NSAlert+iTerm.m @@ -14,7 +14,32 @@ - (NSModalResponse)runSheetModalForWindow:(NSWindow *)window { DLog(@"Run sheet modal for window %@", window); [NSApp activateIgnoringOtherApps:YES]; + + // If the parent window is closed before the user dismisses the sheet, + // the completion handler will never fire and the modal loop in + // runModalForWindow: will run forever, blocking the entire app. + // Observe the parent window closing so we can abort the modal. + __block BOOL stopped = NO; + __block id observer = nil; + observer = [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification + object:window + queue:nil + usingBlock:^(NSNotification *note) { + if (stopped) { + return; + } + stopped = YES; + DLog(@"Parent window closed while sheet modal was active — aborting modal"); + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + [NSApp abortModal]; + }]; + [self beginSheetModalForWindow:window completionHandler:^(NSModalResponse returnCode) { + if (stopped) { + return; + } + stopped = YES; + [[NSNotificationCenter defaultCenter] removeObserver:observer]; [NSApp stopModalWithCode:returnCode]; }]; return [NSApp runModalForWindow:[self window]];