From 059c8eee93ee7192f5e15df11251ef220f4c1b13 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 24 May 2023 16:41:21 +0200 Subject: [PATCH 1/2] Fix support for Ctrl-C on the macosx backend. Support is largely copy-pasted from, and tests are shared with, the qt implementation (qt_compat._maybe_allow_interrupt), the main difference being that what we need from QSocketNotifier, as well as the equivalent for QApplication.quit(), are reimplemented in ObjC. qt_compat._maybe_allow_interrupt is also slightly cleaned up by moving out the "do-nothing" case (`old_sigint_handler in (None, SIG_IGN, SIG_DFL)`) and dedenting the rest, instead of keeping track of whether signals were actually manipulated via a `skip` variable. Factoring out the common parts of _maybe_allow_interrupt is left as a follow-up. (Test e.g. with `MPLBACKEND=macosx python -c "from pylab import *; plot(); show()"` followed by Ctrl-C.) --- lib/matplotlib/backends/backend_macosx.py | 34 ++- lib/matplotlib/backends/qt_compat.py | 75 +++--- lib/matplotlib/tests/test_backend_qt.py | 254 ++++-------------- .../tests/test_backends_interactive.py | 140 ++++++++++ src/_macosx.m | 46 ++++ 5 files changed, 312 insertions(+), 237 deletions(-) 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..ca73d82837ab 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -305,6 +305,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)); @@ -1953,6 +1990,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, From ec6717aa7ad66868a3dc6186af031b970bb21c8e Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Wed, 24 May 2023 21:07:09 -0600 Subject: [PATCH 2/2] macosx: Cleaning up old sigint handling from objc --- src/_macosx.m | 263 -------------------------------------------------- 1 file changed, 263 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index ca73d82837ab..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* @@ -551,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]; @@ -597,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; } @@ -1198,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 {