Skip to content

Commit

Permalink
Merge pull request #2430 from Exirel/common-connection-errors-output
Browse files Browse the repository at this point in the history
irc: Improve output for common connection errors
  • Loading branch information
dgw authored May 23, 2023
2 parents 6e64c96 + c4a34f7 commit ccb6138
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 19 deletions.
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:
"""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
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.',
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)
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.',
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

0 comments on commit ccb6138

Please sign in to comment.