jellyfin-kodi/libraries/cheroot/cli.py

234 lines
6.4 KiB
Python

"""Command line tool for starting a Cheroot WSGI/HTTP server instance.
Basic usage::
# Start a server on 127.0.0.1:8000 with the default settings
# for the WSGI app myapp/wsgi.py:application()
cheroot myapp.wsgi
# Start a server on 0.0.0.0:9000 with 8 threads
# for the WSGI app myapp/wsgi.py:main_app()
cheroot myapp.wsgi:main_app --bind 0.0.0.0:9000 --threads 8
# Start a server for the cheroot.server.Gateway subclass
# myapp/gateway.py:HTTPGateway
cheroot myapp.gateway:HTTPGateway
# Start a server on the UNIX socket /var/spool/myapp.sock
cheroot myapp.wsgi --bind /var/spool/myapp.sock
# Start a server on the abstract UNIX socket CherootServer
cheroot myapp.wsgi --bind @CherootServer
"""
import argparse
from importlib import import_module
import os
import sys
import contextlib
import six
from . import server
from . import wsgi
__metaclass__ = type
class BindLocation:
"""A class for storing the bind location for a Cheroot instance."""
class TCPSocket(BindLocation):
"""TCPSocket."""
def __init__(self, address, port):
"""Initialize.
Args:
address (str): Host name or IP address
port (int): TCP port number
"""
self.bind_addr = address, port
class UnixSocket(BindLocation):
"""UnixSocket."""
def __init__(self, path):
"""Initialize."""
self.bind_addr = path
class AbstractSocket(BindLocation):
"""AbstractSocket."""
def __init__(self, addr):
"""Initialize."""
self.bind_addr = '\0{}'.format(self.abstract_socket)
class Application:
"""Application."""
@classmethod
def resolve(cls, full_path):
"""Read WSGI app/Gateway path string and import application module."""
mod_path, _, app_path = full_path.partition(':')
app = getattr(import_module(mod_path), app_path or 'application')
with contextlib.suppress(TypeError):
if issubclass(app, server.Gateway):
return GatewayYo(app)
return cls(app)
def __init__(self, wsgi_app):
"""Initialize."""
if not callable(wsgi_app):
raise TypeError(
'Application must be a callable object or '
'cheroot.server.Gateway subclass'
)
self.wsgi_app = wsgi_app
def server_args(self, parsed_args):
"""Return keyword args for Server class."""
args = {
arg: value
for arg, value in vars(parsed_args).items()
if not arg.startswith('_') and value is not None
}
args.update(vars(self))
return args
def server(self, parsed_args):
"""Server."""
return wsgi.Server(**self.server_args(parsed_args))
class GatewayYo:
"""Gateway."""
def __init__(self, gateway):
"""Init."""
self.gateway = gateway
def server(self, parsed_args):
"""Server."""
server_args = vars(self)
server_args['bind_addr'] = parsed_args['bind_addr']
if parsed_args.max is not None:
server_args['maxthreads'] = parsed_args.max
if parsed_args.numthreads is not None:
server_args['minthreads'] = parsed_args.numthreads
return server.HTTPServer(**server_args)
def parse_wsgi_bind_location(bind_addr_string):
"""Convert bind address string to a BindLocation."""
# try and match for an IP/hostname and port
match = six.moves.urllib.parse.urlparse('//{}'.format(bind_addr_string))
try:
addr = match.hostname
port = match.port
if addr is not None or port is not None:
return TCPSocket(addr, port)
except ValueError:
pass
# else, assume a UNIX socket path
# if the string begins with an @ symbol, use an abstract socket
if bind_addr_string.startswith('@'):
return AbstractSocket(bind_addr_string[1:])
return UnixSocket(path=bind_addr_string)
def parse_wsgi_bind_addr(bind_addr_string):
"""Convert bind address string to bind address parameter."""
return parse_wsgi_bind_location(bind_addr_string).bind_addr
_arg_spec = {
'_wsgi_app': dict(
metavar='APP_MODULE',
type=Application.resolve,
help='WSGI application callable or cheroot.server.Gateway subclass',
),
'--bind': dict(
metavar='ADDRESS',
dest='bind_addr',
type=parse_wsgi_bind_addr,
default='[::1]:8000',
help='Network interface to listen on (default: [::1]:8000)',
),
'--chdir': dict(
metavar='PATH',
type=os.chdir,
help='Set the working directory',
),
'--server-name': dict(
dest='server_name',
type=str,
help='Web server name to be advertised via Server HTTP header',
),
'--threads': dict(
metavar='INT',
dest='numthreads',
type=int,
help='Minimum number of worker threads',
),
'--max-threads': dict(
metavar='INT',
dest='max',
type=int,
help='Maximum number of worker threads',
),
'--timeout': dict(
metavar='INT',
dest='timeout',
type=int,
help='Timeout in seconds for accepted connections',
),
'--shutdown-timeout': dict(
metavar='INT',
dest='shutdown_timeout',
type=int,
help='Time in seconds to wait for worker threads to cleanly exit',
),
'--request-queue-size': dict(
metavar='INT',
dest='request_queue_size',
type=int,
help='Maximum number of queued connections',
),
'--accepted-queue-size': dict(
metavar='INT',
dest='accepted_queue_size',
type=int,
help='Maximum number of active requests in queue',
),
'--accepted-queue-timeout': dict(
metavar='INT',
dest='accepted_queue_timeout',
type=int,
help='Timeout in seconds for putting requests into queue',
),
}
def main():
"""Create a new Cheroot instance with arguments from the command line."""
parser = argparse.ArgumentParser(
description='Start an instance of the Cheroot WSGI/HTTP server.')
for arg, spec in _arg_spec.items():
parser.add_argument(arg, **spec)
raw_args = parser.parse_args()
# ensure cwd in sys.path
'' in sys.path or sys.path.insert(0, '')
# create a server based on the arguments provided
raw_args._wsgi_app.server(raw_args).safe_start()