uzbl/bin/uzbl-event-manager
author Jan Vrany <jan.vrany@fit.cvut.cz>
Thu, 19 Apr 2012 08:35:05 +0000
changeset 30 39fbc84d4033
parent 12 48db03aa2650
permissions -rwxr-xr-x
- WebKitRenderer added: #KEY_PRESS: #KEY_RELEASE: #SCROLL_HORIZ: #SCROLL_VERT:

#!/usr/bin/env python

# Event Manager for Uzbl
# Copyright (c) 2009-2010, Mason Larobina <mason.larobina@gmail.com>
# Copyright (c) 2009, Dieter Plaetinck <dieter@plaetinck.be>
#
# 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/>.

'''

E V E N T _ M A N A G E R . P Y
===============================

Event manager for uzbl written in python.

'''

import atexit
import imp
import logging
import os
import socket
import sys
import time
import weakref
import re
from collections import defaultdict
from functools import partial
from glob import glob
from itertools import count
from optparse import OptionParser
from select import select
from signal import signal, SIGTERM, SIGINT
from socket import socket, AF_UNIX, SOCK_STREAM
from traceback import format_exc

def xdghome(key, default):
    '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
    use $HOME and the default path.'''

    xdgkey = "XDG_%s_HOME" % key
    if xdgkey in os.environ.keys() and os.environ[xdgkey]:
        return os.environ[xdgkey]

    return os.path.join(os.environ['HOME'], default)

# `make install` will put the correct value here for your system
PREFIX = '/usr/local/'

# Setup xdg paths.
DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/')
CACHE_DIR = os.path.join(xdghome('CACHE', '.cache/'), 'uzbl/')

# Define some globals.
SCRIPTNAME = os.path.basename(sys.argv[0])

def get_exc():
    '''Format `format_exc` for logging.'''
    return "\n%s" % format_exc().rstrip()

def expandpath(path):
    '''Expand and realpath paths.'''
    return os.path.realpath(os.path.expandvars(path))

def ascii(u):
    '''Convert unicode strings into ascii for transmission over
    ascii-only streams/sockets/devices.'''
    return u.encode('utf-8')


def daemonize():
    '''Daemonize the process using the Stevens' double-fork magic.'''

    logger.info('entering daemon mode')

    try:
        if os.fork():
            os._exit(0)

    except OSError:
        logger.critical(get_exc())
        sys.exit(1)

    os.chdir('/')
    os.setsid()
    os.umask(0)

    try:
        if os.fork():
            os._exit(0)

    except OSError:
        logger.critical(get_exc())
        sys.exit(1)

    if sys.stdout.isatty():
        sys.stdout.flush()
        sys.stderr.flush()

    devnull = '/dev/null'
    stdin = file(devnull, 'r')
    stdout = file(devnull, 'a+')
    stderr = file(devnull, 'a+', 0)

    os.dup2(stdin.fileno(), sys.stdin.fileno())
    os.dup2(stdout.fileno(), sys.stdout.fileno())
    os.dup2(stderr.fileno(), sys.stderr.fileno())

    logger.info('entered daemon mode')


def make_dirs(path):
    '''Make all basedirs recursively as required.'''

    try:
        dirname = os.path.dirname(path)
        if not os.path.isdir(dirname):
            logger.debug('creating directories %r' % dirname)
            os.makedirs(dirname)

    except OSError:
        logger.error(get_exc())


class EventHandler(object):
    '''Event handler class. Used to store args and kwargs which are merged
    come time to call the callback with the event args and kwargs.'''

    nextid = count().next

    def __init__(self, plugin, event, callback, args, kwargs):
        self.id = self.nextid()
        self.plugin = plugin
        self.event = event
        self.callback = callback
        self.args = args
        self.kwargs = kwargs


    def __repr__(self):
        elems = ['id=%d' % self.id, 'event=%s' % self.event,
            'callback=%r' % self.callback]

        if self.args:
            elems.append('args=%s' % repr(self.args))

        if self.kwargs:
            elems.append('kwargs=%s' % repr(self.kwargs))

        elems.append('plugin=%s' % self.plugin.name)
        return u'<handler(%s)>' % ', '.join(elems)


    def call(self, uzbl, *args, **kwargs):
        '''Execute the handler function and merge argument lists.'''

        args = args + self.args
        kwargs = dict(self.kwargs.items() + kwargs.items())
        self.callback(uzbl, *args, **kwargs)





