1414import os
1515from pathlib import Path
1616import shlex
17+ import signal
18+ import socket
1719import subprocess
1820import sys
1921import time
@@ -82,6 +84,64 @@ def _get_running_interactive_framework():
8284 return None
8385
8486
87+ @contextlib .contextmanager
88+ def _allow_interrupt (prepare_notifier , handle_sigint ):
89+ """
90+ A context manager that allows terminating a plot by sending a SIGINT. It
91+ is necessary because the running backend prevents Python interpreter to
92+ run and process signals (i.e., to raise KeyboardInterrupt exception). To
93+ solve this one needs to somehow wake up the interpreter and make it close
94+ the plot window. We do this by using the signal.set_wakeup_fd() function
95+ which organizes a write of the signal number into a socketpair. A
96+ backend-specific function, *prepare_notifier*, arranges to listen to the
97+ pair's read socket while the event loop is running. (If it returns a
98+ notifier object, that object is kept alive while the context manager runs.)
99+
100+ If SIGINT was indeed caught, after exiting the on_signal() function the
101+ interpreter reacts to the signal according to the handler function which
102+ had been set up by a signal.signal() call; here, we arrange to call the
103+ backend-specific *handle_sigint* function. Finally, we call the old SIGINT
104+ handler with the same arguments that were given to our custom handler.
105+
106+ We do this only if the old handler for SIGINT was not None, which means
107+ that a non-python handler was installed, i.e. in Julia, and not SIG_IGN
108+ which means we should ignore the interrupts.
109+
110+ Parameters
111+ ----------
112+ prepare_notifier : Callable[[socket.socket], object]
113+ handle_sigint : Callable[[], object]
114+ """
115+
116+ old_sigint_handler = signal .getsignal (signal .SIGINT )
117+ if old_sigint_handler in (None , signal .SIG_IGN , signal .SIG_DFL ):
118+ yield
119+ return
120+
121+ handler_args = None
122+ wsock , rsock = socket .socketpair ()
123+ wsock .setblocking (False )
124+ rsock .setblocking (False )
125+ old_wakeup_fd = signal .set_wakeup_fd (wsock .fileno ())
126+ notifier = prepare_notifier (rsock )
127+
128+ def save_args_and_handle_sigint (* args ):
129+ nonlocal handler_args
130+ handler_args = args
131+ handle_sigint ()
132+
133+ signal .signal (signal .SIGINT , save_args_and_handle_sigint )
134+ try :
135+ yield
136+ finally :
137+ wsock .close ()
138+ rsock .close ()
139+ signal .set_wakeup_fd (old_wakeup_fd )
140+ signal .signal (signal .SIGINT , old_sigint_handler )
141+ if handler_args is not None :
142+ old_sigint_handler (* handler_args )
143+
144+
85145def _exception_printer (exc ):
86146 if _get_running_interactive_framework () in ["headless" , None ]:
87147 raise exc
@@ -242,19 +302,19 @@ def _remove_proxy(self, proxy, *, _is_finalizing=sys.is_finalizing):
242302 if _is_finalizing ():
243303 # Weakrefs can't be properly torn down at that point anymore.
244304 return
245- for signal , proxy_to_cid in list (self ._func_cid_map .items ()):
305+ for sig , proxy_to_cid in list (self ._func_cid_map .items ()):
246306 cid = proxy_to_cid .pop (proxy , None )
247307 if cid is not None :
248- del self .callbacks [signal ][cid ]
308+ del self .callbacks [sig ][cid ]
249309 self ._pickled_cids .discard (cid )
250310 break
251311 else :
252312 # Not found
253313 return
254314 # Clean up empty dicts
255- if len (self .callbacks [signal ]) == 0 :
256- del self .callbacks [signal ]
257- del self ._func_cid_map [signal ]
315+ if len (self .callbacks [sig ]) == 0 :
316+ del self .callbacks [sig ]
317+ del self ._func_cid_map [sig ]
258318
259319 def disconnect (self , cid ):
260320 """
@@ -264,23 +324,23 @@ def disconnect(self, cid):
264324 """
265325 self ._pickled_cids .discard (cid )
266326 # Clean up callbacks
267- for signal , cid_to_proxy in list (self .callbacks .items ()):
327+ for sig , cid_to_proxy in list (self .callbacks .items ()):
268328 proxy = cid_to_proxy .pop (cid , None )
269329 if proxy is not None :
270330 break
271331 else :
272332 # Not found
273333 return
274334
275- proxy_to_cid = self ._func_cid_map [signal ]
335+ proxy_to_cid = self ._func_cid_map [sig ]
276336 for current_proxy , current_cid in list (proxy_to_cid .items ()):
277337 if current_cid == cid :
278338 assert proxy is current_proxy
279339 del proxy_to_cid [current_proxy ]
280340 # Clean up empty dicts
281- if len (self .callbacks [signal ]) == 0 :
282- del self .callbacks [signal ]
283- del self ._func_cid_map [signal ]
341+ if len (self .callbacks [sig ]) == 0 :
342+ del self .callbacks [sig ]
343+ del self ._func_cid_map [sig ]
284344
285345 def process (self , s , * args , ** kwargs ):
286346 """
0 commit comments