uvicorn 0.28.1__py3-none-any.whl → 0.30.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uvicorn/__init__.py +1 -1
- uvicorn/_subprocess.py +8 -2
- uvicorn/_types.py +3 -4
- uvicorn/config.py +5 -4
- uvicorn/main.py +16 -13
- uvicorn/middleware/proxy_headers.py +1 -0
- uvicorn/protocols/http/flow_control.py +14 -24
- uvicorn/protocols/http/h11_impl.py +2 -13
- uvicorn/protocols/http/httptools_impl.py +16 -31
- uvicorn/protocols/utils.py +1 -2
- uvicorn/protocols/websockets/websockets_impl.py +17 -16
- uvicorn/protocols/websockets/wsproto_impl.py +8 -9
- uvicorn/server.py +25 -14
- uvicorn/supervisors/multiprocess.py +184 -31
- uvicorn/workers.py +7 -0
- {uvicorn-0.28.1.dist-info → uvicorn-0.30.0.dist-info}/METADATA +1 -1
- {uvicorn-0.28.1.dist-info → uvicorn-0.30.0.dist-info}/RECORD +20 -20
- {uvicorn-0.28.1.dist-info → uvicorn-0.30.0.dist-info}/WHEEL +1 -1
- {uvicorn-0.28.1.dist-info → uvicorn-0.30.0.dist-info}/entry_points.txt +0 -0
- {uvicorn-0.28.1.dist-info → uvicorn-0.30.0.dist-info}/licenses/LICENSE.md +0 -0
uvicorn/__init__.py
CHANGED
uvicorn/_subprocess.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
Some light wrappers around Python's multiprocessing, to deal with cleanly
|
3
3
|
starting child processes.
|
4
4
|
"""
|
5
|
+
|
5
6
|
from __future__ import annotations
|
6
7
|
|
7
8
|
import multiprocessing
|
@@ -74,5 +75,10 @@ def subprocess_started(
|
|
74
75
|
# Logging needs to be setup again for each child.
|
75
76
|
config.configure_logging()
|
76
77
|
|
77
|
-
|
78
|
-
|
78
|
+
try:
|
79
|
+
# Now we can call into `Server.run(sockets=sockets)`
|
80
|
+
target(sockets=sockets)
|
81
|
+
except KeyboardInterrupt: # pragma: no cover
|
82
|
+
# supress the exception to avoid a traceback from subprocess.Popen
|
83
|
+
# the parent already expects us to end, so no vital information is lost
|
84
|
+
pass
|
uvicorn/_types.py
CHANGED
@@ -27,6 +27,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
27
27
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
28
28
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
29
|
"""
|
30
|
+
|
30
31
|
from __future__ import annotations
|
31
32
|
|
32
33
|
import sys
|
@@ -274,11 +275,9 @@ ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]]
|
|
274
275
|
|
275
276
|
|
276
277
|
class ASGI2Protocol(Protocol):
|
277
|
-
def __init__(self, scope: Scope) -> None:
|
278
|
-
... # pragma: no cover
|
278
|
+
def __init__(self, scope: Scope) -> None: ... # pragma: no cover
|
279
279
|
|
280
|
-
async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
281
|
-
... # pragma: no cover
|
280
|
+
async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... # pragma: no cover
|
282
281
|
|
283
282
|
|
284
283
|
ASGI2Application = Type[ASGI2Protocol]
|
uvicorn/config.py
CHANGED
@@ -9,8 +9,9 @@ import os
|
|
9
9
|
import socket
|
10
10
|
import ssl
|
11
11
|
import sys
|
12
|
+
from configparser import RawConfigParser
|
12
13
|
from pathlib import Path
|
13
|
-
from typing import Any, Awaitable, Callable, Literal
|
14
|
+
from typing import IO, Any, Awaitable, Callable, Literal
|
14
15
|
|
15
16
|
import click
|
16
17
|
|
@@ -189,7 +190,7 @@ class Config:
|
|
189
190
|
ws_per_message_deflate: bool = True,
|
190
191
|
lifespan: LifespanType = "auto",
|
191
192
|
env_file: str | os.PathLike[str] | None = None,
|
192
|
-
log_config: dict[str, Any] | str | None = LOGGING_CONFIG,
|
193
|
+
log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
|
193
194
|
log_level: str | int | None = None,
|
194
195
|
access_log: bool = True,
|
195
196
|
use_colors: bool | None = None,
|
@@ -362,11 +363,11 @@ class Config:
|
|
362
363
|
self.log_config["formatters"]["default"]["use_colors"] = self.use_colors
|
363
364
|
self.log_config["formatters"]["access"]["use_colors"] = self.use_colors
|
364
365
|
logging.config.dictConfig(self.log_config)
|
365
|
-
elif self.log_config.endswith(".json"):
|
366
|
+
elif isinstance(self.log_config, str) and self.log_config.endswith(".json"):
|
366
367
|
with open(self.log_config) as file:
|
367
368
|
loaded_config = json.load(file)
|
368
369
|
logging.config.dictConfig(loaded_config)
|
369
|
-
elif self.log_config.endswith((".yaml", ".yml")):
|
370
|
+
elif isinstance(self.log_config, str) and self.log_config.endswith((".yaml", ".yml")):
|
370
371
|
# Install the PyYAML package or the uvicorn[standard] optional
|
371
372
|
# dependencies to enable this functionality.
|
372
373
|
import yaml
|
uvicorn/main.py
CHANGED
@@ -6,7 +6,8 @@ import os
|
|
6
6
|
import platform
|
7
7
|
import ssl
|
8
8
|
import sys
|
9
|
-
from
|
9
|
+
from configparser import RawConfigParser
|
10
|
+
from typing import IO, Any, Callable
|
10
11
|
|
11
12
|
import click
|
12
13
|
|
@@ -47,7 +48,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
47
48
|
if not value or ctx.resilient_parsing:
|
48
49
|
return
|
49
50
|
click.echo(
|
50
|
-
"Running uvicorn {version} with {py_implementation} {py_version} on {system}".format(
|
51
|
+
"Running uvicorn {version} with {py_implementation} {py_version} on {system}".format( # noqa: UP032
|
51
52
|
version=uvicorn.__version__,
|
52
53
|
py_implementation=platform.python_implementation(),
|
53
54
|
py_version=platform.python_version(),
|
@@ -481,7 +482,7 @@ def run(
|
|
481
482
|
reload_delay: float = 0.25,
|
482
483
|
workers: int | None = None,
|
483
484
|
env_file: str | os.PathLike[str] | None = None,
|
484
|
-
log_config: dict[str, Any] | str | None = LOGGING_CONFIG,
|
485
|
+
log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
|
485
486
|
log_level: str | int | None = None,
|
486
487
|
access_log: bool = True,
|
487
488
|
proxy_headers: bool = True,
|
@@ -565,16 +566,18 @@ def run(
|
|
565
566
|
logger.warning("You must pass the application as an import string to enable 'reload' or " "'workers'.")
|
566
567
|
sys.exit(1)
|
567
568
|
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
569
|
+
try:
|
570
|
+
if config.should_reload:
|
571
|
+
sock = config.bind_socket()
|
572
|
+
ChangeReload(config, target=server.run, sockets=[sock]).run()
|
573
|
+
elif config.workers > 1:
|
574
|
+
sock = config.bind_socket()
|
575
|
+
Multiprocess(config, target=server.run, sockets=[sock]).run()
|
576
|
+
else:
|
577
|
+
server.run()
|
578
|
+
finally:
|
579
|
+
if config.uds and os.path.exists(config.uds):
|
580
|
+
os.remove(config.uds) # pragma: py-win32
|
578
581
|
|
579
582
|
if not server.started and not config.should_reload and config.workers == 1:
|
580
583
|
sys.exit(STARTUP_FAILURE)
|
@@ -1,12 +1,6 @@
|
|
1
1
|
import asyncio
|
2
2
|
|
3
|
-
from uvicorn._types import
|
4
|
-
ASGIReceiveCallable,
|
5
|
-
ASGISendCallable,
|
6
|
-
HTTPResponseBodyEvent,
|
7
|
-
HTTPResponseStartEvent,
|
8
|
-
Scope,
|
9
|
-
)
|
3
|
+
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
10
4
|
|
11
5
|
CLOSE_HEADER = (b"connection", b"close")
|
12
6
|
|
@@ -45,20 +39,16 @@ class FlowControl:
|
|
45
39
|
self._is_writable_event.set()
|
46
40
|
|
47
41
|
|
48
|
-
async def service_unavailable(scope:
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
"body": b"Service Unavailable",
|
62
|
-
"more_body": False,
|
63
|
-
}
|
64
|
-
await send(response_body)
|
42
|
+
async def service_unavailable(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
43
|
+
await send(
|
44
|
+
{
|
45
|
+
"type": "http.response.start",
|
46
|
+
"status": 503,
|
47
|
+
"headers": [
|
48
|
+
(b"content-type", b"text/plain; charset=utf-8"),
|
49
|
+
(b"content-length", b"19"),
|
50
|
+
(b"connection", b"close"),
|
51
|
+
],
|
52
|
+
}
|
53
|
+
)
|
54
|
+
await send({"type": "http.response.body", "body": b"Service Unavailable", "more_body": False})
|
@@ -20,19 +20,8 @@ from uvicorn._types import (
|
|
20
20
|
)
|
21
21
|
from uvicorn.config import Config
|
22
22
|
from uvicorn.logging import TRACE_LOG_LEVEL
|
23
|
-
from uvicorn.protocols.http.flow_control import
|
24
|
-
|
25
|
-
HIGH_WATER_LIMIT,
|
26
|
-
FlowControl,
|
27
|
-
service_unavailable,
|
28
|
-
)
|
29
|
-
from uvicorn.protocols.utils import (
|
30
|
-
get_client_addr,
|
31
|
-
get_local_addr,
|
32
|
-
get_path_with_query_string,
|
33
|
-
get_remote_addr,
|
34
|
-
is_ssl,
|
35
|
-
)
|
23
|
+
from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable
|
24
|
+
from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl
|
36
25
|
from uvicorn.server import ServerState
|
37
26
|
|
38
27
|
|
@@ -16,29 +16,17 @@ from uvicorn._types import (
|
|
16
16
|
ASGIReceiveEvent,
|
17
17
|
ASGISendEvent,
|
18
18
|
HTTPRequestEvent,
|
19
|
-
HTTPResponseBodyEvent,
|
20
19
|
HTTPResponseStartEvent,
|
21
20
|
HTTPScope,
|
22
21
|
)
|
23
22
|
from uvicorn.config import Config
|
24
23
|
from uvicorn.logging import TRACE_LOG_LEVEL
|
25
|
-
from uvicorn.protocols.http.flow_control import
|
26
|
-
|
27
|
-
HIGH_WATER_LIMIT,
|
28
|
-
FlowControl,
|
29
|
-
service_unavailable,
|
30
|
-
)
|
31
|
-
from uvicorn.protocols.utils import (
|
32
|
-
get_client_addr,
|
33
|
-
get_local_addr,
|
34
|
-
get_path_with_query_string,
|
35
|
-
get_remote_addr,
|
36
|
-
is_ssl,
|
37
|
-
)
|
24
|
+
from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable
|
25
|
+
from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl
|
38
26
|
from uvicorn.server import ServerState
|
39
27
|
|
40
|
-
HEADER_RE = re.compile(b'[\x00-\
|
41
|
-
HEADER_VALUE_RE = re.compile(b"[\x00-\
|
28
|
+
HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:[]={} \t\\"]')
|
29
|
+
HEADER_VALUE_RE = re.compile(b"[\x00-\x1f\x7f]")
|
42
30
|
|
43
31
|
|
44
32
|
def _get_status_line(status_code: int) -> bytes:
|
@@ -435,21 +423,18 @@ class RequestResponseCycle:
|
|
435
423
|
self.on_response = lambda: None
|
436
424
|
|
437
425
|
async def send_500_response(self) -> None:
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
"more_body": False,
|
451
|
-
}
|
452
|
-
await self.send(response_body_event)
|
426
|
+
await self.send(
|
427
|
+
{
|
428
|
+
"type": "http.response.start",
|
429
|
+
"status": 500,
|
430
|
+
"headers": [
|
431
|
+
(b"content-type", b"text/plain; charset=utf-8"),
|
432
|
+
(b"content-length", b"21"),
|
433
|
+
(b"connection", b"close"),
|
434
|
+
],
|
435
|
+
}
|
436
|
+
)
|
437
|
+
await self.send({"type": "http.response.body", "body": b"Internal Server Error", "more_body": False})
|
453
438
|
|
454
439
|
# ASGI interface
|
455
440
|
async def send(self, message: ASGISendEvent) -> None:
|
uvicorn/protocols/utils.py
CHANGED
@@ -7,14 +7,17 @@ from typing import Any, Literal, Optional, Sequence, cast
|
|
7
7
|
from urllib.parse import unquote
|
8
8
|
|
9
9
|
import websockets
|
10
|
+
import websockets.legacy.handshake
|
10
11
|
from websockets.datastructures import Headers
|
11
12
|
from websockets.exceptions import ConnectionClosed
|
13
|
+
from websockets.extensions.base import ServerExtensionFactory
|
12
14
|
from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory
|
13
15
|
from websockets.legacy.server import HTTPResponse
|
14
16
|
from websockets.server import WebSocketServerProtocol
|
15
17
|
from websockets.typing import Subprotocol
|
16
18
|
|
17
19
|
from uvicorn._types import (
|
20
|
+
ASGI3Application,
|
18
21
|
ASGISendEvent,
|
19
22
|
WebSocketAcceptEvent,
|
20
23
|
WebSocketCloseEvent,
|
@@ -53,6 +56,7 @@ class Server:
|
|
53
56
|
|
54
57
|
class WebSocketProtocol(WebSocketServerProtocol):
|
55
58
|
extra_headers: list[tuple[str, str]]
|
59
|
+
logger: logging.Logger | logging.LoggerAdapter[Any]
|
56
60
|
|
57
61
|
def __init__(
|
58
62
|
self,
|
@@ -65,7 +69,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
65
69
|
config.load()
|
66
70
|
|
67
71
|
self.config = config
|
68
|
-
self.app = config.loaded_app
|
72
|
+
self.app = cast(ASGI3Application, config.loaded_app)
|
69
73
|
self.loop = _loop or asyncio.get_event_loop()
|
70
74
|
self.root_path = config.root_path
|
71
75
|
self.app_state = app_state
|
@@ -92,7 +96,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
92
96
|
|
93
97
|
self.ws_server: Server = Server() # type: ignore[assignment]
|
94
98
|
|
95
|
-
extensions = []
|
99
|
+
extensions: list[ServerExtensionFactory] = []
|
96
100
|
if self.config.ws_per_message_deflate:
|
97
101
|
extensions.append(ServerPerMessageDeflateFactory())
|
98
102
|
|
@@ -147,10 +151,10 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
147
151
|
self.send_500_response()
|
148
152
|
self.transport.close()
|
149
153
|
|
150
|
-
def on_task_complete(self, task: asyncio.Task) -> None:
|
154
|
+
def on_task_complete(self, task: asyncio.Task[None]) -> None:
|
151
155
|
self.tasks.discard(task)
|
152
156
|
|
153
|
-
async def process_request(self, path: str,
|
157
|
+
async def process_request(self, path: str, request_headers: Headers) -> HTTPResponse | None:
|
154
158
|
"""
|
155
159
|
This hook is called to determine if the websocket should return
|
156
160
|
an HTTP response and close.
|
@@ -161,15 +165,15 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
161
165
|
"""
|
162
166
|
path_portion, _, query_string = path.partition("?")
|
163
167
|
|
164
|
-
websockets.legacy.handshake.check_request(
|
168
|
+
websockets.legacy.handshake.check_request(request_headers)
|
165
169
|
|
166
|
-
subprotocols = []
|
167
|
-
for header in
|
170
|
+
subprotocols: list[str] = []
|
171
|
+
for header in request_headers.get_all("Sec-WebSocket-Protocol"):
|
168
172
|
subprotocols.extend([token.strip() for token in header.split(",")])
|
169
173
|
|
170
174
|
asgi_headers = [
|
171
175
|
(name.encode("ascii"), value.encode("ascii", errors="surrogateescape"))
|
172
|
-
for name, value in
|
176
|
+
for name, value in request_headers.raw_items()
|
173
177
|
]
|
174
178
|
path = unquote(path_portion)
|
175
179
|
full_path = self.root_path + path
|
@@ -237,14 +241,13 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
237
241
|
termination states.
|
238
242
|
"""
|
239
243
|
try:
|
240
|
-
result = await self.app(self.scope, self.asgi_receive, self.asgi_send)
|
244
|
+
result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value]
|
241
245
|
except ClientDisconnected:
|
242
246
|
self.closed_event.set()
|
243
247
|
self.transport.close()
|
244
|
-
except BaseException
|
248
|
+
except BaseException:
|
245
249
|
self.closed_event.set()
|
246
|
-
|
247
|
-
self.logger.error(msg, exc_info=exc)
|
250
|
+
self.logger.exception("Exception in ASGI application\n")
|
248
251
|
if not self.handshake_started_event.is_set():
|
249
252
|
self.send_500_response()
|
250
253
|
else:
|
@@ -253,13 +256,11 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
253
256
|
else:
|
254
257
|
self.closed_event.set()
|
255
258
|
if not self.handshake_started_event.is_set():
|
256
|
-
|
257
|
-
self.logger.error(msg)
|
259
|
+
self.logger.error("ASGI callable returned without sending handshake.")
|
258
260
|
self.send_500_response()
|
259
261
|
self.transport.close()
|
260
262
|
elif result is not None:
|
261
|
-
|
262
|
-
self.logger.error(msg, result)
|
263
|
+
self.logger.error("ASGI callable should return None, but returned '%s'.", result)
|
263
264
|
await self.handshake_completed_event.wait()
|
264
265
|
self.transport.close()
|
265
266
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import asyncio
|
4
4
|
import logging
|
5
5
|
import typing
|
6
|
-
from typing import Literal
|
6
|
+
from typing import Literal, cast
|
7
7
|
from urllib.parse import unquote
|
8
8
|
|
9
9
|
import wsproto
|
@@ -13,6 +13,7 @@ from wsproto.extensions import Extension, PerMessageDeflate
|
|
13
13
|
from wsproto.utilities import LocalProtocolError, RemoteProtocolError
|
14
14
|
|
15
15
|
from uvicorn._types import (
|
16
|
+
ASGI3Application,
|
16
17
|
ASGISendEvent,
|
17
18
|
WebSocketAcceptEvent,
|
18
19
|
WebSocketCloseEvent,
|
@@ -46,7 +47,7 @@ class WSProtocol(asyncio.Protocol):
|
|
46
47
|
config.load()
|
47
48
|
|
48
49
|
self.config = config
|
49
|
-
self.app = config.loaded_app
|
50
|
+
self.app = cast(ASGI3Application, config.loaded_app)
|
50
51
|
self.loop = _loop or asyncio.get_event_loop()
|
51
52
|
self.logger = logging.getLogger("uvicorn.error")
|
52
53
|
self.root_path = config.root_path
|
@@ -156,7 +157,7 @@ class WSProtocol(asyncio.Protocol):
|
|
156
157
|
self.send_500_response()
|
157
158
|
self.transport.close()
|
158
159
|
|
159
|
-
def on_task_complete(self, task: asyncio.Task) -> None:
|
160
|
+
def on_task_complete(self, task: asyncio.Task[None]) -> None:
|
160
161
|
self.tasks.discard(task)
|
161
162
|
|
162
163
|
# Event handlers
|
@@ -220,7 +221,7 @@ class WSProtocol(asyncio.Protocol):
|
|
220
221
|
def send_500_response(self) -> None:
|
221
222
|
if self.response_started or self.handshake_complete:
|
222
223
|
return # we cannot send responses anymore
|
223
|
-
headers = [
|
224
|
+
headers: list[tuple[bytes, bytes]] = [
|
224
225
|
(b"content-type", b"text/plain; charset=utf-8"),
|
225
226
|
(b"connection", b"close"),
|
226
227
|
]
|
@@ -230,7 +231,7 @@ class WSProtocol(asyncio.Protocol):
|
|
230
231
|
|
231
232
|
async def run_asgi(self) -> None:
|
232
233
|
try:
|
233
|
-
result = await self.app(self.scope, self.receive, self.send)
|
234
|
+
result = await self.app(self.scope, self.receive, self.send) # type: ignore[func-returns-value]
|
234
235
|
except ClientDisconnected:
|
235
236
|
self.transport.close()
|
236
237
|
except BaseException:
|
@@ -239,13 +240,11 @@ class WSProtocol(asyncio.Protocol):
|
|
239
240
|
self.transport.close()
|
240
241
|
else:
|
241
242
|
if not self.handshake_complete:
|
242
|
-
|
243
|
-
self.logger.error(msg)
|
243
|
+
self.logger.error("ASGI callable returned without completing handshake.")
|
244
244
|
self.send_500_response()
|
245
245
|
self.transport.close()
|
246
246
|
elif result is not None:
|
247
|
-
|
248
|
-
self.logger.error(msg, result)
|
247
|
+
self.logger.error("ASGI callable should return None, but returned '%s'.", result)
|
249
248
|
self.transport.close()
|
250
249
|
|
251
250
|
async def send(self, message: ASGISendEvent) -> None:
|
uvicorn/server.py
CHANGED
@@ -1,6 +1,7 @@
|
|
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,7 +12,7 @@ import threading
|
|
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 @@ class Server:
|
|
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 @@ class Server:
|
|
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 @@ class Server:
|
|
302
307
|
for server in self.servers:
|
303
308
|
await server.wait_closed()
|
304
309
|
|
305
|
-
|
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
|
-
|
314
|
+
yield
|
308
315
|
return
|
309
|
-
|
310
|
-
|
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
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
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:
|
@@ -4,24 +4,101 @@ import logging
|
|
4
4
|
import os
|
5
5
|
import signal
|
6
6
|
import threading
|
7
|
-
from multiprocessing
|
7
|
+
from multiprocessing import Pipe
|
8
8
|
from socket import socket
|
9
|
-
from
|
10
|
-
from typing import Callable
|
9
|
+
from typing import Any, Callable
|
11
10
|
|
12
11
|
import click
|
13
12
|
|
14
13
|
from uvicorn._subprocess import get_subprocess
|
15
14
|
from uvicorn.config import Config
|
16
15
|
|
17
|
-
|
18
|
-
signal
|
19
|
-
|
20
|
-
)
|
16
|
+
SIGNALS = {
|
17
|
+
getattr(signal, f"SIG{x}"): x
|
18
|
+
for x in "INT TERM BREAK HUP QUIT TTIN TTOU USR1 USR2 WINCH".split()
|
19
|
+
if hasattr(signal, f"SIG{x}")
|
20
|
+
}
|
21
21
|
|
22
22
|
logger = logging.getLogger("uvicorn.error")
|
23
23
|
|
24
24
|
|
25
|
+
class Process:
|
26
|
+
def __init__(
|
27
|
+
self,
|
28
|
+
config: Config,
|
29
|
+
target: Callable[[list[socket] | None], None],
|
30
|
+
sockets: list[socket],
|
31
|
+
) -> None:
|
32
|
+
self.real_target = target
|
33
|
+
|
34
|
+
self.parent_conn, self.child_conn = Pipe()
|
35
|
+
self.process = get_subprocess(config, self.target, sockets)
|
36
|
+
|
37
|
+
def ping(self, timeout: float = 5) -> bool:
|
38
|
+
self.parent_conn.send(b"ping")
|
39
|
+
if self.parent_conn.poll(timeout):
|
40
|
+
self.parent_conn.recv()
|
41
|
+
return True
|
42
|
+
return False
|
43
|
+
|
44
|
+
def pong(self) -> None:
|
45
|
+
self.child_conn.recv()
|
46
|
+
self.child_conn.send(b"pong")
|
47
|
+
|
48
|
+
def always_pong(self) -> None:
|
49
|
+
while True:
|
50
|
+
self.pong()
|
51
|
+
|
52
|
+
def target(self, sockets: list[socket] | None = None) -> Any: # pragma: no cover
|
53
|
+
if os.name == "nt": # pragma: py-not-win32
|
54
|
+
# Windows doesn't support SIGTERM, so we use SIGBREAK instead.
|
55
|
+
# And then we raise SIGTERM when SIGBREAK is received.
|
56
|
+
# https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/signal?view=msvc-170
|
57
|
+
signal.signal(
|
58
|
+
signal.SIGBREAK, # type: ignore[attr-defined]
|
59
|
+
lambda sig, frame: signal.raise_signal(signal.SIGTERM),
|
60
|
+
)
|
61
|
+
|
62
|
+
threading.Thread(target=self.always_pong, daemon=True).start()
|
63
|
+
return self.real_target(sockets)
|
64
|
+
|
65
|
+
def is_alive(self, timeout: float = 5) -> bool:
|
66
|
+
if not self.process.is_alive():
|
67
|
+
return False
|
68
|
+
|
69
|
+
return self.ping(timeout)
|
70
|
+
|
71
|
+
def start(self) -> None:
|
72
|
+
self.process.start()
|
73
|
+
|
74
|
+
def terminate(self) -> None:
|
75
|
+
if self.process.exitcode is None: # Process is still running
|
76
|
+
assert self.process.pid is not None
|
77
|
+
if os.name == "nt": # pragma: py-not-win32
|
78
|
+
# Windows doesn't support SIGTERM.
|
79
|
+
# So send SIGBREAK, and then in process raise SIGTERM.
|
80
|
+
os.kill(self.process.pid, signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined]
|
81
|
+
else:
|
82
|
+
os.kill(self.process.pid, signal.SIGTERM)
|
83
|
+
logger.info(f"Terminated child process [{self.process.pid}]")
|
84
|
+
|
85
|
+
self.parent_conn.close()
|
86
|
+
self.child_conn.close()
|
87
|
+
|
88
|
+
def kill(self) -> None:
|
89
|
+
# In Windows, the method will call `TerminateProcess` to kill the process.
|
90
|
+
# In Unix, the method will send SIGKILL to the process.
|
91
|
+
self.process.kill()
|
92
|
+
|
93
|
+
def join(self) -> None:
|
94
|
+
logger.info(f"Waiting for child process [{self.process.pid}]")
|
95
|
+
self.process.join()
|
96
|
+
|
97
|
+
@property
|
98
|
+
def pid(self) -> int | None:
|
99
|
+
return self.process.pid
|
100
|
+
|
101
|
+
|
25
102
|
class Multiprocess:
|
26
103
|
def __init__(
|
27
104
|
self,
|
@@ -32,39 +109,115 @@ class Multiprocess:
|
|
32
109
|
self.config = config
|
33
110
|
self.target = target
|
34
111
|
self.sockets = sockets
|
35
|
-
self.processes: list[SpawnProcess] = []
|
36
|
-
self.should_exit = threading.Event()
|
37
|
-
self.pid = os.getpid()
|
38
|
-
|
39
|
-
def signal_handler(self, sig: int, frame: FrameType | None) -> None:
|
40
|
-
"""
|
41
|
-
A signal handler that is registered with the parent process.
|
42
|
-
"""
|
43
|
-
self.should_exit.set()
|
44
112
|
|
45
|
-
|
46
|
-
self.
|
47
|
-
self.should_exit.wait()
|
48
|
-
self.shutdown()
|
113
|
+
self.processes_num = config.workers
|
114
|
+
self.processes: list[Process] = []
|
49
115
|
|
50
|
-
|
51
|
-
message = f"Started parent process [{str(self.pid)}]"
|
52
|
-
color_message = "Started parent process [{}]".format(click.style(str(self.pid), fg="cyan", bold=True))
|
53
|
-
logger.info(message, extra={"color_message": color_message})
|
116
|
+
self.should_exit = threading.Event()
|
54
117
|
|
55
|
-
|
56
|
-
|
118
|
+
self.signal_queue: list[int] = []
|
119
|
+
for sig in SIGNALS:
|
120
|
+
signal.signal(sig, lambda sig, frame: self.signal_queue.append(sig))
|
57
121
|
|
58
|
-
|
59
|
-
|
122
|
+
def init_processes(self) -> None:
|
123
|
+
for _ in range(self.processes_num):
|
124
|
+
process = Process(self.config, self.target, self.sockets)
|
60
125
|
process.start()
|
61
126
|
self.processes.append(process)
|
62
127
|
|
63
|
-
def
|
128
|
+
def terminate_all(self) -> None:
|
64
129
|
for process in self.processes:
|
65
130
|
process.terminate()
|
131
|
+
|
132
|
+
def join_all(self) -> None:
|
133
|
+
for process in self.processes:
|
134
|
+
process.join()
|
135
|
+
|
136
|
+
def restart_all(self) -> None:
|
137
|
+
for idx, process in enumerate(tuple(self.processes)):
|
138
|
+
process.terminate()
|
66
139
|
process.join()
|
140
|
+
new_process = Process(self.config, self.target, self.sockets)
|
141
|
+
new_process.start()
|
142
|
+
self.processes[idx] = new_process
|
143
|
+
|
144
|
+
def run(self) -> None:
|
145
|
+
message = f"Started parent process [{os.getpid()}]"
|
146
|
+
color_message = "Started parent process [{}]".format(click.style(str(os.getpid()), fg="cyan", bold=True))
|
147
|
+
logger.info(message, extra={"color_message": color_message})
|
67
148
|
|
68
|
-
|
69
|
-
|
149
|
+
self.init_processes()
|
150
|
+
|
151
|
+
while not self.should_exit.wait(0.5):
|
152
|
+
self.handle_signals()
|
153
|
+
self.keep_subprocess_alive()
|
154
|
+
|
155
|
+
self.terminate_all()
|
156
|
+
self.join_all()
|
157
|
+
|
158
|
+
message = f"Stopping parent process [{os.getpid()}]"
|
159
|
+
color_message = "Stopping parent process [{}]".format(click.style(str(os.getpid()), fg="cyan", bold=True))
|
70
160
|
logger.info(message, extra={"color_message": color_message})
|
161
|
+
|
162
|
+
def keep_subprocess_alive(self) -> None:
|
163
|
+
if self.should_exit.is_set():
|
164
|
+
return # parent process is exiting, no need to keep subprocess alive
|
165
|
+
|
166
|
+
for idx, process in enumerate(tuple(self.processes)):
|
167
|
+
if process.is_alive():
|
168
|
+
continue
|
169
|
+
|
170
|
+
process.kill() # process is hung, kill it
|
171
|
+
process.join()
|
172
|
+
|
173
|
+
if self.should_exit.is_set():
|
174
|
+
return
|
175
|
+
|
176
|
+
logger.info(f"Child process [{process.pid}] died")
|
177
|
+
del self.processes[idx]
|
178
|
+
process = Process(self.config, self.target, self.sockets)
|
179
|
+
process.start()
|
180
|
+
self.processes.append(process)
|
181
|
+
|
182
|
+
def handle_signals(self) -> None:
|
183
|
+
for sig in tuple(self.signal_queue):
|
184
|
+
self.signal_queue.remove(sig)
|
185
|
+
sig_name = SIGNALS[sig]
|
186
|
+
sig_handler = getattr(self, f"handle_{sig_name.lower()}", None)
|
187
|
+
if sig_handler is not None:
|
188
|
+
sig_handler()
|
189
|
+
else: # pragma: no cover
|
190
|
+
logger.debug(f"Received signal {sig_name}, but no handler is defined for it.")
|
191
|
+
|
192
|
+
def handle_int(self) -> None:
|
193
|
+
logger.info("Received SIGINT, exiting.")
|
194
|
+
self.should_exit.set()
|
195
|
+
|
196
|
+
def handle_term(self) -> None:
|
197
|
+
logger.info("Received SIGTERM, exiting.")
|
198
|
+
self.should_exit.set()
|
199
|
+
|
200
|
+
def handle_break(self) -> None: # pragma: py-not-win32
|
201
|
+
logger.info("Received SIGBREAK, exiting.")
|
202
|
+
self.should_exit.set()
|
203
|
+
|
204
|
+
def handle_hup(self) -> None: # pragma: py-win32
|
205
|
+
logger.info("Received SIGHUP, restarting processes.")
|
206
|
+
self.restart_all()
|
207
|
+
|
208
|
+
def handle_ttin(self) -> None: # pragma: py-win32
|
209
|
+
logger.info("Received SIGTTIN, increasing the number of processes.")
|
210
|
+
self.processes_num += 1
|
211
|
+
process = Process(self.config, self.target, self.sockets)
|
212
|
+
process.start()
|
213
|
+
self.processes.append(process)
|
214
|
+
|
215
|
+
def handle_ttou(self) -> None: # pragma: py-win32
|
216
|
+
logger.info("Received SIGTTOU, decreasing number of processes.")
|
217
|
+
if self.processes_num <= 1:
|
218
|
+
logger.info("Already reached one process, cannot decrease the number of processes anymore.")
|
219
|
+
return
|
220
|
+
self.processes_num -= 1
|
221
|
+
process = self.processes.pop()
|
222
|
+
process.terminate()
|
223
|
+
process.join()
|
uvicorn/workers.py
CHANGED
@@ -4,6 +4,7 @@ import asyncio
|
|
4
4
|
import logging
|
5
5
|
import signal
|
6
6
|
import sys
|
7
|
+
import warnings
|
7
8
|
from typing import Any
|
8
9
|
|
9
10
|
from gunicorn.arbiter import Arbiter
|
@@ -12,6 +13,12 @@ from gunicorn.workers.base import Worker
|
|
12
13
|
from uvicorn.config import Config
|
13
14
|
from uvicorn.main import Server
|
14
15
|
|
16
|
+
warnings.warn(
|
17
|
+
"The `uvicorn.workers` module is deprecated. Please use `uvicorn-worker` package instead.\n"
|
18
|
+
"For more details, see https://github.com/Kludex/uvicorn-worker.",
|
19
|
+
DeprecationWarning,
|
20
|
+
)
|
21
|
+
|
15
22
|
|
16
23
|
class UvicornWorker(Worker):
|
17
24
|
"""
|
@@ -1,14 +1,14 @@
|
|
1
|
-
uvicorn/__init__.py,sha256=
|
1
|
+
uvicorn/__init__.py,sha256=vTrtzT1tyLLpXdJOdibN6a_QQYT8gZ4yAepJkXz_bl0,147
|
2
2
|
uvicorn/__main__.py,sha256=DQizy6nKP0ywhPpnCHgmRDYIMfcqZKVEzNIWQZjqtVQ,62
|
3
|
-
uvicorn/_subprocess.py,sha256=
|
4
|
-
uvicorn/_types.py,sha256=
|
5
|
-
uvicorn/config.py,sha256=
|
3
|
+
uvicorn/_subprocess.py,sha256=wc7tS3hmHLX9RHBJchu0ZHjUeQEuXehi3xvQvK4uUTY,2741
|
4
|
+
uvicorn/_types.py,sha256=KzJumVocO3k6pAFvLTSHtug9TqO9CC2cbe5r1NvJlbA,7778
|
5
|
+
uvicorn/config.py,sha256=gk9sg4XHc1FcUbx9mN56kAfKN4ERMKM_qc3V9qlVPOI,20671
|
6
6
|
uvicorn/importer.py,sha256=nRt0QQ3qpi264-n_mR0l55C2ddM8nowTNzT1jsWaam8,1128
|
7
7
|
uvicorn/logging.py,sha256=sg4D9lHaW_kKQj_kmP-bolbChjKfhBuihktlWp8RjSI,4236
|
8
|
-
uvicorn/main.py,sha256=
|
8
|
+
uvicorn/main.py,sha256=Ugv-CHZ1IBEuXXoXOE4J2MjDzPZX3XA4FSqUqvyYclU,16715
|
9
9
|
uvicorn/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
10
|
-
uvicorn/server.py,sha256=
|
11
|
-
uvicorn/workers.py,sha256=
|
10
|
+
uvicorn/server.py,sha256=bXpsRamwT2cRoe7u6Nk8Czp_la2sfSnV9V0dva5-gkE,12729
|
11
|
+
uvicorn/workers.py,sha256=DukTKlrCyyvWVHbJWBJflIV2yUe-q6KaGdrEwLrNmyc,3893
|
12
12
|
uvicorn/lifespan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
uvicorn/lifespan/off.py,sha256=nfI6qHAUo_8-BEXMBKoHQ9wUbsXrPaXLCbDSS0vKSr8,332
|
14
14
|
uvicorn/lifespan/on.py,sha256=1KYuFNNyQONIjtEHhKZAJp-OOokIyjj74wpGCGBv4lk,5184
|
@@ -19,27 +19,27 @@ uvicorn/loops/uvloop.py,sha256=K4QybYVxtK9C2emDhDPUCkBXR4XMT5Ofv9BPFPoX0ok,148
|
|
19
19
|
uvicorn/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
20
|
uvicorn/middleware/asgi2.py,sha256=YQrQNm3RehFts3mzk3k4yw8aD8Egtj0tRS3N45YkQa0,394
|
21
21
|
uvicorn/middleware/message_logger.py,sha256=IHEZUSnFNaMFUFdwtZO3AuFATnYcSor-gVtOjbCzt8M,2859
|
22
|
-
uvicorn/middleware/proxy_headers.py,sha256
|
22
|
+
uvicorn/middleware/proxy_headers.py,sha256=CT7cYPf1FmmAWxnpKwLYPLPNBM6WxgU2NqYYZrmXPWQ,3040
|
23
23
|
uvicorn/middleware/wsgi.py,sha256=TBeG4W_gEmWddbGfWyxdzJ0IDaWWkJZyF8eIp-1fv0U,7111
|
24
24
|
uvicorn/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
|
-
uvicorn/protocols/utils.py,sha256=
|
25
|
+
uvicorn/protocols/utils.py,sha256=kBwTa7T6Ed1-tSpNfLWhJjxhTbH163GqPwwAcXshPVc,1849
|
26
26
|
uvicorn/protocols/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
27
|
uvicorn/protocols/http/auto.py,sha256=YfXGyzWTaaE2p_jkTPWrJCXsxEaQnC3NK0-G7Wgmnls,403
|
28
|
-
uvicorn/protocols/http/flow_control.py,sha256=
|
29
|
-
uvicorn/protocols/http/h11_impl.py,sha256=
|
30
|
-
uvicorn/protocols/http/httptools_impl.py,sha256=
|
28
|
+
uvicorn/protocols/http/flow_control.py,sha256=lwEBY3zsxJrxykud6OB-jQQd9rUQkFXDmqQyPGBm5ag,1626
|
29
|
+
uvicorn/protocols/http/h11_impl.py,sha256=TycBaEdfs74j2t-EqsOHpd1s0RNDx6wYpgIaQVd9zm8,20330
|
30
|
+
uvicorn/protocols/http/httptools_impl.py,sha256=owVsTiJ7lSip4LoIpXAuEBdVabnjmsQdhdHDuPD0uB8,21291
|
31
31
|
uvicorn/protocols/websockets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
32
|
uvicorn/protocols/websockets/auto.py,sha256=kNP-h07ZzjA9dKRUd7MNO0J7xhRJ5xVBfit7wCbdB0A,574
|
33
|
-
uvicorn/protocols/websockets/websockets_impl.py,sha256=
|
34
|
-
uvicorn/protocols/websockets/wsproto_impl.py,sha256=
|
33
|
+
uvicorn/protocols/websockets/websockets_impl.py,sha256=bagg9R71eBqb5EyB2tUV3OLuw8uBhPZUuVykgzfrki4,15457
|
34
|
+
uvicorn/protocols/websockets/wsproto_impl.py,sha256=sktHUkyrpdlwsai_tQ8mahy248KZQ9iJaeLXkZ5rfWI,15199
|
35
35
|
uvicorn/supervisors/__init__.py,sha256=UVJYW3RVHMDSgUytToyAgGyd9NUQVqbNpVrQrvm4Tpc,700
|
36
36
|
uvicorn/supervisors/basereload.py,sha256=u83LepHT28QFH84wwsxJypwBKO1apG4c4WziPMfOqmE,3850
|
37
|
-
uvicorn/supervisors/multiprocess.py,sha256=
|
37
|
+
uvicorn/supervisors/multiprocess.py,sha256=BeYZ8vgRfFLSKDHC59QXutiNhM7Kw-ACMvw9dHXnrwE,7508
|
38
38
|
uvicorn/supervisors/statreload.py,sha256=gc-HUB44f811PvxD_ZIEQYenM7mWmhQQjYg7KKQ1c5o,1542
|
39
39
|
uvicorn/supervisors/watchfilesreload.py,sha256=RMhWgInlOr0MJB0RvmW50RZY1ls9Kp9VT3eaLjdRTpw,2935
|
40
40
|
uvicorn/supervisors/watchgodreload.py,sha256=kd-gOvp14ArTNIc206Nt5CEjZZ4NP2UmMVYE7571yRQ,5486
|
41
|
-
uvicorn-0.
|
42
|
-
uvicorn-0.
|
43
|
-
uvicorn-0.
|
44
|
-
uvicorn-0.
|
45
|
-
uvicorn-0.
|
41
|
+
uvicorn-0.30.0.dist-info/METADATA,sha256=qiB2nEtFeOiRCSeMiAm8BJlXdEMs6Wk07FFQbEoq17M,6330
|
42
|
+
uvicorn-0.30.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
|
43
|
+
uvicorn-0.30.0.dist-info/entry_points.txt,sha256=FW1w-hkc9QgwaGoovMvm0ZY73w_NcycWdGAUfDsNGxw,46
|
44
|
+
uvicorn-0.30.0.dist-info/licenses/LICENSE.md,sha256=7-Gs8-YvuZwoiw7HPlp3O3Jo70Mg_nV-qZQhTktjw3E,1526
|
45
|
+
uvicorn-0.30.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|