hack-house/.venv/lib/python3.12/site-packages/sanic/server/runners.py
leetcrypt bb1d662ee1 chore: rename project coven → hack-house ⛧
Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 13:29:14 -07:00

405 lines
13 KiB
Python

from __future__ import annotations
from ssl import SSLContext
from typing import TYPE_CHECKING
from sanic.config import Config
from sanic.exceptions import ServerError
from sanic.http.constants import HTTP
from sanic.http.tls import get_ssl_context
if TYPE_CHECKING:
from sanic.app import Sanic
import asyncio
import os
import socket
from functools import partial
from signal import SIG_IGN, SIGINT, SIGTERM
from signal import signal as signal_func
from sanic.application.ext import setup_ext
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.http.http3 import SessionTicketStore, get_config
from sanic.log import error_logger, server_logger
from sanic.logging.setup import setup_logging
from sanic.models.server_types import Signal
from sanic.server.async_server import AsyncioServer
from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol
from sanic.server.socket import bind_unix_socket, remove_unix_socket
try:
from aioquic.asyncio import serve as quic_serve
HTTP3_AVAILABLE = True
except ModuleNotFoundError: # no cov
HTTP3_AVAILABLE = False
def serve(
host,
port,
app: Sanic,
ssl: SSLContext | None = None,
sock: socket.socket | None = None,
unix: str | None = None,
reuse_port: bool = False,
loop=None,
protocol: type[asyncio.Protocol] = HttpProtocol,
backlog: int = 100,
register_sys_signals: bool = True,
run_multiple: bool = False,
run_async: bool = False,
connections=None,
signal=Signal(),
state=None,
asyncio_server_kwargs=None,
version=HTTP.VERSION_1,
):
"""Start asynchronous HTTP Server on an individual process.
:param host: Address to host on
:param port: Port to host on
:param before_start: function to be executed before the server starts
listening. Takes arguments `app` instance and `loop`
:param after_start: function to be executed after the server starts
listening. Takes arguments `app` instance and `loop`
:param before_stop: function to be executed when a stop signal is
received before it is respected. Takes arguments
`app` instance and `loop`
:param after_stop: function to be executed when a stop signal is
received after it is respected. Takes arguments
`app` instance and `loop`
:param ssl: SSLContext
:param sock: Socket for the server to accept connections from
:param unix: Unix socket to listen on instead of TCP port
:param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop
:param run_async: bool: Do not create a new event loop for the server,
and return an AsyncServer object rather than running it
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
create_server method
:return: Nothing
Args:
host (str): Address to host on
port (int): Port to host on
app (Sanic): Sanic app instance
ssl (Optional[SSLContext], optional): SSLContext. Defaults to `None`.
sock (Optional[socket.socket], optional): Socket for the server to
accept connections from. Defaults to `None`.
unix (Optional[str], optional): Unix socket to listen on instead of
TCP port. Defaults to `None`.
reuse_port (bool, optional): `True` for multiple workers. Defaults
to `False`.
loop: asyncio compatible event loop. Defaults
to `None`.
protocol (Type[asyncio.Protocol], optional): Protocol to use. Defaults
to `HttpProtocol`.
backlog (int, optional): The maximum number of queued connections
passed to socket.listen(). Defaults to `100`.
register_sys_signals (bool, optional): Register SIGINT and SIGTERM.
Defaults to `True`.
run_multiple (bool, optional): Run multiple workers. Defaults
to `False`.
run_async (bool, optional): Return an AsyncServer object.
Defaults to `False`.
connections: Connections. Defaults to `None`.
signal (Signal, optional): Signal. Defaults to `Signal()`.
state: State. Defaults to `None`.
asyncio_server_kwargs (Optional[Dict[str, Union[int, float]]], optional):
key-value args for asyncio/uvloop create_server method. Defaults
to `None`.
version (str, optional): HTTP version. Defaults to `HTTP.VERSION_1`.
Raises:
ServerError: Cannot run HTTP/3 server without aioquic installed.
Returns:
AsyncioServer: AsyncioServer object if `run_async` is `True`.
""" # noqa: E501
if not run_async and not loop:
# create new event_loop after fork
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
setup_logging(app.debug, app.config.NO_COLOR, app.config.LOG_EXTRA)
if app.debug:
loop.set_debug(app.debug)
app.asgi = False
if version is HTTP.VERSION_3:
return _serve_http_3(host, port, app, loop, ssl)
return _serve_http_1(
host,
port,
app,
ssl,
sock,
unix,
reuse_port,
loop,
protocol,
backlog,
register_sys_signals,
run_multiple,
run_async,
connections,
signal,
state,
asyncio_server_kwargs,
)
def _setup_system_signals(
app: Sanic,
run_multiple: bool,
register_sys_signals: bool,
loop: asyncio.AbstractEventLoop,
) -> None: # no cov
signal_func(SIGINT, SIG_IGN)
signal_func(SIGTERM, SIG_IGN)
os.environ["SANIC_WORKER_PROCESS"] = "true"
# Register signals for graceful termination
if register_sys_signals:
if OS_IS_WINDOWS:
ctrlc_workaround_for_windows(app)
else:
for _signal in [SIGINT, SIGTERM]:
loop.add_signal_handler(
_signal, partial(app.stop, terminate=False)
)
def _run_shutdown_coro(loop, coro):
"""Run a shutdown coroutine, handling the case where the loop was stopped.
When loop.stop() is called during run_forever(), asyncio sets an internal
flag that causes the first run_until_complete() to fail. For standard
asyncio, we clear the _stopping flag. For uvloop (which doesn't expose
this flag), the first attempt may fail but subsequent attempts succeed.
"""
# Clear asyncio's stopped state if accessible
if hasattr(loop, "_stopping"):
loop._stopping = False
try:
loop.run_until_complete(coro())
except (RuntimeError, KeyboardInterrupt):
# RuntimeError: loop was stopped (uvloop behavior)
# KeyboardInterrupt: signal arrived during select (asyncio behavior)
# Try once more - this handles uvloop's behavior where the first
# run_until_complete after stop() fails but subsequent calls succeed.
if hasattr(loop, "_stopping"):
loop._stopping = False
try:
loop.run_until_complete(coro())
except (RuntimeError, KeyboardInterrupt):
# If it still fails, the loop is truly unusable
pass
def _run_server_forever(loop, before_stop, after_stop, cleanup, unix, pid):
try:
server_logger.info("Worker ready [%s]", pid)
loop.run_forever()
finally:
server_logger.info("Stopping worker [%s]", pid)
for _signal in [SIGINT, SIGTERM]:
try:
loop.remove_signal_handler(_signal)
except (NotImplementedError, OSError):
pass
_run_shutdown_coro(loop, before_stop)
if cleanup:
cleanup()
_run_shutdown_coro(loop, after_stop)
remove_unix_socket(unix)
loop.close()
server_logger.info("Worker complete [%s]", pid)
def _serve_http_1(
host,
port,
app,
ssl,
sock,
unix,
reuse_port,
loop,
protocol,
backlog,
register_sys_signals,
run_multiple,
run_async,
connections,
signal,
state,
asyncio_server_kwargs,
):
connections = connections if connections is not None else set()
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
server = partial(
protocol,
loop=loop,
connections=connections,
signal=signal,
app=app,
state=state,
unix=unix,
**protocol_kwargs,
)
asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {}
)
if OS_IS_WINDOWS and sock:
pid = os.getpid()
sock = sock.share(pid)
sock = socket.fromshare(sock)
# UNIX sockets are always bound by us (to preserve semantics between modes)
elif unix:
sock = bind_unix_socket(unix, backlog=backlog)
server_coroutine = loop.create_server(
server,
None if sock else host,
None if sock else port,
ssl=ssl,
reuse_port=reuse_port,
sock=sock,
backlog=backlog,
**asyncio_server_kwargs,
)
setup_ext(app)
if run_async:
return AsyncioServer(
app=app,
loop=loop,
serve_coro=server_coroutine,
connections=connections,
)
pid = os.getpid()
server_logger.info("Starting worker [%s]", pid)
loop.run_until_complete(app._startup())
loop.run_until_complete(app._server_event("init", "before"))
app.ack()
try:
http_server = loop.run_until_complete(server_coroutine)
except BaseException:
error_logger.exception("Unable to start server", exc_info=True)
return
def _cleanup():
# Wait for event loop to finish and all connections to drain
http_server.close()
loop.run_until_complete(http_server.wait_closed())
# Complete all tasks on the loop
signal.stopped = True
for connection in connections:
connection.close_if_idle()
# Gracefully shutdown timeout.
# We should provide graceful_shutdown_timeout,
# instead of letting connection hangs forever.
# Let's roughly calcucate time.
graceful = app.config.GRACEFUL_SHUTDOWN_TIMEOUT
start_shutdown: float = 0
while connections and (start_shutdown < graceful):
loop.run_until_complete(asyncio.sleep(0.1))
start_shutdown = start_shutdown + 0.1
app.shutdown_tasks(graceful - start_shutdown)
# Force close non-idle connection after waiting for
# graceful_shutdown_timeout
for conn in connections:
if hasattr(conn, "websocket") and conn.websocket:
conn.websocket.fail_connection(code=1001)
else:
conn.abort()
try:
app.set_serving(False)
except (BrokenPipeError, ConnectionResetError, EOFError):
pass
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
loop.run_until_complete(app._server_event("init", "after"))
app.set_serving(True)
_run_server_forever(
loop,
partial(app._server_event, "shutdown", "before"),
partial(app._server_event, "shutdown", "after"),
_cleanup,
unix,
pid,
)
def _serve_http_3(
host,
port,
app,
loop,
ssl,
register_sys_signals: bool = True,
run_multiple: bool = False,
):
if not HTTP3_AVAILABLE:
raise ServerError(
"Cannot run HTTP/3 server without aioquic installed. "
)
pid = os.getpid()
server_logger.info("Starting worker [%s]", pid)
protocol = partial(Http3Protocol, app=app)
ticket_store = SessionTicketStore()
ssl_context = get_ssl_context(app, ssl)
config = get_config(app, ssl_context)
coro = quic_serve(
host,
port,
configuration=config,
create_protocol=protocol,
session_ticket_fetcher=ticket_store.pop,
session_ticket_handler=ticket_store.add,
)
server = AsyncioServer(app, loop, coro, [])
loop.run_until_complete(server.startup())
loop.run_until_complete(server.before_start())
app.ack()
loop.run_until_complete(server)
_setup_system_signals(app, run_multiple, register_sys_signals, loop)
loop.run_until_complete(server.after_start())
# TODO: Create connection cleanup and graceful shutdown
cleanup = None
_run_server_forever(
loop, server.before_stop, server.after_stop, cleanup, None, pid
)
def _build_protocol_kwargs(
protocol: type[asyncio.Protocol], config: Config
) -> dict[str, int | float]:
if hasattr(protocol, "websocket_handshake"):
return {
"websocket_max_size": config.WEBSOCKET_MAX_SIZE,
"websocket_ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
"websocket_ping_interval": config.WEBSOCKET_PING_INTERVAL,
}
return {}