Fix code comment.
[uzbl-mobile] / examples / data / uzbl / scripts / uzbl_tabbed.py
index 7a9abf5..9ffa97d 100755 (executable)
@@ -23,7 +23,7 @@
 #   Tom Adams <tom@holizz.com>
 #       Wrote the original uzbl_tabbed.py as a proof of concept.
 #
-#  Chris van Dijk (quigybo) <cn.vandijk@hotmail.com>
+#   Chris van Dijk (quigybo) <cn.vandijk@hotmail.com>
 #       Made signifigant headway on the old uzbl_tabbing.py script on the
 #       uzbl wiki <http://www.uzbl.org/wiki/uzbl_tabbed>
 #
@@ -47,7 +47,7 @@
 # Optional dependencies:
 #   simplejson - save uzbl_tabbed.py sessions & presets in json.
 #
-# Note: I haven't included version numbers with this dependency list because 
+# Note: I haven't included version numbers with this dependency list because
 # I've only ever tested uzbl_tabbed.py on the latest stable versions of these
 # packages in Gentoo's portage. Package names may vary on different systems.
 
@@ -161,17 +161,27 @@ import gobject
 import socket
 import random
 import hashlib
+import atexit
+
+from gobject import io_add_watch, source_remove, timeout_add, IO_IN, IO_HUP
+from signal import signal, SIGTERM, SIGINT
+from optparse import OptionParser, OptionGroup
 
 pygtk.require('2.0')
 
+_scriptname = os.path.basename(sys.argv[0])
 def error(msg):
-    sys.stderr.write("%s\n"%msg)
+    sys.stderr.write("%s: %s\n" % (_scriptname, msg))
+
+def echo(msg):
+    print "%s: %s" % (_scriptname, msg)
 
 
 # ============================================================================
 # ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
 # ============================================================================
 
+
 # Location of your uzbl data directory.
 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
     data_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
@@ -251,7 +261,7 @@ config = {
   'selected_https':         'foreground = "#fff"',
   'selected_https_text':    'foreground = "gold"',
 
-  } # End of config dict.
+} # End of config dict.
 
 # This is the tab style policy handler. Every time the tablist is updated
 # this function is called to determine how to colourise that specific tab
@@ -421,7 +431,7 @@ class UzblTabbed:
                     self._connected = True
 
                     if timer_call in self.timers.keys():
-                        gobject.source_remove(self.timers[timer_call])
+                        source_remove(self.timers[timer_call])
                         del self.timers[timer_call]
 
                     if self._switch:
@@ -465,11 +475,13 @@ class UzblTabbed:
     def __init__(self):
         '''Create tablist, window and notebook.'''
 
-        self._fifos = {}
+        # Store information about the applications fifo_socket.
+        self._fifo = None
+
         self._timers = {}
         self._buffer = ""
         self._killed = False
-        
+
         # A list of the recently closed tabs
         self._closed = []
 
@@ -560,19 +572,84 @@ class UzblTabbed:
         self.window.show()
         self.wid = self.notebook.window.xid
 
-        # Create the uzbl_tabbed fifo
+        # Generate the fifo socket filename.
         fifo_filename = 'uzbltabbed_%d' % os.getpid()
         self.fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
-        self._create_fifo_socket(self.fifo_socket)
-        self._setup_fifo_watcher(self.fifo_socket)
-        
 
-    def _create_fifo_socket(self, fifo_socket):
+        # Now initialise the fifo socket at self.fifo_socket
+        self.init_fifo_socket()
+
+        # If we are using sessions then load the last one if it exists.
+        if config['save_session']:
+            self.load_session()
+
+
+    def run(self):
+        '''UzblTabbed main function that calls the gtk loop.'''
+
+        if not len(self.tabs):
+            self.new_tab()
+
+        # Update tablist timer
+        #timer = "update-tablist"
+        #timerid = timeout_add(500, self.update_tablist,timer)
+        #self._timers[timer] = timerid
+
+        # Probe clients every second for window titles and location
+        timer = "probe-clients"
+        timerid = timeout_add(1000, self.probe_clients, timer)
+        self._timers[timer] = timerid
+
+        # Make SIGTERM act orderly.
+        signal(SIGTERM, lambda signum, stack_frame: self.terminate(SIGTERM))
+
+        # Catch keyboard interrupts
+        signal(SIGINT, lambda signum, stack_frame: self.terminate(SIGINT))
+
+        try:
+            gtk.main()
+
+        except:
+            error("encounted error %r" % sys.exc_info()[1])
+
+            # Unlink fifo socket
+            self.unlink_fifo_socket()
+
+            # Attempt to close all uzbl instances nicely.
+            self.quitrequest()
+
+            # Allow time for all the uzbl instances to quit.
+            time.sleep(1)
+
+            raise
+
+
+    def terminate(self, termsig=None):
+        '''Handle termination signals and exit safely and cleanly.'''
+
+        # Not required but at least it lets the user know what killed his
+        # browsing session.
+        if termsig == SIGTERM:
+            error("caught SIGTERM signal")
+
+        elif termsig == SIGINT:
+            error("caught keyboard interrupt")
+
+        else:
+            error("caught unknown signal")
+
+        error("commencing infanticide!")
+
+        # Sends the exit signal to all uzbl instances.
+        self.quitrequest()
+
+
+    def init_fifo_socket(self):
         '''Create interprocess communication fifo socket.'''
 
