@@ -66,8 +66,19 @@ class AsyncSafeSignalManager:
6666 :func:`signal.signal`.
6767 All signals received are forwarded to the previously setup file
6868 descriptor, if any.
69+
70+ ..warning::
71+ Within (potentially nested) contexts, :func:`signal.set_wakeup_fd`
72+ calls are intercepted such that the given file descriptor overrides
73+ the previously setup file descriptor for the outermost manager.
74+ This ensures the manager's chain of signal wakeup file descriptors
75+ is not broken by third-party code or by asyncio itself in some platforms.
6976 """
7077
78+ __current = None # type: AsyncSafeSignalManager
79+
80+ __set_wakeup_fd = signal .set_wakeup_fd # type: Callable[[int], int]
81+
7182 def __init__ (
7283 self ,
7384 loop : asyncio .AbstractEventLoop
@@ -77,6 +88,7 @@ def __init__(
7788
7889 :param loop: event loop that will handle the signals.
7990 """
91+ self .__parent = None # type: AsyncSafeSignalManager
8092 self .__loop = loop # type: asyncio.AbstractEventLoop
8193 self .__background_loop = None # type: Optional[asyncio.AbstractEventLoop]
8294 self .__handlers = {} # type: dict
@@ -86,12 +98,31 @@ def __init__(
8698 self .__rsock .setblocking (False )
8799
88100 def __enter__ (self ):
101+ self .__add_signal_readers ()
102+ try :
103+ self .__install_signal_writers ()
104+ except Exception :
105+ self .__remove_signal_readers ()
106+ raise
107+ self .__chain ()
108+ return self
109+
110+ def __exit__ (self , exc_type , exc_value , exc_traceback ):
111+ try :
112+ try :
113+ self .__uninstall_signal_writers ()
114+ finally :
115+ self .__remove_signal_readers ()
116+ finally :
117+ self .__unchain ()
118+
119+ def __add_signal_readers (self ):
89120 try :
90121 self .__loop .add_reader (self .__rsock .fileno (), self .__handle_signal )
91122 except NotImplementedError :
92123 # Some event loops, like the asyncio.ProactorEventLoop
93124 # on Windows, do not support asynchronous socket reads.
94- # So we emulate it.
125+ # Emulate it.
95126 self .__background_loop = asyncio .SelectorEventLoop ()
96127 self .__background_loop .add_reader (
97128 self .__rsock .fileno (),
@@ -102,29 +133,65 @@ def run_background_loop():
102133 asyncio .set_event_loop (self .__background_loop )
103134 self .__background_loop .run_forever ()
104135
105- self .__background_thread = threading .Thread (target = run_background_loop )
136+ self .__background_thread = threading .Thread (
137+ target = run_background_loop , daemon = True )
106138 self .__background_thread .start ()
107- self .__prev_wakeup_handle = signal .set_wakeup_fd (self .__wsock .fileno ())
108- if self .__prev_wakeup_handle != - 1 and is_winsock_handle (self .__prev_wakeup_handle ):
109- # On Windows, os.write will fail on a WinSock handle. There is no WinSock API
110- # in the standard library either. Thus we wrap it in a socket.socket instance.
111- self .__prev_wakeup_handle = socket .socket (fileno = self .__prev_wakeup_handle )
112- return self
113139
114- def __exit__ (self , type_ , value , traceback ):
115- if isinstance (self .__prev_wakeup_handle , socket .socket ):
116- # Detach (Windows) socket and retrieve the raw OS handle.
117- prev_wakeup_handle = self .__prev_wakeup_handle .fileno ()
118- self .__prev_wakeup_handle .detach ()
119- self .__prev_wakeup_handle = prev_wakeup_handle
120- assert self .__wsock .fileno () == signal .set_wakeup_fd (self .__prev_wakeup_handle )
140+ def __remove_signal_readers (self ):
121141 if self .__background_loop :
122142 self .__background_loop .call_soon_threadsafe (self .__background_loop .stop )
123143 self .__background_thread .join ()
124144 self .__background_loop .close ()
145+ self .__background_loop = None
125146 else :
126147 self .__loop .remove_reader (self .__rsock .fileno ())
127148
149+ def __install_signal_writers (self ):
150+ prev_wakeup_handle = self .__set_wakeup_fd (self .__wsock .fileno ())
151+ try :
152+ self .__chain_wakeup_handle (prev_wakeup_handle )
153+ except Exception :
154+ own_wakeup_handle = self .__set_wakeup_fd (prev_wakeup_handle )
155+ assert self .__wsock .fileno () == own_wakeup_handle
156+ raise
157+
158+ def __uninstall_signal_writers (self ):
159+ prev_wakeup_handle = self .__chain_wakeup_handle (- 1 )
160+ own_wakeup_handle = self .__set_wakeup_fd (prev_wakeup_handle )
161+ assert self .__wsock .fileno () == own_wakeup_handle
162+
163+ def __chain (self ):
164+ self .__parent = AsyncSafeSignalManager .__current
165+ AsyncSafeSignalManager .__current = self
166+ if self .__parent is None :
167+ # Do not trust signal.set_wakeup_fd calls within context.
168+ # Overwrite handle at the start of the managers' chain.
169+ def modified_set_wakeup_fd (signum ):
170+ if threading .current_thread () is not threading .main_thread ():
171+ raise ValueError (
172+ 'set_wakeup_fd only works in main'
173+ ' thread of the main interpreter'
174+ )
175+ return self .__chain_wakeup_handle (signum )
176+ signal .set_wakeup_fd = modified_set_wakeup_fd
177+
178+ def __unchain (self ):
179+ if self .__parent is None :
180+ signal .set_wakeup_fd = self .__set_wakeup_fd
181+ AsyncSafeSignalManager .__current = self .__parent
182+
183+ def __chain_wakeup_handle (self , wakeup_handle ):
184+ prev_wakeup_handle = self .__prev_wakeup_handle
185+ if isinstance (prev_wakeup_handle , socket .socket ):
186+ # Detach (Windows) socket and retrieve the raw OS handle.
187+ prev_wakeup_handle = prev_wakeup_handle .detach ()
188+ if wakeup_handle != - 1 and is_winsock_handle (wakeup_handle ):
189+ # On Windows, os.write will fail on a WinSock handle. There is no WinSock API
190+ # in the standard library either. Thus we wrap it in a socket.socket instance.
191+ wakeup_handle = socket .socket (fileno = wakeup_handle )
192+ self .__prev_wakeup_handle = wakeup_handle
193+ return prev_wakeup_handle
194+
128195 def __handle_signal (self ):
129196 while True :
130197 try :
0 commit comments