[Audio] Refactor internal Lavalink server management (#2495)

* Refactor internal Lavalink server management

Killing many birds with one stone here.
- Made server manager into class-based API with two public methods: `start()` and `shutdown()`. Must be re-instantiated each time it is restarted.
- Using V3 universal Lavalink.jar hosted on Cog-Creators/Lavalink-Jars repository.
- Uses output of `java -jar Lavalink.jar --version` to check if a new jar needs to be downloaded.
- `ServerManager.start()` won't return until server is ready, i.e. when "Started Launcher in X seconds" message is printed to STDOUT.
- `shlex.quote()` is used so spaces in path to Lavalink.jar don't cause issues.
- Enabling external Lavalink will cause internal server to be terminated.
- Disabling internal Lavalink will no longer reset settings in config - instead, hard-coded values will be used when connecting to an internal server.
- Internal server will now run both WS and REST servers on port 2333, meaning one less port will need to be taken up.
- Now using `asyncio.subprocess` module so waiting on and reading from subprocesses can be done asynchronously.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>

* Don't use shlex.quote on Windows

Signed-off-by: Toby <tobyharradine@gmail.com>

* Don't use shlex.quote at all

I misread a note in the python docs and assumed it was best to use it. Turns out the note only applies to `asyncio.create_subprocess_shell`.

Signed-off-by: Toby <tobyharradine@gmail.com>

* Missed the port on the rebase

* Ignore invalid architectures and inform users when commands are used.

* Style fix
This commit is contained in:
Toby Harradine
2019-04-30 11:31:28 +10:00
committed by Will
parent c79b5e6179
commit 476f441c9b
4 changed files with 287 additions and 202 deletions

View File

@@ -14,6 +14,7 @@ import os
import random
import re
import time
from typing import Optional
import redbot.core
from redbot.core import Config, commands, checks, bank
from redbot.core.data_manager import cog_data_path
@@ -29,7 +30,7 @@ from redbot.core.utils.menus import (
)
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
from urllib.parse import urlparse
from .manager import shutdown_lavalink_server, start_lavalink_server, maybe_download_lavalink
from .manager import ServerManager
_ = Translator("Audio", __file__)
@@ -43,41 +44,45 @@ log = logging.getLogger("red.audio")
class Audio(commands.Cog):
"""Play audio through voice channels."""
_default_lavalink_settings = {
"host": "localhost",
"rest_port": 2333,
"ws_port": 2333,
"password": "youshallnotpass",
}
def __init__(self, bot):
super().__init__()
self.bot = bot
self.config = Config.get_conf(self, 2711759130, force_registration=True)
default_global = {
"host": "localhost",
"rest_port": "2333",
"ws_port": "2332",
"password": "youshallnotpass",
"status": False,
"current_version": redbot.core.VersionInfo.from_str("3.0.0a0").to_json(),
"use_external_lavalink": False,
"restrict": True,
"localpath": str(cog_data_path(raw_name="Audio")),
}
default_global = dict(
status=False,
use_external_lavalink=False,
restrict=True,
current_version=redbot.core.VersionInfo.from_str("3.0.0a0").to_json(),
localpath=str(cog_data_path(raw_name="Audio")),
**self._default_lavalink_settings,
)
default_guild = {
"disconnect": False,
"dj_enabled": False,
"dj_role": None,
"emptydc_enabled": False,
"emptydc_timer": 0,
"jukebox": False,
"jukebox_price": 0,
"maxlength": 0,
"playlists": {},
"notify": False,
"repeat": False,
"shuffle": False,
"thumbnail": False,
"volume": 100,
"vote_enabled": False,
"vote_percent": 0,
}
default_guild = dict(
disconnect=False,
dj_enabled=False,
dj_role=None,
emptydc_enabled=False,
emptydc_timer=0,
jukebox=False,
jukebox_price=0,
maxlength=0,
playlists={},
notify=False,
repeat=False,
shuffle=False,
thumbnail=False,
volume=100,
vote_enabled=False,
vote_percent=0,
)
self.config.register_guild(**default_guild)
self.config.register_global(**default_global)
@@ -86,9 +91,24 @@ class Audio(commands.Cog):
self._connect_task = None
self._disconnect_task = None
self._cleaned_up = False
self.spotify_token = None
self.play_lock = {}
self._manager: Optional[ServerManager] = None
async def cog_before_invoke(self, ctx):
if self.llsetup in [ctx.command, ctx.command.root_parent]:
pass
elif self._connect_task.cancelled:
await ctx.send(
"You have attempted to run Audio's Lavalink server on an unsupported"
" architecture. Only settings related commands will be available."
)
raise RuntimeError(
"Not running audio command due to invalid machine architecture for Lavalink."
)
async def initialize(self):
self._restart_connect()
self._disconnect_task = self.bot.loop.create_task(self.disconnect_timer())
@@ -103,16 +123,33 @@ class Audio(commands.Cog):
async def attempt_connect(self, timeout: int = 30):
while True: # run until success
external = await self.config.use_external_lavalink()
if not external:
shutdown_lavalink_server()
await maybe_download_lavalink(self.bot.loop, self)
await start_lavalink_server(self.bot.loop)
try:
if external is False:
settings = self._default_lavalink_settings
host = settings["host"]
password = settings["password"]
rest_port = settings["rest_port"]
ws_port = settings["ws_port"]
if self._manager is not None:
await self._manager.shutdown()
self._manager = ServerManager()
try:
await self._manager.start()
except RuntimeError as exc:
log.exception(
"Exception whilst starting internal Lavalink server, retrying...",
exc_info=exc,
)
await asyncio.sleep(1)
continue
except asyncio.CancelledError:
log.exception("Invalid machine architecture, cannot run Lavalink.")
break
else:
host = await self.config.host()
password = await self.config.password()
rest_port = await self.config.rest_port()
ws_port = await self.config.ws_port()
try:
await lavalink.initialize(
bot=self.bot,
host=host,
@@ -122,9 +159,10 @@ class Audio(commands.Cog):
timeout=timeout,
)
return # break infinite loop
except Exception:
if not external:
shutdown_lavalink_server()
except asyncio.TimeoutError:
log.error("Connecting to Lavalink server timed out, retrying...")
if external is False and self._manager is not None:
await self._manager.shutdown()
await asyncio.sleep(1) # prevent busylooping
async def event_handler(self, player, event_type, extra):
@@ -3104,19 +3142,16 @@ class Audio(commands.Cog):
await self.config.use_external_lavalink.set(not external)
if external:
await self.config.host.set("localhost")
await self.config.password.set("youshallnotpass")
await self.config.rest_port.set(2333)
await self.config.ws_port.set(2332)
embed = discord.Embed(
colour=await ctx.embed_colour(),
title=_("External lavalink server: {true_or_false}.").format(
true_or_false=not external
),
)
embed.set_footer(text=_("Defaults reset."))
await ctx.send(embed=embed)
else:
if self._manager is not None:
await self._manager.shutdown()
await self._embed_msg(
ctx,
_("External lavalink server: {true_or_false}.").format(true_or_false=not external),
@@ -3229,6 +3264,8 @@ class Audio(commands.Cog):
async def _check_external(self):
external = await self.config.use_external_lavalink()
if not external:
if self._manager is not None:
await self._manager.shutdown()
await self.config.use_external_lavalink.set(True)
return True
else:
@@ -3597,7 +3634,8 @@ class Audio(commands.Cog):
lavalink.unregister_event_listener(self.event_handler)
self.bot.loop.create_task(lavalink.close())
shutdown_lavalink_server()
if self._manager is not None:
self.bot.loop.create_task(self._manager.shutdown())
self._cleaned_up = True
__del__ = cog_unload