|
1
|
|
|
# coding=utf-8 |
|
2
|
|
|
# irc.py - An Utility IRC Bot |
|
3
|
|
|
# Copyright 2008, Sean B. Palmer, inamidst.com |
|
4
|
|
|
# Copyright 2012, Elsie Powell, http://embolalia.com |
|
5
|
|
|
# Copyright © 2012, Elad Alfassa <[email protected]> |
|
6
|
|
|
# |
|
7
|
|
|
# Licensed under the Eiffel Forum License 2. |
|
8
|
|
|
# |
|
9
|
|
|
# When working on core IRC protocol related features, consult protocol |
|
10
|
|
|
# documentation at http://www.irchelp.org/irchelp/rfc/ |
|
11
|
|
|
from __future__ import unicode_literals, absolute_import, print_function, division |
|
12
|
|
|
|
|
13
|
|
|
import sys |
|
14
|
|
|
import time |
|
15
|
|
|
import socket |
|
16
|
|
|
import asyncore |
|
17
|
|
|
import asynchat |
|
18
|
|
|
import os |
|
19
|
|
|
import logging |
|
20
|
|
|
from sopel.tools import Identifier |
|
21
|
|
|
from sopel.trigger import PreTrigger |
|
22
|
|
|
try: |
|
23
|
|
|
import ssl |
|
24
|
|
|
if not hasattr(ssl, 'match_hostname'): |
|
25
|
|
|
# Attempt to import ssl_match_hostname from python-backports |
|
26
|
|
|
import backports.ssl_match_hostname |
|
27
|
|
|
ssl.match_hostname = backports.ssl_match_hostname.match_hostname |
|
28
|
|
|
ssl.CertificateError = backports.ssl_match_hostname.CertificateError |
|
29
|
|
|
has_ssl = True |
|
30
|
|
|
except ImportError: |
|
31
|
|
|
# no SSL support |
|
32
|
|
|
has_ssl = False |
|
33
|
|
|
|
|
34
|
|
|
import errno |
|
35
|
|
|
import threading |
|
36
|
|
|
from datetime import datetime |
|
37
|
|
|
if sys.version_info.major >= 3: |
|
38
|
|
|
unicode = str |
|
39
|
|
|
|
|
40
|
|
|
|
|
41
|
|
|
__all__ = ['Bot'] |
|
42
|
|
|
|
|
43
|
|
|
LOGGER = logging.getLogger(__name__) |
|
44
|
|
|
|
|
45
|
|
|
|
|
46
|
|
|
class Bot(asynchat.async_chat): |
|
47
|
|
|
def __init__(self, config): |
|
48
|
|
|
ca_certs = config.core.ca_certs |
|
49
|
|
|
|
|
50
|
|
|
asynchat.async_chat.__init__(self) |
|
51
|
|
|
self.set_terminator(b'\n') |
|
52
|
|
|
self.buffer = '' |
|
53
|
|
|
|
|
54
|
|
|
self.nick = Identifier(config.core.nick) |
|
55
|
|
|
"""Sopel's current ``Identifier``. Changing this while Sopel is running is |
|
56
|
|
|
untested.""" |
|
57
|
|
|
self.user = config.core.user |
|
58
|
|
|
"""Sopel's user/ident.""" |
|
59
|
|
|
self.name = config.core.name |
|
60
|
|
|
"""Sopel's "real name", as used for whois.""" |
|
61
|
|
|
|
|
62
|
|
|
self.stack = {} |
|
63
|
|
|
self.ca_certs = ca_certs |
|
64
|
|
|
self.enabled_capabilities = set() |
|
65
|
|
|
self.hasquit = False |
|
66
|
|
|
self.wantsrestart = False |
|
67
|
|
|
|
|
68
|
|
|
self.sending = threading.RLock() |
|
69
|
|
|
self.writing_lock = threading.Lock() |
|
70
|
|
|
self.raw = None |
|
71
|
|
|
|
|
72
|
|
|
# Right now, only accounting for two op levels. |
|
73
|
|
|
# This might be expanded later. |
|
74
|
|
|
# These lists are filled in startup.py, as of right now. |
|
75
|
|
|
# Are these even touched at all anymore? Remove in 7.0. |
|
76
|
|
|
self.ops = dict() |
|
77
|
|
|
"""Deprecated. Use bot.channels instead.""" |
|
78
|
|
|
self.halfplus = dict() |
|
79
|
|
|
"""Deprecated. Use bot.channels instead.""" |
|
80
|
|
|
self.voices = dict() |
|
81
|
|
|
"""Deprecated. Use bot.channels instead.""" |
|
82
|
|
|
|
|
83
|
|
|
# We need this to prevent error loops in handle_error |
|
84
|
|
|
self.error_count = 0 |
|
85
|
|
|
|
|
86
|
|
|
self.connection_registered = False |
|
87
|
|
|
""" Set to True when a server has accepted the client connection and |
|
88
|
|
|
messages can be sent and received. """ |
|
89
|
|
|
|
|
90
|
|
|
# Work around bot.connecting missing in Python older than 2.7.4 |
|
91
|
|
|
if not hasattr(self, "connecting"): |
|
92
|
|
|
self.connecting = False |
|
93
|
|
|
|
|
94
|
|
|
def log_raw(self, line, prefix): |
|
95
|
|
|
"""Log raw line to the raw log.""" |
|
96
|
|
|
if not self.config.core.log_raw: |
|
97
|
|
|
return |
|
98
|
|
|
logger = logging.getLogger('sopel.raw') |
|
99
|
|
|
logger.info('\t'.join([prefix, line.strip()])) |
|
100
|
|
|
|
|
101
|
|
|
def safe(self, string): |
|
102
|
|
|
"""Remove newlines from a string.""" |
|
103
|
|
|
if sys.version_info.major >= 3 and isinstance(string, bytes): |
|
104
|
|
|
string = string.decode("utf8") |
|
105
|
|
|
elif sys.version_info.major < 3: |
|
106
|
|
|
if not isinstance(string, unicode): |
|
|
|
|
|
|
107
|
|
|
string = unicode(string, encoding='utf8') |
|
108
|
|
|
string = string.replace('\n', '') |
|
109
|
|
|
string = string.replace('\r', '') |
|
110
|
|
|
return string |
|
111
|
|
|
|
|
112
|
|
|
def write(self, args, text=None): |
|
113
|
|
|
args = [self.safe(arg) for arg in args] |
|
114
|
|
|
if text is not None: |
|
115
|
|
|
text = self.safe(text) |
|
116
|
|
|
try: |
|
117
|
|
|
# Blocking lock, can't send two things at a time |
|
118
|
|
|
self.writing_lock.acquire() |
|
119
|
|
|
|
|
120
|
|
|
# From RFC2812 Internet Relay Chat: Client Protocol |
|
121
|
|
|
# Section 2.3 |
|
122
|
|
|
# |
|
123
|
|
|
# https://tools.ietf.org/html/rfc2812.html |
|
124
|
|
|
# |
|
125
|
|
|
# IRC messages are always lines of characters terminated with a |
|
126
|
|
|
# CR-LF (Carriage Return - Line Feed) pair, and these messages SHALL |
|
127
|
|
|
# NOT exceed 512 characters in length, counting all characters |
|
128
|
|
|
# including the trailing CR-LF. Thus, there are 510 characters |
|
129
|
|
|
# maximum allowed for the command and its parameters. There is no |
|
130
|
|
|
# provision for continuation of message lines. |
|
131
|
|
|
|
|
132
|
|
|
max_length = unicode_max_length = 510 |
|
133
|
|
|
if text is not None: |
|
134
|
|
|
temp = (' '.join(args) + ' :' + text) |
|
135
|
|
|
else: |
|
136
|
|
|
temp = ' '.join(args) |
|
137
|
|
|
|
|
138
|
|
|
# The max length of 512 is in bytes, not unicode |
|
139
|
|
|
while len(temp.encode('utf-8')) > max_length: |
|
140
|
|
|
temp = temp[:unicode_max_length] |
|
141
|
|
|
unicode_max_length = unicode_max_length - 1 |
|
142
|
|
|
|
|
143
|
|
|
# Ends the message with CR-LF |
|
144
|
|
|
temp = temp + '\r\n' |
|
145
|
|
|
|
|
146
|
|
|
# Log and output the message |
|
147
|
|
|
self.log_raw(temp, '>>') |
|
148
|
|
|
self.send(temp.encode('utf-8')) |
|
149
|
|
|
finally: |
|
150
|
|
|
self.writing_lock.release() |
|
151
|
|
|
|
|
152
|
|
|
# Simulate echo-message |
|
153
|
|
|
if ('echo-message' not in self.enabled_capabilities and |
|
154
|
|
|
args[0].upper() in ['PRIVMSG', 'NOTICE']): |
|
155
|
|
|
# Use the hostmask we think the IRC server is using for us, |
|
156
|
|
|
# or something reasonable if that's not available |
|
157
|
|
|
host = 'localhost' |
|
158
|
|
|
if self.config.core.bind_host: |
|
159
|
|
|
host = self.config.core.bind_host |
|
160
|
|
|
else: |
|
161
|
|
|
try: |
|
162
|
|
|
host = self.hostmask |
|
163
|
|
|
except KeyError: |
|
164
|
|
|
pass # we tried, and that's good enough |
|
165
|
|
|
|
|
166
|
|
|
pretrigger = PreTrigger( |
|
167
|
|
|
self.nick, |
|
168
|
|
|
":{0}!{1}@{2} {3}".format(self.nick, self.user, host, temp) |
|
|
|
|
|
|
169
|
|
|
) |
|
170
|
|
|
self.dispatch(pretrigger) |
|
171
|
|
|
|
|
172
|
|
|
def run(self, host, port=6667): |
|
173
|
|
|
try: |
|
174
|
|
|
self.initiate_connect(host, port) |
|
175
|
|
|
except socket.error as e: |
|
176
|
|
|
LOGGER.exception('Connection error: %s', e) |
|
177
|
|
|
self.handle_close() |
|
178
|
|
|
|
|
179
|
|
|
def initiate_connect(self, host, port): |
|
180
|
|
|
LOGGER.info('Connecting to %s:%s...', host, port) |
|
181
|
|
|
source_address = ((self.config.core.bind_host, 0) |
|
182
|
|
|
if self.config.core.bind_host else None) |
|
183
|
|
|
self.set_socket(socket.create_connection((host, port), |
|
184
|
|
|
source_address=source_address)) |
|
185
|
|
|
if self.config.core.use_ssl and has_ssl: |
|
186
|
|
|
self.send = self._ssl_send |
|
187
|
|
|
self.recv = self._ssl_recv |
|
188
|
|
|
elif not has_ssl and self.config.core.use_ssl: |
|
189
|
|
|
LOGGER.warning( |
|
190
|
|
|
'SSL is not available on your system; ' |
|
191
|
|
|
'attempting connection without it') |
|
192
|
|
|
self.connect((host, port)) |
|
193
|
|
|
try: |
|
194
|
|
|
asyncore.loop() |
|
195
|
|
|
except KeyboardInterrupt: |
|
196
|
|
|
LOGGER.warning('KeyboardInterrupt') |
|
197
|
|
|
self.quit('KeyboardInterrupt') |
|
198
|
|
|
|
|
199
|
|
|
def restart(self, message): |
|
200
|
|
|
"""Disconnect from IRC and restart the bot.""" |
|
201
|
|
|
self.write(['QUIT'], message) |
|
202
|
|
|
self.wantsrestart = True |
|
203
|
|
|
self.hasquit = True |
|
204
|
|
|
|
|
205
|
|
|
def quit(self, message): |
|
206
|
|
|
"""Disconnect from IRC and close the bot.""" |
|
207
|
|
|
if self.connected: # Only send QUIT message if socket is open |
|
208
|
|
|
self.write(['QUIT'], message) |
|
209
|
|
|
self.hasquit = True |
|
210
|
|
|
# Wait for acknowledgement from the server. By RFC 2812 it should be |
|
211
|
|
|
# an ERROR msg, but many servers just close the connection. Either way |
|
212
|
|
|
# is fine by us. |
|
213
|
|
|
# Closing the connection now would mean that stuff in the buffers that |
|
214
|
|
|
# has not yet been processed would never be processed. It would also |
|
215
|
|
|
# release the main thread, which is problematic because whomever called |
|
216
|
|
|
# quit might still want to do something before main thread quits. |
|
217
|
|
|
|
|
218
|
|
|
def handle_close(self): |
|
219
|
|
|
self.connection_registered = False |
|
220
|
|
|
|
|
221
|
|
|
if hasattr(self, '_shutdown'): |
|
222
|
|
|
self._shutdown() |
|
223
|
|
|
|
|
224
|
|
|
# This will eventually call asyncore dispatchers close method, which |
|
225
|
|
|
# will release the main thread. This should be called last to avoid |
|
226
|
|
|
# race conditions. |
|
227
|
|
|
if self.socket: |
|
228
|
|
|
LOGGER.debug('Closing socket') |
|
229
|
|
|
self.close() |
|
230
|
|
|
|
|
231
|
|
|
LOGGER.info('Closed!') |
|
232
|
|
|
|
|
233
|
|
|
def handle_connect(self): |
|
234
|
|
|
""" |
|
235
|
|
|
Connect to IRC server, handle TLS and authenticate |
|
236
|
|
|
user if an account exists. |
|
237
|
|
|
""" |
|
238
|
|
|
# handle potential TLS connection |
|
239
|
|
|
if self.config.core.use_ssl and has_ssl: |
|
240
|
|
|
if not self.config.core.verify_ssl: |
|
241
|
|
|
self.ssl = ssl.wrap_socket(self.socket, |
|
242
|
|
|
do_handshake_on_connect=True, |
|
243
|
|
|
suppress_ragged_eofs=True) |
|
244
|
|
|
else: |
|
245
|
|
|
self.ssl = ssl.wrap_socket(self.socket, |
|
246
|
|
|
do_handshake_on_connect=True, |
|
247
|
|
|
suppress_ragged_eofs=True, |
|
248
|
|
|
cert_reqs=ssl.CERT_REQUIRED, |
|
249
|
|
|
ca_certs=self.ca_certs) |
|
250
|
|
|
# connect to host specified in config first |
|
251
|
|
|
try: |
|
252
|
|
|
ssl.match_hostname(self.ssl.getpeercert(), self.config.core.host) |
|
253
|
|
|
except ssl.CertificateError: |
|
254
|
|
|
# the host in config and certificate don't match |
|
255
|
|
|
LOGGER.error("hostname mismatch between configuration and certificate") |
|
256
|
|
|
# check (via exception) if a CNAME matches as a fallback |
|
257
|
|
|
has_matched = False |
|
258
|
|
|
for hostname in self._get_cnames(self.config.core.host): |
|
259
|
|
|
try: |
|
260
|
|
|
ssl.match_hostname(self.ssl.getpeercert(), hostname) |
|
261
|
|
|
LOGGER.warning("using {0} instead of {1} for TLS connection" |
|
262
|
|
|
.format(hostname, self.config.core.host)) |
|
263
|
|
|
has_matched = True |
|
264
|
|
|
break |
|
265
|
|
|
except ssl.CertificateError: |
|
266
|
|
|
pass |
|
267
|
|
|
if not has_matched: |
|
268
|
|
|
# everything is broken |
|
269
|
|
|
LOGGER.error("invalid certificate, no hostname matches") |
|
270
|
|
|
if hasattr(self.config.core, 'pid_file_path'): |
|
271
|
|
|
os.unlink(self.config.core.pid_file_path) |
|
272
|
|
|
os._exit(1) |
|
273
|
|
|
self.set_socket(self.ssl) |
|
274
|
|
|
|
|
275
|
|
|
# Request list of server capabilities. IRCv3 servers will respond with |
|
276
|
|
|
# CAP * LS (which we handle in coretasks). v2 servers will respond with |
|
277
|
|
|
# 421 Unknown command, which we'll ignore |
|
278
|
|
|
self.write(('CAP', 'LS', '302')) |
|
279
|
|
|
|
|
280
|
|
|
# authenticate account if needed |
|
281
|
|
|
if self.config.core.auth_method == 'server': |
|
282
|
|
|
self.write(('PASS', self.config.core.auth_password)) |
|
283
|
|
|
elif self.config.core.server_auth_method == 'server': |
|
284
|
|
|
self.write(('PASS', self.config.core.server_auth_password)) |
|
285
|
|
|
self.write(('NICK', self.nick)) |
|
286
|
|
|
self.write(('USER', self.user, '+iw', self.nick), self.name) |
|
287
|
|
|
|
|
288
|
|
|
# maintain connection |
|
289
|
|
|
LOGGER.info('Connected.') |
|
290
|
|
|
self.last_ping_time = datetime.now() |
|
291
|
|
|
timeout_check_thread = threading.Thread(target=self._timeout_check) |
|
292
|
|
|
timeout_check_thread.daemon = True |
|
293
|
|
|
timeout_check_thread.start() |
|
294
|
|
|
ping_thread = threading.Thread(target=self._send_ping) |
|
295
|
|
|
ping_thread.daemon = True |
|
296
|
|
|
ping_thread.start() |
|
297
|
|
|
|
|
298
|
|
|
def _get_cnames(self, domain): |
|
299
|
|
|
""" |
|
300
|
|
|
Determine the CNAMEs for a given domain. |
|
301
|
|
|
|
|
302
|
|
|
:param domain: domain to check |
|
303
|
|
|
:type domain: str |
|
304
|
|
|
:returns: list (of str) |
|
305
|
|
|
""" |
|
306
|
|
|
import dns.resolver |
|
307
|
|
|
cnames = [] |
|
308
|
|
|
try: |
|
309
|
|
|
answer = dns.resolver.query(domain, "CNAME") |
|
310
|
|
|
except dns.resolver.NoAnswer: |
|
311
|
|
|
return [] |
|
312
|
|
|
for data in answer: |
|
313
|
|
|
if isinstance(data, dns.rdtypes.ANY.CNAME.CNAME): |
|
314
|
|
|
cname = data.to_text()[:-1] |
|
315
|
|
|
cnames.append(cname) |
|
316
|
|
|
return cnames |
|
317
|
|
|
|
|
318
|
|
|
def _timeout_check(self): |
|
319
|
|
|
while self.connected or self.connecting: |
|
320
|
|
|
if (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout): |
|
321
|
|
|
LOGGER.warning( |
|
322
|
|
|
'Ping timeout reached after %s seconds; ' |
|
323
|
|
|
'closing connection', |
|
324
|
|
|
self.config.core.timeout) |
|
325
|
|
|
self.handle_close() |
|
326
|
|
|
break |
|
327
|
|
|
else: |
|
328
|
|
|
time.sleep(int(self.config.core.timeout)) |
|
329
|
|
|
|
|
330
|
|
|
def _send_ping(self): |
|
331
|
|
|
while self.connected or self.connecting: |
|
332
|
|
|
if self.connected and (datetime.now() - self.last_ping_time).seconds > int(self.config.core.timeout) / 2: |
|
333
|
|
|
try: |
|
334
|
|
|
self.write(('PING', self.config.core.host)) |
|
335
|
|
|
except socket.error: |
|
336
|
|
|
pass |
|
337
|
|
|
time.sleep(int(self.config.core.timeout) / 2) |
|
338
|
|
|
|
|
339
|
|
|
def _ssl_send(self, data): |
|
340
|
|
|
"""Replacement for self.send() during SSL connections.""" |
|
341
|
|
|
try: |
|
342
|
|
|
result = self.socket.send(data) |
|
343
|
|
|
return result |
|
344
|
|
|
except ssl.SSLError as why: |
|
345
|
|
|
if why[0] in (asyncore.EWOULDBLOCK, errno.ESRCH): |
|
346
|
|
|
return 0 |
|
347
|
|
|
else: |
|
348
|
|
|
raise why |
|
349
|
|
|
return 0 |
|
350
|
|
|
|
|
351
|
|
|
def _ssl_recv(self, buffer_size): |
|
352
|
|
|
"""Replacement for self.recv() during SSL connections. |
|
353
|
|
|
|
|
354
|
|
|
From: http://evanfosmark.com/2010/09/ssl-support-in-asynchatasync_chat |
|
355
|
|
|
|
|
356
|
|
|
""" |
|
357
|
|
|
try: |
|
358
|
|
|
data = self.socket.read(buffer_size) |
|
359
|
|
|
if not data: |
|
360
|
|
|
self.handle_close() |
|
361
|
|
|
return b'' |
|
362
|
|
|
return data |
|
363
|
|
|
except ssl.SSLError as why: |
|
364
|
|
|
if why[0] in (asyncore.ECONNRESET, asyncore.ENOTCONN, |
|
365
|
|
|
asyncore.ESHUTDOWN): |
|
366
|
|
|
self.handle_close() |
|
367
|
|
|
return '' |
|
368
|
|
|
elif why[0] == errno.ENOENT: |
|
369
|
|
|
# Required in order to keep it non-blocking |
|
370
|
|
|
return b'' |
|
371
|
|
|
else: |
|
372
|
|
|
raise |
|
373
|
|
|
|
|
374
|
|
|
def collect_incoming_data(self, data): |
|
375
|
|
|
# We can't trust clients to pass valid unicode. |
|
376
|
|
|
try: |
|
377
|
|
|
data = unicode(data, encoding='utf-8') |
|
|
|
|
|
|
378
|
|
|
except UnicodeDecodeError: |
|
379
|
|
|
# not unicode, let's try cp1252 |
|
380
|
|
|
try: |
|
381
|
|
|
data = unicode(data, encoding='cp1252') |
|
382
|
|
|
except UnicodeDecodeError: |
|
383
|
|
|
# Okay, let's try ISO8859-1 |
|
384
|
|
|
try: |
|
385
|
|
|
data = unicode(data, encoding='iso8859-1') |
|
386
|
|
|
except UnicodeDecodeError: |
|
387
|
|
|
# Discard line if encoding is unknown |
|
388
|
|
|
return |
|
389
|
|
|
|
|
390
|
|
|
if data: |
|
391
|
|
|
self.log_raw(data, '<<') |
|
392
|
|
|
self.buffer += data |
|
393
|
|
|
|
|
394
|
|
|
def found_terminator(self): |
|
395
|
|
|
line = self.buffer |
|
396
|
|
|
if line.endswith('\r'): |
|
397
|
|
|
line = line[:-1] |
|
398
|
|
|
self.buffer = '' |
|
399
|
|
|
self.last_ping_time = datetime.now() |
|
400
|
|
|
pretrigger = PreTrigger(self.nick, line) |
|
401
|
|
|
if all(cap not in self.enabled_capabilities for cap in ['account-tag', 'extended-join']): |
|
402
|
|
|
pretrigger.tags.pop('account', None) |
|
403
|
|
|
|
|
404
|
|
|
if pretrigger.event == 'PING': |
|
405
|
|
|
self.write(('PONG', pretrigger.args[-1])) |
|
406
|
|
|
elif pretrigger.event == 'ERROR': |
|
407
|
|
|
LOGGER.error("ERROR received from server: %s", pretrigger.args[-1]) |
|
408
|
|
|
if self.hasquit: |
|
409
|
|
|
self.close_when_done() |
|
410
|
|
|
elif pretrigger.event == '433': |
|
411
|
|
|
LOGGER.error('Nickname already in use!') |
|
412
|
|
|
self.handle_close() |
|
413
|
|
|
|
|
414
|
|
|
self.dispatch(pretrigger) |
|
415
|
|
|
|
|
416
|
|
|
def dispatch(self, pretrigger): |
|
417
|
|
|
pass |
|
418
|
|
|
|
|
419
|
|
|
def error(self, trigger=None, exception=None): |
|
420
|
|
|
"""Called internally when a module causes an error.""" |
|
421
|
|
|
message = 'Unexpected error' |
|
422
|
|
|
if exception: |
|
423
|
|
|
message = '{} ({})'.format(message, exception) |
|
424
|
|
|
|
|
425
|
|
|
if trigger: |
|
426
|
|
|
message = '{} from {} at {}. Message was: {}'.format( |
|
427
|
|
|
message, trigger.nick, str(datetime.now()), trigger.group(0) |
|
428
|
|
|
) |
|
429
|
|
|
|
|
430
|
|
|
LOGGER.exception(message) |
|
431
|
|
|
|
|
432
|
|
|
if trigger and self.config.core.reply_errors and trigger.sender is not None: |
|
433
|
|
|
self.say(message, trigger.sender) |
|
434
|
|
|
|
|
435
|
|
|
def handle_error(self): |
|
436
|
|
|
"""Handle any uncaptured error in the core. |
|
437
|
|
|
|
|
438
|
|
|
This method is an override of :meth:`asyncore.dispatcher.handle_error`, |
|
439
|
|
|
the :class:`asynchat.async_chat` being a subclass of |
|
440
|
|
|
:class:`asyncore.dispatcher`. |
|
441
|
|
|
""" |
|
442
|
|
|
LOGGER.error('Fatal error in core; please review exception log.') |
|
443
|
|
|
|
|
444
|
|
|
err_log = logging.getLogger('sopel.exceptions') |
|
445
|
|
|
err_log.error( |
|
446
|
|
|
'Fatal error in core; handle_error() was called.\n' |
|
447
|
|
|
'Last raw line was: %s\n' |
|
448
|
|
|
'Buffer:\n%s\n', |
|
449
|
|
|
self.raw, self.buffer |
|
450
|
|
|
) |
|
451
|
|
|
err_log.exception('Fatal error traceback') |
|
452
|
|
|
err_log.error('----------------------------------------') |
|
453
|
|
|
|
|
454
|
|
|
if self.error_count > 10: |
|
455
|
|
|
# quit if too many errors |
|
456
|
|
|
if (datetime.now() - self.last_error_timestamp).seconds < 5: |
|
457
|
|
|
LOGGER.error("Too many errors; can't continue") |
|
458
|
|
|
os._exit(1) |
|
459
|
|
|
# TODO: should we reset error_count? |
|
460
|
|
|
|
|
461
|
|
|
self.last_error_timestamp = datetime.now() |
|
462
|
|
|
self.error_count = self.error_count + 1 |
|
463
|
|
|
|