With small changes, comes great change

Introduced asynchronous plugins that gather their data on different
intervals than the mainloop. Here it is used for the modsde plugin.

The statushandler has a new class Module, which acts as documentation
for the API


These changes let the output evenly flow, even if an async plugin hangs
due to network problems or similiar issues.
This commit is contained in:
enkore 2013-02-11 21:01:38 +01:00
parent 80d7184e14
commit db94df07a3
3 changed files with 86 additions and 67 deletions

View File

@ -2,75 +2,84 @@
import sys import sys
import json import json
from datetime import datetime,timedelta import time
import threading
import urllib.request, urllib.parse, urllib.error, urllib.request, urllib.error, urllib.parse import urllib.request, urllib.parse, urllib.error, urllib.request, urllib.error, urllib.parse
import re import re
import http.cookiejar import http.cookiejar
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
class LoginError(Exception):
pass
class ModsDeChecker(object): class ModsDeChecker(object):
""" """
This class returns i3status parsable output of the number of This class returns i3status parsable output of the number of
unread posts in any bookmark in the mods.de forums. unread posts in any bookmark in the mods.de forums.
""" """
last_checked = datetime.now() async = True
unread_cache = 0 output = None
login_url = 'http://login.mods.de/'
login_url = "http://login.mods.de/"
bookmark_url = "http://forum.mods.de/bb/xml/bookmarks.php" bookmark_url = "http://forum.mods.de/bb/xml/bookmarks.php"
opener = None opener = None
cj = None cj = None
logged_in = False logged_in = False
settings = { settings = {
'color': '#7181fe', "color": "#7181fe",
'pause': 20, "pause": 20,
'username': "", "username": "",
'password': "" "password": "",
"offset": 0,
} }
def __init__(self, settings = None): def __init__(self, settings = None):
self.settings.update(settings) self.settings.update(settings)
self.cj = http.cookiejar.CookieJar() self.cj = http.cookiejar.CookieJar()
self.last_checked = \ self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cj))
datetime.now() - timedelta(seconds=self.settings['pause'])
self.opener = urllib.request.build_opener( self.thread = threading.Thread(target=self.mainloop)
urllib.request.HTTPCookieProcessor(self.cj)) self.thread.daemon = True
self.thread.start()
def mainloop(self):
while True:
unread = self.get_unread_count()
if not unread:
self.output = None
else:
self.output = {"full_text" : "%d new posts in bookmarks" % unread,
"name" : "modsde",
"urgent" : "true",
"color" : self.settings["color"]}
time.sleep(self.settings["pause"])
def get_unread_count(self): def get_unread_count(self):
delta = datetime.now() - self.last_checked
if delta.total_seconds() > self.settings['pause']:
if not self.logged_in: if not self.logged_in:
try:
self.login() self.login()
except Exception:
pass
try: try:
f = self.opener.open(self.bookmark_url) f = self.opener.open(self.bookmark_url)
root = ET.fromstring(f.read()) root = ET.fromstring(f.read())
self.last_checked = datetime.now() return int(root.attrib["newposts"]) - self.settings["offset"]
self.unread_cache = int(root.attrib['newposts'])
except Exception: except Exception:
self.cj.clear() self.cj.clear()
self.opener = urllib.request.build_opener( self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cj))
urllib.request.HTTPCookieProcessor(self.cj))
self.logged_in = False self.logged_in = False
return self.unread_cache
def login(self): def login(self):
data = urllib.parse.urlencode({ data = urllib.parse.urlencode({
"login_username": self.settings["username"], "login_username": self.settings["username"],
"login_password": self.settings["password"], "login_password": self.settings["password"],
"login_lifetime": "31536000" "login_lifetime": "31536000"
}) })
response = self.opener.open(self.login_url, data) response = self.opener.open(self.login_url, data.encode("ascii"))
m = re.search("http://forum.mods.de/SSO.php[^']*", response.read()) m = re.search("http://forum.mods.de/SSO.php[^']*", response.read().decode("ISO-8859-15"))
self.cj.clear() self.cj.clear()
if m and m.group(0): if m and m.group(0):
@ -79,20 +88,5 @@ class ModsDeChecker(object):
for cookie in self.cj: for cookie in self.cj:
self.cj.clear self.cj.clear
self.logged_in = True self.logged_in = True
self.opener.addheaders.append(('Cookie', self.opener.addheaders.append(('Cookie', '{}={}'.format(cookie.name, cookie.value)))
'{}={}'.format(cookie.name, cookie.value)))
return True return True
return False
def output(self):
unread = self.get_unread_count()
if not unread:
return None
return {'full_text' : '%d new posts in bookmarks' % unread,
'name' : 'modsde',
'urgent' : 'true',
'color' : self.settings['color']}