class Plugin(object):
    '''Plugin module wrapper object.'''

    # Special functions exported from the Plugin instance to the
    # plugin namespace.
    special_functions = ['require', 'export', 'export_dict', 'connect',
            'connect_dict', 'logger', 'unquote', 'splitquoted']


    def __init__(self, parent, name, path, plugin):
        self.parent = parent
        self.name = name
        self.path = path
        self.plugin = plugin
        self.logger = get_logger('plugin.%s' % name)

        # Weakrefs to all handlers created by this plugin
        self.handlers = set([])

        # Plugins init hook
        init = getattr(plugin, 'init', None)
        self.init = init if callable(init) else None

        # Plugins optional after hook
        after = getattr(plugin, 'after', None)
        self.after = after if callable(after) else None

        # Plugins optional cleanup hook
        cleanup = getattr(plugin, 'cleanup', None)
        self.cleanup = cleanup if callable(cleanup) else None

        assert init or after or cleanup, "missing hooks in plugin"

        # Export plugin's instance methods to plugin namespace
        for attr in self.special_functions:
            plugin.__dict__[attr] = getattr(self, attr)


    def __repr__(self):
        return u'<plugin(%r)>' % self.plugin


    def export(self, uzbl, attr, object, prepend=True):
        '''Attach `object` to `uzbl` instance. This is the preferred method
        of sharing functionality, functions, data and objects between
        plugins.

        If the object is callable you may wish to turn the callable object
        in to a meta-instance-method by prepending `uzbl` to the call stack.
        You can change this behaviour with the `prepend` argument.
        '''

        assert attr not in uzbl.exports, "attr %r already exported by %r" %\
            (attr, uzbl.exports[attr][0])

        prepend = True if prepend and callable(object) else False
        uzbl.__dict__[attr] = partial(object, uzbl) if prepend else object
        uzbl.exports[attr] = (self, object, prepend)
        uzbl.logger.info('exported %r to %r by plugin %r, prepended %r'
            % (object, 'uzbl.%s' % attr, self.name, prepend))


    def export_dict(self, uzbl, exports):
        for (attr, object) in exports.items():
            self.export(uzbl, attr, object)


    def find_handler(self, event, callback, args, kwargs):
        '''Check if a handler with the identical callback and arguments
        exists and return it.'''

        # Remove dead refs
        self.handlers -= set(filter(lambda ref: not ref(), self.handlers))

        # Find existing identical handler
        for handler in [ref() for ref in self.handlers]:
            if handler.event == event and handler.callback == callback \
              and handler.args == args and handler.kwargs == kwargs:
                return handler


    def connect(self, uzbl, event, callback, *args, **kwargs):
        '''Create an event handler object which handles `event` events.

        Arguments passed to the connect function (`args` and `kwargs`) are
        stored in the handler object and merged with the event arguments
        come handler execution.

        All handler functions must behave like a `uzbl` instance-method (that
        means `uzbl` is prepended to the callback call arguments).'''

        # Sanitise and check event name
        event = event.upper().strip()
        assert event and ' ' not in event

        assert callable(callback), 'callback must be callable'

        # Check if an identical handler already exists
        handler = self.find_handler(event, callback, args, kwargs)
        if not handler:
            # Create a new handler
            handler = EventHandler(self, event, callback, args, kwargs)
            self.handlers.add(weakref.ref(handler))
            self.logger.info('new %r' % handler)

        uzbl.handlers[event].append(handler)
        uzbl.logger.info('connected %r' % handler)
        return handler


    def connect_dict(self, uzbl, connects):
        for (event, callback) in connects.items():
            self.connect(uzbl, event, callback)


    def require(self, plugin):
        '''Check that plugin with name `plugin` has been loaded. Use this to
        ensure that your plugins dependencies have been met.'''

        assert plugin in self.parent.plugins, self.logger.critical(
            'plugin %r required by plugin %r' (plugin, self.name))

    @classmethod
    def unquote(cls, s):
        '''Removes quotation marks around strings if any and interprets
        \\-escape sequences using `string_escape`'''
        if s and s[0] == s[-1] and s[0] in ['"', "'"]:
            s = s[1:-1]
        return s.encode('utf-8').decode('string_escape').decode('utf-8')

    _splitquoted = re.compile("( |\"(?:\\\\.|[^\"])*?\"|'(?:\\\\.|[^'])*?')")
    @classmethod
    def splitquoted(cls, text):
        '''Splits string on whitespace while respecting quotations'''
        return [cls.unquote(p) for p in cls._splitquoted.split(text) if p.strip()]


