Initial commit
authorKonstantin Stepanov <kstep@p-nut.info>
Sun, 17 Oct 2010 13:18:37 +0000 (16:18 +0300)
committerKonstantin Stepanov <kstep@p-nut.info>
Sun, 17 Oct 2010 13:27:29 +0000 (16:27 +0300)
.gitignore [new file with mode: 0644]
dbuscron.py [new file with mode: 0755]
dbuscron/__init__.py [new file with mode: 0644]
dbuscron/bus.py [new file with mode: 0644]
dbuscron/command.py [new file with mode: 0644]
dbuscron/daemonize.py [new file with mode: 0644]
dbuscron/parser.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..34fcef1
--- /dev/null
@@ -0,0 +1,2 @@
+*.pyo
+*.pyc
diff --git a/dbuscron.py b/dbuscron.py
new file mode 100755 (executable)
index 0000000..a2bfb17
--- /dev/null
@@ -0,0 +1,47 @@
+#!/usr/bin/python
+#
+# bus type sender interface path member destination args command
+#
+# Examples for N900:
+#
+# Headphones unplugged:
+# S signal * org.freedesktop.Hal.Manager /org/freedesktop/Hal/Manager DeviceRemoved * * echo Headphones unplugged;
+#
+# Call recieved:
+# S signal * com.nokia.csd.Call /com/nokia/csd/call Coming * * echo $DBUS_ARG1 is calling
+#
+
+import sys
+
+if __name__ == '__main__':
+
+    daemon = (len(sys.argv) < 2) or (sys.argv[1] != '-f')
+    if daemon:
+        from dbuscron.daemonize import daemonize
+        daemonize(
+            pidfile='/var/run/dbuscron.pid',
+            logfile='/var/log/dbuscron.log'
+            )
+
+    from dbuscron import DbusBus, DbusRuleMatcher, Command, Commands, CrontabParser
+
+    bus = DbusBus()
+    commands = Commands()
+    crontab = CrontabParser('/etc/dbuscrontab')
+
+    for rule, cmd in crontab:
+        matcher = DbusRuleMatcher(**rule)
+        command = Command(cmd)
+        matcher.register()
+        commands.add(matcher, command)
+
+    commands.environ = crontab.environ
+    bus.attach_handler(commands.handler)
+
+    try:
+        bus.listen()
+    except KeyboardInterrupt:
+        sys.exit(2)
+
+# vim: ts=8 sts=4 sw=4 et
+
diff --git a/dbuscron/__init__.py b/dbuscron/__init__.py
new file mode 100644 (file)
index 0000000..c01666e
--- /dev/null
@@ -0,0 +1,8 @@
+
+from dbuscron.bus import DbusRuleMatcher, DbusBus
+from dbuscron.command import Command, Commands
+from dbuscron.daemonize import daemonize
+from dbuscron.parser import CrontabParser
+
+__all__ = ['DbusRuleMatcher', 'DbusBus', 'Command', 'Commands', 'daemonize', 'CrontabParser']
+
diff --git a/dbuscron/bus.py b/dbuscron/bus.py
new file mode 100644 (file)
index 0000000..300c215
--- /dev/null
@@ -0,0 +1,104 @@
+
+import dbus
+
+def get_dbus_message_type(message):
+    return message.__class__.__name__.lower()[0:-7]
+
+class DbusBus(object):
+    __bus = None
+    __system_bus = None
+    __session_bus = None
+
+    def __new__(cls):
+        if not cls.__bus:
+            cls.__bus = super(DbusBus, cls).__new__(cls)
+        return cls.__bus
+
+    @property
+    def system(self):
+        if not self.__system_bus:
+            self.__system_bus = dbus.SystemBus()
+        return self.__system_bus
+
+    @property
+    def session(self):
+        if not self.__session_bus:
+            self.__session_bus = dbus.SessionBus()
+        return self.__session_bus
+
+    def attach_handler(self, handler):
+        if self.__system_bus:
+            self.__system_bus.add_message_filter(handler)
+        if self.__session_bus:
+            self.__session_bus.add_message_filter(handler)
+
+    def listen(self):
+        from dbus.mainloop.glib import DBusGMainLoop
+        from gobject import MainLoop
+        DBusGMainLoop(set_as_default=True)
+        loop = MainLoop()
+        loop.run()
+
+class DbusRuleMatcher(object):
+    def __init__(self, bus_=None, type_=None, sender_=None, interface_=None, path_=None, member_=None, destination_=None, args_=[]):
+        self._bus         = bus_
+        self._type        = type_
+        self._sender      = sender_
+        self._interface   = interface_
+        self._path        = path_
+        self._member      = member_
+        self._destination = destination_
+        self._args        = args_
+
+    def register(self):
+        rule = str(self)
+        if rule:
+            self._bus.add_match_string(str(self))
+
+    def __str__(self):
+        rule = []
+        for key in ['type', 'sender', 'interface', 'path', 'member', 'destination']:
+            value = getattr(self, '_'+key)
+            if value is not None:
+                rule.append("%s='%s'" % (key, value))
+
+        if self._args:
+            for i, arg in enumerate(self._args):
+                rule.append("arg%d%s='%s'" % (i, 'path' if arg.startswith('/') else '', arg))
+
+        return ','.join(rule)
+
+    def match(self, bus, message):
+
+        if self._bus not in (None, bus):
+            return False
+
+        if self._type is not None:
+            type_ = get_dbus_message_type(message)
+            if self._type != type_:
+                return False
+
+        if self._sender not in (None, message.get_sender()):
+            return False
+
+        if self._interface not in (None, message.get_interface()):
+            return False
+
+        if self._path not in (None, message.get_path()):
+            return False
+
+        if self._member not in (None, message.get_member()):
+            return False
+
+        if self._destination not in (None, message.get_destination()):
+            return False
+
+        args_ = message.get_args_list()
+        for i, arg in enumerate(args_):
+            if i >= len(self._args):
+                break
+            if self._args[i] not in (None, arg):
+                return False
+
+        return True
+
diff --git a/dbuscron/command.py b/dbuscron/command.py
new file mode 100644 (file)
index 0000000..e7d8c68
--- /dev/null
@@ -0,0 +1,65 @@
+
+import os
+
+class Command(object):
+    def __init__(self, cmd):
+        self.__value = cmd
+        if self.is_shell_cmd:
+            self.__file = os.environ.get('SHELL', '/bin/sh')
+            self.__args = [self.__file, '-c', self.__value]
+        else:
+            self.__args = cmd.split(' ')
+            self.__file = self.__args[0]
+
+    def __call__(self, bus, message, environ):
+
+        args_list = message.get_args_list()
+        env = dict()
+        env.update(environ)
+        env.update(dict(
+                (('DBUS_ARG%d' % i, str(args_list[i])) for i in range(0, len(args_list))),
+                DBUS_ARGN   = str(len(args_list)),
+                DBUS_SENDER = str(message.get_sender()),
+                DBUS_DEST   = str(message.get_destination()),
+                DBUS_IFACE  = str(message.get_interface()),
+                DBUS_PATH   = str(message.get_path()),
+                DBUS_MEMBER = str(message.get_member()),
+                DBUS_BUS    = bus.__class__.__name__.lower()[0:-3],
+                DBUS_TYPE   = get_dbus_message_type(message)
+                ))
+        result = os.spawnvpe(os.P_WAIT, self.__file, self.__args, env)
+        return result
+
+    @property
+    def is_shell_cmd(self):
+        for c in '|><$&;{}':
+            if c in self.__value:
+                return True
+        return False
+
+    def __str__(self):
+        return self.__value
+
+class Commands(object):
+    __commands = {}
+    __environ = {}
+
+    def _get_environ(self):
+        return self.__environ
+
+    def _set_environ(self, value):
+        self.__environ = dict()
+        self.__environ.update(os.environ)
+        self.__environ.update(value)
+
+    environ = property(_get_environ, _set_environ)
+
+    def handler(self, bus, message):
+        for rule, command in self.__commands.iteritems():
+            if rule.match(bus, message):
+                command(bus, message, self.__environ)
+                return
+
+    def add(self, matcher, command):
+        self.__commands[matcher] = command
+
diff --git a/dbuscron/daemonize.py b/dbuscron/daemonize.py
new file mode 100644 (file)
index 0000000..4096289
--- /dev/null
@@ -0,0 +1,57 @@
+import os, sys
+
+def daemonize(logfile=None, errfile=None, pidfile=None):
+    devnull = os.devnull if hasattr(os, 'devnull') else '/dev/null'
+
+    initwd = os.getcwd()
+    def absolutize(path):
+        if path.startswith('/'):
+            return path
+        return os.path.join(initwd, path)
+
+    try:
+        if os.fork() == 0:
+            os.setsid()
+            if os.fork() == 0:
+                os.chdir('/')
+                os.umask(0)
+            else:
+                os._exit(0)
+        else:
+            os._exit(0)
+    except OSError, e:
+        raise Exception('Failed daemonization: %s' % str(e))
+
+    for i in range(0, 3):
+        os.close(i)
+
+    os.open(devnull, os.O_RDWR)
+
+    def open_trunc(fname):
+        f = os.open(fname, \
+            os.O_WRONLY|os.O_CREAT)
+        os.ftruncate(f, 0)
+        return f
+
+    def open_or_dup(fname, fd=None):
+        if fname:
+            return open_trunc(absolutize(fname))
+        elif fd:
+            os.dup2(*fd)
+
+    open_or_dup(logfile, (0, 1))
+    open_or_dup(errfile, (1, 2))
+
+    pid = os.getpid()
+    if pidfile:
+        pidfile = absolutize(pidfile)
+        fd = open_trunc(pidfile)
+        os.write(fd, str(pid))
+        os.close(fd)
+
+        def remove_pidfile():
+            os.unlink(pidfile)
+        sys.exitfunc = remove_pidfile
+
+    return pid
+
diff --git a/dbuscron/parser.py b/dbuscron/parser.py
new file mode 100644 (file)
index 0000000..6dcd6cb
--- /dev/null
@@ -0,0 +1,80 @@
+from __future__ import with_statement
+import re
+from dbuscron.bus import DbusBus
+
+try:
+    from itertools import product
+except ImportError:
+    def product(*args):
+        if args:
+            head, tail = args[0], args[1:]
+            for h in head:
+                for t in product(*tail):
+                    yield (h,) + t
+    
+        else:
+            yield ()
+
+class CrontabParser(object):
+    __fields_sep = re.compile(r'\s+')
+    __envvar_sep = re.compile(r'\s*=\s*')
+    __fields = [
+            'bus_',
+            'type_',
+            'sender_',
+            'interface_',
+            'path_',
+            'member_',
+            'destination_',
+            'args_',
+            #'command'
+            ]
+
+    def __init__(self, fname):
+        self.__bus = DbusBus()
+        self.__filename = fname
+        self.__environ = dict()
+
+    @property
+    def environ(self):
+        return self.__environ
+
+    def __iter__(self):
+        # bus type sender interface path member destination args command
+        with open(self.__filename) as f:
+            for line in f:
+                line = line.strip()
+
+                if not line or line.startswith('#'):
+                    continue
+
+                parts = self.__fields_sep.split(line, 8)
+                if len(parts) < 9:
+                    parts = self.__envvar_sep(line, 1)
+                    if len(parts) == 2:
+                        self.__environ[parts[0]] = parts[1]
+                    continue
+
+                rule = [(None,), (None,), (None,), (None,), (None,), (None,), (None,), (None,)]
+
+                for p in range(1, 8):
+                    if parts[p] != '*':
+                        rule[p] = parts[p].split(',')
+
+                command = parts[8]
+
+                if parts[0] == '*' or parts[0] == 'S,s' or parts[0] == 's,S':
+                    rule[0] = (self.__bus.system, self.__bus.session)
+                elif parts[0] == 's':
+                    rule[0] = (self.__bus.session,)
+                elif parts[0] == 'S':
+                    rule[0] = (self.__bus.system,)
+                    
+                for r in product(*rule):
+                    if r[7]:
+                        r[7] = r[7].split(';')
+                    ruled = dict()
+                    for i, f in enumerate(self.__fields):
+                        ruled[f] = r[i]
+                    yield ruled, command
+