1
1
from __future__ import annotations
2
2
3
3
import asyncio
4
+ import contextlib
4
5
import logging
5
6
import os
6
7
import platform
11
12
import time
12
13
from email .utils import formatdate
13
14
from types import FrameType
14
- from typing import TYPE_CHECKING , Sequence , Union
15
+ from typing import TYPE_CHECKING , Generator , Sequence , Union
15
16
16
17
import click
17
18
@@ -57,11 +58,17 @@ def __init__(self, config: Config) -> None:
57
58
self .force_exit = False
58
59
self .last_notified = 0.0
59
60
61
+ self ._captured_signals : list [int ] = []
62
+
60
63
def run (self , sockets : list [socket .socket ] | None = None ) -> None :
61
64
self .config .setup_event_loop ()
62
65
return asyncio .run (self .serve (sockets = sockets ))
63
66
64
67
async def serve (self , sockets : list [socket .socket ] | None = None ) -> None :
68
+ with self .capture_signals ():
69
+ await self ._serve (sockets )
70
+
71
+ async def _serve (self , sockets : list [socket .socket ] | None = None ) -> None :
65
72
process_id = os .getpid ()
66
73
67
74
config = self .config
@@ -70,8 +77,6 @@ async def serve(self, sockets: list[socket.socket] | None = None) -> None:
70
77
71
78
self .lifespan = config .lifespan_class (config )
72
79
73
- self .install_signal_handlers ()
74
-
75
80
message = "Started server process [%d]"
76
81
color_message = "Started server process [" + click .style ("%d" , fg = "cyan" ) + "]"
77
82
logger .info (message , process_id , extra = {"color_message" : color_message })
@@ -302,22 +307,28 @@ async def _wait_tasks_to_complete(self) -> None:
302
307
for server in self .servers :
303
308
await server .wait_closed ()
304
309
305
- def install_signal_handlers (self ) -> None :
310
+ @contextlib .contextmanager
311
+ def capture_signals (self ) -> Generator [None , None , None ]:
312
+ # Signals can only be listened to from the main thread.
306
313
if threading .current_thread () is not threading .main_thread ():
307
- # Signals can only be listened to from the main thread.
314
+ yield
308
315
return
309
-
310
- loop = asyncio . get_event_loop ()
311
-
316
+ # always use signal.signal, even if loop.add_signal_handler is available
317
+ # this allows to restore previous signal handlers later on
318
+ original_handlers = { sig : signal . signal ( sig , self . handle_exit ) for sig in HANDLED_SIGNALS }
312
319
try :
313
- for sig in HANDLED_SIGNALS :
314
- loop .add_signal_handler (sig , self .handle_exit , sig , None )
315
- except NotImplementedError : # pragma: no cover
316
- # Windows
317
- for sig in HANDLED_SIGNALS :
318
- signal .signal (sig , self .handle_exit )
320
+ yield
321
+ finally :
322
+ for sig , handler in original_handlers .items ():
323
+ signal .signal (sig , handler )
324
+ # If we did gracefully shut down due to a signal, try to
325
+ # trigger the expected behaviour now; multiple signals would be
326
+ # done LIFO, see https://stackoverflow.com/questions/48434964
327
+ for captured_signal in reversed (self ._captured_signals ):
328
+ signal .raise_signal (captured_signal )
319
329
320
330
def handle_exit (self , sig : int , frame : FrameType | None ) -> None :
331
+ self ._captured_signals .append (sig )
321
332
if self .should_exit and sig == signal .SIGINT :
322
333
self .force_exit = True
323
334
else :
0 commit comments