[telepathy-ashes/master] Refactoring to enable reconnecting on disconnect.

David Laban david.laban at collabora.co.uk
Thu Oct 22 10:04:22 PDT 2009


All connection objects now take a parent argument to their constructor,
which gets connection_disconnected() called back on it.

This pattern of keeping a ref to your parent is used by channels too.
I also use it as a mechanism for passing config variables around.

The refs should probably be weakrefs, thinking about it. Sorting out
garbage collection and suchlike is next on my todo.
---
 ashes/tools/bases.py       |   23 +++----
 ashes/tools/dispatchers.py |    9 ++-
 ashes/tools/echo_bot.py    |  159 +++++++++++++++++++++++++++++++-------------
 3 files changed, 127 insertions(+), 64 deletions(-)

diff --git a/ashes/tools/bases.py b/ashes/tools/bases.py
index 5b496c3..e291e34 100644
--- a/ashes/tools/bases.py
+++ b/ashes/tools/bases.py
@@ -97,20 +97,14 @@ class ConnectionListener(ObjectListener):
     This is what finish_setup is for.
     """
     CONNECTION = telepathy.interfaces.CONNECTION
-    def __init__(self, connection, contact_regexp='.*'):
-        """
-        contact_regexp is used in derived classes to selectively echo only specific users.
-        """
-        self.init(connection, contact_regexp)
-
-    def init(self, connection, contact_regexp='.*'):
+    def __init__(self, parent, connection):
         print "Listening to connection"
         ObjectListener.__init__(self, connection)
         # I could use self.dbus_object everywhere, but
-        # self.connection is clearer.
+        # self.connection is clearer, so I will duplicate it here.
         self.connection = connection
-        self.contact_regexp = contact_regexp
-        self.channel_extra_args = [contact_regexp]
+        self.parent = parent
+        self.config = parent.config
         self.connection.Connect()
         connection.call_when_ready(self.finish_setup)
 
@@ -134,11 +128,10 @@ class ConnectionListener(ObjectListener):
                     self.connect_to_signal(iface_name, signal_name)
 
     def StatusChanged(self, status, reason=0):
-        if status == 2: #Disconnected
-            print "Disconnected. Exiting."
-            gobject.idle_add(quit)
+        if status == 2: # Disconnected.
+            self.parent.connection_disconnected(self, status, reason)
 
-    #FIXME: this should really be auto-generated from the spec globally.
+    # FIXME: this should really be auto-generated from the spec globally.
     _signal_names = {
         'org.freedesktop.Telepathy.Connection':
             ['SelfHandleChanged', 'NewChannel', 'ConnectionError', 'StatusChanged'],
@@ -232,6 +225,8 @@ class ChannelListener(ObjectListener):
         ObjectListener.__init__(self, channel)
         self.connection = connection
         self.channel = channel
+        self.parent = connection
+        self.config = connection.config
         self.properties = properties
         channel_type = properties['org.freedesktop.Telepathy.Channel.ChannelType']
         self._register_callbacks()
diff --git a/ashes/tools/dispatchers.py b/ashes/tools/dispatchers.py
index e9878bb..ce7a8dc 100644
--- a/ashes/tools/dispatchers.py
+++ b/ashes/tools/dispatchers.py
@@ -22,6 +22,8 @@ class ChannelDispatcher(ConnectionListener):
 
     Add a mixin with a list/tuple called __handler_classes which contains
     all of the channel handler classes you want to include.
+
+    Note that this could be implemented using Client.Approver and Client.Handler
     """
     REQUESTS = telepathy.interfaces.CONNECTION_INTERFACE_REQUESTS
     CAPS = telepathy.interfaces.CONNECTION_INTERFACE_CAPABILITIES
@@ -137,16 +139,17 @@ class ChannelDispatcher(ConnectionListener):
         id = properties['org.freedesktop.Telepathy.Channel.TargetID']
         # FIXME. This optimisation is probably not very general.
         # Don't be putting it in any libraries.
