diff options
author | keis <keijser@gmail.com> | 2011-07-28 22:31:30 +0200 |
---|---|---|
committer | keis <keijser@gmail.com> | 2011-07-28 22:31:30 +0200 |
commit | db61f092140205e71f40fb14dc98a1e650c7e527 (patch) | |
tree | 6a2459ae08c6baa4875b1db226dc7eb615a3cd3c /bin | |
parent | 1e20430333aee952f55f6caec4d55238a0160bf9 (diff) | |
parent | e035f6f991fdbe9862a72fba6e8d348dcc25532f (diff) |
Merge branch 'experimental' of git://github.com/Dieterbe/uzbl into mouse-events
Diffstat (limited to 'bin')
-rwxr-xr-x | bin/uzbl-event-manager | 300 | ||||
-rwxr-xr-x | bin/uzbl-tabbed | 564 |
2 files changed, 351 insertions, 513 deletions
diff --git a/bin/uzbl-event-manager b/bin/uzbl-event-manager index cb462c7..64a1354 100755 --- a/bin/uzbl-event-manager +++ b/bin/uzbl-event-manager @@ -30,21 +30,22 @@ import atexit import imp import logging import os -import socket import sys import time import weakref import re +import errno 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 signal import signal, SIGTERM, SIGINT, SIGKILL 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.''' @@ -65,14 +66,19 @@ CACHE_DIR = os.path.join(xdghome('CACHE', '.cache/'), 'uzbl/') # Define some globals. SCRIPTNAME = os.path.basename(sys.argv[0]) +logger = logging.getLogger(SCRIPTNAME) + + 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.''' @@ -89,7 +95,7 @@ def daemonize(): os._exit(0) except OSError: - logger.critical(get_exc()) + logger.critical('failed to daemonize', exc_info=True) sys.exit(1) os.chdir('/') @@ -101,7 +107,7 @@ def daemonize(): os._exit(0) except OSError: - logger.critical(get_exc()) + logger.critical('failed to daemonize', exc_info=True) sys.exit(1) if sys.stdout.isatty(): @@ -126,11 +132,11 @@ def make_dirs(path): try: dirname = os.path.dirname(path) if not os.path.isdir(dirname): - logger.debug('creating directories %r' % dirname) + logger.debug('creating directories %r', dirname) os.makedirs(dirname) except OSError: - logger.error(get_exc()) + logger.error('failed to create directories', exc_info=True) class EventHandler(object): @@ -147,7 +153,6 @@ class EventHandler(object): self.args = args self.kwargs = kwargs - def __repr__(self): elems = ['id=%d' % self.id, 'event=%s' % self.event, 'callback=%r' % self.callback] @@ -161,7 +166,6 @@ class EventHandler(object): 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.''' @@ -170,9 +174,6 @@ class EventHandler(object): self.callback(uzbl, *args, **kwargs) - - - class Plugin(object): '''Plugin module wrapper object.''' @@ -181,13 +182,12 @@ class Plugin(object): 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) + self.logger = logging.getLogger('plugin.%s' % name) # Weakrefs to all handlers created by this plugin self.handlers = set([]) @@ -210,13 +210,11 @@ class Plugin(object): 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 + def export(self, uzbl, attr, obj, prepend=True): + '''Attach `obj` to `uzbl` instance. This is the preferred method of sharing functionality, functions, data and objects between plugins. @@ -228,18 +226,16 @@ class Plugin(object): 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)) - + prepend = True if prepend and callable(obj) else False + uzbl.__dict__[attr] = partial(obj, uzbl) if prepend else obj + uzbl.exports[attr] = (self, obj, prepend) + uzbl.logger.info('exported %r to %r by plugin %r, prepended %r', + obj, '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.''' @@ -253,7 +249,6 @@ class Plugin(object): 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. @@ -276,24 +271,22 @@ class Plugin(object): # Create a new handler handler = EventHandler(self, event, callback, args, kwargs) self.handlers.add(weakref.ref(handler)) - self.logger.info('new %r' % handler) + self.logger.info('new %r', handler) uzbl.handlers[event].append(handler) - uzbl.logger.info('connected %r' % 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)) + 'plugin %r required by plugin %r', plugin, self.name) @classmethod def unquote(cls, s): @@ -304,10 +297,12 @@ class Plugin(object): 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()] + parts = cls._splitquoted.split(text) + return [cls.unquote(p) for p in parts if p.strip()] class Uzbl(object): @@ -315,6 +310,7 @@ class Uzbl(object): self.opts = opts self.parent = parent self.child_socket = child_socket + self.child_buffer = [] self.time = time.time() self.pid = None self.name = None @@ -323,7 +319,7 @@ class Uzbl(object): self.instance_start = False # Use name "unknown" until name is discovered. - self.logger = get_logger('uzbl-instance[]') + self.logger = logging.getLogger('uzbl-instance[]') # Track plugin event handlers and exported functions. self.exports = {} @@ -333,16 +329,14 @@ class Uzbl(object): 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), + '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.''' @@ -350,17 +344,16 @@ class Uzbl(object): # 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) + 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) + 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.''' @@ -371,8 +364,27 @@ class Uzbl(object): if opts.print_events: print ascii(u'%s<-- %s' % (' ' * self._depth, msg)) - self.child_socket.send(ascii("%s\n" % msg)) + self.child_buffer.append(ascii("%s\n" % msg)) + def do_send(self): + data = ''.join(self.child_buffer) + try: + bsent = self.child_socket.send(data) + except socket.error as e: + if e.errno in (errno.EAGAIN, errno.EINTR): + self.child_buffer = [data] + return + else: + self.logger.error('failed to send', exc_info=True) + return self.close() + else: + if bsent == 0: + self.logger.debug('write end of connection closed') + self.close() + elif bsent < len(data): + self.child_buffer = [data[bsent:]] + else: + del self.child_buffer[:] def read(self): '''Read data from the child socket and pass lines to the parse_msg @@ -385,7 +397,7 @@ class Uzbl(object): return self.close() except: - self.logger.error(get_exc()) + self.logger.error('failed to read', exc_info=True) return self.close() lines = (self._buffer + raw).split('\n') @@ -399,17 +411,16 @@ class Uzbl(object): 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] + elems = (line.split(' ', 3) + [''] * 3)[:4] # Ignore non-event messages. if elems[0] != 'EVENT': - logger.info('non-event message: %r' % line) + logger.info('non-event message: %r', line) if opts.print_events: print '--- %s' % ascii(line) return @@ -419,31 +430,32 @@ class Uzbl(object): 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) + self.logger = logging.getLogger('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)) + 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.logger.info('found instance pid %r', self.pid) self.init_plugins() @@ -460,16 +472,14 @@ class Uzbl(object): handler.call(self, *args, **kargs) except: - self.logger.error(get_exc()) + self.logger.error('error in handler', exc_info=True) 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.''' @@ -486,7 +496,7 @@ class Uzbl(object): self.child_socket.close() except: - self.logger.error(get_exc()) + self.logger.error('failed to close socket', exc_info=True) finally: self.child_socket = None @@ -494,11 +504,11 @@ class Uzbl(object): # Call plugins cleanup hooks. for plugin in self.parent.plugins.values(): if plugin.cleanup: - self.logger.debug('calling %r plugin cleanup hook' - % plugin.name) + self.logger.debug('calling %r plugin cleanup hook', + plugin.name) plugin.cleanup(self) - logger.info('removed %r' % self) + logger.info('removed %r', self) class UzblEventDaemon(object): @@ -529,16 +539,15 @@ class UzblEventDaemon(object): # 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) + 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,]) + info = imp.find_module(name, [dir]) module = imp.load_module(name, *info) # Check if the plugin has a callable hook. @@ -546,11 +555,10 @@ class UzblEventDaemon(object): for attr in ['init', 'after', 'cleanup']]) assert hooks, "no hooks in plugin %r" % module - logger.debug('creating plugin instance for %r plugin' % name) + logger.debug('creating plugin instance for %r plugin', name) plugin = Plugin(self, name, path, module) self.plugins[name] = plugin - logger.info('new %r' % plugin) - + logger.info('new %r', plugin) def create_server_socket(self): '''Create the event manager daemon socket for uzbl instance duplex @@ -564,8 +572,7 @@ class UzblEventDaemon(object): sock.listen(5) self.server_socket = sock - logger.debug('bound server socket to %r' % opts.server_socket) - + logger.debug('bound server socket to %r', opts.server_socket) def run(self): '''Main event daemon loop.''' @@ -588,35 +595,39 @@ class UzblEventDaemon(object): except: if not self._quit: - logger.critical(get_exc()) + logger.critical('failed to listen', exc_info=True) # 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) + 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) + wsocks = [k for k, v in self.uzbls.items() if v.child_buffer] + reads, writes, errors = select(socks, wsocks, 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] + child_socket.setblocking(False) self.uzbls[child_socket] = Uzbl(self, child_socket) connections += 1 + for uzbl in [self.uzbls[s] for s in writes]: + uzbl.do_send() + for uzbl in [self.uzbls[s] for s in reads]: uzbl.read() @@ -626,7 +637,6 @@ class UzblEventDaemon(object): logger.info('auto closing') - def close_server_socket(self): '''Close and delete the server socket.''' @@ -637,12 +647,11 @@ class UzblEventDaemon(object): self.server_socket = None if os.path.exists(opts.server_socket): - logger.info('unlinking %r' % opts.server_socket) + logger.info('unlinking %r', opts.server_socket) os.unlink(opts.server_socket) except: - logger.error(get_exc()) - + logger.error('failed to close server socket', exc_info=True) def quit(self, sigint=None, *args): '''Close all instance socket objects, server socket and delete the @@ -673,16 +682,16 @@ def make_pid_file(pid_file): '''Creates a pid file at `pid_file`, fails silently.''' try: - logger.debug('creating pid file %r' % pid_file) + 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)) + logger.info('created pid file %r with pid %d', pid_file, pid) except: - logger.error(get_exc()) + logger.error('failed to create pid file', exc_info=True) def del_pid_file(pid_file): @@ -690,27 +699,27 @@ def del_pid_file(pid_file): if os.path.isfile(pid_file): try: - logger.debug('deleting pid file %r' % pid_file) + logger.debug('deleting pid file %r', pid_file) os.remove(pid_file) - logger.info('deleted pid file %r' % pid_file) + logger.info('deleted pid file %r', pid_file) except: - logger.error(get_exc()) + logger.error('failed to delete pid file', exc_info=True) def get_pid(pid_file): '''Reads a pid from pid file `pid_file`, fails None.''' try: - logger.debug('reading pid file %r' % pid_file) + 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)) + logger.info('read pid %d from pid file %r', pid, pid_file) return pid except (IOError, ValueError): - logger.error(get_exc()) + logger.error('failed to read pid', exc_info=True) return None @@ -729,30 +738,30 @@ 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) + 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) + 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) + 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) + 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()) + logger.critical('failed to kill %d', pid, exc_info=True) raise - if (time.time()-start) > 10: - logger.critical('unable to kill process with pid %d' % pid) + if (time.time() - start) > 10: + logger.critical('unable to kill process with pid %d', pid) raise OSError time.sleep(0.25) @@ -763,20 +772,20 @@ def stop_action(): 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) + logger.error('could not find running event manager with pid file %r', + pid_file) return pid = get_pid(pid_file) if not pid_running(pid): - logger.debug('no process with pid %r' % pid) + logger.debug('no process with pid %r', pid) del_pid_file(pid_file) return - logger.debug('terminating process with pid %r' % pid) + 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) + logger.info('stopped event manager process with pid %d', pid) def start_action(): @@ -786,10 +795,10 @@ def start_action(): 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) + logger.error('event manager already started with pid %d', pid) return - logger.info('no process with pid %d' % pid) + logger.info('no process with pid %d', pid) del_pid_file(pid_file) UzblEventDaemon().run() @@ -816,7 +825,7 @@ def list_action(): print plugin -if __name__ == "__main__": +def make_parser(): parser = OptionParser('usage: %prog [options] {start|stop|restart|list}') add = parser.add_option @@ -833,6 +842,7 @@ if __name__ == "__main__": 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') @@ -862,6 +872,34 @@ if __name__ == "__main__": dest='print_events', action="store_false", default=True, help="silence the printing of events to stdout") + return parser + + +def init_logger(): + log_level = logging.CRITICAL - opts.verbose * 10 + logger = logging.getLogger() + logger.setLevel(max(log_level, 10)) + + # Console + handler = logging.StreamHandler() + handler.setLevel(max(log_level + 10, 10)) + handler.setFormatter(logging.Formatter( + '%(name)s: %(levelname)s: %(message)s')) + logger.addHandler(handler) + + # Logfile + handler = logging.FileHandler(opts.log_file, 'w', 'utf-8', 1) + handler.setLevel(max(log_level, 10)) + handler.setFormatter(logging.Formatter( + '[%(created)f] %(name)s: %(levelname)s: %(message)s')) + logger.addHandler(handler) + + +def main(): + global opts + + parser = make_parser() + (opts, args) = parser.parse_args() opts.server_socket = expandpath(opts.server_socket) @@ -881,35 +919,8 @@ if __name__ == "__main__": 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) + init_logger() + logger.info('logging to %r', opts.log_file) plugins = {} @@ -923,11 +934,11 @@ if __name__ == "__main__": for plugin in matches: (head, tail) = os.path.split(plugin) if tail not in plugins: - logger.debug('found plugin: %r' % plugin) + logger.debug('found plugin: %r', plugin) plugins[tail] = plugin else: - logger.debug('ignoring plugin: %r' % plugin) + logger.debug('ignoring plugin: %r', plugin) # Add default plugin locations if opts.default_dirs: @@ -941,15 +952,15 @@ if __name__ == "__main__": # 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) + 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) + logger.debug('found plugin: %r', plugin) plugins[tail] = plugin else: - logger.debug('ignoring plugin: %r' % plugin) + logger.debug('ignoring plugin: %r', plugin) plugins = plugins.values() @@ -958,11 +969,15 @@ if __name__ == "__main__": 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.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') + if opts.daemon_mode: + logger.debug('will daemonize') + else: + logger.debug('will not daemonize') opts.plugins = plugins @@ -976,16 +991,21 @@ if __name__ == "__main__": parser.error('invalid action: %r' % action) elif not args: - logger.warning('no daemon action given, assuming %r' % 'start') action = 'start' + logger.warning('no daemon action given, assuming %r', action) else: parser.error('invalid action argument: %r' % args) - logger.info('daemon action %r' % action) + logger.info('daemon action %r', action) # Do action daemon_actions[action]() - logger.debug('process CPU time: %f' % time.clock()) + logger.debug('process CPU time: %f', time.clock()) + + +if __name__ == "__main__": + main() + # vi: set et ts=4: diff --git a/bin/uzbl-tabbed b/bin/uzbl-tabbed index a15967a..1a65788 100755 --- a/bin/uzbl-tabbed +++ b/bin/uzbl-tabbed @@ -72,7 +72,6 @@ # gtk_tab_pos = (top|left|bottom|right) # gtk_refresh = 1000 # switch_to_new_tabs = 1 -# capture_new_windows = 1 # multiline_tabs = 1 # # Tab title options: @@ -88,8 +87,6 @@ # session_file = $HOME/.local/share/uzbl/session # # Inherited uzbl options: -# fifo_dir = /tmp -# socket_dir = /tmp # icon_path = $HOME/.local/share/uzbl/uzbl.png # status_background = #303030 # @@ -199,7 +196,6 @@ config = { 'gtk_tab_pos': 'top', # Gtk tab position (top|left|bottom|right) 'gtk_refresh': 1000, # Tablist refresh millisecond interval 'switch_to_new_tabs': True, # Upon opening a new tab switch to it - 'capture_new_windows': True, # Use uzbl_tabbed to catch new windows 'multiline_tabs': True, # Tabs overflow onto new tablist lines. # Tab title options @@ -215,8 +211,6 @@ config = { 'session_file': os.path.join(DATA_DIR, 'session'), # Inherited uzbl options - 'fifo_dir': '/tmp', # Path to look for uzbl fifo. - 'socket_dir': '/tmp', # Path to look for uzbl socket. 'icon_path': os.path.join(DATA_DIR, 'uzbl.png'), 'status_background': "#303030", # Default background for all panels. @@ -298,17 +292,20 @@ def escape(s): return s class SocketClient: - '''Represents a Uzbl instance, which is not necessarly linked with a UzblInstance''' + '''Represents a connection to the uzbl-tabbed socket.''' # List of UzblInstance objects not already linked with a SocketClient instances_queue = {} - def __init__(self, socket): + def __init__(self, socket, uzbl_tabbed): self._buffer = "" self._socket = socket self._watchers = [io_add_watch(socket, IO_IN, self._socket_recv),\ io_add_watch(socket, IO_HUP, self._socket_closed)] + self.uzbl = None + self.uzbl_tabbed = uzbl_tabbed + self.dispatcher = GlobalEventDispatcher(uzbl_tabbed) def _socket_recv(self, fd, condition): @@ -328,26 +325,44 @@ class SocketClient: '''An Uzbl instance sent some data, parse it''' self._buffer += data - if self.uzbl: - if "\n" in self._buffer: - cmds = self._buffer.split("\n") - if cmds[-1]: # Last command has been received incomplete, don't process it - self._buffer, cmds = cmds[-1], cmds[:-1] - else: - self._buffer = "" + if "\n" in self._buffer: + cmds = self._buffer.split("\n") - for cmd in cmds: - if cmd: - self.uzbl.parse_command(cmd) - else: - name = re.findall('^EVENT \[(\d+-\d+)\] INSTANCE_START \d+$', self._buffer, re.M) - uzbl = self.instances_queue.get(name[0]) + if cmds[-1]: # Last command has been received incomplete, don't process it + self._buffer, cmds = cmds[-1], cmds[:-1] + else: + self._buffer = "" + + for cmd in cmds: + if cmd: + self.handle_event(cmd) + + def handle_event(self, cmd): + cmd = parse_event(cmd) + message, instance_name, message_type = cmd[0:3] + args = cmd[3:] + + if not message == "EVENT": + return + + # strip the surrounding [] + instance_name = instance_name[1:-1] + + if self.uzbl: + if not self.dispatcher.dispatch(message_type, args): + self.uzbl.dispatcher.dispatch(message_type, args) + elif message_type == 'INSTANCE_START': + uzbl = self.instances_queue.get(instance_name) if uzbl: - del self.instances_queue[name[0]] - self.uzbl = uzbl - self.uzbl.got_socket(self) - self._feed("") + # we've found the uzbl we were waiting for + del self.instances_queue[instance_name] + else: + # an unsolicited uzbl has connected, how exciting! + uzbl = UzblInstance(self.uzbl_tabbed, None, '', '', False) + self.uzbl = uzbl + self.uzbl.got_socket(self) + self._feed("") def send(self, data): '''Child socket send function.''' @@ -363,18 +378,84 @@ class SocketClient: map(source_remove, self._watchers) self._watchers = [] -class EventDispatcher: - def __init__(self, uzbl): - self.uzbl = uzbl - self.parent = self.uzbl.parent +def unquote(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("( |\"(?:\\\\.|[^\"])*?\"|'(?:\\\\.|[^'])*?')") +def parse_event(text): + '''Splits string on whitespace while respecting quotations''' + return [unquote(p) for p in _splitquoted.split(text) if p.strip()] + +class EventDispatcher: def dispatch(self, message_type, args): + '''Returns True if the message was handled, False otherwise.''' + method = getattr(self, message_type.lower(), None) if method is None: - return + return False + + method(*args) + return True + +class GlobalEventDispatcher(EventDispatcher): + def __init__(self, uzbl_tabbed): + self.uzbl_tabbed = uzbl_tabbed + + def new_tab(self, uri = ''): + self.uzbl_tabbed.new_tab(uri) + + def new_tab_bg(self, uri = ''): + self.uzbl_tabbed.new_tab(uri, switch = False) - return method(*args) + def new_tab_next(self, uri = ''): + self.uzbl_tabbed.new_tab(uri, next=True) + + def new_bg_tab_next(self, uri = ''): + self.uzbl_tabbed.new_tab(uri, switch = False, next = True) + + def next_tab(self, step = 1): + self.uzbl_tabbed.next_tab(int(step)) + + def prev_tab(self, step = 1): + self.uzbl_tabbed.prev_tab(int(step)) + + def goto_tab(self, index): + self.uzbl_tabbed.goto_tab(int(index)) + + def first_tab(self): + self.uzbl_tabbed.goto_tab(0) + + def last_tab(self): + self.uzbl_tabbed.goto_tab(-1) + + def preset_tabs(self, *args): + self.uzbl_tabbed.run_preset_command(*args) + + def bring_to_front(self): + self.uzbl_tabbed.window.present() + + def clean_tabs(self): + self.uzbl_tabbed.clean_slate() + + def exit_all_tabs(self): + self.uzbl_tabbed.quitrequest() + +class InstanceEventDispatcher(EventDispatcher): + def __init__(self, uzbl): + self.uzbl = uzbl + self.parent = self.uzbl.parent + + def plug_created(self, plug_id): + if not self.uzbl.tab: + tab = self.parent.create_tab() + tab.add_id(int(plug_id)) + self.uzbl.set_tab(tab) def title_changed(self, title): self.uzbl.title = title.strip() @@ -417,71 +498,14 @@ class EventDispatcher: def load_commit(self, uri): self.uzbl.uri = uri - def new_tab(self, uri = None): - if uri: - self.parent.new_tab(uri) - else: - self.parent.new_tab() - - def new_tab_bg(self, uri = None): - if uri: - self.parent.new_tab(uri, switch = False) - else: - self.parent.new_tab(switch = False) - - def new_tab_next(self, uri = None): - if uri: - self.parent.new_tab(uri, next=True) - else: - self.parent.new_tab(next=True) - - def new_bg_tab_next(self, uri = None): - if uri: - self.parent.new_tab(uri, switch = False, next = True) - else: - self.parent.new_tab(switch = False, next = True) - - def next_tab(self, step = None): - if step: - self.parent.next_tab(int(step)) - else: - self.parent.next_tab() - - def prev_tab(self, step = None): - if step: - self.parent.prev_tab(int(step)) - else: - self.parent.prev_tab() - - def goto_tab(self, index): - self.parent.goto_tab(int(index)) - - def first_tab(self): - self.parent.goto_tab(0) - - def last_tab(self): - self.parent.goto_tab(-1) - - def preset_tabs(self, *args): - self.parent.parse_command(["preset"] + [ a for a in args ]) - - def bring_to_front(self): - self.parent.window.present() - - def clean_tabs(self): - self.parent.clean_slate() - - def exit_all_tabs(self): - self.parent.quitrequest() - class UzblInstance: '''Uzbl instance meta-data/meta-action object.''' - def __init__(self, parent, tab, name, uri, title, switch): + def __init__(self, parent, name, uri, title, switch): self.parent = parent - self.tab = tab - self.dispatcher = EventDispatcher(self) + self.tab = None + self.dispatcher = InstanceEventDispatcher(self) self.name = name self.title = title @@ -490,8 +514,11 @@ class UzblInstance: self._client = None self._switch = switch # Switch to tab after loading ? - self.title_changed() + def set_tab(self, tab): + self.tab = tab + self.title_changed() + self.parent.tabs[self.tab] = self def got_socket(self, client): '''Uzbl instance is now connected''' @@ -506,6 +533,9 @@ class UzblInstance: def title_changed(self, gtk_only = True): # GTK-only is for indexes '''self.title has changed, update the tabs list''' + if not self.tab: + return + tab_titles = config['tab_titles'] tab_indexes = config['tab_indexes'] show_ellipsis = config['show_ellipsis'] @@ -546,7 +576,8 @@ class UzblInstance: ''' Send the SET command to Uzbl ''' if self._client: - self._client.send('set %s = %s') #TODO: escape chars ? + line = 'set %s = %s' % (key, val) #TODO: escape chars ? + self._client.send(line) def exit(self): @@ -555,27 +586,6 @@ class UzblInstance: if self._client: self._client.send('exit') - def unquote(self, 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("( |\"(?:\\\\.|[^\"])*?\"|'(?:\\\\.|[^'])*?')") - def parse_event(self, text): - '''Splits string on whitespace while respecting quotations''' - return [self.unquote(p) for p in self._splitquoted.split(text) if p.strip()] - - def parse_command(self, cmd): - ''' Parse event givent by the Uzbl instance ''' - - cmd = self.parse_event(cmd) - message, message_type, args = cmd[0], cmd[2], cmd[3:] - - if message == "EVENT": - self.dispatcher.dispatch(message_type, args) - def close(self): '''The remote instance exited''' @@ -606,6 +616,13 @@ class UzblTabbed: # Generates a unique id for uzbl socket filenames. self.next_pid = counter().next + # Whether to reconfigure new uzbl instances + self.force_socket_dir = False + self.force_fifo_dir = False + + self.fifo_dir = '/tmp' # Path to look for uzbl fifo. + self.socket_dir = '/tmp' # Path to look for uzbl socket. + # Create main window self.window = gtk.Window() try: @@ -684,16 +701,12 @@ class UzblTabbed: self.window.show() self.wid = self.notebook.window.xid - # Store information about the applications fifo and socket. - fifo_filename = 'uzbltabbed_%d.fifo' % os.getpid() + # Store information about the application's socket. socket_filename = 'uzbltabbed_%d.socket' % os.getpid() - self._fifo = None self._socket = None - self.fifo_path = os.path.join(config['fifo_dir'], fifo_filename) - self.socket_path = os.path.join(config['socket_dir'], socket_filename) + self.socket_path = os.path.join(self.socket_dir, socket_filename) - # Now initialise the fifo and the socket - self.init_fifo() + # Now initialise the the socket self.init_socket() # If we are using sessions then load the last one if it exists. @@ -724,8 +737,7 @@ class UzblTabbed: print_exc() error("encounted error %r" % sys.exc_info()[1]) - # Unlink fifo socket - self.unlink_fifo() + # Unlink socket self.close_socket() # Attempt to close all uzbl instances nicely. @@ -763,7 +775,7 @@ class UzblTabbed: '''A new uzbl instance was created''' client, _ = sock.accept() - self.clients[client] = SocketClient(client) + self.clients[client] = SocketClient(client, self) return True @@ -791,249 +803,45 @@ class UzblTabbed: self._socket = None - def init_fifo(self): - '''Create interprocess communication fifo.''' - - if os.path.exists(self.fifo_path): - if not os.access(self.fifo_path, os.F_OK | os.R_OK | os.W_OK): - os.mkfifo(self.fifo_path) - - else: - basedir = os.path.dirname(self.fifo_path) - if not os.path.exists(basedir): - os.makedirs(basedir) - - os.mkfifo(self.fifo_path) - - # Add event handlers for IO_IN & IO_HUP events. - self.setup_fifo_watchers() - - echo("[fifo] listening at %r" % self.fifo_path) - - # Add atexit register to destroy the fifo on program termination. - atexit.register(self.unlink_fifo) - - - def unlink_fifo(self): - '''Unlink the fifo socket. Note: This function is called automatically - on exit by an atexit register.''' - - # Make sure the fifo fd is closed. - self.close_fifo() - - # And unlink if the real fifo exists. - if os.path.exists(self.fifo_path): - os.unlink(self.fifo_path) - echo("unlinked %r" % self.fifo_path) - - - def close_fifo(self): - '''Remove all event handlers watching the fifo and close the fd.''' - - # Already closed - if self._fifo is None: return - - (fd, watchers) = self._fifo - os.close(fd) - - # Stop all gobject io watchers watching the fifo. - for gid in watchers: - source_remove(gid) - - self._fifo = None - - - def setup_fifo_watchers(self): - '''Open fifo socket fd and setup gobject IO_IN & IO_HUP event - handlers.''' - - # Close currently open fifo fd and kill all watchers - self.close_fifo() - - fd = os.open(self.fifo_path, os.O_RDONLY | os.O_NONBLOCK) - - # Add gobject io event handlers to the fifo socket. - watchers = [io_add_watch(fd, IO_IN, self.main_fifo_read),\ - io_add_watch(fd, IO_HUP, self.main_fifo_hangup)] - - self._fifo = (fd, watchers) - - - def main_fifo_hangup(self, fd, cb_condition): - '''Handle main fifo socket hangups.''' - - # Close old fd, open new fifo socket and add io event handlers. - self.setup_fifo_watchers() - - # Kill the gobject event handler calling this handler function. - return False - - - def main_fifo_read(self, fd, cb_condition): - '''Read from main fifo socket.''' - - self._buffer = os.read(fd, 1024) - temp = self._buffer.split("\n") - self._buffer = temp.pop() - cmds = [s.strip().split() for s in temp if len(s.strip())] - - for cmd in cmds: - try: - #print cmd - self.parse_command(cmd) - - except: - print_exc() - error("parse_command: invalid command %s" % ' '.join(cmd)) - raise - - return True - - - def parse_command(self, cmd): - '''Parse instructions from uzbl child processes.''' - - # Commands ( [] = optional, {} = required ) - # new [uri] - # open new tab and head to optional uri. - # newbg [uri] - # open a new tab in the background - # close [tab-num] - # close current tab or close via tab id. - # next [n-tabs] - # open next tab or n tabs down. Supports negative indexing. - # prev [n-tabs] - # open prev tab or n tabs down. Supports negative indexing. - # goto {tab-n} - # goto tab n. - # first - # goto first tab. - # last - # goto last tab. - # title {pid} {document-title} - # updates tablist title. - # uri {pid} {document-location} - # updates tablist uri - # bring_to_front - # brings the gtk window to focus. - # exit - # exits uzbl_tabbed.py - - if cmd[0] == "new": - if len(cmd) == 2: - self.new_tab(cmd[1]) - - else: - self.new_tab() - - elif cmd[0] == "newbg": - if len(cmd) == 2: - self.new_tab(cmd[1], switch=False) - else: - self.new_tab(switch=False) - - elif cmd[0] == "newfromclip": - uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\ - stdout=subprocess.PIPE).communicate()[0] - if uri: - self.new_tab(uri) + def run_preset_command(self, cmd, *args): + if len(args) < 1: + error("parse_command: invalid preset command") - elif cmd[0] == "close": - if len(cmd) == 2: - self.close_tab(int(cmd[1])) - - else: - self.close_tab() - - elif cmd[0] == "next": - if len(cmd) == 2: - self.next_tab(int(cmd[1])) - - else: - self.next_tab() + elif cmd == "save": + path = os.path.join(config['saved_sessions_dir'], args[0]) + self.save_session(path) - elif cmd[0] == "prev": - if len(cmd) == 2: - self.prev_tab(int(cmd[1])) + elif cmd == "load": + path = os.path.join(config['saved_sessions_dir'], args[0]) + self.load_session(path) + elif cmd == "del": + path = os.path.join(config['saved_sessions_dir'], args[0]) + if os.path.isfile(path): + os.remove(path) else: - self.prev_tab() - - elif cmd[0] == "goto": - self.goto_tab(int(cmd[1])) - - elif cmd[0] == "first": - self.goto_tab(0) - - elif cmd[0] == "last": - self.goto_tab(-1) - - elif cmd[0] in ["title", "uri"]: - if len(cmd) > 2: - uzbl = self.get_tab_by_name(int(cmd[1])) - if uzbl: - old = getattr(uzbl, cmd[0]) - new = ' '.join(cmd[2:]) - setattr(uzbl, cmd[0], new) - if old != new: - self.update_tablist() - - else: - error("parse_command: no uzbl with name %r" % int(cmd[1])) - - elif cmd[0] == "preset": - if len(cmd) < 3: - error("parse_command: invalid preset command") - - elif cmd[1] == "save": - path = os.path.join(config['saved_sessions_dir'], cmd[2]) - self.save_session(path) - - elif cmd[1] == "load": - path = os.path.join(config['saved_sessions_dir'], cmd[2]) - self.load_session(path) - - elif cmd[1] == "del": - path = os.path.join(config['saved_sessions_dir'], cmd[2]) - if os.path.isfile(path): - os.remove(path) + error("parse_command: preset %r does not exist." % path) - else: - error("parse_command: preset %r does not exist." % path) - - elif cmd[1] == "list": - # FIXME: what argument is this supposed to be passed, - # and why? - uzbl = self.get_tab_by_name(int(cmd[2])) - if uzbl: - if not os.path.isdir(config['saved_sessions_dir']): - js = "js alert('No saved presets.');" - uzbl._client.send(js) - - else: - listdir = os.listdir(config['saved_sessions_dir']) - listdir = "\\n".join(listdir) - js = "js alert('Session presets:\\n\\n%s');" % listdir - uzbl._client.send(js) + elif cmd == "list": + # FIXME: what argument is this supposed to be passed, + # and why? + uzbl = self.get_tab_by_name(int(args[0])) + if uzbl: + if not os.path.isdir(config['saved_sessions_dir']): + js = "js alert('No saved presets.');" + uzbl._client.send(js) else: - error("parse_command: unknown tab name.") + listdir = os.listdir(config['saved_sessions_dir']) + listdir = "\\n".join(listdir) + js = "js alert('Session presets:\\n\\n%s');" % listdir + uzbl._client.send(js) else: - error("parse_command: unknown parse command %r"\ - % ' '.join(cmd)) - - elif cmd[0] == "bring_to_front": - self.window.present() - - elif cmd[0] == "clean": - self.clean_slate() - - elif cmd[0] == "exit": - self.quitrequest() + error("parse_command: unknown tab name.") else: - error("parse_command: unknown command %r" % ' '.join(cmd)) + error("parse_command: unknown parse command %r" % cmd) def get_tab_by_name(self, name): @@ -1045,6 +853,18 @@ class UzblTabbed: return False + def create_tab(self, beside = False): + tab = gtk.Socket() + tab.show() + + if beside: + pos = self.notebook.get_current_page() + 1 + self.notebook.insert_page(tab, position=pos) + else: + self.notebook.append_page(tab) + + self.notebook.set_tab_reorderable(tab, True) + return tab def new_tab(self, uri='', title='', switch=None, next=False): '''Add a new tab to the notebook and start a new instance of uzbl. @@ -1052,10 +872,7 @@ class UzblTabbed: when you need to load multiple tabs at a time (I.e. like when restoring a session from a file).''' - tab = gtk.Socket() - tab.show() - self.notebook.insert_page(tab, position=next and self.notebook.get_current_page() + 1 or -1) - self.notebook.set_tab_reorderable(tab, True) + tab = self.create_tab(next) sid = tab.get_id() uri = uri.strip() name = "%d-%d" % (os.getpid(), self.next_pid()) @@ -1070,9 +887,10 @@ class UzblTabbed: '--connect-socket', self.socket_path, '--uri', str(uri)] gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) - uzbl = UzblInstance(self, tab, name, uri, title, switch) + uzbl = UzblInstance(self, name, uri, title, switch) + uzbl.set_tab(tab) + SocketClient.instances_queue[name] = uzbl - self.tabs[tab] = uzbl def clean_slate(self): @@ -1085,16 +903,15 @@ class UzblTabbed: uzbl = self.tabs[tab] uzbl.exit() - def config_uzbl(self, uzbl): '''Send bind commands for tab new/close/next/prev to a uzbl instance.''' - # Set definitions here - # set(key, command back to fifo) - if config['capture_new_windows']: - uzbl.set("new_window", r'new $8') + if self.force_socket_dir: + uzbl.set("socket_dir", self.socket_dir) + if self.force_fifo_dir: + uzbl.set("fifo_dir", self.fifo_dir) def goto_tab(self, index): '''Goto tab n (supports negative indexing).''' @@ -1411,9 +1228,8 @@ class UzblTabbed: def quit(self, *args): '''Cleanup and quit. Called by delete-event signal.''' - # Close the fifo socket, remove any gobject io event handlers and + # Close the socket, remove any gobject io event handlers and # delete socket. - self.unlink_fifo() self.close_socket() # Remove all gobject timers that are still ticking. @@ -1455,13 +1271,15 @@ if __name__ == "__main__": import pprint sys.stderr.write("%s\n" % pprint.pformat(config)) + uzbl = UzblTabbed() + if options.socketdir: - config['socket_dir'] = options.socketdir + uzbl.socket_dir = options.socketdir + uzbl.force_socket_dir = True if options.fifodir: - config['fifo_dir'] = options.fifodir - - uzbl = UzblTabbed() + uzbl.fifo_dir = options.fifodir + uzbl.force_fifo_dir = True # All extra arguments given to uzbl_tabbed.py are interpreted as # web-locations to opened in new tabs. |