From 985e8f4f92c35518fd452e95c127425411e36fd8 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 30 Oct 2024 16:05:32 -0400 Subject: [PATCH] Backport PR #28981: FIX: macos: Use standard NSApp run loop in our input hook --- src/_macosx.m | 110 +++++++++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index fda928536ab5..1f291b52f6ba 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -40,60 +40,84 @@ static bool keyChangeCapsLock = false; /* Keep track of the current mouse up/down state for open/closed cursor hand */ static bool leftMouseGrabbing = false; -/* Keep track of whether stdin has been received */ -static bool stdin_received = false; -static bool stdin_sigint = false; // Global variable to store the original SIGINT handler static PyOS_sighandler_t originalSigintAction = NULL; -// Signal handler for SIGINT, only sets a flag to exit the run loop +// Stop the current app's run loop, sending an event to ensure it actually stops +static void stopWithEvent() { + [NSApp stop: nil]; + // Post an event to trigger the actual stopping. + [NSApp postEvent: [NSEvent otherEventWithType: NSEventTypeApplicationDefined + location: NSZeroPoint + modifierFlags: 0 + timestamp: 0 + windowNumber: 0 + context: nil + subtype: 0 + data1: 0 + data2: 0] + atStart: YES]; +} + +// Signal handler for SIGINT, only argument matching for stopWithEvent static void handleSigint(int signal) { - stdin_sigint = true; + stopWithEvent(); +} + +// Helper function to flush all events. +// This is needed in some instances to ensure e.g. that windows are properly closed. +// It is used in the input hook as well as wrapped in a version callable from Python. +static void flushEvents() { + while (true) { + NSEvent* event = [NSApp nextEventMatchingMask: NSEventMaskAny + untilDate: [NSDate distantPast] + inMode: NSDefaultRunLoopMode + dequeue: YES]; + if (!event) { + break; + } + [NSApp sendEvent:event]; + } } static int wait_for_stdin() { - @autoreleasepool { - stdin_received = false; - stdin_sigint = false; + // Short circuit if no windows are active + // Rely on Python's input handling to manage CPU usage + // This queries the NSApp, rather than using our FigureWindowCount because that is decremented when events still + // need to be processed to properly close the windows. + if (![[NSApp windows] count]) { + flushEvents(); + return 1; + } + @autoreleasepool { // Set up a SIGINT handler to interrupt the event loop if ctrl+c comes in too originalSigintAction = PyOS_setsig(SIGINT, handleSigint); // Create an NSFileHandle for standard input NSFileHandle *stdinHandle = [NSFileHandle fileHandleWithStandardInput]; + // Register for data available notifications on standard input - [[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification - object: stdinHandle - queue: [NSOperationQueue mainQueue] // Use the main queue - usingBlock: ^(NSNotification *notification) { - // Mark that input has been received - stdin_received = true; - } + id notificationID = [[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification + object: stdinHandle + queue: [NSOperationQueue mainQueue] // Use the main queue + usingBlock: ^(NSNotification *notification) {stopWithEvent();} ]; // Wait in the background for anything that happens to stdin [stdinHandle waitForDataInBackgroundAndNotify]; - // continuously run an event loop until the stdin_received flag is set to exit - while (!stdin_received && !stdin_sigint) { - // This loop is similar to the main event loop and flush_events which have - // Py_[BEGIN|END]_ALLOW_THREADS surrounding the loop. - // This should not be necessary here because PyOS_InputHook releases the GIL for us. - while (true) { - NSEvent *event = [NSApp nextEventMatchingMask: NSEventMaskAny - untilDate: [NSDate distantPast] - inMode: NSDefaultRunLoopMode - dequeue: YES]; - if (!event) { break; } - [NSApp sendEvent: event]; - } - } + // Run the application's event loop, which will be interrupted on stdin or SIGINT + [NSApp run]; + // Remove the input handler as an observer - [[NSNotificationCenter defaultCenter] removeObserver: stdinHandle]; + [[NSNotificationCenter defaultCenter] removeObserver: notificationID]; + // Restore the original SIGINT handler upon exiting the function PyOS_setsig(SIGINT, originalSigintAction); + return 1; } } @@ -236,18 +260,7 @@ static void lazy_init(void) { static PyObject* stop(PyObject* self) { - [NSApp stop: nil]; - // Post an event to trigger the actual stopping. - [NSApp postEvent: [NSEvent otherEventWithType: NSEventTypeApplicationDefined - location: NSZeroPoint - modifierFlags: 0 - timestamp: 0 - windowNumber: 0 - context: nil - subtype: 0 - data1: 0 - data2: 0] - atStart: YES]; + stopWithEvent(); Py_RETURN_NONE; } @@ -382,20 +395,9 @@ static CGFloat _get_device_scale(CGContextRef cr) // We run the app, matching any events that are waiting in the queue // to process, breaking out of the loop when no events remain and // displaying the canvas if needed. - NSEvent *event; - Py_BEGIN_ALLOW_THREADS - while (true) { - event = [NSApp nextEventMatchingMask: NSEventMaskAny - untilDate: [NSDate distantPast] - inMode: NSDefaultRunLoopMode - dequeue: YES]; - if (!event) { - break; - } - [NSApp sendEvent:event]; - } + flushEvents(); Py_END_ALLOW_THREADS