Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

irc: Improve output for common connection errors #2430

Merged
merged 5 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion sopel/irc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,13 +448,19 @@ def on_message_sent(self, raw: str) -> None:
)
self.dispatch(pretrigger)

@deprecated(
'This method was used to log errors with asynchat; '
'use logging.getLogger("sopel.exception") instead.',
version='8.0',
removed_in='9.0',
)
def on_error(self) -> None:
"""Handle any uncaptured error in the bot itself."""
LOGGER.error('Fatal error in core, please review exceptions log.')

err_log = logging.getLogger('sopel.exceptions')
err_log.error(
'Fatal error in core, handle_error() was called.\n'
'Fatal error in core, bot.on_error() was called.\n'
'Last Line:\n%s',
self.last_raw_line,
)
Expand Down
12 changes: 12 additions & 0 deletions sopel/irc/abstract_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations

import abc
import logging
from typing import Optional, TYPE_CHECKING

from .utils import safe
Expand All @@ -41,6 +42,17 @@ def __init__(self, bot: AbstractBot):
def is_connected(self) -> bool:
"""Tell if the backend is connected or not."""

def log_exception(self) -> None:
"""Log an exception to ``sopel.exceptions``.

The IRC backend must use this method to log any exception that isn't
caught by the bot itself (i.e. while handling messages), such as
connection errors, SSL errors, etc.
"""
err_log = logging.getLogger('sopel.exceptions')
err_log.exception('Exception in core')
err_log.error('----------------------------------------')

@abc.abstractmethod
def on_irc_error(self, pretrigger: PreTrigger) -> None:
"""Action to perform when the server sends an error event.
Expand Down
122 changes: 104 additions & 18 deletions sopel/irc/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import asyncio
import logging
import signal
import socket
import ssl
import threading
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
Expand Down Expand Up @@ -56,7 +57,7 @@ def __init__(
):
super().__init__(bot)

def is_connected(self) -> False:
def is_connected(self) -> bool:
Exirel marked this conversation as resolved.
Show resolved Hide resolved
"""Check if the backend is connected to an IRC server.

**Always returns False:** This backend type is never connected.
Expand Down Expand Up @@ -377,40 +378,124 @@ def get_connection_kwargs(self) -> Dict:
'local_addr': self._source_address,
}

async def _run_forever(self) -> None:
self._loop = asyncio.get_running_loop()

# register signal handlers
for quit_signal in QUIT_SIGNALS:
self._loop.add_signal_handler(quit_signal, self._signal_quit)
for restart_signal in RESTART_SIGNALS:
self._loop.add_signal_handler(restart_signal, self._signal_restart)
async def _connect_to_server(
self, **connection_kwargs
) -> Tuple[
Optional[asyncio.StreamReader],
Optional[asyncio.StreamWriter],
]:
reader: Optional[asyncio.StreamReader] = None
writer: Optional[asyncio.StreamWriter] = None

# open connection
Exirel marked this conversation as resolved.
Show resolved Hide resolved
try:
self._reader, self._writer = await asyncio.open_connection(
**self.get_connection_kwargs(),
reader, writer = await asyncio.open_connection(
**connection_kwargs,
)

# SSL Errors (certificate verification and generic SSL errors)
except ssl.SSLCertVerificationError as err:
LOGGER.error(
'Unable to connect due to '
'SSL certificate verification failure: %s',
err,
)
except ssl.SSLError:
LOGGER.exception('Unable to connect due to SSL error.')
self.log_exception()
# tell the bot to quit without restart
self.bot.hasquit = True
self.bot.wantsrestart = False
return
except Exception:
LOGGER.exception('Unable to connect.')
except ssl.SSLError as err:
LOGGER.error('Unable to connect due to an SSL error: %s', err)
self.log_exception()
# tell the bot to quit without restart
self.bot.hasquit = True
self.bot.wantsrestart = False

# Specific connection error (invalid address and timeout)
except socket.gaierror as err:
LOGGER.error(
'Unable to connect due to invalid IRC server address: %s',
err,
)
LOGGER.error(
'You should verify that "%s:%s" is the correct address '
'to connect to the IRC server.',
Exirel marked this conversation as resolved.
Show resolved Hide resolved
connection_kwargs.get('host'),
connection_kwargs.get('port'),
)
self.log_exception()
# tell the bot to quit without restart
self.bot.hasquit = True
self.bot.wantsrestart = False
except TimeoutError as err:
LOGGER.error('Unable to connect due to a timeout: %s', err)
self.log_exception()
# tell the bot to quit without restart
self.bot.hasquit = True
self.bot.wantsrestart = False

# Generic connection error
except ConnectionError as err:
LOGGER.error('Unable to connect: %s', err)
self.log_exception()
# tell the bot to quit without restart
self.bot.hasquit = True
self.bot.wantsrestart = False

# Generic OSError (used for any unspecific connection error)
Exirel marked this conversation as resolved.
Show resolved Hide resolved
except OSError as err:
LOGGER.error('Unable to connect: %s', err)
LOGGER.error(
'You should verify that "%s:%s" is the correct address '
'to connect to the IRC server.',
Exirel marked this conversation as resolved.
Show resolved Hide resolved
connection_kwargs.get('host'),
connection_kwargs.get('port'),
)
self.log_exception()
# tell the bot to quit without restart
self.bot.hasquit = True
self.bot.wantsrestart = False

# Unexpected error
except Exception as err:
LOGGER.error(
'Unable to connect due to an unexpected error: %s',
err,
)
self.log_exception()
# until there is a way to prevent an infinite loop of connection
# error and reconnect, we have to tell the bot to quit here
# TODO: prevent infinite connection failure loop
self.bot.hasquit = True
self.bot.wantsrestart = False
return

self._connected = True
return reader, writer

async def _run_forever(self) -> None:
self._loop = asyncio.get_running_loop()
connection_kwargs = self.get_connection_kwargs()

# register signal handlers
for quit_signal in QUIT_SIGNALS:
self._loop.add_signal_handler(quit_signal, self._signal_quit)
for restart_signal in RESTART_SIGNALS:
self._loop.add_signal_handler(restart_signal, self._signal_restart)

# connect to socket
LOGGER.debug('Attempt connection.')
self._reader, self._writer = await self._connect_to_server(
**connection_kwargs
)
if not self._reader or not self._writer:
LOGGER.debug('Connection attempt failed.')
return

# on socket connection
LOGGER.debug('Connection registered.')
self._connected = True
self.bot.on_connect()

# read forever
LOGGER.debug('Waiting for messages...')
self._read_task = asyncio.create_task(self.read_forever())
try:
Expand All @@ -420,6 +505,7 @@ async def _run_forever(self) -> None:
else:
LOGGER.debug('Reader received EOF.')

# on socket disconnection
self._connected = False

# cancel timeout tasks
Expand Down