class Uzbl(object):
    def __init__(self, parent, child_socket):
        self.opts = opts
        self.parent = parent
        self.child_socket = child_socket
        self.time = time.time()
        self.pid = None
        self.name = None

        # Flag if the instance has raised the INSTANCE_START event.
        self.instance_start = False

        # Use name "unknown" until name is discovered.
        self.logger = get_logger('uzbl-instance[]')

        # Track plugin event handlers and exported functions.
        self.exports = {}
        self.handlers = defaultdict(list)

        # Internal vars
        self._depth = 0
        self._buffer = ''


    def __repr__(self):
        return '<uzbl(%s)>' % ', '.join([
            'pid=%s' % (self.pid if self.pid else "Unknown"),
            'name=%s' % ('%r' % self.name if self.name else "Unknown"),
            'uptime=%f' % (time.time()-self.time),
            '%d exports' % len(self.exports.keys()),
            '%d handlers' % sum([len(l) for l in self.handlers.values()])])


    def init_plugins(self):
        '''Call the init and after hooks in all loaded plugins for this
        instance.'''

        # Initialise each plugin with the current uzbl instance.
        for plugin in self.parent.plugins.values():
            if plugin.init:
                self.logger.debug('calling %r plugin init hook' % plugin.name)
                plugin.init(self)

        # Allow plugins to use exported features of other plugins by calling an
        # optional `after` function in the plugins namespace.
        for plugin in self.parent.plugins.values():
            if plugin.after:
                self.logger.debug('calling %r plugin after hook'%plugin.name)
                plugin.after(self)


    def send(self, msg):
        '''Send a command to the uzbl instance via the child socket
        instance.'''

        msg = msg.strip()
        assert self.child_socket, "socket inactive"

        if opts.print_events:
            print ascii(u'%s<-- %s' % ('  ' * self._depth, msg))

        self.child_socket.send(ascii("%s\n" % msg))


    def read(self):
        '''Read data from the child socket and pass lines to the parse_msg
        function.'''

        try:
            raw = unicode(self.child_socket.recv(8192), 'utf-8', 'ignore')
            if not raw:
                self.logger.debug('read null byte')
                return self.close()

        except:
            self.logger.error(get_exc())
            return self.close()

        lines = (self._buffer + raw).split('\n')
        self._buffer = lines.pop()

        for line in filter(None, map(unicode.strip, lines)):
            try:
                self.parse_msg(line.strip())

            except:
                self.logger.error(get_exc())
                self.logger.error('erroneous event: %r' % line)


    def parse_msg(self, line):
        '''Parse an incoming message from a uzbl instance. Event strings
        will be parsed into `self.event(event, args)`.'''

        # Split by spaces (and fill missing with nulls)
        elems = (line.split(' ', 3) + ['',]*3)[:4]

        # Ignore non-event messages.
        if elems[0] != 'EVENT':
            logger.info('non-event message: %r' % line)
            if opts.print_events:
                print '--- %s' % ascii(line)
            return

        # Check event string elements
        (name, event, args) = elems[1:]
        assert name and event, 'event string missing elements'
        if not self.name:
            self.name = name
            self.logger = get_logger('uzbl-instance%s' % name)
            self.logger.info('found instance name %r' % name)

        assert self.name == name, 'instance name mismatch'

        # Handle the event with the event handlers through the event method
        self.event(event, args)


    def event(self, event, *args, **kargs):
        '''Raise an event.'''

        event = event.upper()

        if not opts.daemon_mode and opts.print_events:
            elems = [event,]
            if args: elems.append(unicode(args))
            if kargs: elems.append(unicode(kargs))
            print ascii(u'%s--> %s' % ('  ' * self._depth, ' '.join(elems)))

        if event == "INSTANCE_START" and args:
            assert not self.instance_start, 'instance already started'

            self.pid = int(args[0])
            self.logger.info('found instance pid %r' % self.pid)

            self.init_plugins()

        elif event == "INSTANCE_EXIT":
            self.logger.info('uzbl instance exit')
            self.close()

        if event not in self.handlers:
            return

        for handler in self.handlers[event]:
            self._depth += 1
            try:
                handler.call(self, *args, **kargs)

            except:
                self.logger.error(get_exc())

            self._depth -= 1


    def close_connection(self, child_socket):
        '''Close child socket and delete the uzbl instance created for that
        child socket connection.'''


    def close(self):
        '''Close the client socket and call the plugin cleanup hooks.'''

        self.logger.debug('called close method')

        # Remove self from parent uzbls dict.
        if self.child_socket in self.parent.uzbls:
            self.logger.debug('removing self from uzbls list')
            del self.parent.uzbls[self.child_socket]

        try:
            if self.child_socket:
                self.logger.debug('closing child socket')
                self.child_socket.close()

        except:
            self.logger.error(get_exc())

        finally:
            self.child_socket = None

        # Call plugins cleanup hooks.
        for plugin in self.parent.plugins.values():
            if plugin.cleanup:
                self.logger.debug('calling %r plugin cleanup hook'
                    % plugin.name)
                plugin.cleanup(self)

        logger.info('removed %r' % self)


