From 9d9b01839cf3639e59d29c27e70688bdbf44db96 Mon Sep 17 00:00:00 2001 From: James Lu Date: Fri, 31 Mar 2017 16:25:28 -0700 Subject: [PATCH] Split Irc.reply() into _reply() to make 'networks.remote' actually thread-safe Previously, the Irc.reply_lock check was in the reply() function itself: replacing it with another function checking for the same lock would delay execution, but then run the wrong reply() code if another module used irc.reply() while 'remote' was executing. --- classes.py | 49 +++++++++++++++++++++++++++------------------ plugins/networks.py | 6 +++--- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/classes.py b/classes.py index 774a6ec..a535f64 100644 --- a/classes.py +++ b/classes.py @@ -62,7 +62,7 @@ class Irc(utils.DeprecatedAttributesObject): self.connected = threading.Event() self.aborted = threading.Event() - self.reply_lock = threading.Lock() + self.reply_lock = threading.RLock() self.pingTimer = None @@ -559,27 +559,38 @@ class Irc(utils.DeprecatedAttributesObject): # replies across relay. self.callHooks([source, cmd, {'target': target, 'text': text}]) - def reply(self, text, notice=None, source=None, private=None, force_privmsg_in_private=False, + def _reply(self, text, notice=None, source=None, private=None, force_privmsg_in_private=False, loopback=True): - """Replies to the last caller in the right context (channel or PM).""" + """ + Core of the reply() function - replies to the last caller in the right context + (channel or PM). + """ + if private is None: + # Allow using private replies as the default, if no explicit setting was given. + private = conf.conf['bot'].get("prefer_private_replies") + # Private reply is enabled, or the caller was originally a PM + if private or (self.called_in in self.users): + if not force_privmsg_in_private: + # For private replies, the default is to override the notice=True/False argument, + # and send replies as notices regardless. This is standard behaviour for most + # IRC services, but can be disabled if force_privmsg_in_private is given. + notice = True + target = self.called_by + else: + target = self.called_in + + self.msg(target, text, notice=notice, source=source, loopback=loopback) + + def reply(self, *args, **kwargs): + """ + Replies to the last caller in the right context (channel or PM). + + This function wraps around _reply() and can be monkey-patched in a thread-safe manner + to temporarily redirect plugin output to another target. + """ with self.reply_lock: - if private is None: - # Allow using private replies as the default, if no explicit setting was given. - private = conf.conf['bot'].get("prefer_private_replies") - - # Private reply is enabled, or the caller was originally a PM - if private or (self.called_in in self.users): - if not force_privmsg_in_private: - # For private replies, the default is to override the notice=True/False argument, - # and send replies as notices regardless. This is standard behaviour for most - # IRC services, but can be disabled if force_privmsg_in_private is given. - notice = True - target = self.called_by - else: - target = self.called_in - - self.msg(target, text, notice=notice, source=source, loopback=loopback) + self._reply(*args, **kwargs) def error(self, text, **kwargs): """Replies with an error to the last caller in the right context (channel or PM).""" diff --git a/plugins/networks.py b/plugins/networks.py index c52af0d..54d9f5b 100644 --- a/plugins/networks.py +++ b/plugins/networks.py @@ -103,19 +103,19 @@ def remote(irc, source, args): del kwargs['source'] irc.reply(text, source=irc.pseudoclient.uid, **kwargs) - old_reply = remoteirc.reply + old_reply = remoteirc._reply with remoteirc.reply_lock: try: # Remotely call the command (use the PyLink client as a dummy user). # Override the remote irc.reply() to send replies HERE. log.debug('(%s) networks.remote: overriding reply() of IRC object %s', irc.name, netname) - remoteirc.reply = types.MethodType(_remote_reply, remoteirc) + remoteirc._reply = types.MethodType(_remote_reply, remoteirc) world.services[args.service].call_cmd(remoteirc, remoteirc.pseudoclient.uid, ' '.join(args.command)) finally: # Restore the original remoteirc.reply() log.debug('(%s) networks.remote: restoring reply() of IRC object %s', irc.name, netname) - remoteirc.reply = old_reply + remoteirc._reply = old_reply # Remove the identification override after we finish. remoteirc.pseudoclient.account = ''