From d901a252bb7ca0f20ffe3134e4395b2c9b47f979 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Fri, 6 Apr 2012 14:41:27 -0400 Subject: [PATCH] More cleanup for IRC stuff --- earwigbot/bot.py | 69 ++++++++++++++++++++----------------- earwigbot/commands/__init__.py | 77 ++++++++++++++++++++++-------------------- earwigbot/commands/restart.py | 13 +++++-- earwigbot/irc/connection.py | 10 +++--- earwigbot/irc/frontend.py | 35 +++++++++---------- earwigbot/irc/watcher.py | 32 +++++++++--------- 6 files changed, 125 insertions(+), 111 deletions(-) diff --git a/earwigbot/bot.py b/earwigbot/bot.py index 15c5fef..c6da54b 100644 --- a/earwigbot/bot.py +++ b/earwigbot/bot.py @@ -54,23 +54,13 @@ class Bot(object): self.frontend = None self.watcher = None - self._keep_scheduling = True - self._lock = threading.Lock() - - def _wiki_scheduler(self): - while self._keep_scheduling: - time_start = time() - task_manager.schedule() - time_end = time() - time_diff = time_start - time_end - if time_diff < 60: # Sleep until the next minute - sleep(60 - time_diff) - - def _start_components(self): + self.component_lock = threading.Lock() + self._keep_looping = True + + def _start_irc_components(self): if self.config.components.get("irc_frontend"): self.logger.info("Starting IRC frontend") self.frontend = Frontend(self) - self.commands.load() threading.Thread(name=name, target=self.frontend.loop).start() if self.config.components.get("irc_watcher"): @@ -78,17 +68,35 @@ class Bot(object): self.watcher = Watcher(self) threading.Thread(name=name, target=self.watcher.loop).start() + def _start_wiki_scheduler(self): + def wiki_scheduler(): + while self._keep_looping: + time_start = time() + task_manager.schedule() + time_end = time() + time_diff = time_start - time_end + if time_diff < 60: # Sleep until the next minute + sleep(60 - time_diff) + if self.config.components.get("wiki_scheduler"): self.logger.info("Starting wiki scheduler") - threading.Thread(name=name, target=self._wiki_scheduler).start() + threading.Thread(name=name, target=wiki_scheduler).start() + + def _stop_irc_components(self): + if self.frontend: + self.frontend.stop() + if self.watcher: + self.watcher.stop() def _loop(self): - while 1: - with self._lock: + while self._keep_looping: + with self.component_lock: if self.frontend and self.frontend.is_stopped(): - self.frontend._connect() + self.frontend = Frontend(self) + threading.Thread(name=name, target=self.frontend.loop).start() if self.watcher and self.watcher.is_stopped(): - self.watcher._connect() + self.watcher = Watcher(self) + threading.Thread(name=name, target=self.watcher.loop).start() sleep(5) def run(self): @@ -97,21 +105,20 @@ class Bot(object): self.config.decrypt(config.wiki, "search", "credentials", "key") self.config.decrypt(config.wiki, "search", "credentials", "secret") self.config.decrypt(config.irc, "frontend", "nickservPassword") - self.config.decrypt(config.irc, "watcher", "nickservPassword") - self._start_components() + self.config.decrypt(config.irc, "watcher", "nickservPassword") + self.commands.load() + self._start_irc_components() + self._start_wiki_scheduler() self._loop() - def reload(self): - #components = self.config.components - with self._lock: + def restart(self): + with self.component_lock: + self._stop_irc_components() self.config.load() - #if self.config.components.get("irc_frontend"): - # self.commands.load() + self.commands.load() + self._start_irc_components() def stop(self): - if self.frontend: - self.frontend.stop() - if self.watcher: - self.watcher.stop() - self._keep_scheduling = False + self._stop_irc_components() + self._keep_looping = False sleep(3) # Give a few seconds to finish closing IRC connections diff --git a/earwigbot/commands/__init__.py b/earwigbot/commands/__init__.py index 824fe2b..da152cb 100644 --- a/earwigbot/commands/__init__.py +++ b/earwigbot/commands/__init__.py @@ -30,9 +30,9 @@ class. This can be accessed through `bot.commands`. """ import imp -import logging from os import listdir, path from re import sub +from threading import Lock __all__ = ["BaseCommand", "CommandManager"] @@ -49,21 +49,24 @@ class BaseCommand(object): # command subclass: hooks = ["msg"] - def __init__(self, connection): + def __init__(self, bot): """Constructor for new commands. This is called once when the command is loaded (from - commands._load_command()). `connection` is a Connection object, - allowing us to do self.connection.say(), self.connection.send(), etc, - from within a method. + commands._load_command()). `bot` is out base Bot object. Generally you + shouldn't need to override this; if you do, call + super(Command, self).__init__() first. """ - self.connection = connection - logger_name = ".".join(("earwigbot", "commands", self.name)) - self.logger = logging.getLogger(logger_name) - self.logger.setLevel(logging.DEBUG) + self.bot = bot + self.logger = bot.commands.getLogger(self.name) + + def _execute(self, data): + """Make a quick connection alias and then process() the message.""" + self.connection = self.bot.frontend + self.process(data) def check(self, data): - """Returns whether this command should be called in response to 'data'. + """Return whether this command should be called in response to 'data'. Given a Data() instance, return True if we should respond to this activity, or False if we should ignore it or it doesn't apply to us. @@ -72,17 +75,16 @@ class BaseCommand(object): return False. This is the default behavior of check(); you need only override it if you wish to change that. """ - if data.is_command and data.command == self.name: - return True - return False + return data.is_command and data.command == self.name def process(self, data): """Main entry point for doing a command. Handle an activity (usually a message) on IRC. At this point, thanks to self.check() which is called automatically by the command handler, - we know this is something we should respond to, so (usually) something - like 'if data.command != "command_name": return' is unnecessary. + we know this is something we should respond to, so something like + `if data.command != "command_name": return` is usually unnecessary. + Note that """ pass @@ -90,9 +92,9 @@ class BaseCommand(object): class CommandManager(object): def __init__(self, bot): self.bot = bot - self.logger = logging.getLogger("earwigbot.tasks") - self._dirs = [path.dirname(__file__), bot.config.root_dir] + self.logger = bot.logger.getLogger("commands") self._commands = {} + self._command_access_lock = Lock() def _load_command(self, name, path): """Load a specific command from a module, identified by name and path. @@ -113,7 +115,7 @@ class CommandManager(object): f.close() try: - command = module.Command(self.bot.frontend) + command = module.Command(self.bot) except AttributeError: return # No command in this module if not isinstance(command, BaseCommand): @@ -124,14 +126,16 @@ class CommandManager(object): def load(self): """Load (or reload) all valid commands into self._commands.""" - dirs = [path.join(path.dirname(__file__), "commands"), - path.join(bot.config.root_dir, "commands")] - for dir in dirs: - files = listdir(dir) - files = [sub("\.pyc?$", "", f) for f in files if f[0] != "_"] - files = list(set(files)) # Remove duplicates - for filename in sorted(files): - self._load_command(filename, dir) + self._commands = {} + with self._command_access_lock: + dirs = [path.join(path.dirname(__file__), "commands"), + path.join(bot.config.root_dir, "commands")] + for dir in dirs: + files = listdir(dir) + files = [sub("\.pyc?$", "", f) for f in files if f[0] != "_"] + files = list(set(files)) # Remove duplicates + for filename in sorted(files): + self._load_command(filename, dir) msg = "Found {0} commands: {1}" commands = ", ".join(self._commands.keys()) @@ -143,14 +147,13 @@ class CommandManager(object): def check(self, hook, data): """Given an IRC event, check if there's anything we can respond to.""" - # Parse command arguments into data.command and data.args: - data.parse_args() - for command in self._commands.values(): - if hook in command.hooks: - if command.check(data): - try: - command.process(data) - except Exception: - e = "Error executing command '{0}'" - self.logger.exception(e.format(data.command)) - break + with self._command_access_lock: + for command in self._commands.values(): + if hook in command.hooks: + if command.check(data): + try: + command._execute(data) + except Exception: + e = "Error executing command '{0}':" + self.logger.exception(e.format(data.command)) + break diff --git a/earwigbot/commands/restart.py b/earwigbot/commands/restart.py index 2b39e5e..4902551 100644 --- a/earwigbot/commands/restart.py +++ b/earwigbot/commands/restart.py @@ -27,11 +27,20 @@ class Command(BaseCommand): """Restart the bot. Only the owner can do this.""" name = "restart" + def check(self, data): + commands = ["restart", "reload"] + return data.is_command and data.command in commands + def process(self, data): if data.host not in config.irc["permissions"]["owners"]: msg = "you must be a bot owner to use this command." self.connection.reply(data, msg) return - self.connection.logger.info("Restarting bot per owner request") - self.connection.stop() + if data.command == "restart": + self.connection.logger.info("Restarting bot per owner request") + self.connection.bot.restart() + + elif data.command == "reload": + self.connection.bot.commands.load() + self.connection.logger.info("IRC commands reloaded") diff --git a/earwigbot/irc/connection.py b/earwigbot/irc/connection.py index cd99421..7e2bb19 100644 --- a/earwigbot/irc/connection.py +++ b/earwigbot/irc/connection.py @@ -21,7 +21,7 @@ # SOFTWARE. import socket -import threading +from threading import Lock from time import sleep __all__ = ["BrokenSocketException", "IRCConnection"] @@ -36,17 +36,16 @@ class BrokenSocketException(Exception): class IRCConnection(object): """A class to interface with IRC.""" - def __init__(self, host, port, nick, ident, realname, logger): + def __init__(self, host, port, nick, ident, realname): self.host = host self.port = port self.nick = nick self.ident = ident self.realname = realname - self.logger = logger self._is_running = False # A lock to prevent us from sending two messages at once: - self._lock = threading.Lock() + self._send_lock = Lock() def _connect(self): """Connect to our IRC server.""" @@ -78,8 +77,7 @@ class IRCConnection(object): def _send(self, msg): """Send data to the server.""" - # Ensure that we only send one message at a time with a blocking lock: - with self._lock: + with self._send_lock: self._sock.sendall(msg + "\r\n") self.logger.debug(msg) diff --git a/earwigbot/irc/frontend.py b/earwigbot/irc/frontend.py index 83a0780..88fddbd 100644 --- a/earwigbot/irc/frontend.py +++ b/earwigbot/irc/frontend.py @@ -20,10 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import logging import re -from earwigbot.irc import IRCConnection, Data, BrokenSocketException +from earwigbot.irc import IRCConnection, Data __all__ = ["Frontend"] @@ -41,13 +40,12 @@ class Frontend(IRCConnection): def __init__(self, bot): self.bot = bot - self.config = bot.config - self.logger = logging.getLogger("earwigbot.frontend") + self.logger = bot.logger.getLogger("frontend") - cf = config.irc["frontend"] + cf = bot.config.irc["frontend"] base = super(Frontend, self) base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], - cf["realname"], self.logger) + cf["realname"]) self._connect() def _process_message(self, line): @@ -58,36 +56,35 @@ class Frontend(IRCConnection): if line[1] == "JOIN": data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] data.chan = line[2] - # Check for 'join' hooks in our commands: - command_manager.check("join", data) + data.parse_args() + self.bot.commands.check("join", data) elif line[1] == "PRIVMSG": data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] data.msg = " ".join(line[3:])[1:] data.chan = line[2] + data.parse_args() - if data.chan == self.config.irc["frontend"]["nick"]: + if data.chan == self.bot.config.irc["frontend"]["nick"]: # This is a privmsg to us, so set 'chan' as the nick of the # sender, then check for private-only command hooks: data.chan = data.nick - command_manager.check("msg_private", data) + self.bot.commands.check("msg_private", data) else: # Check for public-only command hooks: - command_manager.check("msg_public", data) + self.bot.commands.check("msg_public", data) # Check for command hooks that apply to all messages: - command_manager.check("msg", data) + self.bot.commands.check("msg", data) - # If we are pinged, pong back: - elif line[0] == "PING": + elif line[0] == "PING": # If we are pinged, pong back self.pong(line[1]) - # On successful connection to the server: - elif line[1] == "376": + elif line[1] == "376": # On successful connection to the server # If we're supposed to auth to NickServ, do that: try: - username = self.config.irc["frontend"]["nickservUsername"] - password = self.config.irc["frontend"]["nickservPassword"] + username = self.bot.config.irc["frontend"]["nickservUsername"] + password = self.bot.config.irc["frontend"]["nickservPassword"] except KeyError: pass else: @@ -95,5 +92,5 @@ class Frontend(IRCConnection): self.say("NickServ", msg) # Join all of our startup channels: - for chan in self.config.irc["frontend"]["channels"]: + for chan in self.bot.config.irc["frontend"]["channels"]: self.join(chan) diff --git a/earwigbot/irc/watcher.py b/earwigbot/irc/watcher.py index b29f4f8..572a3aa 100644 --- a/earwigbot/irc/watcher.py +++ b/earwigbot/irc/watcher.py @@ -21,9 +21,8 @@ # SOFTWARE. import imp -import logging -from earwigbot.irc import IRCConnection, RC, BrokenSocketException +from earwigbot.irc import IRCConnection, RC __all__ = ["Watcher"] @@ -40,14 +39,12 @@ class Watcher(IRCConnection): def __init__(self, bot): self.bot = bot - self.config = bot.config - self.frontend = bot.frontend - self.logger = logging.getLogger("earwigbot.watcher") + self.logger = bot.logger.getLogger("watcher") - cf = config.irc["watcher"] + cf = bot.config.irc["watcher"] base = super(Watcher, self) base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], - cf["realname"], self.logger) + cf["realname"]) self._prepare_process_hook() self._connect() @@ -60,7 +57,7 @@ class Watcher(IRCConnection): # Ignore messages originating from channels not in our list, to # prevent someone PMing us false data: - if chan not in self.config.irc["watcher"]["channels"]: + if chan not in self.bot.config.irc["watcher"]["channels"]: return msg = " ".join(line[3:])[1:] @@ -74,7 +71,7 @@ class Watcher(IRCConnection): # When we've finished starting up, join all watcher channels: elif line[1] == "376": - for chan in self.config.irc["watcher"]["channels"]: + for chan in self.bot.config.irc["watcher"]["channels"]: self.join(chan) def _prepare_process_hook(self): @@ -86,14 +83,15 @@ class Watcher(IRCConnection): # Set a default RC process hook that does nothing: self._process_hook = lambda rc: () try: - rules = self.config.data["rules"] + rules = self.bot.config.data["rules"] except KeyError: return module = imp.new_module("_rc_event_processing_rules") + path = self.bot.config.path try: - exec compile(rules, self.config.path, "exec") in module.__dict__ + exec compile(rules, path, "exec") in module.__dict__ except Exception: - e = "Could not compile config file's RC event rules" + e = "Could not compile config file's RC event rules:" self.logger.exception(e) return self._process_hook_module = module @@ -113,7 +111,9 @@ class Watcher(IRCConnection): our config. """ chans = self._process_hook(rc) - if chans and self.frontend: - pretty = rc.prettify() - for chan in chans: - self.frontend.say(chan, pretty) + with self.bot.component_lock: + frontend = self.bot.frontend + if chans and frontend and not frontend.is_stopped(): + pretty = rc.prettify() + for chan in chans: + frontend.say(chan, pretty)