class UzblEventDaemon(object):
    def __init__(self):
        self.opts = opts
        self.server_socket = None
        self._quit = False

        # Hold uzbl instances
        # {child socket: Uzbl instance, ..}
        self.uzbls = {}

        # Hold plugins
        # {plugin name: Plugin instance, ..}
        self.plugins = {}

        # Register that the event daemon server has started by creating the
        # pid file.
        make_pid_file(opts.pid_file)

        # Register a function to clean up the socket and pid file on exit.
        atexit.register(self.quit)

        # Add signal handlers.
        for sigint in [SIGTERM, SIGINT]:
            signal(sigint, self.quit)

        # Load plugins into self.plugins
        self.load_plugins(opts.plugins)


    def load_plugins(self, plugins):
        '''Load event manager plugins.'''

        for path in plugins:
            logger.debug('loading plugin %r' % path)
            (dir, file) = os.path.split(path)
            name = file[:-3] if file.lower().endswith('.py') else file

            info = imp.find_module(name, [dir,])
            module = imp.load_module(name, *info)

            # Check if the plugin has a callable hook.
            hooks = filter(callable, [getattr(module, attr, None) \
                for attr in ['init', 'after', 'cleanup']])
            assert hooks, "no hooks in plugin %r" % module

            logger.debug('creating plugin instance for %r plugin' % name)
            plugin = Plugin(self, name, path, module)
            self.plugins[name] = plugin
            logger.info('new %r' % plugin)


    def create_server_socket(self):
        '''Create the event manager daemon socket for uzbl instance duplex
        communication.'''

        # Close old socket.
        self.close_server_socket()

        sock = socket(AF_UNIX, SOCK_STREAM)
        sock.bind(opts.server_socket)
        sock.listen(5)

        self.server_socket = sock
        logger.debug('bound server socket to %r' % opts.server_socket)


    def run(self):
        '''Main event daemon loop.'''

        logger.debug('entering main loop')

        # Create and listen on the server socket
        self.create_server_socket()

        if opts.daemon_mode:
            # Daemonize the process
            daemonize()

            # Update the pid file
            make_pid_file(opts.pid_file)

        try:
            # Accept incoming connections and listen for incoming data
            self.listen()

        except:
            if not self._quit:
                logger.critical(get_exc())

        # Clean up and exit
        self.quit()

        logger.debug('exiting main loop')


    def listen(self):
        '''Accept incoming connections and constantly poll instance sockets
        for incoming data.'''

        logger.info('listening on %r' % opts.server_socket)

        # Count accepted connections
        connections = 0

        while (self.uzbls or not connections) or (not opts.auto_close):
            socks = [self.server_socket] + self.uzbls.keys()
            reads, _, errors = select(socks, [], socks, 1)

            if self.server_socket in reads:
                reads.remove(self.server_socket)

                # Accept connection and create uzbl instance.
                child_socket = self.server_socket.accept()[0]
                self.uzbls[child_socket] = Uzbl(self, child_socket)
                connections += 1

            for uzbl in [self.uzbls[s] for s in reads]:
                uzbl.read()

            for uzbl in [self.uzbls[s] for s in errors]:
                uzbl.logger.error('socket read error')
                uzbl.close()

        logger.info('auto closing')


    def close_server_socket(self):
        '''Close and delete the server socket.'''

        try:
            if self.server_socket:
                logger.debug('closing server socket')
                self.server_socket.close()
                self.server_socket = None

            if os.path.exists(opts.server_socket):
                logger.info('unlinking %r' % opts.server_socket)
                os.unlink(opts.server_socket)

        except:
            logger.error(get_exc())


    def quit(self, sigint=None, *args):
        '''Close all instance socket objects, server socket and delete the
        pid file.'''

        if sigint == SIGTERM:
            logger.critical('caught SIGTERM, exiting')

        elif sigint == SIGINT:
            logger.critical('caught SIGINT, exiting')

        elif not self._quit:
            logger.debug('shutting down event manager')

        self.close_server_socket()

        for uzbl in self.uzbls.values():
            uzbl.close()

        del_pid_file(opts.pid_file)

        if not self._quit:
            logger.info('event manager shut down')
            self._quit = True


