1
0
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:
Alban Féron 2021-11-01 17:41:56 +01:00
parent 359e391fcc
commit f8c3d99e87
No known key found for this signature in database
GPG Key ID: 8CE0313646D16165
8 changed files with 296 additions and 0 deletions

View File

@ -12,6 +12,7 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
reqs = [ reqs = [
"click",
"flask>=0.11", "flask>=0.11",
"pony>=0.7.6", "pony>=0.7.6",
"Pillow", "Pillow",
@ -37,6 +38,7 @@ setup(
"console_scripts": [ "console_scripts": [
"supysonic-cli=supysonic.cli:main", "supysonic-cli=supysonic.cli:main",
"supysonic-daemon=supysonic.daemon:main", "supysonic-daemon=supysonic.daemon:main",
"supysonic-server=supysonic.server:main"
] ]
}, },
zip_safe=False, zip_safe=False,

View 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()

View 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
View 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())

View 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

View 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

View 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
View File