diff --git a/earwigbot/bot.py b/earwigbot/bot.py index 65c5442..15c5fef 100644 --- a/earwigbot/bot.py +++ b/earwigbot/bot.py @@ -23,10 +23,13 @@ import threading from time import sleep, time +from earwigbot.commands import CommandManager from earwigbot.config import BotConfig from earwigbot.irc import Frontend, Watcher from earwigbot.tasks import task_manager +__all__ = ["Bot"] + class Bot(object): """ The Bot class is the core of EarwigBot, essentially responsible for @@ -46,16 +49,14 @@ class Bot(object): def __init__(self, root_dir): self.config = BotConfig(root_dir) self.logger = logging.getLogger("earwigbot") + self.commands = CommandManager(self) + self.tasks = None self.frontend = None self.watcher = None self._keep_scheduling = True self._lock = threading.Lock() - def _start_thread(self, name, target): - thread = threading.Thread(name=name, target=target) - thread.start() - def _wiki_scheduler(self): while self._keep_scheduling: time_start = time() @@ -68,17 +69,18 @@ class Bot(object): def _start_components(self): if self.config.components.get("irc_frontend"): self.logger.info("Starting IRC frontend") - self.frontend = Frontend(self.config) - self._start_thread(name, self.frontend.loop) + self.frontend = Frontend(self) + self.commands.load() + threading.Thread(name=name, target=self.frontend.loop).start() if self.config.components.get("irc_watcher"): self.logger.info("Starting IRC watcher") - self.watcher = Watcher(self.config, self.frontend) - self._start_thread(name, self.watcher.loop) + self.watcher = Watcher(self) + threading.Thread(name=name, target=self.watcher.loop).start() if self.config.components.get("wiki_scheduler"): self.logger.info("Starting wiki scheduler") - self._start_thread(name, self._wiki_scheduler) + threading.Thread(name=name, target=self._wiki_scheduler).start() def _loop(self): while 1: @@ -104,6 +106,7 @@ class Bot(object): with self._lock: self.config.load() #if self.config.components.get("irc_frontend"): + # self.commands.load() def stop(self): if self.frontend: @@ -111,3 +114,4 @@ class Bot(object): if self.watcher: self.watcher.stop() self._keep_scheduling = 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 d5543bb..824fe2b 100644 --- a/earwigbot/commands/__init__.py +++ b/earwigbot/commands/__init__.py @@ -25,17 +25,16 @@ EarwigBot's IRC Command Manager This package provides the IRC "commands" used by the bot's front-end component. This module contains the BaseCommand class (import with -`from earwigbot.commands import BaseCommand`) and an internal _CommandManager -class. This can be accessed through the `command_manager` singleton. +`from earwigbot.commands import BaseCommand`) and an internal CommandManager +class. This can be accessed through `bot.commands`. """ +import imp import logging -import os -import sys +from os import listdir, path +from re import sub -from earwigbot.config import config - -__all__ = ["BaseCommand", "command_manager"] +__all__ = ["BaseCommand", "CommandManager"] class BaseCommand(object): """A base class for commands on IRC. @@ -88,32 +87,33 @@ class BaseCommand(object): pass -class _CommandManager(object): - def __init__(self): +class CommandManager(object): + def __init__(self, bot): + self.bot = bot self.logger = logging.getLogger("earwigbot.tasks") - self._base_dir = os.path.dirname(os.path.abspath(__file__)) - self._connection = None + self._dirs = [path.dirname(__file__), bot.config.root_dir] self._commands = {} - def _load_command(self, filename): - """Load a specific command from a module, identified by filename. + def _load_command(self, name, path): + """Load a specific command from a module, identified by name and path. - Given a Connection object and a filename, we'll first try to import - it, and if that works, make an instance of the 'Command' class inside - (assuming it is an instance of BaseCommand), add it to self._commands, - and log the addition. Any problems along the way will either be - ignored or logged. + We'll first try to import it using imp magic, and if that works, make + an instance of the 'Command' class inside (assuming it is an instance + of BaseCommand), add it to self._commands, and log the addition. Any + problems along the way will either be ignored or logged. """ - # Strip .py from the filename's end and join with our package name: - name = ".".join(("commands", filename[:-3])) + f, path, desc = imp.find_module(name, [path]) try: - __import__(name) - except: - self.logger.exception("Couldn't load file {0}".format(filename)) + module = imp.load_module(name, f, path, desc) + except Exception: + e = "Couldn't load module {0} from {1}" + self.logger.exception(e.format(name, path)) return + finally: + f.close() try: - command = sys.modules[name].Command(self._connection) + command = module.Command(self.bot.frontend) except AttributeError: return # No command in this module if not isinstance(command, BaseCommand): @@ -122,20 +122,16 @@ class _CommandManager(object): self._commands[command.name] = command self.logger.debug("Added command {0}".format(command.name)) - def load(self, connection): - """Load all valid commands into self._commands. - - `connection` is a Connection object that is given to each command's - constructor. - """ - self._connection = connection - - files = os.listdir(self._base_dir) - files.sort() - for filename in files: - if filename.startswith("_") or not filename.endswith(".py"): - continue - self._load_command(filename) + 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) msg = "Found {0} commands: {1}" commands = ", ".join(self._commands.keys()) @@ -158,6 +154,3 @@ class _CommandManager(object): e = "Error executing command '{0}'" self.logger.exception(e.format(data.command)) break - - -command_manager = _CommandManager() diff --git a/earwigbot/irc/frontend.py b/earwigbot/irc/frontend.py index 13c6bc3..83a0780 100644 --- a/earwigbot/irc/frontend.py +++ b/earwigbot/irc/frontend.py @@ -23,7 +23,6 @@ import logging import re -from earwigbot.commands import command_manager from earwigbot.irc import IRCConnection, Data, BrokenSocketException __all__ = ["Frontend"] @@ -40,14 +39,15 @@ class Frontend(IRCConnection): """ sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z") - def __init__(self, config): - self.config = config + def __init__(self, bot): + self.bot = bot + self.config = bot.config self.logger = logging.getLogger("earwigbot.frontend") + cf = config.irc["frontend"] base = super(Frontend, self) base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], cf["realname"], self.logger) - command_manager.load(self) self._connect() def _process_message(self, line): diff --git a/earwigbot/irc/watcher.py b/earwigbot/irc/watcher.py index f387b13..b29f4f8 100644 --- a/earwigbot/irc/watcher.py +++ b/earwigbot/irc/watcher.py @@ -38,14 +38,16 @@ class Watcher(IRCConnection): to channels on the IRC frontend. """ - def __init__(self, config, frontend=None): - self.config = config + def __init__(self, bot): + self.bot = bot + self.config = bot.config + self.frontend = bot.frontend self.logger = logging.getLogger("earwigbot.watcher") + cf = config.irc["watcher"] base = super(Watcher, self) base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], cf["realname"], self.logger) - self.frontend = frontend self._prepare_process_hook() self._connect() diff --git a/earwigbot/util.py b/earwigbot/util.py index 6f87740..915747a 100755 --- a/earwigbot/util.py +++ b/earwigbot/util.py @@ -36,12 +36,22 @@ class BotUtility(object): return __version__ def run(self): - print "EarwigBot v{0}\n".format(self.version()) - - def main(self): root_dir = path.abspath(path.curdir()) bot = Bot(root_dir) - bot.run() + try: + bot.run() + finally: + bot.stop() + + def main(self): + print "EarwigBot v{0}\n".format(self.version()) + parser = argparse.ArgumentParser(description=BotUtility.__doc__) + + parser.add_argument("-V", "--version", action="version", + version=self.version()) + + args = parser.parse_args() +# args.func(args) main = BotUtility().main diff --git a/setup.py b/setup.py index 41dc7d9..348b593 100644 --- a/setup.py +++ b/setup.py @@ -30,8 +30,10 @@ from setuptools import setup setup( name = "earwigbot", entry_points = {"console_scripts": ["earwigbot = earwigbot.util:main"]}, - install_requires = ["PyYAML>=3.10", "oursql>=0.9.3", "oauth2>=1.5.211", - "numpy>=1.6.1", "matplotlib>=1.1.0"], + install_requires = ["PyYAML>=3.10", + "oursql>=0.9.3", + "oauth2>=1.5.211", + "matplotlib>=1.1.0"], version = "0.1.dev", author = "Ben Kurtovic", author_email = "ben.kurtovic@verizon.net",