def make_pid_file(pid_file):
    '''Creates a pid file at `pid_file`, fails silently.'''

    try:
        logger.debug('creating pid file %r' % pid_file)
        make_dirs(pid_file)
        pid = os.getpid()
        fileobj = open(pid_file, 'w')
        fileobj.write('%d' % pid)
        fileobj.close()
        logger.info('created pid file %r with pid %d' % (pid_file, pid))

    except:
        logger.error(get_exc())


def del_pid_file(pid_file):
    '''Deletes a pid file at `pid_file`, fails silently.'''

    if os.path.isfile(pid_file):
        try:
            logger.debug('deleting pid file %r' % pid_file)
            os.remove(pid_file)
            logger.info('deleted pid file %r' % pid_file)

        except:
            logger.error(get_exc())


def get_pid(pid_file):
    '''Reads a pid from pid file `pid_file`, fails None.'''

    try:
        logger.debug('reading pid file %r' % pid_file)
        fileobj = open(pid_file, 'r')
        pid = int(fileobj.read())
        fileobj.close()
        logger.info('read pid %d from pid file %r' % (pid, pid_file))
        return pid

    except (IOError, ValueError):
        logger.error(get_exc())
        return None


def pid_running(pid):
    '''Checks if a process with a pid `pid` is running.'''

    try:
        os.kill(pid, 0)
    except OSError:
        return False
    else:
        return True


