From 1f37a67c53233dc8e814d22620e1ff8a6fdb14b6 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 20 May 2026 15:21:37 +0200 Subject: health checks for vc Signed-off-by: salmonik --- cogs/music/audio_playback.py | 12 +++++ cogs/vclogs.py | 10 ++++ cogs/voice.py | 122 +++++++++++++++++++++++++++++++------------ 3 files changed, 111 insertions(+), 33 deletions(-) (limited to 'cogs') diff --git a/cogs/music/audio_playback.py b/cogs/music/audio_playback.py index 1313d01..0c885c3 100644 --- a/cogs/music/audio_playback.py +++ b/cogs/music/audio_playback.py @@ -173,6 +173,18 @@ class AudioPlaybackManager: vc = await target_channel.connect() except discord.ClientException: vc = discord.utils.get(ctx.bot.voice_clients, guild=guild) + elif vc.channel == target_channel: + from cogs.voice import is_voice_client_alive + if not is_voice_client_alive(vc, guild): + logger.warning("Stale voice client, reconnecting...") + try: + await vc.disconnect() + except Exception: + pass + try: + vc = await target_channel.connect() + except discord.ClientException: + vc = discord.utils.get(ctx.bot.voice_clients, guild=guild) elif vc.channel != target_channel: logger.info(f"Moving to voice channel: {target_channel.name}") await vc.move_to(target_channel) diff --git a/cogs/vclogs.py b/cogs/vclogs.py index e5ca202..b538265 100644 --- a/cogs/vclogs.py +++ b/cogs/vclogs.py @@ -129,6 +129,16 @@ class VcLogs(commands.Cog): is_connected = voice_status[1] if voice_status else False is_playing = voice_status[2] if voice_status else False + if is_connected and not is_playing: + from cogs.voice import is_voice_client_alive + if not is_voice_client_alive(voice_client, channel.guild): + logger.warning("Stale voice client in play_tts, reconnecting") + try: + await voice_client.disconnect() + except Exception: + pass + is_connected = False + if not is_connected: voice_client = await voice_mgr.join_channel(channel) voice_status = await voice_mgr.get_voice_status(channel.guild) diff --git a/cogs/voice.py b/cogs/voice.py index 046c2a8..6f9a269 100644 --- a/cogs/voice.py +++ b/cogs/voice.py @@ -24,60 +24,116 @@ def get_voice_manager(bot) -> "VoiceManager": return _voice_managers[bot_id] +def is_voice_client_alive(voice_client, guild): + """Check if a voice client connection is healthy. + + Verifies the client is connected, the channel still exists, + and the underlying voice socket is still open. + """ + if voice_client is None: + return False + if not voice_client.is_connected(): + return False + if voice_client.channel is None: + return False + if guild.get_channel(voice_client.channel.id) is None: + return False + + try: + ws = voice_client._connection + s = ws.socket + if s is None or s.fileno() == -1: + return False + except Exception: + return False + + return True + + @dataclass class VoiceManager: bot: Any # type: ignore[no-any-import] _voice_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False) + _connecting: set[int] = field(default_factory=set, init=False) async def join_channel(self, channel): """Join a voice channel by channel object.""" async with self._voice_lock: guild = channel.guild + guild_id = guild.id + + if guild_id in self._connecting: + logger.info(f"Connection already in progress for guild {guild_id}, skipping") + return None + voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) if voice_client and voice_client.is_connected(): if voice_client.channel == channel: - logger.info(f"Already connected to {channel}") - return voice_client - try: - await voice_client.move_to(channel) - logger.info(f"Moved to voice channel: {channel}") - return voice_client - except Exception as e: - logger.error(f"Failed to move voice client: {e}") - logger.info("Disconnecting after failed move attempt") + if is_voice_client_alive(voice_client, guild): + logger.info(f"Already connected to {channel}") + return voice_client + logger.warning(f"Stale connection to {channel}, reconnecting") try: await voice_client.disconnect() except Exception: pass - - await asyncio.sleep(0.3) - try: - voice_client = await channel.connect(timeout=30.0, reconnect=False) - logger.info(f"Connected to voice channel: {channel}") - return voice_client - except discord.ClientException as e: - if "Already connected" in str(e): - logger.warning(f"Stale voice state detected, forcing disconnect and reconnect: {e}") - voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) - if voice_client: + else: + try: + await voice_client.move_to(channel) + logger.info(f"Moved to voice channel: {channel}") + return voice_client + except Exception as e: + logger.error(f"Failed to move voice client: {e}") + logger.info("Disconnecting after failed move attempt") try: await voice_client.disconnect() except Exception: pass - await asyncio.sleep(0.3) - try: - voice_client = await channel.connect(timeout=30.0, reconnect=False) - logger.info(f"Reconnected to voice channel: {channel}") - return voice_client - except Exception as e2: - logger.error(f"Failed to reconnect after stale cleanup: {e2}") - return None - logger.error(f"Failed to connect to voice channel: {e}") - return None - except Exception as e: - logger.error(f"Failed to connect to voice channel: {e}") - return None + elif voice_client and not voice_client.is_connected(): + logger.info("Voice client exists but disconnected, cleaning up stale entry") + try: + await voice_client.disconnect() + except Exception: + pass + voice_client = None + + self._connecting.add(guild_id) + try: + await asyncio.sleep(0.3) + try: + voice_client = await channel.connect(timeout=30.0, reconnect=False) + logger.info(f"Connected to voice channel: {channel}") + return voice_client + except discord.ClientException as e: + if "Already connected" in str(e): + logger.warning(f"Stale voice state detected, forcing disconnect and reconnect: {e}") + voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) + if voice_client: + try: + await voice_client.disconnect() + except Exception: + pass + else: + try: + channel._state._remove_voice_client(guild.id) + except Exception: + pass + await asyncio.sleep(0.3) + try: + voice_client = await channel.connect(timeout=30.0, reconnect=False) + logger.info(f"Reconnected to voice channel: {channel}") + return voice_client + except Exception as e2: + logger.error(f"Failed to reconnect after stale cleanup: {e2}") + return None + logger.error(f"Failed to connect to voice channel: {e}") + return None + except Exception as e: + logger.error(f"Failed to connect to voice channel: {e}") + return None + finally: + self._connecting.discard(guild_id) async def join_by_channel_id(self, channel_id): """Join a voice channel by channel ID.""" -- cgit v1.2.3