mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 08:56:17 +00:00
Embedded server using poorly designed wrappers on some WSGI servers
This commit is contained in:
parent
359e391fcc
commit
f8c3d99e87
2
setup.py
2
setup.py
@ -12,6 +12,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
reqs = [
|
||||
"click",
|
||||
"flask>=0.11",
|
||||
"pony>=0.7.6",
|
||||
"Pillow",
|
||||
@ -37,6 +38,7 @@ setup(
|
||||
"console_scripts": [
|
||||
"supysonic-cli=supysonic.cli:main",
|
||||
"supysonic-daemon=supysonic.daemon:main",
|
||||
"supysonic-server=supysonic.server:main"
|
||||
]
|
||||
},
|
||||
zip_safe=False,
|
||||
|
127
supysonic/server/__init__.py
Normal file
127
supysonic/server/__init__.py
Normal file
@ -0,0 +1,127 @@
|
||||
# This file is part of Supysonic.
|
||||
# Supysonic is a Python implementation of the Subsonic server API.
|
||||
#
|
||||
# Copyright (C) 2021 Alban 'spl0k' Féron
|
||||
#
|
||||
# Distributed under terms of the GNU AGPLv3 license.
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from click import command, option, Option
|
||||
from click.exceptions import UsageError, ClickException
|
||||
from click.types import Choice
|
||||
|
||||
from ..web import create_application
|
||||
|
||||
_servers = [
|
||||
e.name[:-3]
|
||||
for e in os.scandir(os.path.dirname(__file__))
|
||||
if not e.name.startswith("_") and e.name.endswith(".py")
|
||||
]
|
||||
|
||||
|
||||
class MutuallyExclusiveOption(Option):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
|
||||
help = kwargs.get("help", "")
|
||||
if self.mutually_exclusive:
|
||||
ex_str = ", ".join(self.mutually_exclusive)
|
||||
kwargs[
|
||||
"help"
|
||||
] = "{} NOTE: This argument is mutually exclusive with arguments: [{}].".format(
|
||||
help, ex_str
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def handle_parse_result(self, ctx, opts, args):
|
||||
if self.mutually_exclusive.intersection(opts) and self.name in opts:
|
||||
raise UsageError(
|
||||
"Illegal usage: `{}` is mutually exclusive with arguments `{}`.".format(
|
||||
self.name, ", ".join(self.mutually_exclusive)
|
||||
)
|
||||
)
|
||||
|
||||
return super().handle_parse_result(ctx, opts, args)
|
||||
|
||||
|
||||
def get_server(name):
|
||||
return importlib.import_module("." + name, __package__).server
|
||||
|
||||
|
||||
def find_first_available_server():
|
||||
for module in _servers:
|
||||
try:
|
||||
return get_server(module)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@command()
|
||||
@option(
|
||||
"-S",
|
||||
"--server",
|
||||
type=Choice(_servers),
|
||||
help="Specify which WSGI server to use. Pick the first available if none is set",
|
||||
)
|
||||
@option(
|
||||
"-h",
|
||||
"--host",
|
||||
default="0.0.0.0",
|
||||
show_default=True,
|
||||
help="Hostname or IP address on which to listen",
|
||||
cls=MutuallyExclusiveOption,
|
||||
mutually_exclusive=("socket",),
|
||||
)
|
||||
@option(
|
||||
"-p",
|
||||
"--port",
|
||||
default=5722,
|
||||
show_default=True,
|
||||
help="TCP port on which to listen",
|
||||
cls=MutuallyExclusiveOption,
|
||||
mutually_exclusive=("socket",),
|
||||
)
|
||||
@option(
|
||||
"-s",
|
||||
"--socket",
|
||||
help="Unix socket on which to bind to, Can't be used with --host and --port",
|
||||
cls=MutuallyExclusiveOption,
|
||||
mutually_exclusive=("host", "port"),
|
||||
)
|
||||
@option(
|
||||
"--processes",
|
||||
type=int,
|
||||
help="Number of processes to spawn. May not be supported by all servers",
|
||||
)
|
||||
@option(
|
||||
"--threads",
|
||||
type=int,
|
||||
help="Number of threads used to process application logic. May not be supported by all servers",
|
||||
)
|
||||
def main(server, host, port, socket, processes, threads):
|
||||
if server is None:
|
||||
server = find_first_available_server()
|
||||
if server is None:
|
||||
raise ClickException(
|
||||
"Couldn't load any server, please install one of {}".format(_servers)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
server = get_server(server)
|
||||
except ImportError:
|
||||
raise ClickException(
|
||||
"Couldn't load {}, please install it first".format(server)
|
||||
)
|
||||
|
||||
if socket is not None:
|
||||
host = None
|
||||
port = None
|
||||
|
||||
app = create_application()
|
||||
server(
|
||||
app, host=host, port=port, socket=socket, processes=processes, threads=threads
|
||||
).run()
|
11
supysonic/server/__main__.py
Normal file
11
supysonic/server/__main__.py
Normal file
@ -0,0 +1,11 @@
|
||||
# This file is part of Supysonic.
|
||||
# Supysonic is a Python implementation of the Subsonic server API.
|
||||
#
|
||||
# Copyright (C) 2021 Alban 'spl0k' Féron
|
||||
#
|
||||
# Distributed under terms of the GNU AGPLv3 license.
|
||||
|
||||
from . import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
32
supysonic/server/_base.py
Normal file
32
supysonic/server/_base.py
Normal file
@ -0,0 +1,32 @@
|
||||
# This file is part of Supysonic.
|
||||
# Supysonic is a Python implementation of the Subsonic server API.
|
||||
#
|
||||
# Copyright (C) 2021 Alban 'spl0k' Féron
|
||||
#
|
||||
# Distributed under terms of the GNU AGPLv3 license.
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class BaseServer(metaclass=ABCMeta):
|
||||
def __init__(
|
||||
self, app, *, host=None, port=None, socket=None, processes=None, threads=None
|
||||
):
|
||||
self._app = app
|
||||
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._socket = socket
|
||||
self._processes = processes
|
||||
self._threads = threads
|
||||
|
||||
@abstractmethod
|
||||
def _build_kwargs(self):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def _run(self, **kwargs):
|
||||
...
|
||||
|
||||
def run(self):
|
||||
self._run(**self._build_kwargs())
|
39
supysonic/server/gevent.py
Normal file
39
supysonic/server/gevent.py
Normal file
@ -0,0 +1,39 @@
|
||||
# This file is part of Supysonic.
|
||||
# Supysonic is a Python implementation of the Subsonic server API.
|
||||
#
|
||||
# Copyright (C) 2021 Alban 'spl0k' Féron
|
||||
#
|
||||
# Distributed under terms of the GNU AGPLv3 license.
|
||||
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from gevent import socket
|
||||
from gevent.pywsgi import WSGIServer
|
||||
|
||||
from ._base import BaseServer
|
||||
|
||||
|
||||
class GeventServer(BaseServer):
|
||||
def _build_kwargs(self):
|
||||
rv = {"application": self._app}
|
||||
|
||||
if self._socket is not None:
|
||||
if os.path.exists(self._socket):
|
||||
os.remove(self._socket)
|
||||
|
||||
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
listener.bind(self._socket)
|
||||
listener.listen()
|
||||
|
||||
rv["listener"] = listener
|
||||
else:
|
||||
rv["listener"] = (self._host, self._port)
|
||||
|
||||
return rv
|
||||
|
||||
def _run(self, **kwargs):
|
||||
return WSGIServer(**kwargs).serve_forever()
|
||||
|
||||
|
||||
server = GeventServer
|
53
supysonic/server/gunicorn.py
Normal file
53
supysonic/server/gunicorn.py
Normal file
@ -0,0 +1,53 @@
|
||||
# This file is part of Supysonic.
|
||||
# Supysonic is a Python implementation of the Subsonic server API.
|
||||
#
|
||||
# Copyright (C) 2021 Alban 'spl0k' Féron
|
||||
#
|
||||
# Distributed under terms of the GNU AGPLv3 license.
|
||||
|
||||
from gunicorn.app.base import BaseApplication
|
||||
|
||||
from ._base import BaseServer
|
||||
|
||||
|
||||
class GunicornApp(BaseApplication):
|
||||
def __init__(self, app, **config):
|
||||
self.__app = app
|
||||
self.__config = config
|
||||
|
||||
super().__init__()
|
||||
|
||||
def load(self):
|
||||
return self.__app
|
||||
|
||||
def load_config(self):
|
||||
socket = self.__config["socket"]
|
||||
host = self.__config["host"]
|
||||
port = self.__config["port"]
|
||||
processes = self.__config["processes"]
|
||||
threads = self.__config["threads"]
|
||||
|
||||
if socket is not None:
|
||||
self.cfg.set("bind", "unix:{}".format(socket))
|
||||
else:
|
||||
self.cfg.set("bind", "{}:{}".format(host, port))
|
||||
|
||||
if processes is not None:
|
||||
self.cfg.set("workers", processes)
|
||||
if threads is not None:
|
||||
self.cfg.set("threads", threads)
|
||||
|
||||
|
||||
class GunicornServer(BaseServer):
|
||||
def __init__(self, app, **kwargs):
|
||||
super().__init__(app, **kwargs)
|
||||
self.__server = GunicornApp(app, **kwargs)
|
||||
|
||||
def _build_kwargs(self):
|
||||
return {}
|
||||
|
||||
def _run(self, **kwargs):
|
||||
return self.__server.run()
|
||||
|
||||
|
||||
server = GunicornServer
|
32
supysonic/server/waitress.py
Normal file
32
supysonic/server/waitress.py
Normal file
@ -0,0 +1,32 @@
|
||||
# This file is part of Supysonic.
|
||||
# Supysonic is a Python implementation of the Subsonic server API.
|
||||
#
|
||||
# Copyright (C) 2021 Alban 'spl0k' Féron
|
||||
#
|
||||
# Distributed under terms of the GNU AGPLv3 license.
|
||||
|
||||
from waitress import serve
|
||||
|
||||
from ._base import BaseServer
|
||||
|
||||
|
||||
class WaitressServer(BaseServer):
|
||||
def _build_kwargs(self):
|
||||
rv = {"app": self._app}
|
||||
|
||||
if self._host is not None:
|
||||
rv["host"] = self._host
|
||||
if self._port is not None:
|
||||
rv["port"] = self._port
|
||||
if self._socket is not None:
|
||||
rv["unix_socket"] = self._socket
|
||||
if self._threads is not None:
|
||||
rv["threads"] = self._threads
|
||||
|
||||
return rv
|
||||
|
||||
def _run(self, **kwargs):
|
||||
return serve(**kwargs)
|
||||
|
||||
|
||||
server = WaitressServer
|
0
tests/issue221.py
Executable file → Normal file
0
tests/issue221.py
Executable file → Normal file
Loading…
Reference in New Issue
Block a user