diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index 695667dc48c3..1d92ec602d3b 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -1,4 +1,6 @@ import os +import signal +import socket import matplotlib as mpl from matplotlib import _api, cbook @@ -166,7 +168,37 @@ def destroy(self): @classmethod def start_main_loop(cls): - _macosx.show() + # Set up a SIGINT handler to allow terminating a plot via CTRL-C. + # The logic is largely copied from qt_compat._maybe_allow_interrupt; see its + # docstring for details. Parts are implemented by wake_on_fd_write in ObjC. + + old_sigint_handler = signal.getsignal(signal.SIGINT) + if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): + _macosx.show() + return + + handler_args = None + wsock, rsock = socket.socketpair() + wsock.setblocking(False) + rsock.setblocking(False) + old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno()) + _macosx.wake_on_fd_write(rsock.fileno()) + + def handle(*args): + nonlocal handler_args + handler_args = args + _macosx.stop() + + signal.signal(signal.SIGINT, handle) + try: + _macosx.show() + finally: + wsock.close() + rsock.close() + signal.set_wakeup_fd(old_wakeup_fd) + signal.signal(signal.SIGINT, old_sigint_handler) + if handler_args is not None: + old_sigint_handler(*handler_args) def show(self): if not self._shown: diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index bd2aa0d2c968..d587223ab9cf 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -186,48 +186,45 @@ def _maybe_allow_interrupt(qapp): that a non-python handler was installed, i.e. in Julia, and not SIG_IGN which means we should ignore the interrupts. """ + old_sigint_handler = signal.getsignal(signal.SIGINT) - handler_args = None - skip = False if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): - skip = True - else: - wsock, rsock = socket.socketpair() - wsock.setblocking(False) - old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno()) - sn = QtCore.QSocketNotifier( - rsock.fileno(), QtCore.QSocketNotifier.Type.Read - ) + yield + return + + handler_args = None + wsock, rsock = socket.socketpair() + wsock.setblocking(False) + rsock.setblocking(False) + old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno()) + sn = QtCore.QSocketNotifier(rsock.fileno(), QtCore.QSocketNotifier.Type.Read) + + # We do not actually care about this value other than running some Python code to + # ensure that the interpreter has a chance to handle the signal in Python land. We + # also need to drain the socket because it will be written to as part of the wakeup! + # There are some cases where this may fire too soon / more than once on Windows so + # we should be forgiving about reading an empty socket. + # Clear the socket to re-arm the notifier. + @sn.activated.connect + def _may_clear_sock(*args): + try: + rsock.recv(1) + except BlockingIOError: + pass + + def handle(*args): + nonlocal handler_args + handler_args = args + qapp.quit() - # We do not actually care about this value other than running some - # Python code to ensure that the interpreter has a chance to handle the - # signal in Python land. We also need to drain the socket because it - # will be written to as part of the wakeup! There are some cases where - # this may fire too soon / more than once on Windows so we should be - # forgiving about reading an empty socket. - rsock.setblocking(False) - # Clear the socket to re-arm the notifier. - @sn.activated.connect - def _may_clear_sock(*args): - try: - rsock.recv(1) - except BlockingIOError: - pass - - def handle(*args): - nonlocal handler_args - handler_args = args - qapp.quit() - - signal.signal(signal.SIGINT, handle) + signal.signal(signal.SIGINT, handle) try: yield finally: - if not skip: - wsock.close() - rsock.close() - sn.setEnabled(False) - signal.set_wakeup_fd(old_wakeup_fd) - signal.signal(signal.SIGINT, old_sigint_handler) - if handler_args is not None: - old_sigint_handler(*handler_args) + wsock.close() + rsock.close() + sn.setEnabled(False) + signal.set_wakeup_fd(old_wakeup_fd) + signal.signal(signal.SIGINT, old_sigint_handler) + if handler_args is not None: + old_sigint_handler(*handler_args) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 8d7f239e23b1..f4a7ef6755f2 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -1,9 +1,7 @@ import copy import importlib -import inspect import os import signal -import subprocess import sys from datetime import date, datetime @@ -53,201 +51,6 @@ def test_fig_close(): assert init_figs == Gcf.figs -class WaitForStringPopen(subprocess.Popen): - """ - A Popen that passes flags that allow triggering KeyboardInterrupt. - """ - - def __init__(self, *args, **kwargs): - if sys.platform == 'win32': - kwargs['creationflags'] = subprocess.CREATE_NEW_CONSOLE - super().__init__( - *args, **kwargs, - # Force Agg so that each test can switch to its desired Qt backend. - env={**os.environ, "MPLBACKEND": "Agg", "SOURCE_DATE_EPOCH": "0"}, - stdout=subprocess.PIPE, universal_newlines=True) - - def wait_for(self, terminator): - """Read until the terminator is reached.""" - buf = '' - while True: - c = self.stdout.read(1) - if not c: - raise RuntimeError( - f'Subprocess died before emitting expected {terminator!r}') - buf += c - if buf.endswith(terminator): - return - - -def _test_sigint_impl(backend, target_name, kwargs): - import sys - import matplotlib.pyplot as plt - import os - import threading - - plt.switch_backend(backend) - from matplotlib.backends.qt_compat import QtCore # noqa - - def interrupter(): - if sys.platform == 'win32': - import win32api - win32api.GenerateConsoleCtrlEvent(0, 0) - else: - import signal - os.kill(os.getpid(), signal.SIGINT) - - target = getattr(plt, target_name) - timer = threading.Timer(1, interrupter) - fig = plt.figure() - fig.canvas.mpl_connect( - 'draw_event', - lambda *args: print('DRAW', flush=True) - ) - fig.canvas.mpl_connect( - 'draw_event', - lambda *args: timer.start() - ) - try: - target(**kwargs) - except KeyboardInterrupt: - print('SUCCESS', flush=True) - - -@pytest.mark.backend('QtAgg', skip_on_importerror=True) -@pytest.mark.parametrize("target, kwargs", [ - ('show', {'block': True}), - ('pause', {'interval': 10}) -]) -def test_sigint(target, kwargs): - backend = plt.get_backend() - proc = WaitForStringPopen( - [sys.executable, "-c", - inspect.getsource(_test_sigint_impl) + - f"\n_test_sigint_impl({backend!r}, {target!r}, {kwargs!r})"]) - try: - proc.wait_for('DRAW') - stdout, _ = proc.communicate(timeout=_test_timeout) - except Exception: - proc.kill() - stdout, _ = proc.communicate() - raise - print(stdout) - assert 'SUCCESS' in stdout - - -def _test_other_signal_before_sigint_impl(backend, target_name, kwargs): - import signal - import matplotlib.pyplot as plt - plt.switch_backend(backend) - from matplotlib.backends.qt_compat import QtCore # noqa - - target = getattr(plt, target_name) - - fig = plt.figure() - fig.canvas.mpl_connect('draw_event', - lambda *args: print('DRAW', flush=True)) - - timer = fig.canvas.new_timer(interval=1) - timer.single_shot = True - timer.add_callback(print, 'SIGUSR1', flush=True) - - def custom_signal_handler(signum, frame): - timer.start() - signal.signal(signal.SIGUSR1, custom_signal_handler) - - try: - target(**kwargs) - except KeyboardInterrupt: - print('SUCCESS', flush=True) - - -@pytest.mark.skipif(sys.platform == 'win32', - reason='No other signal available to send on Windows') -@pytest.mark.backend('QtAgg', skip_on_importerror=True) -@pytest.mark.parametrize("target, kwargs", [ - ('show', {'block': True}), - ('pause', {'interval': 10}) -]) -def test_other_signal_before_sigint(target, kwargs): - backend = plt.get_backend() - proc = WaitForStringPopen( - [sys.executable, "-c", - inspect.getsource(_test_other_signal_before_sigint_impl) + - "\n_test_other_signal_before_sigint_impl(" - f"{backend!r}, {target!r}, {kwargs!r})"]) - try: - proc.wait_for('DRAW') - os.kill(proc.pid, signal.SIGUSR1) - proc.wait_for('SIGUSR1') - os.kill(proc.pid, signal.SIGINT) - stdout, _ = proc.communicate(timeout=_test_timeout) - except Exception: - proc.kill() - stdout, _ = proc.communicate() - raise - print(stdout) - assert 'SUCCESS' in stdout - plt.figure() - - -@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) -def test_fig_sigint_override(qt_core): - from matplotlib.backends.backend_qt5 import _BackendQT5 - # Create a figure - plt.figure() - - # Variable to access the handler from the inside of the event loop - event_loop_handler = None - - # Callback to fire during event loop: save SIGINT handler, then exit - def fire_signal_and_quit(): - # Save event loop signal - nonlocal event_loop_handler - event_loop_handler = signal.getsignal(signal.SIGINT) - - # Request event loop exit - qt_core.QCoreApplication.exit() - - # Timer to exit event loop - qt_core.QTimer.singleShot(0, fire_signal_and_quit) - - # Save original SIGINT handler - original_handler = signal.getsignal(signal.SIGINT) - - # Use our own SIGINT handler to be 100% sure this is working - def custom_handler(signum, frame): - pass - - signal.signal(signal.SIGINT, custom_handler) - - try: - # mainloop() sets SIGINT, starts Qt event loop (which triggers timer - # and exits) and then mainloop() resets SIGINT - matplotlib.backends.backend_qt._BackendQT.mainloop() - - # Assert: signal handler during loop execution is changed - # (can't test equality with func) - assert event_loop_handler != custom_handler - - # Assert: current signal handler is the same as the one we set before - assert signal.getsignal(signal.SIGINT) == custom_handler - - # Repeat again to test that SIG_DFL and SIG_IGN will not be overridden - for custom_handler in (signal.SIG_DFL, signal.SIG_IGN): - qt_core.QTimer.singleShot(0, fire_signal_and_quit) - signal.signal(signal.SIGINT, custom_handler) - - _BackendQT5.mainloop() - - assert event_loop_handler == custom_handler - assert signal.getsignal(signal.SIGINT) == custom_handler - - finally: - # Reset SIGINT handler to what it was before the test - signal.signal(signal.SIGINT, original_handler) - - @pytest.mark.parametrize( "qt_key, qt_mods, answer", [ @@ -515,3 +318,60 @@ def _get_testable_qt_backends(): reason=f"Skipping {env} because {reason}")) envs.append(pytest.param(env, marks=marks, id=str(env))) return envs + + +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_fig_sigint_override(qt_core): + from matplotlib.backends.backend_qt5 import _BackendQT5 + # Create a figure + plt.figure() + + # Variable to access the handler from the inside of the event loop + event_loop_handler = None + + # Callback to fire during event loop: save SIGINT handler, then exit + def fire_signal_and_quit(): + # Save event loop signal + nonlocal event_loop_handler + event_loop_handler = signal.getsignal(signal.SIGINT) + + # Request event loop exit + qt_core.QCoreApplication.exit() + + # Timer to exit event loop + qt_core.QTimer.singleShot(0, fire_signal_and_quit) + + # Save original SIGINT handler + original_handler = signal.getsignal(signal.SIGINT) + + # Use our own SIGINT handler to be 100% sure this is working + def custom_handler(signum, frame): + pass + + signal.signal(signal.SIGINT, custom_handler) + + try: + # mainloop() sets SIGINT, starts Qt event loop (which triggers timer + # and exits) and then mainloop() resets SIGINT + matplotlib.backends.backend_qt._BackendQT.mainloop() + + # Assert: signal handler during loop execution is changed + # (can't test equality with func) + assert event_loop_handler != custom_handler + + # Assert: current signal handler is the same as the one we set before + assert signal.getsignal(signal.SIGINT) == custom_handler + + # Repeat again to test that SIG_DFL and SIG_IGN will not be overridden + for custom_handler in (signal.SIG_DFL, signal.SIG_IGN): + qt_core.QTimer.singleShot(0, fire_signal_and_quit) + signal.signal(signal.SIGINT, custom_handler) + + _BackendQT5.mainloop() + + assert event_loop_handler == custom_handler + assert signal.getsignal(signal.SIGINT) == custom_handler + + finally: + # Reset SIGINT handler to what it was before the test + signal.signal(signal.SIGINT, original_handler) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 4d4ab87b739e..f1338cfffc4e 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -21,6 +21,33 @@ from matplotlib.testing import subprocess_run_helper as _run_helper +class _WaitForStringPopen(subprocess.Popen): + """ + A Popen that passes flags that allow triggering KeyboardInterrupt. + """ + + def __init__(self, *args, **kwargs): + if sys.platform == 'win32': + kwargs['creationflags'] = subprocess.CREATE_NEW_CONSOLE + super().__init__( + *args, **kwargs, + # Force Agg so that each test can switch to its desired backend. + env={**os.environ, "MPLBACKEND": "Agg", "SOURCE_DATE_EPOCH": "0"}, + stdout=subprocess.PIPE, universal_newlines=True) + + def wait_for(self, terminator): + """Read until the terminator is reached.""" + buf = '' + while True: + c = self.stdout.read(1) + if not c: + raise RuntimeError( + f'Subprocess died before emitting expected {terminator!r}') + buf += c + if buf.endswith(terminator): + return + + # Minimal smoke-testing of the backends for which the dependencies are # PyPI-installable on CI. They are not available for all tested Python # versions so we don't fail on missing backends. @@ -698,3 +725,116 @@ def test_interactive_timers(env): pytest.skip("wx backend is deprecated; tests failed on appveyor") _run_helper(_impl_test_interactive_timers, timeout=_test_timeout, extra_env=env) + + +def _test_sigint_impl(backend, target_name, kwargs): + import sys + import matplotlib.pyplot as plt + import os + import threading + + plt.switch_backend(backend) + + def interrupter(): + if sys.platform == 'win32': + import win32api + win32api.GenerateConsoleCtrlEvent(0, 0) + else: + import signal + os.kill(os.getpid(), signal.SIGINT) + + target = getattr(plt, target_name) + timer = threading.Timer(1, interrupter) + fig = plt.figure() + fig.canvas.mpl_connect( + 'draw_event', + lambda *args: print('DRAW', flush=True) + ) + fig.canvas.mpl_connect( + 'draw_event', + lambda *args: timer.start() + ) + try: + target(**kwargs) + except KeyboardInterrupt: + print('SUCCESS', flush=True) + + +@pytest.mark.parametrize("env", _get_testable_interactive_backends()) +@pytest.mark.parametrize("target, kwargs", [ + ('show', {'block': True}), + ('pause', {'interval': 10}) +]) +def test_sigint(env, target, kwargs): + backend = env.get("MPLBACKEND") + if not backend.startswith(("qt", "macosx")): + pytest.skip("SIGINT currently only tested on qt and macosx") + proc = _WaitForStringPopen( + [sys.executable, "-c", + inspect.getsource(_test_sigint_impl) + + f"\n_test_sigint_impl({backend!r}, {target!r}, {kwargs!r})"]) + try: + proc.wait_for('DRAW') + stdout, _ = proc.communicate(timeout=_test_timeout) + except Exception: + proc.kill() + stdout, _ = proc.communicate() + raise + assert 'SUCCESS' in stdout + + +def _test_other_signal_before_sigint_impl(backend, target_name, kwargs): + import signal + import matplotlib.pyplot as plt + + plt.switch_backend(backend) + + target = getattr(plt, target_name) + + fig = plt.figure() + fig.canvas.mpl_connect('draw_event', lambda *args: print('DRAW', flush=True)) + + timer = fig.canvas.new_timer(interval=1) + timer.single_shot = True + timer.add_callback(print, 'SIGUSR1', flush=True) + + def custom_signal_handler(signum, frame): + timer.start() + signal.signal(signal.SIGUSR1, custom_signal_handler) + + try: + target(**kwargs) + except KeyboardInterrupt: + print('SUCCESS', flush=True) + + +@pytest.mark.skipif(sys.platform == 'win32', + reason='No other signal available to send on Windows') +@pytest.mark.parametrize("env", _get_testable_interactive_backends()) +@pytest.mark.parametrize("target, kwargs", [ + ('show', {'block': True}), + ('pause', {'interval': 10}) +]) +def test_other_signal_before_sigint(env, target, kwargs): + backend = env.get("MPLBACKEND") + if not backend.startswith(("qt", "macosx")): + pytest.skip("SIGINT currently only tested on qt and macosx") + if backend == "macosx" and target == "show": + pytest.xfail("test currently failing for macosx + show()") + proc = _WaitForStringPopen( + [sys.executable, "-c", + inspect.getsource(_test_other_signal_before_sigint_impl) + + "\n_test_other_signal_before_sigint_impl(" + f"{backend!r}, {target!r}, {kwargs!r})"]) + try: + proc.wait_for('DRAW') + os.kill(proc.pid, signal.SIGUSR1) + proc.wait_for('SIGUSR1') + os.kill(proc.pid, signal.SIGINT) + stdout, _ = proc.communicate(timeout=_test_timeout) + except Exception: + proc.kill() + stdout, _ = proc.communicate() + raise + print(stdout) + assert 'SUCCESS' in stdout diff --git a/src/_macosx.m b/src/_macosx.m index 76ef8b4144b4..8d92b75f6bfe 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -1,15 +1,9 @@ #define PY_SSIZE_T_CLEAN #include #include -#include #include #include "mplutils.h" -#ifndef PYPY -/* Remove this once Python is fixed: https://bugs.python.org/issue23237 */ -#define PYOSINPUTHOOK_REPETITIVE 1 -#endif - /* Proper way to check for the OS X version we are compiling for, from * https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/cross_development/Using/using.html @@ -48,147 +42,8 @@ /* Keep track of the current mouse up/down state for open/closed cursor hand */ static bool leftMouseGrabbing = false; -/* -------------------------- Helper function ---------------------------- */ - -static void -_stdin_callback(CFReadStreamRef stream, CFStreamEventType eventType, void* info) -{ - CFRunLoopRef runloop = info; - CFRunLoopStop(runloop); -} - -static int sigint_fd = -1; - -static void _sigint_handler(int sig) -{ - const char c = 'i'; - write(sigint_fd, &c, 1); -} - -static void _sigint_callback(CFSocketRef s, - CFSocketCallBackType type, - CFDataRef address, - const void * data, - void *info) -{ - char c; - int* interrupted = info; - CFSocketNativeHandle handle = CFSocketGetNative(s); - CFRunLoopRef runloop = CFRunLoopGetCurrent(); - read(handle, &c, 1); - *interrupted = 1; - CFRunLoopStop(runloop); -} - -static CGEventRef _eventtap_callback( - CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) -{ - CFRunLoopRef runloop = refcon; - CFRunLoopStop(runloop); - return event; -} - -static int wait_for_stdin(void) -{ - int interrupted = 0; - const UInt8 buffer[] = "/dev/fd/0"; - const CFIndex n = (CFIndex)strlen((char*)buffer); - CFRunLoopRef runloop = CFRunLoopGetCurrent(); - CFURLRef url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, - buffer, - n, - false); - CFReadStreamRef stream = CFReadStreamCreateWithFile(kCFAllocatorDefault, - url); - CFRelease(url); - - CFReadStreamOpen(stream); -#ifdef PYOSINPUTHOOK_REPETITIVE - if (!CFReadStreamHasBytesAvailable(stream)) - /* This is possible because of how PyOS_InputHook is called from Python */ -#endif - { - int error; - int channel[2]; - CFSocketRef sigint_socket = NULL; - PyOS_sighandler_t py_sigint_handler = NULL; - CFStreamClientContext clientContext = {0, NULL, NULL, NULL, NULL}; - clientContext.info = runloop; - CFReadStreamSetClient(stream, - kCFStreamEventHasBytesAvailable, - _stdin_callback, - &clientContext); - CFReadStreamScheduleWithRunLoop(stream, runloop, kCFRunLoopDefaultMode); - error = socketpair(AF_UNIX, SOCK_STREAM, 0, channel); - if (!error) { - CFSocketContext context; - context.version = 0; - context.info = &interrupted; - context.retain = NULL; - context.release = NULL; - context.copyDescription = NULL; - fcntl(channel[0], F_SETFL, O_WRONLY | O_NONBLOCK); - sigint_socket = CFSocketCreateWithNative( - kCFAllocatorDefault, - channel[1], - kCFSocketReadCallBack, - _sigint_callback, - &context); - if (sigint_socket) { - CFRunLoopSourceRef source = CFSocketCreateRunLoopSource( - kCFAllocatorDefault, sigint_socket, 0); - CFRelease(sigint_socket); - if (source) { - CFRunLoopAddSource(runloop, source, kCFRunLoopDefaultMode); - CFRelease(source); - sigint_fd = channel[0]; - py_sigint_handler = PyOS_setsig(SIGINT, _sigint_handler); - } - } - } - - NSEvent* event; - while (true) { - while (true) { - event = [NSApp nextEventMatchingMask: NSEventMaskAny - untilDate: [NSDate distantPast] - inMode: NSDefaultRunLoopMode - dequeue: YES]; - if (!event) { break; } - [NSApp sendEvent: event]; - } - CFRunLoopRun(); - if (interrupted || CFReadStreamHasBytesAvailable(stream)) { break; } - } - - if (py_sigint_handler) { PyOS_setsig(SIGINT, py_sigint_handler); } - CFReadStreamUnscheduleFromRunLoop( - stream, runloop, kCFRunLoopCommonModes); - if (sigint_socket) { CFSocketInvalidate(sigint_socket); } - if (!error) { - close(channel[0]); - close(channel[1]); - } - } - CFReadStreamClose(stream); - CFRelease(stream); - if (interrupted) { - errno = EINTR; - raise(SIGINT); - return -1; - } - return 1; -} - /* ---------------------------- Cocoa classes ---------------------------- */ -@interface WindowServerConnectionManager : NSObject -{ -} -+ (WindowServerConnectionManager*)sharedManager; -- (void)launch:(NSNotification*)notification; -@end - @interface Window : NSWindow { PyObject* manager; } @@ -284,15 +139,6 @@ static void lazy_init(void) { NSApp = [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; - PyOS_InputHook = wait_for_stdin; - - WindowServerConnectionManager* connectionManager = [WindowServerConnectionManager sharedManager]; - NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; - NSNotificationCenter* notificationCenter = [workspace notificationCenter]; - [notificationCenter addObserver: connectionManager - selector: @selector(launch:) - name: NSWorkspaceDidLaunchApplicationNotification - object: nil]; } static PyObject* @@ -305,6 +151,43 @@ static void lazy_init(void) { } } +static PyObject* +wake_on_fd_write(PyObject* unused, PyObject* args) +{ + int fd; + if (!PyArg_ParseTuple(args, "i", &fd)) { return NULL; } + NSFileHandle* fh = [[NSFileHandle alloc] initWithFileDescriptor: fd]; + [fh waitForDataInBackgroundAndNotify]; + [[NSNotificationCenter defaultCenter] + addObserverForName: NSFileHandleDataAvailableNotification + object: fh + queue: nil + usingBlock: ^(NSNotification* note) { + PyGILState_STATE gstate = PyGILState_Ensure(); + PyErr_CheckSignals(); + PyGILState_Release(gstate); + }]; + Py_RETURN_NONE; +} + +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]; + Py_RETURN_NONE; +} + static CGFloat _get_device_scale(CGContextRef cr) { CGSize pixelSize = CGContextConvertSizeToDeviceSpace(cr, CGSizeMake(1, 1)); @@ -514,40 +397,6 @@ int mpl_check_modifier( return NULL; } - int error; - int interrupted = 0; - int channel[2]; - CFSocketRef sigint_socket = NULL; - PyOS_sighandler_t py_sigint_handler = NULL; - - CFRunLoopRef runloop = CFRunLoopGetCurrent(); - - error = pipe(channel); - if (!error) { - CFSocketContext context = {0, NULL, NULL, NULL, NULL}; - fcntl(channel[1], F_SETFL, O_WRONLY | O_NONBLOCK); - - context.info = &interrupted; - sigint_socket = CFSocketCreateWithNative(kCFAllocatorDefault, - channel[0], - kCFSocketReadCallBack, - _sigint_callback, - &context); - if (sigint_socket) { - CFRunLoopSourceRef source = CFSocketCreateRunLoopSource( - kCFAllocatorDefault, sigint_socket, 0); - CFRelease(sigint_socket); - if (source) { - CFRunLoopAddSource(runloop, source, kCFRunLoopDefaultMode); - CFRelease(source); - sigint_fd = channel[1]; - py_sigint_handler = PyOS_setsig(SIGINT, _sigint_handler); - } - } - else - close(channel[0]); - } - NSDate* date = (timeout > 0.0) ? [NSDate dateWithTimeIntervalSinceNow: timeout] : [NSDate distantFuture]; @@ -560,11 +409,6 @@ int mpl_check_modifier( [NSApp sendEvent: event]; } - if (py_sigint_handler) { PyOS_setsig(SIGINT, py_sigint_handler); } - if (sigint_socket) { CFSocketInvalidate(sigint_socket); } - if (!error) { close(channel[1]); } - if (interrupted) { raise(SIGINT); } - Py_RETURN_NONE; } @@ -1161,76 +1005,6 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } Py_RETURN_NONE; } -@implementation WindowServerConnectionManager -static WindowServerConnectionManager *sharedWindowServerConnectionManager = nil; - -+ (WindowServerConnectionManager *)sharedManager -{ - if (sharedWindowServerConnectionManager == nil) { - sharedWindowServerConnectionManager = [[super allocWithZone:NULL] init]; - } - return sharedWindowServerConnectionManager; -} - -+ (id)allocWithZone:(NSZone *)zone -{ - return [[self sharedManager] retain]; -} - -+ (id)copyWithZone:(NSZone *)zone -{ - return self; -} - -+ (id)retain -{ - return self; -} - -- (NSUInteger)retainCount -{ - return NSUIntegerMax; //denotes an object that cannot be released -} - -- (oneway void)release -{ - // Don't release a singleton object -} - -- (id)autorelease -{ - return self; -} - -- (void)launch:(NSNotification*)notification -{ - CFRunLoopRef runloop; - CFMachPortRef port; - CFRunLoopSourceRef source; - NSDictionary* dictionary = [notification userInfo]; - if (![[dictionary valueForKey:@"NSApplicationName"] - localizedCaseInsensitiveContainsString:@"python"]) - return; - NSNumber* psnLow = [dictionary valueForKey: @"NSApplicationProcessSerialNumberLow"]; - NSNumber* psnHigh = [dictionary valueForKey: @"NSApplicationProcessSerialNumberHigh"]; - ProcessSerialNumber psn; - psn.highLongOfPSN = [psnHigh intValue]; - psn.lowLongOfPSN = [psnLow intValue]; - runloop = CFRunLoopGetCurrent(); - port = CGEventTapCreateForPSN(&psn, - kCGHeadInsertEventTap, - kCGEventTapOptionListenOnly, - kCGEventMaskForAllEvents, - &_eventtap_callback, - runloop); - source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, - port, - 0); - CFRunLoopAddSource(runloop, source, kCFRunLoopDefaultMode); - CFRelease(port); -} -@end - @implementation Window - (Window*)initWithContentRect:(NSRect)rect styleMask:(unsigned int)mask backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation withManager: (PyObject*)theManager { @@ -1953,6 +1727,15 @@ - (void)flagsChanged:(NSEvent *)event (PyCFunction)event_loop_is_running, METH_NOARGS, "Return whether the OSX backend has set up the NSApp main event loop."}, + {"wake_on_fd_write", + (PyCFunction)wake_on_fd_write, + METH_VARARGS, + "Arrange for Python to invoke its signal handlers when (any) data is\n" + "written on the file descriptor given as argument."}, + {"stop", + (PyCFunction)stop, + METH_NOARGS, + "Stop the NSApp."}, {"show", (PyCFunction)show, METH_NOARGS, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy