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
|
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,
|
||||||
|
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