-        if os.path.exists(fifo_socket):
-            if not os.access(fifo_socket, os.F_OK | os.R_OK | os.W_OK):
-                os.mkfifo(fifo_socket)
+        if os.path.exists(self.fifo_socket):
+            if not os.access(self.fifo_socket, os.F_OK | os.R_OK | os.W_OK):
+                os.mkfifo(self.fifo_socket)
 
         else:
             basedir = os.path.dirname(self.fifo_socket)
@@ -581,63 +658,93 @@ class UzblTabbed:
 
             os.mkfifo(self.fifo_socket)
 
-        print "Listening on %s" % self.fifo_socket
+        # Add event handlers for IO_IN & IO_HUP events.
+        self.setup_fifo_watchers()
 
+        echo("listening at %r" % self.fifo_socket)
 
-    def _setup_fifo_watcher(self, fifo_socket):
-        '''Open fifo socket fd and setup gobject IO_IN & IO_HUP watchers.
-        Also log the creation of a fd and store the the internal
-        self._watchers dictionary along with the filename of the fd.'''
+        # Add atexit register to destroy the socket on program termination.
+        atexit.register(self.unlink_fifo_socket)
 
-        if fifo_socket in self._fifos.keys():
-            fd, watchers = self._fifos[fifo_socket]
-            os.close(fd)
-            for (watcherid, gid) in watchers.items():
-                gobject.source_remove(gid)
-                del watchers[watcherid]
 
-            del self._fifos[fifo_socket]
+    def unlink_fifo_socket(self):
+        '''Unlink the fifo socket. Note: This function is called automatically
+        on exit by an atexit register.'''
 
-        # Re-open fifo and add listeners.
-        fd = os.open(fifo_socket, os.O_RDONLY | os.O_NONBLOCK)
-        watchers = {}
-        self._fifos[fifo_socket] = (fd, watchers)
-        watcher = lambda key, id: watchers.__setitem__(key, id)
+        # Make sure the fifo_socket fd is closed.
+        self.close_fifo()
 
-        # Watch for incoming data.
-        gid = gobject.io_add_watch(fd, gobject.IO_IN, self.main_fifo_read)
-        watcher('main-fifo-read', gid)
+        # And unlink if the real fifo_socket exists.
+        if os.path.exists(self.fifo_socket):
+            os.unlink(self.fifo_socket)
+            echo("unlinked %r" % self.fifo_socket)
 
-        # Watch for fifo hangups.
-        gid = gobject.io_add_watch(fd, gobject.IO_HUP, self.main_fifo_hangup)
-        watcher('main-fifo-hangup', gid)
 
+    def close_fifo(self):
+        '''Remove all event handlers watching the fifo and close the fd.'''
 
-    def run(self):
-        '''UzblTabbed main function that calls the gtk loop.'''
-        
-        if config['save_session']:
-            self.load_session()
-        
-        if not len(self.tabs):
-            self.new_tab()
-        
-        # Update tablist timer
-        #timer = "update-tablist"
-        #timerid = gobject.timeout_add(500, self.update_tablist,timer)
-        #self._timers[timer] = timerid
+        # Already closed
+        if self._fifo is None: return
 
