Package common :: Package zeroconf :: Module zeroconf_avahi
[hide private]
[frames] | no frames]

Source Code for Module common.zeroconf.zeroconf_avahi

  1  ##      common/zeroconf/zeroconf.py 
  2  ## 
  3  ## Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de> 
  4  ## 
  5  ## This file is part of Gajim. 
  6  ## 
  7  ## Gajim is free software; you can redistribute it and/or modify 
  8  ## it under the terms of the GNU General Public License as published 
  9  ## by the Free Software Foundation; version 3 only. 
 10  ## 
 11  ## Gajim is distributed in the hope that it will be useful, 
 12  ## but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  ## GNU General Public License for more details. 
 15  ## 
 16  ## You should have received a copy of the GNU General Public License 
 17  ## along with Gajim.  If not, see <http://www.gnu.org/licenses/>. 
 18  ## 
 19   
 20  import logging 
 21  log = logging.getLogger('gajim.c.z.zeroconf_avahi') 
 22   
 23  try: 
 24      import dbus.glib 
 25  except ImportError, e: 
 26      pass 
 27   
 28  from common.zeroconf.zeroconf import C_BARE_NAME, C_INTERFACE, C_PROTOCOL, C_DOMAIN 
 29   
30 -class Zeroconf:
31 - def __init__(self, new_serviceCB, remove_serviceCB, name_conflictCB, 32 disconnected_CB, error_CB, name, host, port):
33 self.avahi = None 34 self.domain = None # specific domain to browse 35 self.stype = '_presence._tcp' 36 self.port = port # listening port that gets announced 37 self.username = name 38 self.host = host 39 self.txt = {} # service data 40 41 #XXX these CBs should be set to None when we destroy the object 42 # (go offline), because they create a circular reference 43 self.new_serviceCB = new_serviceCB 44 self.remove_serviceCB = remove_serviceCB 45 self.name_conflictCB = name_conflictCB 46 self.disconnected_CB = disconnected_CB 47 self.error_CB = error_CB 48 49 self.service_browser = None 50 self.domain_browser = None 51 self.bus = None 52 self.server = None 53 self.contacts = {} # all current local contacts with data 54 self.entrygroup = None 55 self.connected = False 56 self.announced = False 57 self.invalid_self_contact = {}
58 59 60 ## handlers for dbus callbacks
61 - def entrygroup_commit_error_CB(self, err):
62 # left blank for possible later usage 63 pass
64
65 - def error_callback1(self, err):
66 log.debug('Error while resolving: ' + str(err))
67
68 - def error_callback(self, err):
69 log.debug(str(err)) 70 # timeouts are non-critical 71 if str(err) != 'Timeout reached': 72 self.disconnect() 73 self.disconnected_CB()
74
75 - def new_service_callback(self, interface, protocol, name, stype, domain, 76 flags):
77 log.debug('Found service %s in domain %s on %i.%i.' % (name, domain, 78 interface, protocol)) 79 if not self.connected: 80 return 81 82 # synchronous resolving 83 self.server.ResolveService( int(interface), int(protocol), name, stype, 84 domain, self.avahi.PROTO_UNSPEC, dbus.UInt32(0), 85 reply_handler=self.service_resolved_callback, 86 error_handler=self.error_callback1)
87
88 - def remove_service_callback(self, interface, protocol, name, stype, domain, 89 flags):
90 log.debug('Service %s in domain %s on %i.%i disappeared.' % (name, 91 domain, interface, protocol)) 92 if not self.connected: 93 return 94 if name != self.name: 95 for key in self.contacts.keys(): 96 if self.contacts[key][C_BARE_NAME] == name: 97 del self.contacts[key] 98 self.remove_serviceCB(key) 99 return
100
101 - def new_service_type(self, interface, protocol, stype, domain, flags):
102 # Are we already browsing this domain for this type? 103 if self.service_browser: 104 return 105 106 object_path = self.server.ServiceBrowserNew(interface, protocol, \ 107 stype, domain, dbus.UInt32(0)) 108 109 self.service_browser = dbus.Interface(self.bus.get_object( 110 self.avahi.DBUS_NAME, object_path), 111 self.avahi.DBUS_INTERFACE_SERVICE_BROWSER) 112 self.service_browser.connect_to_signal('ItemNew', 113 self.new_service_callback) 114 self.service_browser.connect_to_signal('ItemRemove', 115 self.remove_service_callback) 116 self.service_browser.connect_to_signal('Failure', self.error_callback)
117
118 - def new_domain_callback(self, interface, protocol, domain, flags):
119 if domain != 'local': 120 self.browse_domain(interface, protocol, domain)
121
122 - def txt_array_to_dict(self, txt_array):
123 txt_dict = {} 124 for els in txt_array: 125 key, val = '', None 126 for c in els: 127 c = chr(c) 128 if val is None: 129 if c == '=': 130 val = '' 131 else: 132 key += c 133 else: 134 val += c 135 if val is None: # missing '=' 136 val = '' 137 txt_dict[key] = val.decode('utf-8', 'ignore') 138 return txt_dict
139
140 - def service_resolved_callback(self, interface, protocol, name, stype, domain, 141 host, aprotocol, address, port, txt, flags):
142 log.debug('Service data for service %s in domain %s on %i.%i:' 143 % (name, domain, interface, protocol)) 144 log.debug('Host %s (%s), port %i, TXT data: %s' % (host, address, 145 port, self.txt_array_to_dict(txt))) 146 if not self.connected: 147 return 148 bare_name = name 149 if name.find('@') == -1: 150 name = name + '@' + name 151 152 # we don't want to see ourselves in the list 153 if name != self.name: 154 self.contacts[name] = (name, domain, interface, protocol, host, 155 address, port, bare_name, txt) 156 self.new_serviceCB(name) 157 else: 158 # remember data 159 # In case this is not our own record but of another 160 # gajim instance on the same machine, 161 # it will be used when we get a new name. 162 self.invalid_self_contact[name] = (name, domain, interface, protocol, 163 host, address, port, bare_name, txt)
164 165 166 # different handler when resolving all contacts
167 - def service_resolved_all_callback(self, interface, protocol, name, stype, 168 domain, host, aprotocol, address, port, txt, flags):
169 if not self.connected: 170 return 171 bare_name = name 172 if name.find('@') == -1: 173 name = name + '@' + name 174 self.contacts[name] = (name, domain, interface, protocol, host, address, 175 port, bare_name, txt)
176
177 - def service_added_callback(self):
178 log.debug('Service successfully added')
179
181 log.debug('Service successfully committed')
182
183 - def service_updated_callback(self):
184 log.debug('Service successfully updated')
185
186 - def service_add_fail_callback(self, err):
187 log.debug('Error while adding service. %s' % str(err)) 188 if 'Local name collision' in str(err): 189 alternative_name = self.server.GetAlternativeServiceName(self.username) 190 self.name_conflictCB(alternative_name) 191 return 192 self.error_CB(_('Error while adding service. %s') % str(err)) 193 self.disconnect()
194
195 - def server_state_changed_callback(self, state, error):
196 log.debug('server state changed to %s' % state) 197 if state == self.avahi.SERVER_RUNNING: 198 self.create_service() 199 elif state in (self.avahi.SERVER_COLLISION, 200 self.avahi.SERVER_REGISTERING): 201 self.disconnect() 202 self.entrygroup.Reset() 203 elif state == self.avahi.CLIENT_FAILURE: 204 # does it ever go here? 205 log.debug('CLIENT FAILURE')
206
207 - def entrygroup_state_changed_callback(self, state, error):
208 # the name is already present, so recreate 209 if state == self.avahi.ENTRY_GROUP_COLLISION: 210 log.debug('zeroconf.py: local name collision') 211 self.service_add_fail_callback('Local name collision') 212 elif state == self.avahi.ENTRY_GROUP_FAILURE: 213 self.disconnect() 214 self.entrygroup.Reset() 215 log.debug('zeroconf.py: ENTRY_GROUP_FAILURE reached(that' 216 ' should not happen)')
217 218 # make zeroconf-valid names
219 - def replace_show(self, show):
220 if show in ['chat', 'online', '']: 221 return 'avail' 222 elif show == 'xa': 223 return 'away' 224 return show
225
226 - def avahi_txt(self):
227 utf8_dict = {} 228 for key in self.txt: 229 val = self.txt[key] 230 if isinstance(val, unicode): 231 utf8_dict[key] = val.encode('utf-8') 232 else: 233 utf8_dict[key] = val 234 return self.avahi.dict_to_txt_array(utf8_dict)
235
236 - def create_service(self):
237 try: 238 if not self.entrygroup: 239 # create an EntryGroup for publishing 240 self.entrygroup = dbus.Interface(self.bus.get_object( 241 self.avahi.DBUS_NAME, self.server.EntryGroupNew()), 242 self.avahi.DBUS_INTERFACE_ENTRY_GROUP) 243 self.entrygroup.connect_to_signal('StateChanged', 244 self.entrygroup_state_changed_callback) 245 246 txt = {} 247 248 # remove empty keys 249 for key, val in self.txt.iteritems(): 250 if val: 251 txt[key] = val 252 253 txt['port.p2pj'] = self.port 254 txt['version'] = 1 255 txt['txtvers'] = 1 256 257 # replace gajim's show messages with compatible ones 258 if 'status' in self.txt: 259 txt['status'] = self.replace_show(self.txt['status']) 260 else: 261 txt['status'] = 'avail' 262 263 self.txt = txt 264 log.debug('Publishing service %s of type %s' % (self.name, 265 self.stype)) 266 self.entrygroup.AddService(self.avahi.IF_UNSPEC, 267 self.avahi.PROTO_UNSPEC, dbus.UInt32(0), self.name, self.stype, '', 268 '', dbus.UInt16(self.port), self.avahi_txt(), 269 reply_handler=self.service_added_callback, 270 error_handler=self.service_add_fail_callback) 271 272 self.entrygroup.Commit(reply_handler=self.service_committed_callback, 273 error_handler=self.entrygroup_commit_error_CB) 274 275 return True 276 277 except dbus.DBusException, e: 278 log.debug(str(e)) 279 return False
280
281 - def announce(self):
282 if not self.connected: 283 return False 284 285 state = self.server.GetState() 286 if state == self.avahi.SERVER_RUNNING: 287 self.create_service() 288 self.announced = True 289 return True
290
291 - def remove_announce(self):
292 if self.announced == False: 293 return False 294 try: 295 if self.entrygroup.GetState() != self.avahi.ENTRY_GROUP_FAILURE: 296 self.entrygroup.Reset() 297 self.entrygroup.Free() 298 # .Free() has mem leaks 299 self.entrygroup._obj._bus = None 300 self.entrygroup._obj = None 301 self.entrygroup = None 302 self.announced = False 303 304 return True 305 else: 306 return False 307 except dbus.DBusException: 308 log.debug("Can't remove service. That should not happen")
309
310 - def browse_domain(self, interface, protocol, domain):
311 self.new_service_type(interface, protocol, self.stype, domain, '')
312
313 - def avahi_dbus_connect_cb(self, a, connect, disconnect):
314 if connect != "": 315 log.debug('Lost connection to avahi-daemon') 316 self.disconnect() 317 if self.disconnected_CB: 318 self.disconnected_CB() 319 else: 320 log.debug('We are connected to avahi-daemon')
321 322 # connect to dbus
323 - def connect_dbus(self):
324 try: 325 import dbus 326 except ImportError: 327 log.debug('Error: python-dbus needs to be installed. No ' 328 'zeroconf support.') 329 return False 330 if self.bus: 331 return True 332 try: 333 self.bus = dbus.SystemBus() 334 self.bus.add_signal_receiver(self.avahi_dbus_connect_cb, 335 'NameOwnerChanged', 'org.freedesktop.DBus', 336 arg0='org.freedesktop.Avahi') 337 except Exception, e: 338 # System bus is not present 339 self.bus = None 340 log.debug(str(e)) 341 return False 342 else: 343 return True
344 345 # connect to avahi
346 - def connect_avahi(self):
347 if not self.connect_dbus(): 348 return False 349 try: 350 import avahi 351 self.avahi = avahi 352 except ImportError: 353 log.debug('Error: python-avahi needs to be installed. No ' 354 'zeroconf support.') 355 return False 356 357 if self.server: 358 return True 359 try: 360 self.server = dbus.Interface(self.bus.get_object(self.avahi.DBUS_NAME, 361 self.avahi.DBUS_PATH_SERVER), self.avahi.DBUS_INTERFACE_SERVER) 362 self.server.connect_to_signal('StateChanged', 363 self.server_state_changed_callback) 364 except Exception, e: 365 # Avahi service is not present 366 self.server = None 367 log.debug(str(e)) 368 return False 369 else: 370 return True
371
372 - def connect(self):
373 self.name = self.username + '@' + self.host # service name 374 if not self.connect_avahi(): 375 return False 376 377 self.connected = True 378 # start browsing 379 if self.domain is None: 380 # Explicitly browse .local 381 self.browse_domain(self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, 382 'local') 383 384 # Browse for other browsable domains 385 self.domain_browser = dbus.Interface(self.bus.get_object( 386 self.avahi.DBUS_NAME, self.server.DomainBrowserNew( 387 self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, '', 388 self.avahi.DOMAIN_BROWSER_BROWSE, dbus.UInt32(0))), 389 self.avahi.DBUS_INTERFACE_DOMAIN_BROWSER) 390 self.domain_browser.connect_to_signal('ItemNew', 391 self.new_domain_callback) 392 self.domain_browser.connect_to_signal('Failure', self.error_callback) 393 else: 394 self.browse_domain(self.avahi.IF_UNSPEC, self.avahi.PROTO_UNSPEC, 395 self.domain) 396 397 return True
398
399 - def disconnect(self):
400 if self.connected: 401 self.connected = False 402 if self.service_browser: 403 self.service_browser.Free() 404 self.service_browser._obj._bus = None 405 self.service_browser._obj = None 406 if self.domain_browser: 407 self.domain_browser.Free() 408 self.domain_browser._obj._bus = None 409 self.domain_browser._obj = None 410 self.remove_announce() 411 self.server._obj._bus = None 412 self.server._obj = None 413 self.server = None 414 self.service_browser = None 415 self.domain_browser = None
416 417 # refresh txt data of all contacts manually (no callback available)
418 - def resolve_all(self):
419 if not self.connected: 420 return 421 for val in self.contacts.values(): 422 self.server.ResolveService(int(val[C_INTERFACE]), int(val[C_PROTOCOL]), 423 val[C_BARE_NAME], self.stype, val[C_DOMAIN], 424 self.avahi.PROTO_UNSPEC, dbus.UInt32(0), 425 reply_handler=self.service_resolved_all_callback, 426 error_handler=self.error_callback)
427
428 - def get_contacts(self):
429 return self.contacts
430
431 - def get_contact(self, jid):
432 if not jid in self.contacts: 433 return None 434 return self.contacts[jid]
435
436 - def update_txt(self, show = None):
437 if show: 438 self.txt['status'] = self.replace_show(show) 439 440 txt = self.avahi_txt() 441 if self.connected and self.entrygroup: 442 self.entrygroup.UpdateServiceTxt(self.avahi.IF_UNSPEC, 443 self.avahi.PROTO_UNSPEC, dbus.UInt32(0), self.name, self.stype, '', 444 txt, reply_handler=self.service_updated_callback, 445 error_handler=self.error_callback) 446 return True 447 else: 448 return False
449 450 451 # END Zeroconf 452