1 # -*- coding: utf-8 -*-
2 # Copyright 2017 Christoph Reiter
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU Lesser General Public
6 # License as published by the Free Software Foundation; either
7 # version 2.1 of the License, or (at your option) any later version.
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, see <http://www.gnu.org/licenses/>.
17 from __future__
import print_function
24 from contextlib
import closing
, contextmanager
29 def ensure_socket_not_inheritable(sock
):
30 """Ensures that the socket is not inherited by child processes
34 NotImplementedError: With Python <3.4 on Windows
37 if hasattr(sock
, "set_inheritable"):
38 sock
.set_inheritable(False)
43 raise NotImplementedError(
44 "Not implemented for older Python on Windows")
47 flags
= fcntl
.fcntl(fd
, fcntl
.F_GETFD
)
48 fcntl
.fcntl(fd
, fcntl
.F_SETFD
, flags | fcntl
.FD_CLOEXEC
)
51 _wakeup_fd_is_active
= False
52 """Since we can't check if set_wakeup_fd() is already used for nested event
53 loops without introducing a race condition we keep track of it globally.
58 def wakeup_on_signal():
59 """A decorator for functions which create a glib event loop to keep
60 Python signal handlers working while the event loop is idling.
62 In case an OS signal is received will wake the default event loop up
63 shortly so that any registered Python signal handlers registered through
64 signal.signal() can run.
66 Works on Windows but needs Python 3.5+.
68 In case the wrapped function is not called from the main thread it will be
69 called as is and it will not wake up the default loop for signals.
72 global _wakeup_fd_is_active
74 if _wakeup_fd_is_active
:
78 from gi
.repository
import GLib
80 # On Windows only Python 3.5+ supports passing sockets to set_wakeup_fd
81 set_wakeup_fd_supports_socket
= (
82 os
.name
!= "nt" or sys
.version_info
[:2] >= (3, 5))
83 # On Windows only Python 3 has an implementation of socketpair()
84 has_socketpair
= hasattr(socket
, "socketpair")
86 if not has_socketpair
or not set_wakeup_fd_supports_socket
:
90 read_socket
, write_socket
= socket
.socketpair()
91 with
closing(read_socket
), closing(write_socket
):
93 for sock
in [read_socket
, write_socket
]:
94 sock
.setblocking(False)
95 ensure_socket_not_inheritable(sock
)
98 orig_fd
= signal
.set_wakeup_fd(write_socket
.fileno())
100 # Raised in case this is not the main thread -> give up.
104 _wakeup_fd_is_active
= True
106 def signal_notify(source
, condition
):
107 if condition
& GLib
.IO_IN
:
109 return bool(read_socket
.recv(1))
110 except EnvironmentError as e
:
119 channel
= GLib
.IOChannel
.win32_new_socket(
120 read_socket
.fileno())
122 channel
= GLib
.IOChannel
.unix_new(read_socket
.fileno())
124 source_id
= GLib
.io_add_watch(
126 GLib
.PRIORITY_DEFAULT
,
127 (GLib
.IOCondition
.IN | GLib
.IOCondition
.HUP |
128 GLib
.IOCondition
.NVAL | GLib
.IOCondition
.ERR
),
133 GLib
.source_remove(source_id
)
135 write_fd
= signal
.set_wakeup_fd(orig_fd
)
136 if write_fd
!= write_socket
.fileno():
137 # Someone has called set_wakeup_fd while func() was active,
138 # so let's re-revert again.
139 signal
.set_wakeup_fd(write_fd
)
140 _wakeup_fd_is_active
= False
143 PyOS_getsig
= _gi
.pyos_getsig
145 # We save the signal pointer so we can detect if glib has changed the
146 # signal handler behind Python's back (GLib.unix_signal_add)
147 if signal
.getsignal(signal
.SIGINT
) is signal
.default_int_handler
:
148 startup_sigint_ptr
= PyOS_getsig(signal
.SIGINT
)
150 # Something has set the handler before import, we can't get a ptr
151 # for the default handler so make sure the pointer will never match.
152 startup_sigint_ptr
= -1
155 def sigint_handler_is_default():
156 """Returns if on SIGINT the default Python handler would be called"""
158 return (signal
.getsignal(signal
.SIGINT
) is signal
.default_int_handler
and
159 PyOS_getsig(signal
.SIGINT
) == startup_sigint_ptr
)
163 def sigint_handler_set_and_restore_default(handler
):
164 """Context manager for saving/restoring the SIGINT handler default state.
166 Will only restore the default handler again if the handler is not changed
167 while the context is active.
170 assert sigint_handler_is_default()
172 signal
.signal(signal
.SIGINT
, handler
)
173 sig_ptr
= PyOS_getsig(signal
.SIGINT
)
177 if signal
.getsignal(signal
.SIGINT
) is handler
and \
178 PyOS_getsig(signal
.SIGINT
) == sig_ptr
:
179 signal
.signal(signal
.SIGINT
, signal
.default_int_handler
)
182 def is_main_thread():
183 """Returns True in case the function is called from the main thread"""
185 return threading
.current_thread().name
== "MainThread"
189 _sigint_called
= False
193 def register_sigint_fallback(callback
):
194 """Installs a SIGINT signal handler in case the default Python one is
195 active which calls 'callback' in case the signal occurs.
197 Only does something if called from the main thread.
199 In case of nested context managers the signal handler will be only
200 installed once and the callbacks will be called in the reverse order
201 of their registration.
203 The old signal handler will be restored in case no signal handler is
204 registered while the context is active.
207 # To handle multiple levels of event loops we need to call the last
208 # callback first, wait until the inner most event loop returns control
209 # and only then call the next callback, and so on... until we
210 # reach the outer most which manages the signal handler and raises
213 global _callback_stack
, _sigint_called
215 if not is_main_thread():
219 if not sigint_handler_is_default():
221 # This is an inner event loop, append our callback
222 # to the stack so the parent context can call it.
223 _callback_stack
.append(callback
)
227 cb
= _callback_stack
.pop()
231 # There is a signal handler set by the user, just do nothing
235 _sigint_called
= False
237 def sigint_handler(sig_num
, frame
):
238 global _callback_stack
, _sigint_called
242 _sigint_called
= True
243 _callback_stack
.pop()()
245 _callback_stack
.append(callback
)
247 with
sigint_handler_set_and_restore_default(sigint_handler
):
251 signal
.default_int_handler(signal
.SIGINT
, None)
253 _callback_stack
.pop()