1
from __future__ import annotations
15
from collections.abc import Generator, Sequence
16
from email.utils import formatdate
17
from types import FrameType
18
from typing import TYPE_CHECKING, TypeAlias
20
from uvicorn._ansi import style
21
from uvicorn._compat import asyncio_run
22
from uvicorn.config import STARTUP_FAILURE, Config
25
from uvicorn.protocols.http.auto_zttp_impl import AutoZttpProtocol
26
from uvicorn.protocols.http.h11_impl import H11Protocol
27
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
28
from uvicorn.protocols.http.zttp_h2_impl import ZttpH2Protocol
29
from uvicorn.protocols.http.zttp_impl import ZttpProtocol
30
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
31
from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol
32
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
34
Protocols: TypeAlias = (
42
| WebSocketsSansIOProtocol
46
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
47
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
49
if sys.platform == "win32": # pragma: py-not-win32
50
HANDLED_SIGNALS += (signal.SIGBREAK,) # Windows signal 21. Sent by Ctrl+Break.
52
logger = logging.getLogger("uvicorn.error")
57
Shared servers state that is available between all protocol instances.
60
def __init__(self) -> None:
61
self.total_requests = 0
62
self.connections: set[Protocols] = set()
63
self.tasks: set[asyncio.Task[None]] = set()
64
self.default_headers: list[tuple[bytes, bytes]] = []
68
def __init__(self, config: Config) -> None:
70
self.server_state = ServerState()
73
self.should_exit = False
74
self.force_exit = False
75
self.last_notified = 0.0
77
self._captured_signals: list[int] = []
79
@functools.cached_property
80
def limit_max_requests(self) -> int | None:
81
if self.config.limit_max_requests is None:
83
return self.config.limit_max_requests + random.randint(0, self.config.limit_max_requests_jitter)
85
def run(self, sockets: list[socket.socket] | None = None) -> None:
86
return asyncio_run(self.serve(sockets=sockets), loop_factory=self.config.get_loop_factory())
88
async def serve(self, sockets: list[socket.socket] | None = None) -> None:
89
with self.capture_signals():
90
await self._serve(sockets)
92
async def _serve(self, sockets: list[socket.socket] | None = None) -> None:
93
process_id = os.getpid()
99
self.lifespan = config.lifespan_class(config)
101
message = "Started server process [%d]"
102
color_message = "Started server process [" + style("%d", fg="cyan") + "]"
103
logger.info(message, process_id, extra={"color_message": color_message})
105
await self.startup(sockets=sockets)
106
if not self.should_exit:
107
await self.main_loop()
109
await self.shutdown(sockets=sockets)
111
message = "Finished server process [%d]"
112
color_message = "Finished server process [" + style("%d", fg="cyan") + "]"
113
logger.info(message, process_id, extra={"color_message": color_message})
115
async def startup(self, sockets: list[socket.socket] | None = None) -> None:
116
await self.lifespan.startup()
117
if self.lifespan.should_exit:
118
sys.exit(STARTUP_FAILURE)
123
_loop: asyncio.AbstractEventLoop | None = None,
124
) -> asyncio.Protocol:
125
2
2
return config.http_protocol_class( # type: ignore[call-arg]
127
server_state=self.server_state,
128
app_state=self.lifespan.state,
132
loop = asyncio.get_running_loop()
134
listeners: Sequence[socket.SocketType]
135
if sockets is not None: # pragma: full coverage
136
# Explicitly passed a list of open sockets.
137
# We use this when the server is run from a Gunicorn worker.
140
sock: socket.SocketType,
141
) -> socket.SocketType: # pragma py-not-win32
142
# Windows requires the socket be explicitly shared across
143
# multiple workers (processes).
144
from socket import fromshare # type: ignore[attr-defined]
146
sock_data = sock.share(os.getpid()) # type: ignore[attr-defined]
147
return fromshare(sock_data)
149
self.servers: list[asyncio.base_events.Server] = []
151
is_windows = platform.system() == "Windows"
152
if config.workers > 1 and is_windows: # pragma: py-not-win32
153
sock = _share_socket(sock) # type: ignore[assignment]
154
server = await loop.create_server(create_protocol, sock=sock, ssl=config.ssl, backlog=config.backlog)
155
self.servers.append(server)
158
elif config.fd is not None: # pragma: py-win32
159
# Use an existing socket, from a file descriptor.
160
sock = socket.fromfd(config.fd, socket.AF_UNIX, socket.SOCK_STREAM)
161
server = await loop.create_server(create_protocol, sock=sock, ssl=config.ssl, backlog=config.backlog)
162
assert server.sockets is not None # mypy
163
listeners = server.sockets
164
self.servers = [server]
166
elif config.uds is not None: # pragma: py-win32
167
# Create a socket using UNIX domain socket.
169
if os.path.exists(config.uds):
170
uds_perms = os.stat(config.uds).st_mode # pragma: full coverage
171
server = await loop.create_unix_server(
172
create_protocol, path=config.uds, ssl=config.ssl, backlog=config.backlog
174
os.chmod(config.uds, uds_perms)
175
assert server.sockets is not None # mypy
176
listeners = server.sockets
177
self.servers = [server]
180
# Standard case. Create a socket from a host/port pair.
182
server = await loop.create_server(
187
backlog=config.backlog,
189
except OSError as exc:
191
await self.lifespan.shutdown()
192
sys.exit(STARTUP_FAILURE)
194
assert server.sockets is not None
195
listeners = server.sockets
196
self.servers = [server]
199
self._log_started_message(listeners)
201
# We're most likely running multiple workers, so a message has already been
202
# logged by `config.bind_socket()`.
203
pass # pragma: full coverage
207
def _log_started_message(self, listeners: Sequence[socket.SocketType]) -> None:
210
if config.fd is not None: # pragma: py-win32
213
"Uvicorn running on socket %s (Press CTRL+C to quit)",
217
elif config.uds is not None: # pragma: py-win32
218
logger.info("Uvicorn running on unix socket %s (Press CTRL+C to quit)", config.uds)
221
addr_format = "%s://%s:%d"
222
host = "0.0.0.0" if config.host is None else config.host
224
# It's an IPv6 address.
225
addr_format = "%s://[%s]:%d"
229
port = listeners[0].getsockname()[1]
231
protocol_name = "https" if config.ssl else "http"
232
message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)"
233
color_message = "Uvicorn running on " + style(addr_format, bold=True) + " (Press CTRL+C to quit)"
239
extra={"color_message": color_message},
242
async def main_loop(self) -> None:
244
should_exit = await self.on_tick(counter)
245
while not should_exit:
247
counter = counter % 864000
248
await asyncio.sleep(0.1)
249
should_exit = await self.on_tick(counter)
251
async def on_tick(self, counter: int) -> bool:
252
# Update the default headers, once per second.
253
if counter % 10 == 0:
254
current_time = time.time()
255
current_date = formatdate(current_time, usegmt=True).encode()
257
if self.config.date_header:
258
date_header = [(b"date", current_date)]
262
self.server_state.default_headers = date_header + self.config.encoded_headers
264
# Callback to `callback_notify` once every `timeout_notify` seconds.
265
if self.config.callback_notify is not None:
266
if current_time - self.last_notified > self.config.timeout_notify: # pragma: full coverage
267
self.last_notified = current_time
268
await self.config.callback_notify()
270
# Determine if we should exit.
274
max_requests = self.limit_max_requests
275
if max_requests is not None and self.server_state.total_requests >= max_requests:
276
logger.info("Maximum request limit of %d exceeded. Terminating process.", max_requests)
281
async def shutdown(self, sockets: list[socket.socket] | None = None) -> None:
282
logger.info("Shutting down")
284
# Stop accepting new connections.
285
for server in self.servers:
287
for sock in sockets or []:
288
sock.close() # pragma: full coverage
290
# Request shutdown on all existing connections.
291
for connection in list(self.server_state.connections):
292
connection.shutdown()
293
await asyncio.sleep(0.1)
295
# When 3.10 is not supported anymore, use `async with asyncio.timeout(...):`.
297
await asyncio.wait_for(
298
self._wait_tasks_to_complete(),
299
timeout=self.config.timeout_graceful_shutdown,
301
except asyncio.TimeoutError:
303
"Cancel %s running task(s), timeout graceful shutdown exceeded",
304
len(self.server_state.tasks),
306
for t in self.server_state.tasks:
307
t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded")
309
# Send the lifespan shutdown event, and wait for application shutdown.
310
if not self.force_exit:
311
await self.lifespan.shutdown()
313
async def _wait_tasks_to_complete(self) -> None:
314
# Wait for existing connections to finish sending responses.
315
if self.server_state.connections and not self.force_exit:
316
msg = "Waiting for connections to close. (CTRL+C to force quit)"
318
while self.server_state.connections and not self.force_exit:
319
await asyncio.sleep(0.1)
321
# Wait for existing tasks to complete.
322
if self.server_state.tasks and not self.force_exit:
323
msg = "Waiting for background tasks to complete. (CTRL+C to force quit)"
325
while self.server_state.tasks and not self.force_exit:
326
await asyncio.sleep(0.1)
328
for server in self.servers:
329
await server.wait_closed()
331
@contextlib.contextmanager
332
def capture_signals(self) -> Generator[None, None, None]:
333
# Signals can only be listened to from the main thread.
334
if threading.current_thread() is not threading.main_thread():
337
# always use signal.signal, even if loop.add_signal_handler is available
338
# this allows to restore previous signal handlers later on
339
original_handlers = {sig: signal.signal(sig, self.handle_exit) for sig in HANDLED_SIGNALS}
343
for sig, handler in original_handlers.items():
344
signal.signal(sig, handler)
345
# If we did gracefully shut down due to a signal, try to
346
# trigger the expected behaviour now; multiple signals would be
347
# done LIFO, see https://stackoverflow.com/questions/48434964
348
for captured_signal in reversed(self._captured_signals):
349
signal.raise_signal(captured_signal)
351
def handle_exit(self, sig: int, frame: FrameType | None) -> None:
352
self._captured_signals.append(sig)
353
if self.should_exit and sig == signal.SIGINT:
354
self.force_exit = True # pragma: full coverage
356
self.should_exit = True