+        contact_regexp = self.config.get("contact_regexp", ".*")
         if handle_type == telepathy.HANDLE_TYPE_CONTACT:
-            if re.match(self.contact_regexp, id):
+            if re.match(contact_regexp, id):
                 #assert not requested, "We shouldn't be creating any new channels."
                 if requested:
                     print "Outgoing channel",
                 else:
                     print "Incoming channel",
-                print id, 'matching', self.contact_regexp
+                print id, 'matching', contact_regexp
             else:
-                print id, 'does not match', self.contact_regexp
+                print id, 'does not match', contact_regexp
                 return
         if channel in self.channels:
             print "Channel already handled:", channel.object_path
diff --git a/ashes/tools/echo_bot.py b/ashes/tools/echo_bot.py
index 5594714..83810d7 100644
--- a/ashes/tools/echo_bot.py
+++ b/ashes/tools/echo_bot.py
@@ -14,19 +14,15 @@ from helper_functions import (get_connections, get_property,
 from account import connection_from_file
 
 from bases import ObjectListener, ConnectionListener, ChannelListener
-from dispatchers import ChannelDispatcher, connections
+from dispatchers import ChannelDispatcher#, connections
 from media_echoer import MediaChannelEchoer, IceEchoer
 from presence import PresenceEchoer, Onlineifier
 from groups import ContactSubscriber
 from text import TextChannelEchoer
 from commands import CommandExecutor
 from file_transfer import FileTransferEchoer
-#TODO: wrap everything to 80 chars so that I can hack on my eeepc.
 
 
-    
-
-#@shiny.debug_class
 class EchoBot(  ContactSubscriber,
                 ChannelDispatcher,
                 Onlineifier,
@@ -39,62 +35,131 @@ class EchoBot(  ContactSubscriber,
     __channel_handler_classes = [MediaChannelEchoer,
         CommandExecutor, FileTransferEchoer]
 
-
-
-def run_echo_service(conn, contact_regexp):
-    """
-    Runs the echo service unless it's already running.
+class EchoBotRunner(object):
     """
-    if conn in connections:
-        #note: requires patch to dbus-python which implements __eq__
-        print conn.service_name, 'already handled'
-    else:
-        print green('Running Echo Service on:'), conn.service_name
-        bot = EchoBot(conn, contact_regexp)
-        assert bot == conn # so that 'if conn in connections:' works.
-        connections.append(bot)
-    return connections
-
+    Runs the echo service, and restarts it if it dies. This is in theory capable
+    of running multiple echo services in the same process, but I prefer to run
+    things separately.
 
-def main(connection_regexp='.*echo123.*', 
-        contact_regexp='.*',
-        account_file=''):
-    """
-    This program will look on the bus for a connection object which matches 
-    connection_regexp.
-    TODO: add an option for data rates, to select codecs.
+    In theory, this could be done using AccountManager, but for some reason,
+    I decided not to, and to use .account files instead.
     """
-    mainloop = gobject.MainLoop(is_running=True)
-    dmainloop = dbus.mainloop.glib.DBusGMainLoop()
-    dbus.set_default_main_loop(dmainloop)
-    regexp = re.compile(connection_regexp)
-
-    if account_file:
+    # A list of all connections, including ones we don't own.
+    connections = []
+
+    def __init__(self, **config):
+        """
+        Config is an arbitrary dict of configuration options that is passed
+        down to connection handlers and channel handlers. This is so that you
+        can extend connection or channel subclasses, and not have to find your
+        own mechanism for configuring them.
+        """
+        self.owned_connections = [] # Connections we own.
+        self.account_files = [] # Account files used to [re]create connections.
+        self.config = config
+        self.mainloop = gobject.MainLoop(is_running=True)
+        dmainloop = dbus.mainloop.glib.DBusGMainLoop()
+        dbus.set_default_main_loop(dmainloop)
+
+    def echo_from_file(self, account_file):
+        """Runs the echo bot on a connection created by reading account_file."""
         print 'Creating connection'
         connection = connection_from_file(account_file)
-        run_echo_service(connection, contact_regexp)
-    else:
+        bot = self.run_echo_service(connection)
+        self.owned_connections.append(bot)
+        self.account_files.append(account_file)
+
+    def echo_from_bus(self, connection_regexp):
         connections_ = get_connections()
 
         for connection in connections_:
             if re.match(connection_regexp, connection.service_name):
-                run_echo_service(connection, contact_regexp)
+                self.run_echo_service(connection)
             else:
                 print connection.service_name, "does not match", connection_regexp
 
-    while mainloop.is_running():
-        print 'running'
+    def run_echo_service(self, conn):
+        """
+        Runs the echo service unless it's already running.
+        """
+        if conn in self.connections:
+            print conn.service_name, 'already handled'
+        else:
+            print green('Running Echo Service on:'), conn.service_name
+            bot = EchoBot(self, conn)
+            assert bot == conn # so that 'if conn in connections:' works.
+            self.connections.append(bot)
+        return bot
+
+    def get_account_file_for(self, conn):
+        index = self.owned_connections.index(conn)
+        return self.account_files[index]
+
+    def remove_connection(self, conn, prevent_quit=False):
+        """
+        Removes the connection from all places we reference it.
+        """
         try:
-            mainloop.run()
-        except KeyboardInterrupt:
+            index = self.owned_connections.index(conn)
+            self.owned_connections.pop(index)
+            self.account_files.pop(index)
+        except ValueError:
+            pass # We don't own this connection.
+        self.connections.remove(conn)
+        if len(self.connections) == 0 and not prevent_quit:
+            print "No longer handling any connections. Exiting."
             gobject.idle_add(quit)
-            if account_file:
-                # If we set it up we should probably take it down.
-                assert connections
-                assert len(connections) == 1
-                connections[0][telepathy.CONN_INTERFACE].Disconnect()
-                account_file = None
-        print 'done\n\n'
+
+    def reconnect(self, conn):
+        """
+        Only works if we were the ones who started the service in the first
+        place.
+        """
+        account_file = self.get_account_file_for(conn)
+        self.remove_connection(conn, prevent_quit=True)
+        self.echo_from_file(account_file)
+
+    def connection_disconnected(self, conn, status, reason):
+        """
+        This is called whenever a connection gets disconnected.
+        """
+        if reason == 2: # network error.
+            print "Network error. Re-connecting"
+            self.reconnect(conn)
+        elif reason == 5: # name in use.
+            print "The service is being run elsewhere. Removing."
+            self.remove_connection(conn)
+        else:
+            print "Disconnected because %s. Removing." % reason
+            self.remove_connection(conn)
+
+    def run(self):
+        while self.mainloop.is_running():
+            print 'running'
+            try:
+                self.mainloop.run()
+            except KeyboardInterrupt:
+                gobject.idle_add(quit, 1)
+                print "Disconnecting %s connections." % len(self.owned_connections)
+                for connection in self.owned_connections:
+                    connection[telepathy.CONN_INTERFACE].Disconnect()
+            print 'done\n\n'
+
+
+def main(connection_regexp='.*echo123.*', 
+        contact_regexp='.*',
+        account_file=''):
+    """
+    If account file is provided, connects to the specified account, and runs
+    the echo service on it. Otherwise looks on the bus for connections matching
+    connection_regexp.
+    """
+    runner = EchoBotRunner(contact_regexp=contact_regexp)
+    if account_file:
+        runner.echo_from_file(account_file)
+    else:
+        runner.echo_from_bus(connection_regexp)
+    runner.run()
 
 
 
-- 
1.5.6.5




More information about the telepathy-commits mailing list