def term_process(pid):
    '''Asks nicely then forces process with pid `pid` to exit.'''

    try:
        logger.info('sending SIGTERM to process with pid %r' % pid)
        os.kill(pid, SIGTERM)

    except OSError:
        logger.error(get_exc())

    logger.debug('waiting for process with pid %r to exit' % pid)
    start = time.time()
    while True:
        if not pid_running(pid):
            logger.debug('process with pid %d exit' % pid)
            return True

        if (time.time()-start) > 5:
            logger.warning('process with pid %d failed to exit' % pid)
            logger.info('sending SIGKILL to process with pid %d' % pid)
            try:
                os.kill(pid, SIGKILL)
            except:
                logger.critical(get_exc())
                raise

        if (time.time()-start) > 10:
            logger.critical('unable to kill process with pid %d' % pid)
            raise OSError

        time.sleep(0.25)


def stop_action():
    '''Stop the event manager daemon.'''

    pid_file = opts.pid_file
    if not os.path.isfile(pid_file):
        logger.error('could not find running event manager with pid file %r'
            % opts.pid_file)
        return

    pid = get_pid(pid_file)
    if not pid_running(pid):
        logger.debug('no process with pid %r' % pid)
        del_pid_file(pid_file)
        return

    logger.debug('terminating process with pid %r' % pid)
    term_process(pid)
    del_pid_file(pid_file)
    logger.info('stopped event manager process with pid %d' % pid)


def start_action():
    '''Start the event manager daemon.'''

    pid_file = opts.pid_file
    if os.path.isfile(pid_file):
        pid = get_pid(pid_file)
        if pid_running(pid):
            logger.error('event manager already started with pid %d' % pid)
            return

        logger.info('no process with pid %d' % pid)
        del_pid_file(pid_file)

    UzblEventDaemon().run()


def restart_action():
    '''Restart the event manager daemon.'''

    stop_action()
    start_action()


def list_action():
    '''List all the plugins that would be loaded in the current search
    dirs.'''

    names = {}
    for plugin in opts.plugins:
        (head, tail) = os.path.split(plugin)
        if tail not in names:
            names[tail] = plugin

    for plugin in sorted(names.values()):
        print plugin


