Skip to content

Commit 3425729

Browse files
authored
Merge pull request CloudBotIRC#83 from linuxdaemon/gonzobot+chan-track-cleanup
Fix race conditions in chan_track.py
2 parents 2d9d793 + f3ff033 commit 3425729

1 file changed

Lines changed: 87 additions & 85 deletions

File tree

plugins/chan_track.py

Lines changed: 87 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,21 @@
44
Requires:
55
server_info.py
66
"""
7-
import re
87
import weakref
98
from collections import defaultdict
109
from operator import attrgetter
1110
from weakref import WeakValueDictionary
1211

1312
from cloudbot import hook
14-
15-
NUH_RE = re.compile(r'(?P<nick>.+?)(?:!(?P<user>.+?))?(?:@(?P<host>.+?))?')
13+
from cloudbot.util.parsers.irc import Prefix
1614

1715

1816
class WeakDict(dict):
1917
# Subclass dict to allow it to be weakly referenced
2018
pass
2119

2220

23-
class DataDict(defaultdict):
24-
def __init__(self, *args, **kwargs):
25-
super().__init__(*args, **kwargs)
26-
21+
class KeyFoldMixin:
2722
def __contains__(self, item):
2823
return super().__contains__(item.casefold())
2924

@@ -36,8 +31,18 @@ def __setitem__(self, key, value):
3631
def __delitem__(self, key):
3732
super().__delitem__(key.casefold())
3833

34+
35+
class KeyFoldDict(dict, KeyFoldMixin):
36+
pass
37+
38+
39+
class KeyFoldWeakValueDict(WeakValueDictionary, KeyFoldMixin):
40+
pass
41+
42+
43+
class ChanDict(defaultdict, KeyFoldMixin):
3944
def __missing__(self, key):
40-
data = WeakDict({"name": key, "users": {}})
45+
data = WeakDict(name=key, users=KeyFoldDict())
4146
self[key] = data
4247
return data
4348

@@ -55,7 +60,7 @@ def update_conn_data(conn):
5560

5661
@hook.on_cap_available("userhost-in-names", "multi-prefix")
5762
def do_caps():
58-
pass
63+
return True
5964

6065

6166
@hook.on_start
@@ -67,36 +72,30 @@ def get_chan_data(bot):
6772

6873
@hook.connect
6974
def init_chan_data(conn):
70-
chan_data = conn.memory.setdefault("chan_data", DataDict())
75+
chan_data = conn.memory.setdefault("chan_data", ChanDict())
7176
chan_data.clear()
7277

73-
users = conn.memory.setdefault("users", WeakValueDictionary())
78+
users = conn.memory.setdefault("users", KeyFoldWeakValueDict())
7479
users.clear()
7580

7681

77-
def parse_nuh(mask):
78-
match = NUH_RE.fullmatch(mask)
79-
if not match:
80-
return None, None, None
81-
82-
nick = match.group('nick')
83-
user = match.group('user')
84-
host = match.group('host')
85-
return nick, user, host
82+
def add_user_membership(user, chan, membership):
83+
chans = user.setdefault("channels", KeyFoldWeakValueDict())
84+
chans[chan] = membership
8685

8786

8887
def replace_user_data(conn, chan_data):
8988
statuses = {status.prefix: status for status in set(conn.memory["server_info"]["statuses"].values())}
9089
users = conn.memory["users"]
9190
old_users = chan_data['users']
9291
new_data = chan_data.pop("new_users", [])
93-
new_users = {}
92+
new_users = KeyFoldDict()
9493
caps = conn.memory.get("server_caps", {})
9594
has_uh_i_n = caps.get("userhost-in-names", False)
9695
has_multi_pfx = caps.get("multi-prefix", False)
9796
for name in new_data:
9897
user_data = WeakDict()
99-
memb_data = WeakDict({"user": user_data, "chan": weakref.proxy(chan_data)})
98+
memb_data = WeakDict(user=user_data, chan=weakref.proxy(chan_data))
10099
user_statuses = []
101100
while name[:1] in statuses:
102101
status, name = name[:1], name[1:]
@@ -111,24 +110,23 @@ def replace_user_data(conn, chan_data):
111110
memb_data["status"] = user_statuses
112111

113112
if has_uh_i_n:
114-
nick, user, host = parse_nuh(name)
115-
user_data.update({"nick": nick, "ident": user, "host": host})
113+
pfx = Prefix.parse(name)
114+
user_data.update({"nick": pfx.nick, "ident": pfx.user, "host": pfx.host})
116115
else:
117116
user_data["nick"] = name
118117

119-
nick_cf = user_data["nick"].casefold()
120-
new_users[nick_cf] = memb_data
121-
if nick_cf in old_users:
122-
old_data = old_users[nick_cf]
118+
nick = user_data["nick"]
119+
new_users[nick] = memb_data
120+
if nick in old_users:
121+
old_data = old_users[nick]
123122
old_data.update(memb_data) # New data takes priority over old data
124123
memb_data.update(old_data)
125124

126-
old_user_data = users.setdefault(nick_cf, user_data)
125+
old_user_data = users.setdefault(nick, user_data)
127126
old_user_data.update(user_data)
128127
user_data = old_user_data
129128
memb_data["user"] = user_data
130-
user_chans = user_data.setdefault("channels", WeakValueDictionary())
131-
user_chans[chan_data["name"].casefold()] = memb_data
129+
add_user_membership(user_data, chan_data["name"], memb_data)
132130

133131
old_users.clear()
134132
old_users.update(new_users) # Reassigning the dict would break other references to the data, so just update instead
@@ -181,11 +179,10 @@ def perm_check(chan, conn, nick):
181179
return False
182180

183181
chan_data = chans[chan]
184-
nick_cf = nick.casefold()
185-
if nick_cf not in chan_data["users"]:
182+
if nick not in chan_data["users"]:
186183
return False
187184

188-
memb = chan_data["users"][nick_cf]
185+
memb = chan_data["users"][nick]
189186
status = memb["status"]
190187
if status and status[0].level > 1:
191188
return True
@@ -218,58 +215,31 @@ def updateusers(bot):
218215
return "Updating all channel data"
219216

220217

221-
@hook.irc_raw('JOIN')
218+
@hook.irc_raw(['JOIN', 'MODE'], singlethread=True)
219+
def on_join_mode(chan, nick, user, host, conn, irc_command, irc_paramlist):
220+
"""
221+
Both JOIN and MODE are handled in one hook with Hook:singlethread=True
222+
to ensure they are handled in order, avoiding a possible race condition
223+
"""
224+
if irc_command == 'JOIN':
225+
return on_join(chan, nick, user, host, conn)
226+
elif irc_command == 'MODE':
227+
return on_mode(chan, irc_paramlist, conn)
228+
229+
222230
def on_join(chan, nick, user, host, conn):
223231
if chan.startswith(':'):
224232
chan = chan[1:]
225233

226234
users = conn.memory['users']
227-
user_data = WeakDict({"nick": nick, "user": user, "host": host})
228-
user_data = users.setdefault(nick.casefold(), user_data)
235+
user_data = WeakDict(nick=nick, user=user, host=host)
236+
user_data = users.setdefault(nick, user_data)
229237
chan_data = conn.memory["chan_data"][chan]
230-
memb_data = WeakDict({"chan": weakref.proxy(chan_data), "user": user_data, "status": []})
231-
chan_data["users"][nick.casefold()] = memb_data
232-
user_chans = user_data.setdefault("channels", WeakValueDictionary())
233-
user_chans[chan.casefold()] = memb_data
234-
235-
236-
@hook.irc_raw('PART')
237-
def on_part(chan, nick, conn):
238-
if chan.startswith(':'):
239-
chan = chan[1:]
240-
241-
channels = conn.memory["chan_data"]
242-
nick_cf = nick.casefold()
243-
if nick_cf == conn.nick.casefold():
244-
try:
245-
del channels[chan]
246-
except KeyError:
247-
pass
248-
else:
249-
chan_data = channels[chan]
250-
try:
251-
del chan_data["users"][nick_cf]
252-
except KeyError:
253-
pass
238+
memb_data = WeakDict(chan=weakref.proxy(chan_data), user=user_data, status=[])
239+
chan_data["users"][nick] = memb_data
240+
add_user_membership(user_data, chan, memb_data)
254241

255242

256-
@hook.irc_raw('KICK')
257-
def on_kick(chan, target, conn):
258-
on_part(chan, target, conn)
259-
260-
261-
@hook.irc_raw('QUIT')
262-
def on_quit(nick, conn):
263-
nick_cf = nick.casefold()
264-
users = conn.memory["users"]
265-
if nick_cf in users:
266-
user = users[nick_cf]
267-
for memb in user.get("channels", {}).values():
268-
chan = memb["chan"]
269-
chan["users"].pop(nick_cf)
270-
271-
272-
@hook.irc_raw('MODE')
273243
def on_mode(chan, irc_paramlist, conn):
274244
if chan.startswith(':'):
275245
chan = chan[1:]
@@ -306,7 +276,7 @@ def on_mode(chan, irc_paramlist, conn):
306276
param = mode_params.pop(0)
307277

308278
if is_status:
309-
memb = chan_data["users"][param.casefold()]
279+
memb = chan_data["users"][param]
310280
status = statuses[c]
311281
if adding:
312282
memb["status"].append(status)
@@ -316,18 +286,50 @@ def on_mode(chan, irc_paramlist, conn):
316286
memb["status"].remove(status)
317287

318288

289+
@hook.irc_raw('PART')
290+
def on_part(chan, nick, conn):
291+
if chan.startswith(':'):
292+
chan = chan[1:]
293+
294+
channels = conn.memory["chan_data"]
295+
if nick.casefold() == conn.nick.casefold():
296+
try:
297+
del channels[chan]
298+
except KeyError:
299+
pass
300+
else:
301+
chan_data = channels[chan]
302+
try:
303+
del chan_data["users"][nick]
304+
except KeyError:
305+
pass
306+
307+
308+
@hook.irc_raw('KICK')
309+
def on_kick(chan, target, conn):
310+
on_part(chan, target, conn)
311+
312+
313+
@hook.irc_raw('QUIT')
314+
def on_quit(nick, conn):
315+
users = conn.memory["users"]
316+
if nick in users:
317+
user = users[nick]
318+
for memb in user.get("channels", {}).values():
319+
chan = memb["chan"]
320+
chan["users"].pop(nick)
321+
322+
319323
@hook.irc_raw('NICK')
320324
def on_nick(nick, irc_paramlist, conn):
321325
users = conn.memory["users"]
322-
nick_cf = nick.casefold()
323326
new_nick = irc_paramlist[0]
324327
if new_nick.startswith(':'):
325328
new_nick = new_nick[1:]
326329

327-
new_nick_cf = new_nick.casefold()
328-
user = users.pop(nick_cf)
329-
users[new_nick_cf] = user
330+
user = users.pop(nick)
331+
users[new_nick] = user
330332
user["nick"] = nick
331-
for memb in user["channels"].values():
333+
for memb in user.get("channels", {}).values():
332334
chan_users = memb["chan"]["users"]
333-
chan_users[new_nick_cf] = chan_users.pop(nick_cf)
335+
chan_users[new_nick] = chan_users.pop(nick)

0 commit comments

Comments
 (0)