root/foreignclient.py

Revision 35, 9.6 kB (checked in by davux, 12 months ago)

Add a TODO about a possible performance issue related to how we use threads.

Line 
1# -*- coding: utf-8 -*-
2# vi: sts=4 et sw=4
3
4from common import debug
5from xmpp.client import Client
6from xmpp.protocol import JID, Presence, NS_PRESENCE, NS_IQ, NodeProcessed
7import threading
8try:
9    from xmpp.jep0106 import JIDEncode
10except ImportError:
11    debug("Using embedded jep0106 module")   # Old versions of xmpppy
12    from jep0106 import JIDEncode            # don't provide this module
13
14class ConnectionError(Exception): pass
15
16class ForeignClient(threading.Thread):
17    '''A connection to a Jabber account, as a client.'''
18
19    def __init__(self, jid, password, resource, registerer, pizzjaInstance):
20        '''Constructor. Initiates a new account from the given information.
21        '''
22        threading.Thread.__init__(self, name=registerer)
23        debug('Initializing client for %s/%s, registered by %s' \
24              % (jid, resource, registerer))
25        self.jid = jid
26        self.password = password
27        self.resource = resource
28        self.registerer = registerer
29        self.component = pizzjaInstance
30        self.cnx = Client(JID(self.jid).getDomain(), debug=['socket'])
31        self.isConnected = self.cnx.isConnected
32        self.presence = Presence()
33        self.bye = False
34        self.controllers = {} # {'Home/Amarok': -42, 'Home/sendxmpp': 1}
35
36    def run(self):
37        '''This method is run when self.start() is called. It overrides the
38        default Thread.run() method. It processes incoming stanzas in a loop
39        until disconnection.
40        '''
41        debug('Client starts looping.')
42        while not self.bye:
43            self.cnx.Process(10)
44        self.onDisconnect()
45        debug('Disconnected. Client stopped looping.')
46
47    def disconnect(self):
48        self.bye = True
49
50    def connect(self):
51        '''Try to connect and auth, then pass every incoming stanza to the
52           main component. If the connection fails, simulate onDisconnect()
53           event, which will have the component clean us up.
54        '''
55        #TODO: Don't crash because of invalid accounts, cf. ticket #5.
56        #TODO: In the future, we might want to call start() earlier for
57        #      performance reasons: until we call start(), the process is
58        #      blocking.
59        debug('Registering disconnect handler')
60        self.cnx.RegisterDisconnectHandler(self.onDisconnect)
61        if not self.cnx.connect():
62            debug('Could not connect.')
63            self.onDisconnect()
64            raise ConnectionError('Unable to connect')
65        debug('Connection OK.')
66        if not self.cnx.auth(self.jid, self.password, self.resource):
67            debug('Could not auth. Disconnecting.')
68            self.cnx.disconnect()
69            raise ConnectionError('Unable to authenticate as %s' % (self.jid))
70        debug('Authentified OK. Registering handlers.')
71        self.cnx.RegisterHandler(NS_IQ, self.iqReceived)
72        self.cnx.RegisterDefaultHandler(self.stanzaReceived)
73        debug('Start listening (in a separate thread)')
74        self.start()
75        debug('Thread started. Life is going on.')
76
77    def iqReceived(self, cnx, iq):
78        '''When an IQ is received, decide what resource it should be
79           forwarded to, then pass it to the component.
80           - get/set: pick the most appropriate recipient. Reply with error
81             service-unavailable if no appropriate recipient is found.
82           - result/error: retrieve who sent the initial IQ get/set (ignore
83             if not found).
84        '''
85        debug('An IQ was received.')
86        resource = None
87        if iq.getType() in ['get', 'set']:
88            debug('IQ get or set, picking a recipient...')
89            resource = self.pickIqRecipient(iq)
90            debug('... %s will do the job.' % resource)
91            if resource is None:
92                debug('Apologizing, nobody can handle the IQ.')
93                cnx.send(Error(iq, 'service-unavailable'))
94        elif iq.getType() in ['result', 'error']:
95            debug('IQ result or error, we should retrieve the initial sender.')
96            try:
97                resource = self.sentIQs.pop(iq.getId())
98                debug('Found, the sender was %s.')
99            except KeyError:
100                debug('Unknown IQ response with id %s. Ignoring.' % iq.getId())
101        if resource is not None:
102            jid = '%s/%s' % (self.registerer, resource)
103            debug('Now we have a recipient (%s), forwarding IQ.' % (jid))
104            self.component.forwardStanzaIn(iq, [jid])
105        debug('All right, IQ forwarded.')
106        raise NodeProcessed
107
108    def stanzaReceived(self, cnx, stz):
109        '''When a stanza (other than IQ) is received, forward it to all the
110           controllers.'''
111        debug('A presence or message was received, forwarding in to everyone.')
112        self.component.forwardStanzaIn(stz, self.getControllers())
113
114    def send(self, stz):
115        '''Send the specified stanza on the client's connection.
116           For IQs get/set, remember which resource sent it.
117           When sending presence, remember it, and have the component reflect
118           it to the controlling resources.
119        '''
120        debug('About to send a stanza as a client.')
121        id = self.cnx.send(stz)
122        debug('Stanza sent, id was %s' % id)
123        if stz.getName() == NS_IQ and stz.getType() in ['get', 'set']:
124            debug('It was an IQ get or set, recording id for the answer.')
125            self.sentIQs[id] = stz.getFrom().getResource()
126        elif stz.getName() == NS_PRESENCE and \
127           stz.getType() not in ['subscribe', 'subscribed', 'unsubscribe',
128                                 'unsubscribed', 'probe', 'error']:
129            debug('It was an presence change, reflecting it locally.')
130            self.presence = stz
131            self.component.reflectClientPresence(self)
132        return id
133
134    def onDisconnect(self):
135        '''When disconnected, inform the component. Also Update self.presence
136           so that the component can reflect it to all controllers.
137        '''
138        debug('Disconnect handler called, setting client unavailable presence',
139              'and informing the component.')
140        self.presence = Presence(typ='unavailable')
141        self.component.clientDisconnected(self.registerer, self.resource)
142
143    def hasController(self, resource):
144        '''Tell whether the given resource already showed up as a controller.'''
145        return (resource in self.controllers)
146
147    def getControllers(self):
148        '''Return a list with the full JID of current controllers.'''
149        return ['%s/%s' % (self.registerer, resource) \
150                for resource in self.controllers]
151
152    def getMostAvailableController(self):
153        '''Return the 'most available' controller. Currently, this means the
154           resource with highest priority (the last one found if several of
155           them have the same priority).
156        '''
157        resources = self.controllers.keys()
158        debug('We are wondering who is the most available controller among %s' \
159              % resources)
160        current_best = resources[0]
161        for resource in resources:
162            debug('Considering %s (%s)' % (resource, self.controllers[resource]))
163            if self.controllers[resource] >= self.controllers[current_best]:
164                current_best = resource
165        debug('And the winner is... %s.' % current_best)
166        return current_best
167
168    def resourceAvailable(self, resource, priority):
169        '''A controller sent an availability information. Create or update the
170           association between the resource and the priority.
171           If it is the first controller, connect.
172        '''
173        debug('Resource %s sent availability, priority=%s' % (resource, priority))
174        if self.controllers == {}:
175            debug('First available controller, congratulations. Connecting.')
176            self.connect()
177        if priority is None:
178            debug('No priority, assuming 0.')
179            priority = 0
180        debug('Recording controller %s with priority %s' % (resource, priority))
181        self.controllers[resource] = priority
182
183    def resourceUnavailable(self, resource):
184        '''A controller sent unavailability information. Delete it from the
185           list of controllers.
186           If is was the last controller, disconnect.
187        '''
188        debug('Resource %s sent unavailable presence.' % (resource))
189        if resource in self.controllers:
190            debug('Removing from controllers.')
191            del self.controllers[resource]
192            if self.controllers == {}:
193                debug('It was the last controller, disconnecting.')
194                self.bye = True
195                debug('Disconnected.')
196        else:
197            debug("We didn't know %s/%s was online anyway." \
198                  % (self.registerer, resource))
199
200    def updatePriority(self):
201        '''Change priority according to controllers' ability to receive
202           messages.
203           THINKME: What criteria should be used? Currently, the highest
204                    priority is reflected.'''
205        debug('We should for some reason re-evaluate our priorities ;)')
206        new_prio = max(self.controllers.values())
207        debug('New priority is %s' % (new_prio))
208        if new_prio != self.presence.getPriority():
209            debug('Change needed, sending new presence.')
210            self.presence.setPriority(new_prio)
211            self.send(self.presence)
212
213    def pickIqRecipient(self, iq):
214        '''Return the name of the resource the incoming IQ should be
215           forwarded to, or None is no acceptable candidate could be
216           determined.
217           Currently, the most available resource is chosen.'''
218        return self.getMostAvailableController()
219
220    def getPresence(self):
221        return self.presence
Note: See TracBrowser for help on using the browser.