if __name__ == "__main__":
    parser = OptionParser('usage: %prog [options] {start|stop|restart|list}')
    add = parser.add_option

    add('-v', '--verbose',
        dest='verbose', default=2, action='count',
        help='increase verbosity')

    add('-d', '--plugin-dir',
        dest='plugin_dirs', action='append', metavar="DIR", default=[],
        help='add extra plugin search dir, same as `-l "DIR/*.py"`')

    add('-l', '--load-plugin',
        dest='load_plugins', action='append', metavar="PLUGIN", default=[],
        help='load plugin, loads before plugins in search dirs')

    socket_location = os.path.join(CACHE_DIR, 'event_daemon')
    add('-s', '--server-socket',
        dest='server_socket', metavar="SOCKET", default=socket_location,
        help='server AF_UNIX socket location')

    add('-p', '--pid-file',
        metavar="FILE", dest='pid_file',
        help='pid file location, defaults to server socket + .pid')

    add('-n', '--no-daemon',
        dest='daemon_mode', action='store_false', default=True,
        help='do not daemonize the process')

    add('-a', '--auto-close',
        dest='auto_close', action='store_true', default=False,
        help='auto close after all instances disconnect')

    add('-i', '--no-default-dirs',
        dest='default_dirs', action='store_false', default=True,
        help='ignore the default plugin search dirs')

    add('-o', '--log-file',
        dest='log_file', metavar='FILE',
        help='write logging output to a file, defaults to server socket +'
        ' .log')

    add('-q', '--quiet-events',
        dest='print_events', action="store_false", default=True,
        help="silence the printing of events to stdout")

    (opts, args) = parser.parse_args()

    opts.server_socket = expandpath(opts.server_socket)

    # Set default pid file location
    if not opts.pid_file:
        opts.pid_file = "%s.pid" % opts.server_socket

    else:
        opts.pid_file = expandpath(opts.pid_file)

    # Set default log file location
    if not opts.log_file:
        opts.log_file = "%s.log" % opts.server_socket

    else:
        opts.log_file = expandpath(opts.log_file)

    # Logging setup
    log_level = logging.CRITICAL - opts.verbose*10

    # Console logging handler
    ch = logging.StreamHandler()
    ch.setLevel(max(log_level+10, 10))
    ch.setFormatter(logging.Formatter(
        '%(name)s: %(levelname)s: %(message)s'))

    # File logging handler
    fh = logging.FileHandler(opts.log_file, 'w', 'utf-8', 1)
    fh.setLevel(max(log_level, 10))
    fh.setFormatter(logging.Formatter(
        '[%(created)f] %(name)s: %(levelname)s: %(message)s'))

    # logging.getLogger wrapper which sets the levels and adds the
    # file and console handlers automagically
    def get_logger(name):
        handlers = [ch, fh]
        level = [max(log_level, 10),]
        logger = logging.getLogger(name)
        logger.setLevel(level[0])
        for handler in handlers:
            logger.addHandler(handler)

        return logger

    # Get main logger
    logger = get_logger(SCRIPTNAME)
    logger.info('logging to %r' % opts.log_file)

    plugins = {}

    # Load all `opts.load_plugins` into the plugins list
    for path in opts.load_plugins:
        path = expandpath(path)
        matches = glob(path)
        if not matches:
            parser.error('cannot find plugin(s): %r' % path)

        for plugin in matches:
            (head, tail) = os.path.split(plugin)
            if tail not in plugins:
                logger.debug('found plugin: %r' % plugin)
                plugins[tail] = plugin

            else:
                logger.debug('ignoring plugin: %r' % plugin)

    # Add default plugin locations
    if opts.default_dirs:
        logger.debug('adding default plugin dirs to plugin dirs list')
        opts.plugin_dirs += [os.path.join(DATA_DIR, 'plugins/'),
            os.path.join(PREFIX, 'share/uzbl/examples/data/plugins/')]

    else:
        logger.debug('ignoring default plugin dirs')

    # Load all plugins in `opts.plugin_dirs` into the plugins list
    for dir in opts.plugin_dirs:
        dir = expandpath(dir)
        logger.debug('searching plugin dir: %r' % dir)
        for plugin in glob(os.path.join(dir, '*.py')):
            (head, tail) = os.path.split(plugin)
            if tail not in plugins:
                logger.debug('found plugin: %r' % plugin)
                plugins[tail] = plugin

            else:
                logger.debug('ignoring plugin: %r' % plugin)

    plugins = plugins.values()

    # Check all the paths in the plugins list are files
    for plugin in plugins:
        if not os.path.isfile(plugin):
            parser.error('plugin not a file: %r' % plugin)

    if opts.auto_close: logger.debug('will auto close')
    else: logger.debug('will not auto close')

    if opts.daemon_mode: logger.debug('will daemonize')
    else: logger.debug('will not daemonize')

    opts.plugins = plugins

    # init like {start|stop|..} daemon actions
    daemon_actions = {'start': start_action, 'stop': stop_action,
        'restart': restart_action, 'list': list_action}

    if len(args) == 1:
        action = args[0]
        if action not in daemon_actions:
            parser.error('invalid action: %r' % action)

    elif not args:
        logger.warning('no daemon action given, assuming %r' % 'start')
        action = 'start'

    else:
        parser.error('invalid action argument: %r' % args)

    logger.info('daemon action %r' % action)
    # Do action
    daemon_actions[action]()

    logger.debug('process CPU time: %f' % time.clock())

# vi: set et ts=4: