| @@ -21,7 +21,7 @@ | |||
| # SOFTWARE. | |||
| import logging | |||
| from threading import Lock, Thread | |||
| from threading import Lock, Thread, enumerate as enumerate_threads | |||
| from time import sleep, time | |||
| from earwigbot.config import BotConfig | |||
| @@ -104,6 +104,31 @@ class Bot(object): | |||
| if self.watcher: | |||
| self.watcher.stop(msg) | |||
| def _stop_task_threads(self): | |||
| """Notify the user of which task threads are going to be killed. | |||
| Unfortunately, there is no method right now of stopping task threads | |||
| safely. This is because there is no way to tell them to stop like the | |||
| IRC components can be told; furthermore, they are run as daemons, and | |||
| daemon threads automatically stop without calling any __exit__ or | |||
| try/finally code when all non-daemon threads stop. They were originally | |||
| implemented as regular non-daemon threads, but this meant there was no | |||
| way to completely stop the bot if tasks were running, because all other | |||
| threads would exit and threading would absorb KeyboardInterrupts. | |||
| The advantage of this is that stopping the bot is truly guarenteed to | |||
| *stop* the bot, while the disadvantage is that the tasks are given no | |||
| advance warning of their forced shutdown. | |||
| """ | |||
| tasks = [] | |||
| non_tasks = self.config.components.keys() + ["MainThread", "reminder"] | |||
| for thread in enumerate_threads(): | |||
| if thread.name not in non_tasks and thread.is_alive(): | |||
| tasks.append(thread.name) | |||
| if tasks: | |||
| log = "The following tasks will be killed: {0}" | |||
| self.logger.warn(log.format(" ".join(tasks))) | |||
| def run(self): | |||
| """Main entry point into running the bot. | |||
| @@ -160,3 +185,4 @@ class Bot(object): | |||
| with self.component_lock: | |||
| self._stop_irc_components(msg) | |||
| self._keep_looping = False | |||
| self._stop_task_threads() | |||
| @@ -37,8 +37,10 @@ class Command(BaseCommand): | |||
| try: | |||
| self.statistics = self.bot.tasks.get("afc_statistics") | |||
| except KeyError: | |||
| e = "Cannot run command: requires afc_statistics task (from earwigbot_plugins)." | |||
| e = "Cannot run command: requires afc_statistics task (from earwigbot_plugins)" | |||
| self.logger.error(e) | |||
| msg = "command requires afc_statistics task (from earwigbot_plugins)" | |||
| self.reply(data, msg) | |||
| return | |||
| if not data.args: | |||
| @@ -301,7 +301,7 @@ class BotConfig(object): | |||
| class _ConfigNode(object): | |||
| def __iter__(self): | |||
| for key in self.__dict__.iterkeys(): | |||
| for key in self.__dict__: | |||
| yield key | |||
| def __getitem__(self, item): | |||
| @@ -330,6 +330,24 @@ class _ConfigNode(object): | |||
| def get(self, *args, **kwargs): | |||
| return self.__dict__.get(*args, **kwargs) | |||
| def keys(self): | |||
| return self.__dict__.keys() | |||
| def values(self): | |||
| return self.__dict__.values() | |||
| def items(self): | |||
| return self.__dict__.items() | |||
| def iterkeys(self): | |||
| return self.__dict__.iterkeys() | |||
| def itervalues(self): | |||
| return self.__dict__.itervalues() | |||
| def iteritems(self): | |||
| return self.__dict__.iteritems() | |||
| class _BotFormatter(logging.Formatter): | |||
| def __init__(self, color=False): | |||
| @@ -53,7 +53,7 @@ class IRCConnection(object): | |||
| try: | |||
| self._sock.connect((self.host, self.port)) | |||
| except socket.error: | |||
| self.logger.exception("Couldn't connect to IRC server") | |||
| self.logger.exception("Couldn't connect to IRC server; retrying") | |||
| sleep(8) | |||
| self._connect() | |||
| self._send("NICK {0}".format(self.nick)) | |||
| @@ -179,21 +179,27 @@ class TaskManager(_ResourceManager): | |||
| self.logger.info(msg.format(task.name)) | |||
| def start(self, task_name, **kwargs): | |||
| """Start a given task in a new thread. kwargs are passed to task.run""" | |||
| """Start a given task in a new daemon thread, and return the thread. | |||
| kwargs are passed to task.run(). If the task is not found, None will be | |||
| returned. | |||
| """ | |||
| msg = "Starting task '{0}' in a new thread" | |||
| self.logger.info(msg.format(task_name)) | |||
| try: | |||
| task = self.get(task_name) | |||
| except KeyError: | |||
| e = "Couldn't find task '{0}':" | |||
| e = "Couldn't find task '{0}'" | |||
| self.logger.error(e.format(task_name)) | |||
| return | |||
| task_thread = Thread(target=self._wrapper, args=(task,), kwargs=kwargs) | |||
| start_time = strftime("%b %d %H:%M:%S") | |||
| task_thread.name = "{0} ({1})".format(task_name, start_time) | |||
| task_thread.daemon = True | |||
| task_thread.start() | |||
| return task_thread | |||
| def schedule(self, now=None): | |||
| """Start all tasks that are supposed to be run at a given time.""" | |||
| @@ -207,7 +207,7 @@ class Task(BaseTask): | |||
| replag = self.site.get_replag() | |||
| self.logger.debug("Server replag is {0}".format(replag)) | |||
| if replag > 600 and not kwargs.get("ignore_replag"): | |||
| msg = "Sync canceled as replag ({0} secs) is greater than ten minutes." | |||
| msg = "Sync canceled as replag ({0} secs) is greater than ten minutes" | |||
| self.logger.warn(msg.format(replag)) | |||
| return | |||
| @@ -26,9 +26,10 @@ This is EarwigBot's command-line utility, enabling you to easily start the | |||
| bot or run specific tasks. | |||
| """ | |||
| import argparse | |||
| from argparse import ArgumentParser | |||
| import logging | |||
| from os import path | |||
| from time import sleep | |||
| from earwigbot import __version__ | |||
| from earwigbot.bot import Bot | |||
| @@ -37,7 +38,7 @@ __all__ = ["main"] | |||
| def main(): | |||
| version = "EarwigBot v{0}".format(__version__) | |||
| parser = argparse.ArgumentParser(description=__doc__) | |||
| parser = 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) | |||
| @@ -63,7 +64,17 @@ def main(): | |||
| bot = Bot(path.abspath(args.path), level=level) | |||
| if args.task: | |||
| bot.tasks.start(args.task) | |||
| thread = bot.tasks.start(args.task) | |||
| if not thread: | |||
| return | |||
| try: | |||
| while thread.is_alive(): # Keep it alive; it's a daemon | |||
| sleep(1) | |||
| except KeyboardInterrupt: | |||
| pass | |||
| finally: | |||
| if thread.is_alive(): | |||
| bot.tasks.logger.warn("The task is will be killed") | |||
| else: | |||
| try: | |||
| bot.run() | |||
| @@ -242,7 +242,7 @@ class Site(object): | |||
| e = "Maximum number of retries reached ({0})." | |||
| raise SiteAPIError(e.format(self._max_retries)) | |||
| tries += 1 | |||
| msg = 'Server says: "{0}". Retrying in {1} seconds ({2}/{3}).' | |||
| msg = 'Server says "{0}"; retrying in {1} seconds ({2}/{3})' | |||
| logger.info(msg.format(info, wait, tries, self._max_retries)) | |||
| sleep(wait) | |||
| return self._api_query(params, tries=tries, wait=wait*3) | |||