-        # Probe clients every second for window titles and location
-        timer = "probe-clients"
-        timerid = gobject.timeout_add(1000, self.probe_clients, timer)
-        self._timers[timer] = timerid
+        (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.'''
 
-        gtk.main()
+        # Close currently open fifo_socket fd and kill all watchers
+        self.close_fifo()
+
+        fd = os.open(self.fifo_socket, 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:
+                error("parse_command: invalid command %s" % ' '.join(cmd))
+                raise
+
+        return True
 
 
     def probe_clients(self, timer_call):
         '''Probe all uzbl clients for up-to-date window titles and uri's.'''
-    
+
         save_session = config['save_session']
 
         sockd = {}
@@ -649,7 +756,7 @@ class UzblTabbed:
             uzbl = self.tabs[tab]
             uzbl.probe()
             if uzbl._socket:
-                sockd[uzbl._socket] = uzbl          
+                sockd[uzbl._socket] = uzbl
 
         sockets = sockd.keys()
         (reading, _, errors) = select.select(sockets, [], sockets, 0)
@@ -672,36 +779,6 @@ class UzblTabbed:
         return True
 
 
-    def main_fifo_hangup(self, fd, cb_condition):
-        '''Handle main fifo socket hangups.'''
-
-        # Close fd, re-open fifo_socket and watch.
-        self._setup_fifo_watcher(self.fifo_socket)
-
-        # And to kill any gobject event handlers calling this 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:
-                error("parse_command: invalid command %s" % ' '.join(cmd))
-                raise
-
-        return True
-
-
     def parse_command(self, cmd):
         '''Parse instructions from uzbl child processes.'''
 
@@ -775,14 +852,15 @@ class UzblTabbed:
                     new = ' '.join(cmd[2:])
                     setattr(uzbl, cmd[0], new)
                     if old != new:
-                       self.update_tablist()
+                        self.update_tablist()
+
                 else:
                     error("parse_command: no uzbl with pid %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)
@@ -798,7 +876,7 @@ class UzblTabbed:
 
                 else:
                     error("parse_command: preset %r does not exist." % path)
-            
+
             elif cmd[1] == "list":
                 uzbl = self.get_tab_by_pid(int(cmd[2]))
                 if uzbl:
@@ -821,7 +899,7 @@ class UzblTabbed:
 
         elif cmd[0] == "clean":
             self.clean_slate()
-            
+
         else:
             error("parse_command: unknown command %r" % ' '.join(cmd))
 
@@ -856,7 +934,7 @@ class UzblTabbed:
 
         if switch is None:
             switch = config['switch_to_new_tabs']
-        
+
         if not title:
             title = config['new_tab_title']
 
@@ -872,7 +950,7 @@ class UzblTabbed:
 
         # Add gobject timer to make sure the config is pushed when fifo socket
         # has been created.
-        timerid = gobject.timeout_add(100, uzbl.flush, "flush-initial-config")
+        timerid = timeout_add(100, uzbl.flush, "flush-initial-config")
         uzbl.timers['flush-initial-config'] = timerid
 
         self.update_tablist()
@@ -887,7 +965,7 @@ class UzblTabbed:
             if tab not in tabs: continue
             uzbl = self.tabs[tab]
             uzbl.send("exit")
-    
+
 
     def config_uzbl(self, uzbl):
         '''Send bind commands for tab new/close/next/prev to a uzbl
@@ -915,8 +993,6 @@ class UzblTabbed:
         bind(config['bind_goto_first'], 'goto 0')
         bind(config['bind_goto_last'], 'goto -1')
         bind(config['bind_clean_slate'], 'clean')
-
-        # session preset binds
         bind(config['bind_save_preset'], 'preset save %s')
         bind(config['bind_load_preset'], 'preset load %s')
         bind(config['bind_del_preset'], 'preset del %s')
@@ -926,7 +1002,7 @@ class UzblTabbed:
         # set(key, command back to fifo)
         if config['capture_new_windows']:
             set("new_window", r'new $8')
-        
+
         # Send config to uzbl instance via its socket file.
         uzbl.send("\n".join(binds+sets))
 
@@ -1016,7 +1092,7 @@ class UzblTabbed:
             uzbl = self.tabs[tab]
             for (timer, gid) in uzbl.timers.items():
                 error("tab_closed: removing timer %r" % timer)
-                gobject.source_remove(gid)
+                source_remove(gid)
                 del uzbl.timers[timer]
 
             if uzbl._socket:
@@ -1122,7 +1198,7 @@ class UzblTabbed:
 
         if session_file is None:
             session_file = config['session_file']
-        
+
         if session is None:
             tabs = self.tabs.keys()
             state = []
@@ -1144,7 +1220,7 @@ class UzblTabbed:
                 lines += ["%s\t%s" % (strip(uri), strip(title)),]
 
             raw = "\n".join(lines)
-        
+
         if not os.path.isfile(session_file):
             dirname = os.path.dirname(session_file)
             if not os.path.isdir(dirname):
@@ -1153,11 +1229,11 @@ class UzblTabbed:
         h = open(session_file, 'w')
         h.write(raw)
         h.close()
-        
+
 
     def load_session(self, session_file=None):
         '''Load a saved session from file.'''
-        
+
         default_path = False
         strip = str.strip
         json_session = config['json_session']
@@ -1178,8 +1254,8 @@ class UzblTabbed:
                   "Trying to load it as a non-json session file."\
                   % session_file)
                 json_session = False
-        
-        if json_session: 
+
+        if json_session:
             try:
                 session = json.loads(raw)
                 curtab, tabs = session['curtab'], session['tabs']
@@ -1198,7 +1274,7 @@ class UzblTabbed:
                 error("Warning: The non-json session file %r looks invalid."\
                   % session_file)
                 return None
-            
+
             try:
                 for line in lines:
                     if line.startswith("curtab"):
@@ -1213,22 +1289,19 @@ class UzblTabbed:
                 return None
 
             session = {'curtab': curtab, 'tabs': tabs}
-        
+
         # Now populate notebook with the loaded session.
         for (index, (uri, title)) in enumerate(tabs):
             self.new_tab(uri=uri, title=title, switch=(curtab==index))
 
-        # There may be other state information in the session dict of use to 
-        # other functions. Of course however the non-json session object is 
+        # There may be other state information in the session dict of use to
+        # other functions. Of course however the non-json session object is
         # just a dummy object of no use to no one.
         return session
 
 
     def quitrequest(self, *args):
         '''Called by delete-event signal to kill all uzbl instances.'''
-        
-        #TODO: Even though I send the kill request to all uzbl instances 
-        # i should add a gobject timeout to check they all die.
 
         self._killed = True
 
@@ -1238,33 +1311,37 @@ class UzblTabbed:
 
             else:
                 # Notebook has no pages so delete session file if it exists.
-                if os.path.isfile(session_file):
-                    os.remove(session_file)
-        
+                if os.path.isfile(config['session_file']):
+                    os.remove(config['session_file'])
+
         for (tab, uzbl) in self.tabs.items():
             uzbl.send("exit")
-    
-        
+
+        # Add a gobject timer to make sure the application force-quits after a
+        # reasonable period. Calling quit when all the tabs haven't had time to
+        # close should be a last resort.
+        timer = "force-quit"
+        timerid = timeout_add(5000, self.quit, timer)
+        self._timers[timer] = timerid
+
+
     def quit(self, *args):
-        '''Cleanup the application and quit. Called by delete-event signal.'''
-        
-        for (fifo_socket, (fd, watchers)) in self._fifos.items():
-            os.close(fd)
-            for (watcherid, gid) in watchers.items():
-                gobject.source_remove(gid)
-                del watchers[watcherid]
+        '''Cleanup and quit. Called by delete-event signal.'''
 
-            del self._fifos[fifo_socket]
+        # Close the fifo socket, remove any gobject io event handlers and
+        # delete socket.
+        self.unlink_fifo_socket()
 
+        # Remove all gobject timers that are still ticking.
         for (timerid, gid) in self._timers.items():
-            gobject.source_remove(gid)
+            source_remove(gid)
             del self._timers[timerid]
 
-        if os.path.exists(self.fifo_socket):
-            os.unlink(self.fifo_socket)
-            print "Unlinked %s" % self.fifo_socket
+        try:
+            gtk.main_quit()
 
-        gtk.main_quit()
+        except:
+            pass
 
 
 if __name__ == "__main__":
@@ -1272,6 +1349,20 @@ if __name__ == "__main__":
     # Read from the uzbl config into the global config dictionary.
     readconfig(uzbl_config, config)
 
+    # Build command line parser
+    parser = OptionParser()
+    parser.add_option('-n', '--no-session', dest='nosession',\
+      action='store_true', help="ignore session saving a loading.")
+    group = OptionGroup(parser, "Note", "All other command line arguments are "\
+      "interpreted as uris and loaded in new tabs.")
+    parser.add_option_group(group)
+
+    # Parse command line options
+    (options, uris) = parser.parse_args()
+
+    if options.nosession:
+        config['save_session'] = False
+
     if config['json_session']:
         try:
             import simplejson as json
@@ -1283,6 +1374,11 @@ if __name__ == "__main__":
             config['json_session'] = False
 
     uzbl = UzblTabbed()
-    uzbl.run()
 
+    # All extra arguments given to uzbl_tabbed.py are interpreted as
+    # web-locations to opened in new tabs.
+    lasturi = len(uris)-1
+    for (index,uri) in enumerate(uris):
+        uzbl.new_tab(uri, switch=(index==lasturi))
 
+    uzbl.run()