Skip to content

Commit cc3acdf

Browse files
authored
Fix handling of connection errors during startup (CloudBotIRC#234)
1 parent 0827658 commit cc3acdf

3 files changed

Lines changed: 60 additions & 8 deletions

File tree

cloudbot/bot.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,11 @@ def run(self):
166166
# Initializes the bot, plugins and connections
167167
self.loop.run_until_complete(self._init_routine())
168168
# Wait till the bot stops. The stopped_future will be set to True to restart, False otherwise
169+
logger.debug("Init done")
169170
restart = self.loop.run_until_complete(self.stopped_future)
171+
logger.debug("Waiting for plugin unload")
170172
self.loop.run_until_complete(self.plugin_manager.unload_all())
173+
logger.debug("Unload complete")
171174
self.loop.close()
172175
return restart
173176

@@ -197,21 +200,34 @@ def stop(self, reason=None, *, restart=False):
197200

198201
self.observer.stop()
199202

203+
logger.debug("Stopping connect loops and shutting down clients")
200204
for connection in self.connections.values():
205+
connection.active = False
206+
if not connection.cancelled_future.done():
207+
connection.cancelled_future.set_result(None)
208+
201209
if not connection.connected:
202210
# Don't quit a connection that hasn't connected
203211
continue
204212
logger.debug("[{}] Closing connection.".format(connection.name))
205213

206214
connection.quit(reason)
207215

216+
logger.debug("Done.")
217+
218+
logger.debug("Sleeping to let clients quit")
208219
yield from asyncio.sleep(1.0) # wait for 'QUIT' calls to take affect
220+
logger.debug("Sleep done.")
209221

222+
logger.debug("Closing clients")
210223
for connection in self.connections.values():
211224
connection.close()
212225

226+
logger.debug("All clients closed")
227+
213228
self.running = False
214229
# Give the stopped_future a result, so that run() will exit
230+
logger.debug("Setting future result for shutdown")
215231
self.stopped_future.set_result(restart)
216232

217233
@asyncio.coroutine
@@ -238,8 +254,12 @@ def _init_routine(self):
238254

239255
self.observer.start()
240256

257+
for conn in self.connections.values():
258+
conn.active = True
259+
241260
# Connect to servers
242-
yield from asyncio.gather(*[conn.connect() for conn in self.connections.values()], loop=self.loop)
261+
yield from asyncio.gather(*[conn.try_connect() for conn in self.connections.values()], loop=self.loop)
262+
logger.debug("Connections created.")
243263

244264
# Activate web interface.
245265
if self.config.get("web", {}).get("enabled", False) and web_installed:

cloudbot/client.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import random
55

66
from cloudbot.permissions import PermissionManager
7+
from cloudbot.util import async_util
78

89
logger = logging.getLogger("cloudbot")
910

@@ -19,6 +20,13 @@ def _decorate(cls):
1920
return lambda cls: _decorate(cls)
2021

2122

23+
class ClientConnectError(Exception):
24+
def __init__(self, client_name, server):
25+
super().__init__("Unable to connect to client {} with server {}".format(client_name, server))
26+
self.client_name = client_name
27+
self.server = server
28+
29+
2230
class Client:
2331
"""
2432
A Client representing each connection the bot makes to a single server
@@ -71,6 +79,8 @@ def __init__(self, bot, name, nick, *, channels=None, config=None):
7179

7280
self._active = False
7381

82+
self.cancelled_future = async_util.create_future(self.loop)
83+
7484
def describe_server(self):
7585
raise NotImplementedError
7686

@@ -84,7 +94,7 @@ def auto_reconnect(self):
8494
@asyncio.coroutine
8595
def try_connect(self):
8696
timeout = 30
87-
while not self.connected:
97+
while self.active and not self.connected:
8898
try:
8999
yield from self.connect(timeout)
90100
except Exception:
@@ -185,3 +195,7 @@ def type(self):
185195
@property
186196
def active(self):
187197
return self._active
198+
199+
@active.setter
200+
def active(self, value):
201+
self._active = value

cloudbot/clients/irc.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
import logging
33
import random
44
import re
5+
import socket
56
import ssl
7+
import traceback
68
from _ssl import PROTOCOL_SSLv23
79
from functools import partial
810
from ssl import SSLContext
911

10-
from cloudbot.client import Client, client
12+
from cloudbot.client import Client, client, ClientConnectError
1113
from cloudbot.event import Event, EventType, IrcOutEvent
1214
from cloudbot.util import async_util
1315
from cloudbot.util.parsers.irc import Message
@@ -114,15 +116,30 @@ def auto_reconnect(self):
114116

115117
@asyncio.coroutine
116118
def try_connect(self):
117-
while not self.connected:
119+
while self.active and not self.connected:
118120
try:
119121
yield from self.connect(self._timeout)
120-
except (asyncio.TimeoutError, OSError):
121-
logger.exception("[%s] Error occurred while connecting", self.name)
122+
except (TimeoutError, asyncio.TimeoutError):
123+
logger.error("[%s] Timeout occurred while connecting to %s", self.name, self.describe_server())
124+
except (socket.error, socket.gaierror, OSError, ssl.SSLError):
125+
logger.error(
126+
"[%s] Error occurred while connecting to %s (%s)",
127+
self.name, self.describe_server(),
128+
traceback.format_exc().splitlines()[-1]
129+
)
130+
except Exception as e:
131+
raise ClientConnectError(self.name, self.describe_server()) from e
122132
else:
123133
break
124134

125-
yield from asyncio.sleep(random.randrange(self._timeout))
135+
sleep_time = random.randrange(self._timeout)
136+
canceller = asyncio.shield(self.cancelled_future)
137+
try:
138+
yield from asyncio.wait_for(
139+
canceller, timeout=sleep_time
140+
)
141+
except asyncio.CancelledError:
142+
pass
126143

127144
@asyncio.coroutine
128145
def connect(self, timeout=None):
@@ -182,7 +199,8 @@ def quit(self, reason=None, set_inactive=True):
182199

183200
def close(self):
184201
self.quit()
185-
self._protocol.close()
202+
if self._protocol:
203+
self._protocol.close()
186204

187205
def message(self, target, *messages):
188206
for text in messages:

0 commit comments

Comments
 (0)