aboutsummaryrefslogtreecommitdiffhomepage
path: root/examples/data/uzbl/scripts/uzbl-event-manager
diff options
context:
space:
mode:
authorGravatar Dieter Plaetinck <dieter@plaetinck.be>2009-11-11 10:27:18 +0100
committerGravatar Dieter Plaetinck <dieter@plaetinck.be>2009-11-11 10:27:18 +0100
commitee46be0cc3dd981dcfe1ac1b13ab5b6c3194f841 (patch)
treea156fd9037aa72120624cbe484781305a534dd72 /examples/data/uzbl/scripts/uzbl-event-manager
parent0e0fba1fcf5372cf67d9dfa5d6817b2e225709d7 (diff)
uniformize naming of helper python scripts
Diffstat (limited to 'examples/data/uzbl/scripts/uzbl-event-manager')
-rwxr-xr-xexamples/data/uzbl/scripts/uzbl-event-manager857
1 files changed, 857 insertions, 0 deletions
diff --git a/examples/data/uzbl/scripts/uzbl-event-manager b/examples/data/uzbl/scripts/uzbl-event-manager
new file mode 100755
index 0000000..dee42c5
--- /dev/null
+++ b/examples/data/uzbl/scripts/uzbl-event-manager
@@ -0,0 +1,857 @@
+#!/usr/bin/env python
+
+# Event Manager for Uzbl
+# Copyright (c) 2009, 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 imp
+import os
+import sys
+import re
+import types
+import socket
+import pprint
+import time
+import atexit
+from select import select
+from signal import signal, SIGTERM
+from optparse import OptionParser
+from traceback import print_exc
+
+
+# ============================================================================
+# ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+# Automagically set during `make install`
+PREFIX = None
+
+# Check if PREFIX not set and set to default /usr/local/
+if not PREFIX:
+ PREFIX = '/usr/local/'
+
+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)
+
+# Setup xdg paths.
+DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/')
+CACHE_DIR = os.path.join(xdghome('CACHE', '.cache/'), 'uzbl/')
+
+
+# Config dict (NOT the same as the uzbl.config).
+config = {
+ 'verbose': False,
+ 'daemon_mode': True,
+ 'auto_close': False,
+
+ 'plugins_load': [],
+ 'plugins_ignore': [],
+
+ 'plugin_dirs': [os.path.join(DATA_DIR, 'plugins/'),
+ os.path.join(PREFIX, 'share/uzbl/examples/data/uzbl/plugins/')],
+
+ 'server_socket': os.path.join(CACHE_DIR, 'event_daemon'),
+ 'pid_file': os.path.join(CACHE_DIR, 'event_daemon.pid'),
+}
+
+
+# ============================================================================
+# ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+
+# Define some globals.
+_SCRIPTNAME = os.path.basename(sys.argv[0])
+_RE_FINDSPACES = re.compile("\s+")
+
+def echo(msg):
+ '''Prints only if the verbose flag has been set.'''
+
+ if config['verbose']:
+ sys.stdout.write("%s: %s\n" % (_SCRIPTNAME, msg))
+
+
+def error(msg):
+ '''Prints error messages to stderr.'''
+
+ sys.stderr.write("%s: error: %s\n" % (_SCRIPTNAME, msg))
+
+
+def counter():
+ '''Generate unique object id's.'''
+
+ i = 0
+ while True:
+ i += 1
+ yield i
+
+
+def iscallable(obj):
+ '''Return true if the object is callable.'''
+
+ return hasattr(obj, "__call__")
+
+
+def isiterable(obj):
+ '''Return true if you can iterate over the item.'''
+
+ return hasattr(obj, "__iter__")
+
+
+def find_plugins(plugin_dirs):
+ '''Find all event manager plugins in the plugin dirs and return a
+ dictionary of {'plugin-name.py': '/full/path/to/plugin-name.py', ...}'''
+
+ plugins = {}
+
+ for plugin_dir in plugin_dirs:
+ plugin_dir = os.path.realpath(os.path.expandvars(plugin_dir))
+ if not os.path.isdir(plugin_dir):
+ continue
+
+ for file in os.listdir(plugin_dir):
+ if not file.lower().endswith('.py'):
+ continue
+
+ path = os.path.join(plugin_dir, file)
+ if not os.path.isfile(path):
+ continue
+
+ if file not in plugins:
+ plugins[file] = plugin_dir
+
+ return plugins
+
+
+def load_plugins(plugin_dirs, load=[], ignore=[]):
+ '''Load event manager plugins found in the plugin_dirs.'''
+
+ # Find the plugins in the plugin_dirs.
+ found = find_plugins(plugin_dirs)
+
+ if load:
+ # Ignore anything not in the load list.
+ for plugin in found.keys():
+ if plugin not in load:
+ del found[plugin]
+
+ if ignore:
+ # Ignore anything in the ignore list.
+ for plugin in found.keys():
+ if plugin in ignore:
+ del found[plugin]
+
+ # Print plugin list to be loaded.
+ pprint.pprint(found)
+
+ loaded = {}
+ # Load all found plugins into the loaded dict.
+ for (filename, dir) in found.items():
+ name = filename[:-3]
+ info = imp.find_module(name, [dir,])
+ plugin = imp.load_module(name, *info)
+ loaded[(dir, filename)] = plugin
+
+ return loaded
+
+
+def daemonize():
+ '''Daemonize the process using the Stevens' double-fork magic.'''
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #1 failed")
+ sys.exit(1)
+
+ os.chdir('/')
+ os.setsid()
+ os.umask(0)
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #2 failed")
+ sys.exit(1)
+
+ 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())
+
+
+def make_dirs(path):
+ '''Make all basedirs recursively as required.'''
+
+ dirname = os.path.dirname(path)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+
+
+def make_pid_file(pid_file):
+ '''Make pid file at given pid_file location.'''
+
+ make_dirs(pid_file)
+ file = open(pid_file, 'w')
+ file.write('%d' % os.getpid())
+ file.close()
+
+
+def del_pid_file(pid_file):
+ '''Delete pid file at given pid_file location.'''
+
+ if os.path.isfile(pid_file):
+ os.remove(pid_file)
+
+
+def get_pid(pid_file):
+ '''Read pid from pid_file.'''
+
+ try:
+ file = open(pid_file, 'r')
+ strpid = file.read()
+ file.close()
+ pid = int(strpid.strip())
+ return pid
+
+ except:
+ print_exc()
+ return None
+
+
+def pid_running(pid):
+ '''Returns True if a process with the given pid is running.'''
+
+ try:
+ os.kill(pid, 0)
+
+ except OSError:
+ return False
+
+ else:
+ return True
+
+
+def term_process(pid):
+ '''Send a SIGTERM signal to the process with the given pid.'''
+
+ if not pid_running(pid):
+ return False
+
+ os.kill(pid, SIGTERM)
+
+ start = time.time()
+ while True:
+ if not pid_running(pid):
+ return True
+
+ if time.time() - start > 5:
+ raise OSError('failed to stop process with pid: %d' % pid)
+
+ time.sleep(0.25)
+
+
+def prepender(function, *pre_args):
+ '''Creates a wrapper around a callable object injecting a list of
+ arguments before the called arguments.'''
+
+ locals = (function, pre_args)
+ def _prepender(*args, **kargs):
+ (function, pre_args) = locals
+ return function(*(pre_args + args), **kargs)
+
+ return _prepender
+
+
+class EventHandler(object):
+
+ nexthid = counter().next
+
+ def __init__(self, event, handler, *args, **kargs):
+ if not iscallable(handler):
+ raise ArgumentError("EventHandler object requires a callable "
+ "object function for the handler argument not: %r" % handler)
+
+ self.function = handler
+ self.args = args
+ self.kargs = kargs
+ self.event = event
+ self.hid = self.nexthid()
+
+
+ def __repr__(self):
+ args = ["event=%s" % self.event, "hid=%d" % self.hid,
+ "function=%r" % self.function]
+
+ if self.args:
+ args.append("args=%r" % self.args)
+
+ if self.kargs:
+ args.append("kargs=%r" % self.kargs)
+
+ return "<EventHandler(%s)>" % ', '.join(args)
+
+
+class UzblInstance(object):
+ def __init__(self, parent, client_socket):
+
+ # Internal variables.
+ self._exports = {}
+ self._handlers = {}
+ self._parent = parent
+ self._client_socket = client_socket
+
+ self.buffer = ''
+
+ # Call the init() function in every plugin. Inside the init function
+ # is where the plugins insert the hooks into the event system.
+ self._init_plugins()
+
+
+ def __getattribute__(self, attr):
+ '''Expose any exported functions before class functions.'''
+
+ if not attr.startswith('_'):
+ exports = object.__getattribute__(self, '_exports')
+ if attr in exports:
+ return exports[attr]
+
+ return object.__getattribute__(self, attr)
+
+
+ def _init_plugins(self):
+ '''Call the init() function in every plugin and expose all exposable
+ functions in the plugins root namespace.'''
+
+ plugins = self._parent['plugins']
+
+ # Map all plugin exports
+ for (name, plugin) in plugins.items():
+ if not hasattr(plugin, '__export__'):
+ continue
+
+ for export in plugin.__export__:
+ if export in self._exports:
+ raise KeyError("conflicting export: %r" % export)
+
+ obj = getattr(plugin, export)
+ if iscallable(obj):
+ obj = prepender(obj, self)
+
+ self._exports[export] = obj
+
+ echo("exposed attribute(s): %s" % ', '.join(self._exports.keys()))
+
+ # Now call the init function in all plugins.
+ for (name, plugin) in plugins.items():
+ try:
+ plugin.init(self)
+
+ except:
+ #print_exc()
+ raise
+
+
+ def send(self, msg):
+ '''Send a command to the uzbl instance via the socket file.'''
+
+ if self._client_socket:
+ print '<-- %s' % msg
+ self._client_socket.send(("%s\n" % msg).encode('utf-8'))
+
+ else:
+ print '!-- %s' % msg
+
+
+ def connect(self, event, handler, *args, **kargs):
+ '''Connect event with handler and return the newly created handler.
+ Handlers can either be a function or a uzbl command string.'''
+
+ if event not in self._handlers.keys():
+ self._handlers[event] = []
+
+ handlerobj = EventHandler(event, handler, *args, **kargs)
+ self._handlers[event].append(handlerobj)
+ print handlerobj
+
+
+ def connect_dict(self, connect_dict):
+ '''Connect a dictionary comprising of {"EVENT_NAME": handler, ..} to
+ the event handler stack.
+
+ If you need to supply args or kargs to an event use the normal connect
+ function.'''
+
+ for (event, handler) in connect_dict.items():
+ self.connect(event, handler)
+
+
+ def remove_by_id(self, hid):
+ '''Remove connected event handler by unique handler id.'''
+
+ for (event, handlers) in self._handlers.items():
+ for handler in list(handlers):
+ if hid != handler.hid:
+ continue
+
+ echo("removed %r" % handler)
+ handlers.remove(handler)
+ return
+
+ echo('unable to find & remove handler with id: %d' % handler.hid)
+
+
+ def remove(self, handler):
+ '''Remove connected event handler.'''
+
+ for (event, handlers) in self._handlers.items():
+ if handler in handlers:
+ echo("removed %r" % handler)
+ handlers.remove(handler)
+ return
+
+ echo('unable to find & remove handler: %r' % handler)
+
+
+ def exec_handler(self, handler, *args, **kargs):
+ '''Execute event handler function.'''
+
+ args += handler.args
+ kargs = dict(handler.kargs.items()+kargs.items())
+ handler.function(self, *args, **kargs)
+
+
+ def event(self, event, *args, **kargs):
+ '''Raise a custom event.'''
+
+ # Silence _printing_ of geo events while debugging.
+ if event != "GEOMETRY_CHANGED":
+ print "--> %s %s %s" % (event, args, '' if not kargs else kargs)
+
+ if event not in self._handlers:
+ return
+
+ for handler in self._handlers[event]:
+ try:
+ self.exec_handler(handler, *args, **kargs)
+
+ except:
+ print_exc()
+
+
+ def close(self):
+ '''Close the client socket and clean up.'''
+
+ try:
+ self._client_socket.close()
+
+ except:
+ pass
+
+ for (name, plugin) in self._parent['plugins'].items():
+ if hasattr(plugin, 'cleanup'):
+ plugin.cleanup(self)
+
+ del self._exports
+ del self._handlers
+ del self._client_socket
+
+
+class UzblEventDaemon(dict):
+ def __init__(self):
+
+ # Init variables and dict keys.
+ dict.__init__(self, {'uzbls': {}})
+ self.running = None
+ self.server_socket = None
+ self.socket_location = None
+
+ # Register that the event daemon server has started by creating the
+ # pid file.
+ make_pid_file(config['pid_file'])
+
+ # Register a function to clean up the socket and pid file on exit.
+ atexit.register(self.quit)
+
+ # Make SIGTERM act orderly.
+ signal(SIGTERM, lambda signum, stack_frame: sys.exit(1))
+
+ # Load plugins, first-build of the plugins may be a costly operation.
+ self['plugins'] = load_plugins(config['plugin_dirs'],
+ config['plugins_load'], config['plugins_ignore'])
+
+
+ def _create_server_socket(self):
+ '''Create the event manager daemon socket for uzbl instance duplex
+ communication.'''
+
+ server_socket = config['server_socket']
+ server_socket = os.path.realpath(os.path.expandvars(server_socket))
+ self.socket_location = server_socket
+
+ # Delete socket if it exists.
+ if os.path.exists(server_socket):
+ os.remove(server_socket)
+
+ self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.server_socket.bind(server_socket)
+ self.server_socket.listen(5)
+
+
+ def _close_server_socket(self):
+ '''Close and delete the server socket.'''
+
+ try:
+ self.server_socket.close()
+ self.server_socket = None
+
+ if os.path.exists(self.socket_location):
+ os.remove(self.socket_location)
+
+ except:
+ pass
+
+
+ def run(self):
+ '''Main event daemon loop.'''
+
+ if config['daemon_mode']:
+ echo('entering daemon mode.')
+ daemonize()
+ # The pid has changed so update the pid file.
+ make_pid_file(config['pid_file'])
+
+ # Create event daemon socket.
+ self._create_server_socket()
+ echo('listening on: %s' % self.socket_location)
+
+ # Now listen for incoming connections and or data.
+ self.listen()
+
+ # Clean up.
+ self.quit()
+
+
+ def listen(self):
+ '''Accept incoming connections and constantly poll instance sockets
+ for incoming data.'''
+
+ self.running = True
+ while self.running:
+
+ sockets = [self.server_socket,] + self['uzbls'].keys()
+
+ read, _, error = select(sockets, [], sockets, 1)
+
+ if self.server_socket in read:
+ self.accept_connection()
+ read.remove(self.server_socket)
+
+ for client in read:
+ self.read_socket(client)
+
+ for client in error:
+ error('Unknown error on socket: %r' % client)
+ self.close_connection(client)
+
+
+ def read_socket(self, client):
+ '''Read data from an instance socket and pass to the uzbl objects
+ event handler function.'''
+
+ try:
+ uzbl = self['uzbls'][client]
+ try:
+ raw = unicode(client.recv(8192), 'utf-8', 'ignore')
+
+ except:
+ print_exc()
+ raw = None
+
+ if not raw:
+ # Read null byte, close socket.
+ return self.close_connection(client)
+
+ uzbl.buffer += raw
+ msgs = uzbl.buffer.split('\n')
+ uzbl.buffer = msgs.pop()
+
+ for msg in msgs:
+ self.parse_msg(uzbl, msg)
+
+ except:
+ raise
+
+
+ def parse_msg(self, uzbl, msg):
+ '''Parse an incoming msg from a uzbl instance. All non-event messages
+ will be printed here and not be passed to the uzbl instance event
+ handler function.'''
+
+ msg = msg.strip()
+ if not msg:
+ return
+
+ cmd = _RE_FINDSPACES.split(msg, 3)
+ if not cmd or cmd[0] != 'EVENT':
+ # Not an event message.
+ print '---', msg
+ return
+
+ if len(cmd) < 4:
+ cmd.append('')
+
+ event, args = cmd[2], cmd[3]
+
+ try:
+ uzbl.event(event, args)
+
+ except:
+ print_exc()
+
+
+ def accept_connection(self):
+ '''Accept incoming connection to the server socket.'''
+
+ client_socket = self.server_socket.accept()[0]
+
+ uzbl = UzblInstance(self, client_socket)
+ self['uzbls'][client_socket] = uzbl
+
+
+ def close_connection(self, client):
+ '''Clean up after instance close.'''
+
+ try:
+ if client in self['uzbls']:
+ uzbl = self['uzbls'][client]
+ uzbl.close()
+ del self['uzbls'][client]
+
+ except:
+ print_exc()
+
+ if not len(self['uzbls']) and config['auto_close']:
+ echo('auto closing event manager.')
+ self.running = False
+
+
+ def quit(self):
+ '''Close all instance socket objects, server socket and delete the
+ pid file.'''
+
+ echo('shutting down event manager.')
+
+ for client in self['uzbls'].keys():
+ self.close_connection(client)
+
+ echo('unlinking: %r' % self.socket_location)
+ self._close_server_socket()
+
+ echo('deleting pid file: %r' % config['pid_file'])
+ del_pid_file(config['pid_file'])
+
+
+def stop():
+ '''Stop the event manager daemon.'''
+
+ pid_file = config['pid_file']
+ if not os.path.isfile(pid_file):
+ return echo('no running daemon found.')
+
+ echo('found pid file: %r' % pid_file)
+ pid = get_pid(pid_file)
+ if not pid_running(pid):
+ echo('no process with pid: %d' % pid)
+ return os.remove(pid_file)
+
+ echo("terminating process with pid: %d" % pid)
+ term_process(pid)
+ if os.path.isfile(pid_file):
+ os.remove(pid_file)
+
+ echo('stopped event daemon.')
+
+
+def start():
+ '''Start the event manager daemon.'''
+
+ pid_file = config['pid_file']
+ if os.path.isfile(pid_file):
+ echo('found pid file: %r' % pid_file)
+ pid = get_pid(pid_file)
+ if pid_running(pid):
+ return echo('event daemon already started with pid: %d' % pid)
+
+ echo('no process with pid: %d' % pid)
+ os.remove(pid_file)
+
+ echo('starting event manager.')
+ UzblEventDaemon().run()
+
+
+def restart():
+ '''Restart the event manager daemon.'''
+
+ echo('restarting event manager daemon.')
+ stop()
+ start()
+
+
+def list_plugins():
+ '''List all the plugins being loaded by the event daemon.'''
+
+ plugins = find_plugins(config['plugin_dirs'])
+ dirs = {}
+
+ for (plugin, dir) in plugins.items():
+ if dir not in dirs:
+ dirs[dir] = []
+
+ dirs[dir].append(plugin)
+
+ for (index, (dir, plugin_list)) in enumerate(sorted(dirs.items())):
+ if index:
+ print
+
+ print "%s:" % dir
+ for plugin in sorted(plugin_list):
+ print " %s" % plugin
+
+
+if __name__ == "__main__":
+ usage = "usage: %prog [options] {start|stop|restart|list}"
+ parser = OptionParser(usage=usage)
+ parser.add_option('-v', '--verbose', dest='verbose', action="store_true",
+ help="print verbose output.")
+
+ parser.add_option('-d', '--plugin-dirs', dest='plugin_dirs', action="store",
+ metavar="DIRS", help="Specify plugin directories in the form of "\
+ "'dir1:dir2:dir3'.")
+
+ parser.add_option('-l', '--load-plugins', dest="load", action="store",
+ metavar="PLUGINS", help="comma separated list of plugins to load")
+
+ parser.add_option('-i', '--ignore-plugins', dest="ignore", action="store",
+ metavar="PLUGINS", help="comma separated list of plugins to ignore")
+
+ parser.add_option('-p', '--pid-file', dest='pid', action='store',
+ metavar='FILE', help="specify pid file location")
+
+ parser.add_option('-s', '--server-socket', dest='socket', action='store',
+ metavar='SOCKET', help="specify the daemon socket location")
+
+ parser.add_option('-n', '--no-daemon', dest="daemon",
+ action="store_true", help="don't enter daemon mode.")
+
+ parser.add_option('-a', '--auto-close', dest='autoclose',
+ action='store_true', help='auto close after all instances disconnect.')
+
+ (options, args) = parser.parse_args()
+
+ # init like {start|stop|..} daemon control section.
+ daemon_controls = {'start': start, 'stop': stop, 'restart': restart,
+ 'list': list_plugins}
+
+ if len(args) == 1:
+ action = args[0]
+ if action not in daemon_controls:
+ error('unknown action: %r' % action)
+ sys.exit(1)
+
+ elif len(args) > 1:
+ error("too many arguments: %r" % args)
+ sys.exit(1)
+
+ else:
+ action = 'start'
+
+ # parse other flags & options.
+ if options.verbose:
+ config['verbose'] = True
+
+ if options.plugin_dirs:
+ plugin_dirs = map(os.path.realpath, map(str.strip,
+ options.plugin_dirs.split(':')))
+ config['plugin_dirs'] = plugin_dirs
+ echo("plugin search dirs: %r" % plugin_dirs)
+
+ if options.load and options.ignore:
+ error("you can't load and ignore at the same time.")
+ sys.exit(1)
+
+ elif options.load:
+ plugins_load = config['plugins_load']
+ for plugin in options.load.split(','):
+ if plugin.strip():
+ plugins_load.append(plugin.strip())
+
+ echo('only loading plugin(s): %s' % ', '.join(plugins_load))
+
+ elif options.ignore:
+ plugins_ignore = config['plugins_ignore']
+ for plugin in options.ignore.split(','):
+ if plugin.strip():
+ plugins_ignore.append(plugin.strip())
+
+ echo('ignoring plugin(s): %s' % ', '.join(plugins_ignore))
+
+ if options.autoclose:
+ config['auto_close'] = True
+ echo('will auto close.')
+
+ if options.pid:
+ config['pid_file'] = os.path.realpath(options.pid)
+ echo("pid file location: %r" % config['pid_file'])
+
+ if options.socket:
+ config['server_socket'] = os.path.realpath(options.socket)
+ echo("daemon socket location: %s" % config['server_socket'])
+
+ if options.daemon:
+ config['daemon_mode'] = False
+
+ # Now {start|stop|...}
+ daemon_controls[action]()