485 lines
15 KiB
Python
485 lines
15 KiB
Python
|
import asyncio
|
||
|
import logging
|
||
|
import socket
|
||
|
import sys
|
||
|
from argparse import ArgumentParser
|
||
|
from collections.abc import Iterable
|
||
|
from importlib import import_module
|
||
|
from typing import Any, Awaitable, Callable, List, Optional, Type, Union, cast
|
||
|
|
||
|
from .abc import AbstractAccessLogger
|
||
|
from .helpers import all_tasks
|
||
|
from .log import access_logger
|
||
|
from .web_app import Application, CleanupError
|
||
|
from .web_exceptions import (
|
||
|
HTTPAccepted,
|
||
|
HTTPBadGateway,
|
||
|
HTTPBadRequest,
|
||
|
HTTPClientError,
|
||
|
HTTPConflict,
|
||
|
HTTPCreated,
|
||
|
HTTPError,
|
||
|
HTTPException,
|
||
|
HTTPExpectationFailed,
|
||
|
HTTPFailedDependency,
|
||
|
HTTPForbidden,
|
||
|
HTTPFound,
|
||
|
HTTPGatewayTimeout,
|
||
|
HTTPGone,
|
||
|
HTTPInsufficientStorage,
|
||
|
HTTPInternalServerError,
|
||
|
HTTPLengthRequired,
|
||
|
HTTPMethodNotAllowed,
|
||
|
HTTPMisdirectedRequest,
|
||
|
HTTPMovedPermanently,
|
||
|
HTTPMultipleChoices,
|
||
|
HTTPNetworkAuthenticationRequired,
|
||
|
HTTPNoContent,
|
||
|
HTTPNonAuthoritativeInformation,
|
||
|
HTTPNotAcceptable,
|
||
|
HTTPNotExtended,
|
||
|
HTTPNotFound,
|
||
|
HTTPNotImplemented,
|
||
|
HTTPNotModified,
|
||
|
HTTPOk,
|
||
|
HTTPPartialContent,
|
||
|
HTTPPaymentRequired,
|
||
|
HTTPPermanentRedirect,
|
||
|
HTTPPreconditionFailed,
|
||
|
HTTPPreconditionRequired,
|
||
|
HTTPProxyAuthenticationRequired,
|
||
|
HTTPRedirection,
|
||
|
HTTPRequestEntityTooLarge,
|
||
|
HTTPRequestHeaderFieldsTooLarge,
|
||
|
HTTPRequestRangeNotSatisfiable,
|
||
|
HTTPRequestTimeout,
|
||
|
HTTPRequestURITooLong,
|
||
|
HTTPResetContent,
|
||
|
HTTPSeeOther,
|
||
|
HTTPServerError,
|
||
|
HTTPServiceUnavailable,
|
||
|
HTTPSuccessful,
|
||
|
HTTPTemporaryRedirect,
|
||
|
HTTPTooManyRequests,
|
||
|
HTTPUnauthorized,
|
||
|
HTTPUnavailableForLegalReasons,
|
||
|
HTTPUnprocessableEntity,
|
||
|
HTTPUnsupportedMediaType,
|
||
|
HTTPUpgradeRequired,
|
||
|
HTTPUseProxy,
|
||
|
HTTPVariantAlsoNegotiates,
|
||
|
HTTPVersionNotSupported,
|
||
|
)
|
||
|
from .web_fileresponse import FileResponse
|
||
|
from .web_log import AccessLogger
|
||
|
from .web_middlewares import middleware, normalize_path_middleware
|
||
|
from .web_protocol import (
|
||
|
PayloadAccessError,
|
||
|
RequestHandler,
|
||
|
RequestPayloadError,
|
||
|
)
|
||
|
from .web_request import BaseRequest, FileField, Request
|
||
|
from .web_response import (
|
||
|
ContentCoding,
|
||
|
Response,
|
||
|
StreamResponse,
|
||
|
json_response,
|
||
|
)
|
||
|
from .web_routedef import (
|
||
|
AbstractRouteDef,
|
||
|
RouteDef,
|
||
|
RouteTableDef,
|
||
|
StaticDef,
|
||
|
delete,
|
||
|
get,
|
||
|
head,
|
||
|
options,
|
||
|
patch,
|
||
|
post,
|
||
|
put,
|
||
|
route,
|
||
|
static,
|
||
|
view,
|
||
|
)
|
||
|
from .web_runner import (
|
||
|
AppRunner,
|
||
|
BaseRunner,
|
||
|
BaseSite,
|
||
|
GracefulExit,
|
||
|
ServerRunner,
|
||
|
SockSite,
|
||
|
TCPSite,
|
||
|
UnixSite,
|
||
|
)
|
||
|
from .web_server import Server
|
||
|
from .web_urldispatcher import (
|
||
|
AbstractResource,
|
||
|
AbstractRoute,
|
||
|
DynamicResource,
|
||
|
PlainResource,
|
||
|
Resource,
|
||
|
ResourceRoute,
|
||
|
StaticResource,
|
||
|
UrlDispatcher,
|
||
|
UrlMappingMatchInfo,
|
||
|
View,
|
||
|
)
|
||
|
from .web_ws import WebSocketReady, WebSocketResponse, WSMsgType
|
||
|
|
||
|
__all__ = (
|
||
|
# web_app
|
||
|
'Application',
|
||
|
'CleanupError',
|
||
|
# web_exceptions
|
||
|
'HTTPAccepted',
|
||
|
'HTTPBadGateway',
|
||
|
'HTTPBadRequest',
|
||
|
'HTTPClientError',
|
||
|
'HTTPConflict',
|
||
|
'HTTPCreated',
|
||
|
'HTTPError',
|
||
|
'HTTPException',
|
||
|
'HTTPExpectationFailed',
|
||
|
'HTTPFailedDependency',
|
||
|
'HTTPForbidden',
|
||
|
'HTTPFound',
|
||
|
'HTTPGatewayTimeout',
|
||
|
'HTTPGone',
|
||
|
'HTTPInsufficientStorage',
|
||
|
'HTTPInternalServerError',
|
||
|
'HTTPLengthRequired',
|
||
|
'HTTPMethodNotAllowed',
|
||
|
'HTTPMisdirectedRequest',
|
||
|
'HTTPMovedPermanently',
|
||
|
'HTTPMultipleChoices',
|
||
|
'HTTPNetworkAuthenticationRequired',
|
||
|
'HTTPNoContent',
|
||
|
'HTTPNonAuthoritativeInformation',
|
||
|
'HTTPNotAcceptable',
|
||
|
'HTTPNotExtended',
|
||
|
'HTTPNotFound',
|
||
|
'HTTPNotImplemented',
|
||
|
'HTTPNotModified',
|
||
|
'HTTPOk',
|
||
|
'HTTPPartialContent',
|
||
|
'HTTPPaymentRequired',
|
||
|
'HTTPPermanentRedirect',
|
||
|
'HTTPPreconditionFailed',
|
||
|
'HTTPPreconditionRequired',
|
||
|
'HTTPProxyAuthenticationRequired',
|
||
|
'HTTPRedirection',
|
||
|
'HTTPRequestEntityTooLarge',
|
||
|
'HTTPRequestHeaderFieldsTooLarge',
|
||
|
'HTTPRequestRangeNotSatisfiable',
|
||
|
'HTTPRequestTimeout',
|
||
|
'HTTPRequestURITooLong',
|
||
|
'HTTPResetContent',
|
||
|
'HTTPSeeOther',
|
||
|
'HTTPServerError',
|
||
|
'HTTPServiceUnavailable',
|
||
|
'HTTPSuccessful',
|
||
|
'HTTPTemporaryRedirect',
|
||
|
'HTTPTooManyRequests',
|
||
|
'HTTPUnauthorized',
|
||
|
'HTTPUnavailableForLegalReasons',
|
||
|
'HTTPUnprocessableEntity',
|
||
|
'HTTPUnsupportedMediaType',
|
||
|
'HTTPUpgradeRequired',
|
||
|
'HTTPUseProxy',
|
||
|
'HTTPVariantAlsoNegotiates',
|
||
|
'HTTPVersionNotSupported',
|
||
|
# web_fileresponse
|
||
|
'FileResponse',
|
||
|
# web_middlewares
|
||
|
'middleware',
|
||
|
'normalize_path_middleware',
|
||
|
# web_protocol
|
||
|
'PayloadAccessError',
|
||
|
'RequestHandler',
|
||
|
'RequestPayloadError',
|
||
|
# web_request
|
||
|
'BaseRequest',
|
||
|
'FileField',
|
||
|
'Request',
|
||
|
# web_response
|
||
|
'ContentCoding',
|
||
|
'Response',
|
||
|
'StreamResponse',
|
||
|
'json_response',
|
||
|
# web_routedef
|
||
|
'AbstractRouteDef',
|
||
|
'RouteDef',
|
||
|
'RouteTableDef',
|
||
|
'StaticDef',
|
||
|
'delete',
|
||
|
'get',
|
||
|
'head',
|
||
|
'options',
|
||
|
'patch',
|
||
|
'post',
|
||
|
'put',
|
||
|
'route',
|
||
|
'static',
|
||
|
'view',
|
||
|
# web_runner
|
||
|
'AppRunner',
|
||
|
'BaseRunner',
|
||
|
'BaseSite',
|
||
|
'GracefulExit',
|
||
|
'ServerRunner',
|
||
|
'SockSite',
|
||
|
'TCPSite',
|
||
|
'UnixSite',
|
||
|
# web_server
|
||
|
'Server',
|
||
|
# web_urldispatcher
|
||
|
'AbstractResource',
|
||
|
'AbstractRoute',
|
||
|
'DynamicResource',
|
||
|
'PlainResource',
|
||
|
'Resource',
|
||
|
'ResourceRoute',
|
||
|
'StaticResource',
|
||
|
'UrlDispatcher',
|
||
|
'UrlMappingMatchInfo',
|
||
|
'View',
|
||
|
# web_ws
|
||
|
'WebSocketReady',
|
||
|
'WebSocketResponse',
|
||
|
'WSMsgType',
|
||
|
# web
|
||
|
'run_app',
|
||
|
)
|
||
|
|
||
|
|
||
|
try:
|
||
|
from ssl import SSLContext
|
||
|
except ImportError: # pragma: no cover
|
||
|
SSLContext = Any # type: ignore
|
||
|
|
||
|
|
||
|
async def _run_app(app: Union[Application, Awaitable[Application]], *,
|
||
|
host: Optional[str]=None,
|
||
|
port: Optional[int]=None,
|
||
|
path: Optional[str]=None,
|
||
|
sock: Optional[socket.socket]=None,
|
||
|
shutdown_timeout: float=60.0,
|
||
|
ssl_context: Optional[SSLContext]=None,
|
||
|
print: Callable[..., None]=print,
|
||
|
backlog: int=128,
|
||
|
access_log_class: Type[AbstractAccessLogger]=AccessLogger,
|
||
|
access_log_format: str=AccessLogger.LOG_FORMAT,
|
||
|
access_log: Optional[logging.Logger]=access_logger,
|
||
|
handle_signals: bool=True,
|
||
|
reuse_address: Optional[bool]=None,
|
||
|
reuse_port: Optional[bool]=None) -> None:
|
||
|
# A internal functio to actually do all dirty job for application running
|
||
|
if asyncio.iscoroutine(app):
|
||
|
app = await app # type: ignore
|
||
|
|
||
|
app = cast(Application, app)
|
||
|
|
||
|
runner = AppRunner(app, handle_signals=handle_signals,
|
||
|
access_log_class=access_log_class,
|
||
|
access_log_format=access_log_format,
|
||
|
access_log=access_log)
|
||
|
|
||
|
await runner.setup()
|
||
|
|
||
|
sites = [] # type: List[BaseSite]
|
||
|
|
||
|
try:
|
||
|
if host is not None:
|
||
|
if isinstance(host, (str, bytes, bytearray, memoryview)):
|
||
|
sites.append(TCPSite(runner, host, port,
|
||
|
shutdown_timeout=shutdown_timeout,
|
||
|
ssl_context=ssl_context,
|
||
|
backlog=backlog,
|
||
|
reuse_address=reuse_address,
|
||
|
reuse_port=reuse_port))
|
||
|
else:
|
||
|
for h in host:
|
||
|
sites.append(TCPSite(runner, h, port,
|
||
|
shutdown_timeout=shutdown_timeout,
|
||
|
ssl_context=ssl_context,
|
||
|
backlog=backlog,
|
||
|
reuse_address=reuse_address,
|
||
|
reuse_port=reuse_port))
|
||
|
elif path is None and sock is None or port is not None:
|
||
|
sites.append(TCPSite(runner, port=port,
|
||
|
shutdown_timeout=shutdown_timeout,
|
||
|
ssl_context=ssl_context, backlog=backlog,
|
||
|
reuse_address=reuse_address,
|
||
|
reuse_port=reuse_port))
|
||
|
|
||
|
if path is not None:
|
||
|
if isinstance(path, (str, bytes, bytearray, memoryview)):
|
||
|
sites.append(UnixSite(runner, path,
|
||
|
shutdown_timeout=shutdown_timeout,
|
||
|
ssl_context=ssl_context,
|
||
|
backlog=backlog))
|
||
|
else:
|
||
|
for p in path:
|
||
|
sites.append(UnixSite(runner, p,
|
||
|
shutdown_timeout=shutdown_timeout,
|
||
|
ssl_context=ssl_context,
|
||
|
backlog=backlog))
|
||
|
|
||
|
if sock is not None:
|
||
|
if not isinstance(sock, Iterable):
|
||
|
sites.append(SockSite(runner, sock,
|
||
|
shutdown_timeout=shutdown_timeout,
|
||
|
ssl_context=ssl_context,
|
||
|
backlog=backlog))
|
||
|
else:
|
||
|
for s in sock:
|
||
|
sites.append(SockSite(runner, s,
|
||
|
shutdown_timeout=shutdown_timeout,
|
||
|
ssl_context=ssl_context,
|
||
|
backlog=backlog))
|
||
|
for site in sites:
|
||
|
await site.start()
|
||
|
|
||
|
if print: # pragma: no branch
|
||
|
names = sorted(str(s.name) for s in runner.sites)
|
||
|
print("======== Running on {} ========\n"
|
||
|
"(Press CTRL+C to quit)".format(', '.join(names)))
|
||
|
while True:
|
||
|
await asyncio.sleep(3600) # sleep forever by 1 hour intervals
|
||
|
finally:
|
||
|
await runner.cleanup()
|
||
|
|
||
|
|
||
|
def _cancel_all_tasks(loop: asyncio.AbstractEventLoop) -> None:
|
||
|
to_cancel = all_tasks(loop)
|
||
|
if not to_cancel:
|
||
|
return
|
||
|
|
||
|
for task in to_cancel:
|
||
|
task.cancel()
|
||
|
|
||
|
loop.run_until_complete(
|
||
|
asyncio.gather(*to_cancel, loop=loop, return_exceptions=True))
|
||
|
|
||
|
for task in to_cancel:
|
||
|
if task.cancelled():
|
||
|
continue
|
||
|
if task.exception() is not None:
|
||
|
loop.call_exception_handler({
|
||
|
'message': 'unhandled exception during asyncio.run() shutdown',
|
||
|
'exception': task.exception(),
|
||
|
'task': task,
|
||
|
})
|
||
|
|
||
|
|
||
|
def run_app(app: Union[Application, Awaitable[Application]], *,
|
||
|
host: Optional[str]=None,
|
||
|
port: Optional[int]=None,
|
||
|
path: Optional[str]=None,
|
||
|
sock: Optional[socket.socket]=None,
|
||
|
shutdown_timeout: float=60.0,
|
||
|
ssl_context: Optional[SSLContext]=None,
|
||
|
print: Callable[..., None]=print,
|
||
|
backlog: int=128,
|
||
|
access_log_class: Type[AbstractAccessLogger]=AccessLogger,
|
||
|
access_log_format: str=AccessLogger.LOG_FORMAT,
|
||
|
access_log: Optional[logging.Logger]=access_logger,
|
||
|
handle_signals: bool=True,
|
||
|
reuse_address: Optional[bool]=None,
|
||
|
reuse_port: Optional[bool]=None) -> None:
|
||
|
"""Run an app locally"""
|
||
|
loop = asyncio.get_event_loop()
|
||
|
|
||
|
# Configure if and only if in debugging mode and using the default logger
|
||
|
if loop.get_debug() and access_log and access_log.name == 'aiohttp.access':
|
||
|
if access_log.level == logging.NOTSET:
|
||
|
access_log.setLevel(logging.DEBUG)
|
||
|
if not access_log.hasHandlers():
|
||
|
access_log.addHandler(logging.StreamHandler())
|
||
|
|
||
|
try:
|
||
|
loop.run_until_complete(_run_app(app,
|
||
|
host=host,
|
||
|
port=port,
|
||
|
path=path,
|
||
|
sock=sock,
|
||
|
shutdown_timeout=shutdown_timeout,
|
||
|
ssl_context=ssl_context,
|
||
|
print=print,
|
||
|
backlog=backlog,
|
||
|
access_log_class=access_log_class,
|
||
|
access_log_format=access_log_format,
|
||
|
access_log=access_log,
|
||
|
handle_signals=handle_signals,
|
||
|
reuse_address=reuse_address,
|
||
|
reuse_port=reuse_port))
|
||
|
except (GracefulExit, KeyboardInterrupt): # pragma: no cover
|
||
|
pass
|
||
|
finally:
|
||
|
_cancel_all_tasks(loop)
|
||
|
if sys.version_info >= (3, 6): # don't use PY_36 to pass mypy
|
||
|
loop.run_until_complete(loop.shutdown_asyncgens())
|
||
|
loop.close()
|
||
|
|
||
|
|
||
|
def main(argv: List[str]) -> None:
|
||
|
arg_parser = ArgumentParser(
|
||
|
description="aiohttp.web Application server",
|
||
|
prog="aiohttp.web"
|
||
|
)
|
||
|
arg_parser.add_argument(
|
||
|
"entry_func",
|
||
|
help=("Callable returning the `aiohttp.web.Application` instance to "
|
||
|
"run. Should be specified in the 'module:function' syntax."),
|
||
|
metavar="entry-func"
|
||
|
)
|
||
|
arg_parser.add_argument(
|
||
|
"-H", "--hostname",
|
||
|
help="TCP/IP hostname to serve on (default: %(default)r)",
|
||
|
default="localhost"
|
||
|
)
|
||
|
arg_parser.add_argument(
|
||
|
"-P", "--port",
|
||
|
help="TCP/IP port to serve on (default: %(default)r)",
|
||
|
type=int,
|
||
|
default="8080"
|
||
|
)
|
||
|
arg_parser.add_argument(
|
||
|
"-U", "--path",
|
||
|
help="Unix file system path to serve on. Specifying a path will cause "
|
||
|
"hostname and port arguments to be ignored.",
|
||
|
)
|
||
|
args, extra_argv = arg_parser.parse_known_args(argv)
|
||
|
|
||
|
# Import logic
|
||
|
mod_str, _, func_str = args.entry_func.partition(":")
|
||
|
if not func_str or not mod_str:
|
||
|
arg_parser.error(
|
||
|
"'entry-func' not in 'module:function' syntax"
|
||
|
)
|
||
|
if mod_str.startswith("."):
|
||
|
arg_parser.error("relative module names not supported")
|
||
|
try:
|
||
|
module = import_module(mod_str)
|
||
|
except ImportError as ex:
|
||
|
arg_parser.error("unable to import %s: %s" % (mod_str, ex))
|
||
|
try:
|
||
|
func = getattr(module, func_str)
|
||
|
except AttributeError:
|
||
|
arg_parser.error("module %r has no attribute %r" % (mod_str, func_str))
|
||
|
|
||
|
# Compatibility logic
|
||
|
if args.path is not None and not hasattr(socket, 'AF_UNIX'):
|
||
|
arg_parser.error("file system paths not supported by your operating"
|
||
|
" environment")
|
||
|
|
||
|
logging.basicConfig(level=logging.DEBUG)
|
||
|
|
||
|
app = func(extra_argv)
|
||
|
run_app(app, host=args.hostname, port=args.port, path=args.path)
|
||
|
arg_parser.exit(message="Stopped\n")
|
||
|
|
||
|
|
||
|
if __name__ == "__main__": # pragma: no branch
|
||
|
main(sys.argv[1:]) # pragma: no cover
|