View File

@ -4,6 +4,16 @@ import sys
import json import json
import urllib.request, urllib.error, urllib.parse import urllib.request, urllib.error, urllib.parse
class Module(object):
output = None
async = False
def registered(self, status_handler):
"""Called when this module is registered with a status handler"""
def run(self):
"""Only called if self.async == False. Called once per tick"""
class I3statusHandler(object): class I3statusHandler(object):
modules = [] modules = []
@ -11,24 +21,24 @@ class I3statusHandler(object):
pass pass
def register_module(self, module): def register_module(self, module):
""" Register a new module. """ """Register a new module."""
# check if module implemented the # check if module implemented the
# correct functions # correct functions
if not hasattr(module, 'output'): #if not hasattr(module, 'output'):
raise Exception("Module %s does not implement \ # raise Exception("Module %s does not implement \
all the needed functions!".format(module)) # all the needed functions!".format(module))
self.modules.append(module) self.modules.append(module)
def print_line(self, message): def print_line(self, message):
""" Non-buffered printing to stdout. """ """Unbuffered printing to stdout."""
sys.stdout.write(message + '\n') sys.stdout.write(message + '\n')
sys.stdout.flush() sys.stdout.flush()
def read_line(self): def read_line(self):
""" Interrupted respecting reader for stdin. """ """Interrupted respecting reader for stdin."""
# try reading a line, removing any extra whitespace # try reading a line, removing any extra whitespace
try: try:
@ -52,13 +62,16 @@ class I3statusHandler(object):
if line.startswith(','): if line.startswith(','):
line, prefix = line[1:], ',' line, prefix = line[1:], ','
j = json.loads(line) j = [] #json.loads(line)
for module in self.modules: for module in self.modules:
output = module.output() if not module.async:
module.tick()
output = module.output
if output: if output:
j.insert(0, module.output()) j.insert(0, output)
# and echo back new encoded json # and echo back new encoded json
self.print_line(prefix+json.dumps(j)) self.print_line(prefix+json.dumps(j))

View File

@ -10,6 +10,7 @@
import dbus, gobject import dbus, gobject
from dbus.mainloop.glib import DBusGMainLoop from dbus.mainloop.glib import DBusGMainLoop
import json import json
import threading
class ThunderbirdMailChecker(object): class ThunderbirdMailChecker(object):
""" """
@ -17,7 +18,10 @@ class ThunderbirdMailChecker(object):
the dbus-sender extension for thunderbird. the dbus-sender extension for thunderbird.
""" """
unread = [] async = False
output = None
unread = set()
def __init__(self): def __init__(self):
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
@ -32,20 +36,28 @@ class ThunderbirdMailChecker(object):
dbus.mainloop.glib.threads_init() dbus.mainloop.glib.threads_init()
self.context = loop.get_context() self.context = loop.get_context()
def tick(self):
self.context.iteration(False)
def new_msg(self, id, author, subject): def new_msg(self, id, author, subject):
if id not in self.unread: if id not in self.unread:
self.unread.append(id) self.unread.add(id)
self._output()
def changed_msg(self, id, event): def changed_msg(self, id, event):
if event == "read" and id in self.unread: if event == "read" and id in self.unread:
self.unread.remove(id) self.unread.remove(id)
self._output()
def output(self): def _output(self):
self.context.iteration(False) self.context.iteration(False)
unread = len(self.unread) unread = len(self.unread)
if unread:
return {'full_text' : '%d new email' % unread, self.output = {'full_text' : '%d new email' % unread,
'name' : 'newmail-tb', 'name' : 'newmail-tb',
'urgent' : True, 'urgent' : True,
'color' : '#ff0000' } if unread else None 'color' : '#ff0000' }
else:
self.output = None