- Fixed loading bugs in CommandLoader and TaskLoadertags/v0.1^2
| @@ -53,8 +53,8 @@ class Bot(object): | |||||
| wiki toolset with bot.wiki.get_site(). | wiki toolset with bot.wiki.get_site(). | ||||
| """ | """ | ||||
| def __init__(self, root_dir): | |||||
| self.config = BotConfig(root_dir) | |||||
| def __init__(self, root_dir, level=logging.INFO): | |||||
| self.config = BotConfig(root_dir, level) | |||||
| self.logger = logging.getLogger("earwigbot") | self.logger = logging.getLogger("earwigbot") | ||||
| self.commands = CommandManager(self) | self.commands = CommandManager(self) | ||||
| self.tasks = TaskManager(self) | self.tasks = TaskManager(self) | ||||
| @@ -65,6 +65,10 @@ class Bot(object): | |||||
| self.component_lock = Lock() | self.component_lock = Lock() | ||||
| self._keep_looping = True | self._keep_looping = True | ||||
| self.config.load() | |||||
| self.commands.load() | |||||
| self.tasks.load() | |||||
| def _start_irc_components(self): | def _start_irc_components(self): | ||||
| if self.config.components.get("irc_frontend"): | if self.config.components.get("irc_frontend"): | ||||
| self.logger.info("Starting IRC frontend") | self.logger.info("Starting IRC frontend") | ||||
| @@ -107,18 +111,10 @@ class Bot(object): | |||||
| self.logger.warn("IRC watcher has stopped; restarting") | self.logger.warn("IRC watcher has stopped; restarting") | ||||
| self.watcher = Watcher(self) | self.watcher = Watcher(self) | ||||
| Thread(name=name, target=self.watcher.loop).start() | Thread(name=name, target=self.watcher.loop).start() | ||||
| sleep(5) | |||||
| sleep(3) | |||||
| def run(self): | def run(self): | ||||
| config = self.config | |||||
| config.load() | |||||
| config.decrypt(config.wiki, "password") | |||||
| config.decrypt(config.wiki, "search", "credentials", "key") | |||||
| config.decrypt(config.wiki, "search", "credentials", "secret") | |||||
| config.decrypt(config.irc, "frontend", "nickservPassword") | |||||
| config.decrypt(config.irc, "watcher", "nickservPassword") | |||||
| self.commands.load() | |||||
| self.tasks.load() | |||||
| self.logger.info("Starting bot") | |||||
| self._start_irc_components() | self._start_irc_components() | ||||
| self._start_wiki_scheduler() | self._start_wiki_scheduler() | ||||
| self._loop() | self._loop() | ||||
| @@ -137,4 +133,3 @@ class Bot(object): | |||||
| with self.component_lock: | with self.component_lock: | ||||
| self._stop_irc_components() | self._stop_irc_components() | ||||
| self._keep_looping = False | self._keep_looping = False | ||||
| sleep(3) # Give a few seconds to finish closing IRC connections | |||||
| @@ -135,22 +135,31 @@ class CommandManager(object): | |||||
| return | return | ||||
| self._commands[command.name] = command | self._commands[command.name] = command | ||||
| self.logger.debug("Added command {0}".format(command.name)) | |||||
| self.logger.debug("Loaded command {0}".format(command.name)) | |||||
| def _load_directory(self, dir): | |||||
| """Load all valid commands in a given directory.""" | |||||
| processed = [] | |||||
| for name in listdir(dir): | |||||
| if not name.endswith(".py") and not name.endswith(".pyc"): | |||||
| continue | |||||
| if name.startswith("_") or name.startswith("."): | |||||
| continue | |||||
| modname = sub("\.pyc?$", "", name) # Remove extension | |||||
| if modname not in processed: | |||||
| self._load_command(modname, dir) | |||||
| processed.append(modname) | |||||
| def load(self): | def load(self): | ||||
| """Load (or reload) all valid commands into self._commands.""" | """Load (or reload) all valid commands into self._commands.""" | ||||
| with self._command_access_lock: | with self._command_access_lock: | ||||
| self._commands.clear() | self._commands.clear() | ||||
| dirs = [path.join(path.dirname(__file__), "commands"), | |||||
| path.join(self.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}" | |||||
| builtin_dir = path.dirname(__file__) | |||||
| plugins_dir = path.join(self.bot.config.root_dir, "commands") | |||||
| self._load_directory(builtin_dir) # Built-in commands | |||||
| self._load_directory(plugins_dir) # Custom commands, aka plugins | |||||
| msg = "Loaded {0} commands: {1}" | |||||
| commands = ", ".join(self._commands.keys()) | commands = ", ".join(self._commands.keys()) | ||||
| self.logger.info(msg.format(len(self._commands), commands)) | self.logger.info(msg.format(len(self._commands), commands)) | ||||
| @@ -59,8 +59,9 @@ class BotConfig(object): | |||||
| aren't encrypted | aren't encrypted | ||||
| """ | """ | ||||
| def __init__(self, root_dir): | |||||
| def __init__(self, root_dir, level): | |||||
| self._root_dir = root_dir | self._root_dir = root_dir | ||||
| self._logging_level = level | |||||
| self._config_path = path.join(self._root_dir, "config.yml") | self._config_path = path.join(self._root_dir, "config.yml") | ||||
| self._log_dir = path.join(self._root_dir, "logs") | self._log_dir = path.join(self._root_dir, "logs") | ||||
| self._decryption_key = None | self._decryption_key = None | ||||
| @@ -74,7 +75,14 @@ class BotConfig(object): | |||||
| self._nodes = [self._components, self._wiki, self._tasks, self._irc, | self._nodes = [self._components, self._wiki, self._tasks, self._irc, | ||||
| self._metadata] | self._metadata] | ||||
| self._decryptable_nodes = [] | |||||
| self._decryptable_nodes = [ # Default nodes to decrypt | |||||
| (self._wiki, ("password")), | |||||
| (self._wiki, ("search", "credentials", "key")), | |||||
| (self._wiki, ("search", "credentials", "secret")), | |||||
| (self._irc, ("frontend", "nickservPassword")), | |||||
| (self._irc, ("watcher", "nickservPassword")), | |||||
| ] | |||||
| def _load(self): | def _load(self): | ||||
| """Load data from our JSON config file (config.yml) into self._data.""" | """Load data from our JSON config file (config.yml) into self._data.""" | ||||
| @@ -119,10 +127,10 @@ class BotConfig(object): | |||||
| h.setFormatter(formatter) | h.setFormatter(formatter) | ||||
| logger.addHandler(h) | logger.addHandler(h) | ||||
| stream_handler = logging.StreamHandler() | |||||
| stream_handler.setLevel(logging.DEBUG) | |||||
| stream_handler.setFormatter(color_formatter) | |||||
| logger.addHandler(stream_handler) | |||||
| self._stream_handler = stream = logging.StreamHandler() | |||||
| stream.setLevel(self._logging_level) | |||||
| stream.setFormatter(color_formatter) | |||||
| logger.addHandler(stream) | |||||
| def _decrypt(self, node, nodes): | def _decrypt(self, node, nodes): | ||||
| """Try to decrypt the contents of a config node. Use self.decrypt().""" | """Try to decrypt the contents of a config node. Use self.decrypt().""" | ||||
| @@ -148,6 +156,15 @@ class BotConfig(object): | |||||
| return self._root_dir | return self._root_dir | ||||
| @property | @property | ||||
| def logging_level(self): | |||||
| return self._logging_level | |||||
| @logging_level.setter | |||||
| def logging_level(self, level): | |||||
| self._logging_level = level | |||||
| self._stream_handler.setLevel(level) | |||||
| @property | |||||
| def path(self): | def path(self): | ||||
| return self._config_path | return self._config_path | ||||
| @@ -213,7 +230,7 @@ class BotConfig(object): | |||||
| if choice.lower().startswith("y"): | if choice.lower().startswith("y"): | ||||
| self._make_new() | self._make_new() | ||||
| else: | else: | ||||
| exit(1) | |||||
| exit(1) # TODO: raise an exception instead | |||||
| self._load() | self._load() | ||||
| data = self._data | data = self._data | ||||
| @@ -30,6 +30,7 @@ internal TaskManager class. This can be accessed through `bot.tasks`. | |||||
| import imp | import imp | ||||
| from os import listdir, path | from os import listdir, path | ||||
| from re import sub | |||||
| from threading import Lock, Thread | from threading import Lock, Thread | ||||
| from time import gmtime, strftime | from time import gmtime, strftime | ||||
| @@ -186,22 +187,31 @@ class TaskManager(object): | |||||
| return | return | ||||
| self._tasks[task.name] = task | self._tasks[task.name] = task | ||||
| self.logger.debug("Added task {0}".format(task.name)) | |||||
| self.logger.debug("Loaded task {0}".format(task.name)) | |||||
| def _load_directory(self, dir): | |||||
| """Load all valid tasks in a given directory.""" | |||||
| processed = [] | |||||
| for name in listdir(dir): | |||||
| if not name.endswith(".py") and not name.endswith(".pyc"): | |||||
| continue | |||||
| if name.startswith("_") or name.startswith("."): | |||||
| continue | |||||
| modname = sub("\.pyc?$", "", name) # Remove extension | |||||
| if modname not in processed: | |||||
| self._load_task(modname, dir) | |||||
| processed.append(modname) | |||||
| def load(self): | def load(self): | ||||
| """Load (or reload) all valid tasks into self._tasks.""" | """Load (or reload) all valid tasks into self._tasks.""" | ||||
| with self._task_access_lock: | with self._task_access_lock: | ||||
| self._tasks.clear() | self._tasks.clear() | ||||
| dirs = [path.join(path.dirname(__file__), "tasks"), | |||||
| path.join(self.bot.config.root_dir, "tasks")] | |||||
| 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_task(filename) | |||||
| msg = "Found {0} tasks: {1}" | |||||
| builtin_dir = path.dirname(__file__) | |||||
| plugins_dir = path.join(self.bot.config.root_dir, "tasks") | |||||
| self._load_directory(builtin_dir) # Built-in tasks | |||||
| self._load_directory(plugins_dir) # Custom tasks, aka plugins | |||||
| msg = "Loaded {0} tasks: {1}" | |||||
| tasks = ', '.join(self._tasks.keys()) | tasks = ', '.join(self._tasks.keys()) | ||||
| self.logger.info(msg.format(len(self._tasks), tasks)) | self.logger.info(msg.format(len(self._tasks), tasks)) | ||||
| @@ -21,46 +21,54 @@ | |||||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| # SOFTWARE. | # SOFTWARE. | ||||
| """ | |||||
| This is EarwigBot's command-line utility, enabling you to easily start the | |||||
| bot or run specific tasks. | |||||
| """ | |||||
| import argparse | import argparse | ||||
| import logging | |||||
| from os import path | from os import path | ||||
| from earwigbot import __version__ | from earwigbot import __version__ | ||||
| from earwigbot.bot import Bot | from earwigbot.bot import Bot | ||||
| __all__ = ["BotUtility", "main"] | |||||
| class BotUtility(object): | |||||
| """ | |||||
| This is a command-line utility for EarwigBot that enables you to easily | |||||
| start the bot without writing generally unnecessary three-line bootstrap | |||||
| scripts. It supports starting the bot from any directory, as well as | |||||
| starting individual tasks instead of the entire bot. | |||||
| """ | |||||
| def version(self): | |||||
| return "EarwigBot v{0}".format(__version__) | |||||
| def run(self, root_dir): | |||||
| bot = Bot(root_dir) | |||||
| print self.version() | |||||
| #try: | |||||
| # bot.run() | |||||
| #finally: | |||||
| # bot.stop() | |||||
| def main(self): | |||||
| parser = argparse.ArgumentParser(description=BotUtility.__doc__) | |||||
| parser.add_argument("-v", "--version", action="version", | |||||
| version=self.version()) | |||||
| parser.add_argument("root_dir", metavar="path", nargs="?", default=path.curdir) | |||||
| args = parser.parse_args() | |||||
| __all__ = ["main"] | |||||
| root_dir = path.abspath(args.root_dir) | |||||
| self.run(root_dir) | |||||
| def main(): | |||||
| version = "EarwigBot v{0}".format(__version__) | |||||
| parser = argparse.ArgumentParser(description=__doc__) | |||||
| parser.add_argument("path", nargs="?", metavar="PATH", default=path.curdir, | |||||
| help="path to the bot's working directory, which will be created if it doesn't exist; current directory assumed if not specified") | |||||
| parser.add_argument("-v", "--version", action="version", version=version) | |||||
| parser.add_argument("-d", "--debug", action="store_true", | |||||
| help="print all logs, including DEBUG-level messages") | |||||
| parser.add_argument("-q", "--quiet", action="store_true", | |||||
| help="don't print any logs except warnings and errors") | |||||
| parser.add_argument("-t", "--task", metavar="NAME", | |||||
| help="given the name of a task, the bot will run it instead of the main bot and then exit") | |||||
| args = parser.parse_args() | |||||
| if args.debug and args.quiet: | |||||
| parser.print_usage() | |||||
| print "earwigbot: error: cannot show debug messages and be quiet at the same time" | |||||
| return | |||||
| level = logging.INFO | |||||
| if args.debug: | |||||
| level = logging.DEBUG | |||||
| elif args.quiet: | |||||
| level = logging.WARNING | |||||
| main = BotUtility().main | |||||
| print version | |||||
| bot = Bot(path.abspath(args.path), level=level) | |||||
| try: | |||||
| if args.task: | |||||
| bot.tasks.start(args.task) | |||||
| else: | |||||
| bot.run() | |||||
| finally: | |||||
| bot.stop() | |||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||
| main() | main() | ||||