diff options
Diffstat (limited to 'module/interaction')
-rw-r--r-- | module/interaction/EventManager.py | 136 | ||||
-rw-r--r-- | module/interaction/InteractionManager.py | 159 | ||||
-rw-r--r-- | module/interaction/InteractionTask.py | 81 | ||||
-rw-r--r-- | module/interaction/__init__.py | 2 |
4 files changed, 378 insertions, 0 deletions
diff --git a/module/interaction/EventManager.py b/module/interaction/EventManager.py new file mode 100644 index 000000000..976a92413 --- /dev/null +++ b/module/interaction/EventManager.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +from threading import Lock +from traceback import print_exc +from time import time + +from module.utils import lock + +class EventManager: + """ + Handles all event-related tasks, also stores an event queue for clients, so they can retrieve them later. + + **Known Events:** + Most addon methods exist as events. These are some additional known events. + + ===================== ================ =========================================================== + Name Arguments Description + ===================== ================ =========================================================== + metaEvent eventName, *args Called for every event, with eventName and original args + downloadPreparing fid A download was just queued and will be prepared now. + downloadStarts fid A plugin will immediately start the download afterwards. + linksAdded links, pid Someone just added links, you are able to modify these links. + allDownloadsProcessed All links were handled, pyLoad would idle afterwards. + allDownloadsFinished All downloads in the queue are finished. + unrarFinished folder, fname An Unrar job finished + configChanged sec, opt, value The config was changed. + ===================== ================ =========================================================== + + | Notes: + | allDownloadsProcessed is *always* called before allDownloadsFinished. + | configChanged is *always* called before pluginConfigChanged. + """ + + CLIENT_EVENTS = ("packageUpdated", "packageInserted", "linkUpdated", "packageDeleted") + + def __init__(self, core): + self.core = core + self.log = core.log + + # uuid : list of events + self.clients = {} + self.events = {"metaEvent": []} + + self.lock = Lock() + + def getEvents(self, uuid): + """ Get accumulated events for uuid since last call, this also registers a new client """ + if uuid not in self.clients: + self.clients[uuid] = Client() + return self.clients[uuid].get() + + def addEvent(self, event, func): + """Adds an event listener for event name""" + if event in self.events: + if func in self.events[event]: + self.log.debug("Function already registered %s" % func) + else: + self.events[event].append(func) + else: + self.events[event] = [func] + + def removeEvent(self, event, func): + """removes previously added event listener""" + if event in self.events: + self.events[event].remove(func) + + def dispatchEvent(self, event, *args): + """dispatches event with args""" + for f in self.events["metaEvent"]: + try: + f(event, *args) + except Exception, e: + self.log.warning("Error calling event handler %s: %s, %s, %s" + % ("metaEvent", f, args, str(e))) + if self.core.debug: + print_exc() + + if event in self.events: + for f in self.events[event]: + try: + f(*args) + except Exception, e: + self.log.warning("Error calling event handler %s: %s, %s, %s" + % (event, f, args, str(e))) + if self.core.debug: + print_exc() + + self.updateClients(event, args) + + @lock + def updateClients(self, event, args): + # append to client event queue + if event in self.CLIENT_EVENTS: + for uuid, client in self.clients.items(): + if client.delete(): + del self.clients[uuid] + else: + client.append(event, args) + + def removeFromEvents(self, func): + """ Removes func from all known events """ + for name, events in self.events.iteritems(): + if func in events: + events.remove(func) + + + +class Client: + + # delete clients after this time + TIMEOUT = 60 * 60 + # max events, if this value is reached you should assume that older events were dropped + MAX = 30 + + def __init__(self): + self.lastActive = time() + self.events = [] + + def delete(self): + return self.lastActive + self.TIMEOUT < time() + + def append(self, event, args): + ev = (event, args) + if ev not in self.events: + self.events.insert(0, ev) + + del self.events[self.MAX:] + + + def get(self): + self.lastActive = time() + + events = self.events + self.events = [] + + return [(ev, [str(x) for x in args]) for ev, args in events]
\ No newline at end of file diff --git a/module/interaction/InteractionManager.py b/module/interaction/InteractionManager.py new file mode 100644 index 000000000..1d26b1665 --- /dev/null +++ b/module/interaction/InteractionManager.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +""" + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, + or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see <http://www.gnu.org/licenses/>. + + @author: RaNaN +""" +from threading import Lock +from time import time + +from new_collections import OrderedDict + +from module.utils import lock, bits_set, to_list +from module.Api import Input, Output + +from InteractionTask import InteractionTask + +class InteractionManager: + """ + Class that gives ability to interact with the user. + Arbitrary tasks with predefined output and input types can be set off. + Asynchronous callbacks and default values keep the ability to fallback if no user is present. + """ + + # number of seconds a client is classified as active + CLIENT_THRESHOLD = 60 + + def __init__(self, core): + self.lock = Lock() + self.core = core + self.tasks = OrderedDict() #task store, for outgoing tasks only + self.notifications = [] #list of notifications + + self.last_clients = { + Output.Notification : 0, + Output.Captcha : 0, + Output.Query : 0, + } + + self.ids = 0 #only for internal purpose + + + def isClientConnected(self, mode=Output.All): + if mode == Output.All: + return max(self.last_clients.values()) + self.CLIENT_THRESHOLD <= time() + else: + self.last_clients.get(mode, 0) + self.CLIENT_THRESHOLD <= time() + + def updateClient(self, mode): + t = time() + for output in self.last_clients: + if bits_set(output, mode): + self.last_clients[output] = t + + @lock + def work(self): + # old notifications will be removed + for n in [x for x in self.notifications if x.timedOut()]: + self.notifications.remove(n) + + # store at most 100 notifications + del self.notifications[50:] + + + @lock + def createNotification(self, title, content, desc="", plugin=""): + """ Creates and queues a new Notification + + :param title: short title + :param content: text content + :param desc: short form of the notification + :param plugin: plugin name + :return: :class:`InteractionTask` + """ + task = InteractionTask(self.ids, Input.Text, [content], Output.Notification, "", title, desc, plugin) + self.ids += 1 + self.notifications.insert(0, task) + self.handleTask(task) + return task + + @lock + def newQueryTask(self, input, data, desc, default="", plugin=""): + task = InteractionTask(self.ids, input, to_list(data), Output.Query, default, _("Query"), desc, plugin) + self.ids += 1 + return task + + @lock + def newCaptchaTask(self, img, format, filename, plugin="", input=Input.Text): + #todo: title desc plugin + task = InteractionTask(self.ids, input, [img, format, filename],Output.Captcha, + "", _("Captcha request"), _("Please solve the captcha."), plugin) + self.ids += 1 + return task + + @lock + def removeTask(self, task): + if task.iid in self.tasks: + del self.tasks[task.iid] + + @lock + def getTask(self, mode=Output.All): + self.updateClient(mode) + + for task in self.tasks.itervalues(): + if mode == Output.All or bits_set(task.output, mode): + return task + + @lock + def getNotifications(self): + """retrieves notifications, old ones are only deleted after a while\ + client has to make sure itself to dont display it twice""" + for n in self.notifications: + n.setWaiting(self.CLIENT_THRESHOLD * 5, True) + #store notification for shorter period, lock the timeout + + return self.notifications + + def isTaskWaiting(self, mode=Output.All): + return self.getTask(mode) is not None + + @lock + def getTaskByID(self, iid): + if iid in self.tasks: + task = self.tasks[iid] + del self.tasks[iid] + return task + + def handleTask(self, task): + cli = self.isClientConnected(task.output) + + if cli: #client connected -> should handle the task + task.setWaiting(self.CLIENT_THRESHOLD) # wait for response + + if task.output == Output.Notification: + task.setWaiting(60 * 60 * 30) # notifications are valid for 30h + + for plugin in self.core.addonManager.activePlugins(): + try: + plugin.newInteractionTask(task) + except: + self.core.print_exc() + + if task.output != Output.Notification: + self.tasks[task.iid] = task + + +if __name__ == "__main__": + + it = InteractionTask()
\ No newline at end of file diff --git a/module/interaction/InteractionTask.py b/module/interaction/InteractionTask.py new file mode 100644 index 000000000..b372321b0 --- /dev/null +++ b/module/interaction/InteractionTask.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +""" + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, + or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see <http://www.gnu.org/licenses/>. + + @author: RaNaN +""" + +from time import time + +from module.Api import InteractionTask as BaseInteractionTask +from module.Api import Input, Output + +#noinspection PyUnresolvedReferences +class InteractionTask(BaseInteractionTask): + """ + General Interaction Task extends ITask defined by thrift with additional fields and methods. + """ + #: Plugins can put needed data here + storage = None + #: Timestamp when task expires + wait_until = 0 + #: The received result + result = None + #: List of registered handles + handler = None + #: Error Message + error = None + #: Timeout locked + locked = False + + def __init__(self, *args, **kwargs): + BaseInteractionTask.__init__(self, *args, **kwargs) + + # additional internal attributes + self.storage = {} + self.handler = [] + self.wait_until = 0 + + def convertResult(self, value): + #TODO: convert based on input/output + return value + + def getResult(self): + return self.result + + def setResult(self, value): + self.result = self.convertResult(value) + + def setWaiting(self, sec, lock=False): + if not self.locked: + self.wait_until = max(time() + sec, self.wait_until) + if lock: self.locked = True + + def isWaiting(self): + if self.result or self.error or time() > self.waitUntil: + return False + + return True + + def timedOut(self): + return time() > self.wait_until > 0 + + def correct(self): + [x.taskCorrect(self) for x in self.handler] + + def invalid(self): + [x.taskInvalid(self) for x in self.handler] + + def __str__(self): + return "<InteractionTask '%s'>" % self.id
\ No newline at end of file diff --git a/module/interaction/__init__.py b/module/interaction/__init__.py new file mode 100644 index 000000000..de6d13128 --- /dev/null +++ b/module/interaction/__init__.py @@ -0,0 +1,2 @@ +__author__ = 'christian' +
\ No newline at end of file |