Merge lp:~cmiller/desktopcouch/replication-daemon into lp:desktopcouch

Proposed by Chad Miller
Status: Merged
Approved by: Eric Casteleijn
Approved revision: 51
Merged at revision: not available
Proposed branch: lp:~cmiller/desktopcouch/replication-daemon
Merge into: lp:desktopcouch
Diff against target: None lines
To merge this branch: bzr merge lp:~cmiller/desktopcouch/replication-daemon
Reviewer Review Type Date Requested Status
Stuart Langridge (community) Approve
Eric Casteleijn (community) Approve
Review via email: mp+10740@code.launchpad.net

This proposal supersedes a proposal from 2009-08-26.

Commit message

Add local-db replication daemon, and fix a few typos.

Since couchdb auth is not yet supported, stop the pairing tool from giving the user that local replication is supported. Once couchdb is not a security risk and we can bind it to an external interface, then replication will work for everyone.

To post a comment you must log in.
Revision history for this message
Chad Miller (cmiller) wrote : Posted in a previous version of this proposal

Buggy first try at replication.

Revision history for this message
Stuart Langridge (sil) wrote : Posted in a previous version of this proposal

We decided to not use relative imports, I think.

I reckon that this code should actually be integrated into startup before it's approved?

Revision history for this message
Stuart Langridge (sil) wrote : Posted in a previous version of this proposal

Also, Xget_replication_list ?

Revision history for this message
Eric Casteleijn (thisfred) wrote : Posted in a previous version of this proposal

Some long lines in bin/desktopcouch-pair

I'd put the recordtype as a constant at the top of the file:

PAIRED_SERVER_TYPE = \
    "http://www.freedesktop.org/wiki/Specifications/desktopcouch/paired_server"

couchdb_io has 4 unused imports:

import urllib2
import json
import tempfile
import os

and relative imports which should be abolutized.

also long lines which I would solve:

base_url = "http://www.freedesktop.org/wiki/Specifications/desktopcouch/"
PAIRED_SERVER_RECORD_TYPE = base_url + "paired_server"
MY_ID_RECORD_TYPE = base_url + "server_identity"

(there are more. It pays to have an editor that shows them. If you're using emacs, I can send you a thingy that does that)

This is broken (calls a method on self outside a class, ViewDefinition is undefined)

def Xget_replication_list(db):
    map_js = """function(doc) { emit(doc.managed_by, doc) }"""
    view_name = "u1_replicators_by_manager"
    design_document = "ubuntuone_replication"
    view = ViewDefinition(design_document, view_name, map_js, None)
    view.sync(db)
    results = self.execute_view(view_name, design_document)
    return results

in dbus_io.py:

long lines, and a lot of *args, **kwargs magic (perhaps that cannot be helped)

review: Needs Fixing
Revision history for this message
Stuart Langridge (sil) wrote :

Code looks good. I can't actually test the functionality because I only have one machine here.

We should use xdg.BaseDirectory to find the cache folder rather than hardcoding ~/.cache, but that's a small point.

review: Approve
51. By Chad Miller

Use XDG module to find log directory.

Revision history for this message
Eric Casteleijn (thisfred) wrote :

Looks good.

Most of the long lines are still in there, but we can fix them at a less pressing date.

review: Approve
52. By Chad Miller

Since we're defined in desktopcouch module now, it's okay to use direct
functions instead of DBus proxies of those functions.

53. By Chad Miller

Explain about local pairing being disabled because of couchdb auth
not working.

54. By Chad Miller

Fix method name typo.

55. By Chad Miller

Also stop listening end, before user gets far.

Revision history for this message
Eric Casteleijn (thisfred) wrote :

I'm probably missing some gtk thing or an implicit import, but it looks like pick_or_listen is not defined in the following:

               fail_note = gtk.MessageDialog(
                    parent=pick_or_listen.window,
                    flags=gtk.DIALOG_DESTROY_WITH_PARENT,
                    buttons=gtk.BUTTONS_OK,
                    type=gtk.MESSAGE_ERROR,
                    message_format =_("Sorry, couchdb authentication is not yet enabled, so local pairing is not supported"))

Revision history for this message
Eric Casteleijn (thisfred) wrote :

also:

in desktopcouch-paired-replication-manager

import json is unused, and import logging.handler seems unnecessary.

56. By Chad Miller

Since a replicate() funciton is missing from python-couchdb , make it
internally.

Fix a few bugs with reversed logic, lack of a port number, and one
change-while-iterating bug.

Remove an unused import.

Log more useful information.

Revision history for this message
Stuart Langridge (sil) wrote :

r51-56 look good to me!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/desktopcouch-pair'
2--- bin/desktopcouch-pair 2009-08-24 19:06:43 +0000
3+++ bin/desktopcouch-pair 2009-08-26 12:30:49 +0000
4@@ -329,10 +329,11 @@
5
6 hostname, domainname = dbus_io.get_local_hostname()
7 username = getpass.getuser()
8- self.advertisement = dbus_io.Advertisement(port=listen_port,
9+ self.advertisement = dbus_io.PairAdvertisement(port=listen_port,
10 name="%s-%s-%d" % (hostname, username, listen_port),
11 text=dict(version=str(discovery_tool_version),
12 description=get_host_info()))
13+ self.advertisement.publish()
14 return hostname, username, listen_port
15
16 def __init__(self, couchdb_instance):
17@@ -725,4 +726,5 @@
18
19 if __name__ == "__main__":
20 import sys
21+ desktopcouch_port = dbus_io.get_desktopcouch_listening_port()
22 main(sys.argv)
23
24=== added file 'bin/desktopcouch-paired-replication-manager'
25--- bin/desktopcouch-paired-replication-manager 1970-01-01 00:00:00 +0000
26+++ bin/desktopcouch-paired-replication-manager 2009-08-26 13:59:05 +0000
27@@ -0,0 +1,122 @@
28+#!/bin/sh
29+""""exec ${PYTHON:-python} -t $0 "$@";" """
30+# vim: filetype=python expandtab smarttab
31+
32+# Copyright 2009 Canonical Ltd.
33+#
34+# This file is part of desktopcouch.
35+#
36+# desktopcouch is free software: you can redistribute it and/or modify
37+# it under the terms of the GNU Lesser General Public License version 3
38+# as published by the Free Software Foundation.
39+#
40+# desktopcouch is distributed in the hope that it will be useful,
41+# but WITHOUT ANY WARRANTY; without even the implied warranty of
42+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
43+# GNU Lesser General Public License for more details.
44+#
45+# You should have received a copy of the GNU Lesser General Public License
46+# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
47+#
48+# Authors: Chad Miller <chad.miller@canonical.com>
49+
50+import os
51+import json
52+import threading
53+import logging
54+import logging.handlers
55+log = logging.getLogger("main")
56+
57+from twisted.internet import gtk2reactor
58+gtk2reactor.install()
59+from twisted.internet import reactor, task
60+
61+import desktopcouch
62+from desktopcouch.pair.couchdb_pairing import couchdb_io
63+from desktopcouch.pair.couchdb_pairing import dbus_io
64+
65+already_replicating = False
66+
67+class ReplicatorThread(threading.Thread):
68+ def __init__(self):
69+ log.debug("starting up replication thread")
70+ super(ReplicatorThread, self).__init__()
71+
72+ def run(self):
73+ global already_replicating # Fuzzy, as not really critical,
74+ already_replicating = True # just trying to be polite.
75+ try:
76+ for uuid, addr, port in dbus_io.get_seen_paired_hosts():
77+ log.debug("host %s is seen; want to replicate to it", uuid)
78+ for db_name in couchdb_io.get_database_names_replicatable():
79+ couchdb_io.replicate(db_name, db_name,
80+ target_host=addr, target_port=port)
81+
82+
83+ # TODO: get static addressed paired hosts and replicate to
84+ # those too.
85+
86+ finally:
87+ already_replicating = False
88+ log.debug("finished replicating")
89+
90+
91+def replicate_local_databases_to_paired_hosts():
92+ if already_replicating:
93+ log.warn("haven't finished replicating before next time to start.")
94+ return False
95+
96+ r = ReplicatorThread()
97+ r.start()
98+
99+def main(args):
100+ log_directory = os.path.expanduser("~/.cache/ubuntuone/log")
101+ try:
102+ os.makedirs(log_directory)
103+ except:
104+ pass
105+ rotating_log = logging.handlers.TimedRotatingFileHandler(
106+ os.path.join(log_directory, "desktop-couch-replication.log"),
107+ "midnight", 1, 14)
108+ rotating_log.setLevel(logging.DEBUG)
109+ formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
110+ rotating_log.setFormatter(formatter)
111+ logging.getLogger('').addHandler(rotating_log)
112+ logging.getLogger('').setLevel(logging.DEBUG)
113+
114+ try:
115+ log.info("Starting.")
116+
117+ unique_identifiers = couchdb_io.get_my_host_unique_id()
118+ if unique_identifiers is None:
119+ log.warn("No unique hostaccount id is set, so pairing not enabled.")
120+ sys.exit(2)
121+
122+ port = desktopcouch.find_port()
123+ beacons = [dbus_io.LocationAdvertisement(port, "desktopcouch " + i)
124+ for i in unique_identifiers]
125+ for b in beacons:
126+ b.publish()
127+
128+ dbus_io.discover_services(None, None, True)
129+
130+ try:
131+ dbus_io.maintain_discovered_servers()
132+ t = task.LoopingCall(replicate_local_databases_to_paired_hosts)
133+ t.start(600)
134+
135+ # TODO: port may change, so every so often, check it and
136+ # perhaps refresh the beacons.
137+
138+ reactor.run()
139+ finally:
140+ for b in beacons:
141+ b.unpublish()
142+
143+ finally:
144+ log.info("Quitting.")
145+
146+if __name__ == "__main__":
147+ import sys
148+
149+ main(sys.argv)
150
151=== modified file 'desktopcouch/pair/couchdb_pairing/couchdb_io.py'
152--- desktopcouch/pair/couchdb_pairing/couchdb_io.py 2009-07-08 17:48:11 +0000
153+++ desktopcouch/pair/couchdb_pairing/couchdb_io.py 2009-08-26 13:59:05 +0000
154@@ -2,7 +2,7 @@
155 #
156 # This file is part of desktopcouch.
157 #
158-# desktopcouch is free software: you can redistribute it and/or modify
159+# desktopcouch is free software: you can redistribute it and/or modify
160 # it under the terms of the GNU Lesser General Public License version 3
161 # as published by the Free Software Foundation.
162 #
163@@ -13,27 +13,118 @@
164 #
165 # You should have received a copy of the GNU Lesser General Public License
166 # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
167+#
168+# Authors: Chad Miller <chad.miller@canonical.com>
169 """Communicate with CouchDB."""
170
171-import urllib2
172-import json
173 import logging
174
175-def replicate_to(port, src_name, dst_host, dst_port, dst_name):
176- """A simple easiest-possible replication instruction for couchdb. It's
177- almost certainly wrong for us."""
178-
179- dst_url = u"http://%(dst_host)s):%(dst_port)d/%(dst_name)s" % locals()
180- doc_data = dict(source_database=src_name.encode("utf8"),
181- target_database=dst_url.encode("utf8"))
182-
183- document = json.dumps(doc_data) # json doesn't mention Unicode.
184-
185- req = urllib2.Request("http://localhost:%d/_replicate" % (port,), document)
186-
187+from desktopcouch import find_port as desktopcouch_find_port
188+from desktopcouch.records import server
189+
190+RECTYPE_BASE = "http://www.freedesktop.org/wiki/Specifications/desktopcouch/"
191+PAIRED_SERVER_RECORD_TYPE = RECTYPE_BASE + "paired_server"
192+MY_ID_RECORD_TYPE = RECTYPE_BASE + "server_identity"
193+
194+def _get_db(name, create=True):
195+ port = desktopcouch_find_port() # make sure d-c is running.
196+ return server.CouchDatabase(name, create=create)
197+
198+def get_database_names_replicatable():
199+ """Find a list of local databases, minus dbs that we do not want to
200+ replicate (explicitly or implicitly)."""
201+
202+ port = int(desktopcouch_find_port())
203+ couchdb_server = server.Server("http://localhost:%(port)d/" % locals())
204+ all = set([db_name for db_name in couchdb_server])
205+
206+ excluded = set()
207+ excluded.add("management")
208+ excluded_msets = _get_management_data(PAIRED_SERVER_RECORD_TYPE,
209+ "excluded_names")
210+ for excluded_mset in excluded_msets:
211+ excluded.update(excluded_mset)
212+
213+ return all - excluded
214+
215+def get_my_host_unique_id():
216+ """Returns a list of ids we call ourselves. We complain in the log if it's
217+ more than one, but it's really no error. If there are zero (id est, we've
218+ never paired with anyone), then returns None."""
219+
220+ db = _get_db("management")
221+ ids = _get_management_data(MY_ID_RECORD_TYPE, "self_identity")
222+ ids = list(set(ids)) # uniqify
223+ if len(ids) > 1:
224+ logging.error("DANGER! We have more than one record claiming to be this host's unique identifier. Which is right? We will try to use them all, but this smells really funny.")
225+ return ids
226+ if len(ids) == 1:
227+ return ids
228+ return None
229+
230+def get_local_paired_uuids():
231+ results = _get_management_data(PAIRED_SERVER_RECORD_TYPE, "pairing_identifier")
232+ return results
233+
234+def _get_management_data(record_type, key):
235+ db = _get_db("management")
236+ results = db.get_records(create_view=True)
237+ values = list()
238+ for record in results[record_type]:
239+ if key in record.value: # EAFP, rather than LBYL? Nones default?
240+ value = record.value[key]
241+ if value is not None:
242+ values.append(value)
243+ else:
244+ logging.debug("skipping record empty %s", key)
245+ else:
246+ logging.debug("skipping record with no %s", key)
247+ logging.debug("found %d %s records", len(values), key)
248+ return values
249+
250+def create_remote_database(dst_host, dst_port, dst_name):
251+ dst_url = u"http://%(dst_host)s:%(dst_port)d/" % locals()
252+ return server.CouchDatabase(dst_name, dst_url, create=True)
253+
254+def replicate(source_database, target_database, target_host=None,
255+ target_port=None, source_host=None, source_port=None):
256+ """This replication is instant and blocking, and does not persist. """
257+
258+ data = {}
259+
260+ if source_host:
261+ if source_port:
262+ source = "http://%(source_host)s/%(source_database)s" % locals()
263+ else:
264+ source = "http://%(source_host)s:%(source_port)d/%(source_database)s" % locals()
265+ else:
266+ source = source_database
267+
268+ if target_host:
269+ if target_port:
270+ target = "http://%(target_host)s/%(target_database)s" % locals()
271+ else:
272+ target = "http://%(target_host)s:%(target_port)d/%(target_database)s" % locals()
273+ else:
274+ target = target_database
275+
276+ record = dict(source=source, target=target)
277 try:
278- conn = urllib2.urlopen(req)
279- logging.info("couchdb request resulted in %r", conn.read())
280+ if target_host:
281+ # Remote databases must exist before replicating to them.
282+ create_remote_database(target_host, target_port, target_database)
283+
284+ # TODO: Get admin username and password from keyring. Populate URL.
285+
286+ port = int(desktopcouch_find_port())
287+ url = "http://localhost:%d/" % (port,)
288+
289+ import couchdb
290+ server = couchdb.client.Server(url)
291+ db = server["_replicate"]
292+ db.create(record)
293+
294+ logging.info("successfully replicated %r", record)
295 except:
296 logging.exception("can't talk to couchdb.")
297 raise
298
299=== modified file 'desktopcouch/pair/couchdb_pairing/dbus_io.py'
300--- desktopcouch/pair/couchdb_pairing/dbus_io.py 2009-07-20 14:34:08 +0000
301+++ desktopcouch/pair/couchdb_pairing/dbus_io.py 2009-08-26 13:59:05 +0000
302@@ -2,7 +2,7 @@
303 #
304 # This file is part of desktopcouch.
305 #
306-# desktopcouch is free software: you can redistribute it and/or modify
307+# desktopcouch is free software: you can redistribute it and/or modify
308 # it under the terms of the GNU Lesser General Public License version 3
309 # as published by the Free Software Foundation.
310 #
311@@ -13,6 +13,8 @@
312 #
313 # You should have received a copy of the GNU Lesser General Public License
314 # along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
315+#
316+# Authors: Chad Miller <chad.miller@canonical.com>
317 """Communicate with DBUS and also the APIs it proxies, like Zeroconf."""
318
319 import logging
320@@ -22,8 +24,11 @@
321 import avahi
322 DBusGMainLoop(set_as_default=True)
323
324-discovery_service_type = "_couchdb_pairing_invitations._tcp"
325+from desktopcouch.pair.couchdb_pairing import couchdb_io
326
327+invitations_discovery_service_type = "_couchdb_pairing_invitations._tcp"
328+location_discovery_service_type = "_couchdb_location._tcp"
329+desktopcouch_dbus_interface = "org.desktopcouch.CouchDB"
330
331 def get_local_hostname():
332 """Get the name of this host, as Unicode host and domain parts."""
333@@ -43,36 +48,34 @@
334
335 return hostname
336
337-def get_dbus_bus_server():
338+def get_desktopcouch_listening_port():
339+ bus, server = get_dbus_bus_server(desktopcouch_dbus_interface)
340+ return server.desktopCouch.getPort()
341+
342+def get_dbus_bus_server(interface="root"):
343 """Common sequence of steps to get a Bus and Server object from DBUS."""
344 bus = dbus.SystemBus()
345 root_name = bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER)
346 server = dbus.Interface(root_name, avahi.DBUS_INTERFACE_SERVER)
347 return bus, server
348
349-
350 class Advertisement(object):
351 """Represents an advertised service that exists on this host."""
352-
353- def __init__(self, port, name="rlx!", stype=discovery_service_type,
354- domain="", host="", text="(unknown)"):
355+ def __init__(self, port, name, stype="", domain="", host="", text={}):
356+ super(Advertisement, self).__init__()
357
358 self.logging = logging.getLogger(self.__class__.__name__)
359-
360- super(Advertisement, self).__init__()
361 self.name = name
362 self.stype = stype
363 self.domain = domain
364 self.host = host
365- self.port = port
366+ self.port = int(port)
367 if hasattr(text, "keys"):
368 self.text = avahi.dict_to_txt_array(text)
369 else:
370 self.text = text
371
372 self.group = None
373-
374- self.publish()
375
376 def publish(self):
377 """Start the advertisement."""
378@@ -87,20 +90,128 @@
379
380 g.Commit()
381 self.logging.info("starting advertising %s on port %d",
382- discovery_service_type, self.port)
383+ self.stype, self.port)
384 self.group = g
385
386 def unpublish(self):
387 """End the advertisement."""
388 self.group.Reset()
389 self.logging.info("ending advertising %s on port %d",
390- discovery_service_type, self.port)
391+ self.stype, self.port)
392 self.group = None
393
394 def die(self):
395 """Quit."""
396 self.unpublish()
397
398+class LocationAdvertisement(Advertisement):
399+ """An advertised couchdb location. See Advertisement class."""
400+ def __init__(self, *args, **kwargs):
401+ if "stype" in kwargs:
402+ kwargs.pop(stype)
403+ super(LocationAdvertisement, self).__init__(
404+ stype=location_discovery_service_type, *args, **kwargs)
405+
406+class PairAdvertisement(Advertisement):
407+ """An advertised couchdb pairing opportunity. See Advertisement class."""
408+ def __init__(self, *args, **kwargs):
409+ if "stype" in kwargs:
410+ kwargs.pop(stype)
411+ super(PairAdvertisement, self).__init__(
412+ stype=invitations_discovery_service_type, *args, **kwargs)
413+
414+def avahitext_to_dict(avahitext):
415+ text = {}
416+ for l in avahitext:
417+ try:
418+ k, v = "".join(chr(i) for i in l).split("=", 1)
419+ text[k] = v
420+ except ValueError, e:
421+ logging.error("k/v field could not be decoded. %s", e)
422+ return text
423+
424+
425+nearby_desktop_couch_instances = dict() # k=uuid, v=(addr, port)
426+
427+def cb_found_desktopcouch_server(uuid, host_address, port):
428+ nearby_desktop_couch_instances[uuid] = (unicode(host_address), int(port))
429+
430+def cb_lost_desktopcouch_server(uuid):
431+ try:
432+ del nearby_desktop_couch_instances[uuid]
433+ except KeyError:
434+ pass
435+
436+def get_seen_paired_hosts():
437+ paired_uuids = couchdb_io.get_local_paired_uuids()
438+ return (
439+ (uuid, addr, port)
440+ for uuid, (addr, port)
441+ in nearby_desktop_couch_instances.iteritems()
442+ if uuid in paired_uuids)
443+
444+def maintain_discovered_servers(add_cb=cb_found_desktopcouch_server,
445+ del_cb=cb_lost_desktopcouch_server):
446+
447+ def remove_item_handler(interface, protocol, name, stype, domain, flags):
448+ """A service disappeared."""
449+
450+ def handle_error(*args):
451+ """An error in resolving a new service."""
452+ logging.error("zeroconf ItemNew error for services, %s", args)
453+
454+ def handle_resolved(*args):
455+ """Successfully resolved a new service, which we decode and send
456+ back to our calling environment with the callback function."""
457+
458+ name, host, port = args[2], args[5], args[8]
459+ if name.startswith("desktopcouch "):
460+ del_cb(name[13:], host, port)
461+ else:
462+ logging.error("no UUID in zeroconf message, %r", args)
463+
464+ del_cb(uuid)
465+
466+ server.ResolveService(interface, protocol, name, stype,
467+ domain, avahi.PROTO_UNSPEC, dbus.UInt32(0),
468+ reply_handler=handle_resolved, error_handler=handle_error)
469+
470+ def new_item_handler(interface, protocol, name, stype, domain, flags):
471+ """A service appeared."""
472+
473+ def handle_error(*args):
474+ """An error in resolving a new service."""
475+ logging.error("zeroconf ItemNew error for services, %s", args)
476+
477+ def handle_resolved(*args):
478+ """Successfully resolved a new service, which we decode and send
479+ back to our calling environment with the callback function."""
480+
481+ name, host, port = args[2], args[5], args[8]
482+ # FIXME strip off "desktopcouch "
483+ if name.startswith("desktopcouch "):
484+ add_cb(name[13:], host, port)
485+ else:
486+ logging.error("no UUID in zeroconf message, %r", name)
487+ return True
488+
489+ server.ResolveService(interface, protocol, name, stype,
490+ domain, avahi.PROTO_UNSPEC, dbus.UInt32(0),
491+ reply_handler=handle_resolved, error_handler=handle_error)
492+
493+ bus, server = get_dbus_bus_server()
494+ domain_name = get_local_hostname()[1]
495+ browser = server.ServiceBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC,
496+ location_discovery_service_type, domain_name, dbus.UInt32(0))
497+ browser_name = bus.get_object(avahi.DBUS_NAME, browser)
498+
499+ sbrowser = dbus.Interface(browser_name,
500+ avahi.DBUS_INTERFACE_SERVICE_BROWSER)
501+ sbrowser.connect_to_signal("ItemNew", new_item_handler)
502+ sbrowser.connect_to_signal("ItemRemove", remove_item_handler)
503+ sbrowser.connect_to_signal("Failure",
504+ lambda *a: logging.error("avahi error %r", a))
505+
506
507 def discover_services(add_commport_name_cb, del_commport_name_cb,
508 show_local=False):
509@@ -125,13 +236,10 @@
510 def handle_resolved(*args):
511 """Successfully resolved a new service, which we decode and send
512 back to our calling environment with the callback function."""
513- text = {}
514- for l in args[9]:
515- k, v = "".join(chr(i) for i in l).split("=", 1)
516- text[k] = v
517-
518- add_commport_name_cb(args[2], text.get("description", "?"),
519- args[5], args[8], text.get("version", None))
520+ text = avahitext_to_dict(args[9])
521+ name, host, port = args[2], args[5], args[8]
522+ add_commport_name_cb(name, text.get("description", "?"),
523+ host, port, text.get("version", None))
524
525 if not show_local and flags & avahi.LOOKUP_RESULT_LOCAL:
526 return
527@@ -142,14 +250,15 @@
528
529
530 bus, server = get_dbus_bus_server()
531-
532 domain_name = get_local_hostname()[1]
533
534 browser = server.ServiceBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC,
535- discovery_service_type, domain_name, dbus.UInt32(0))
536+ invitations_discovery_service_type, domain_name, dbus.UInt32(0))
537 browser_name = bus.get_object(avahi.DBUS_NAME, browser)
538
539 sbrowser = dbus.Interface(browser_name,
540 avahi.DBUS_INTERFACE_SERVICE_BROWSER)
541 sbrowser.connect_to_signal("ItemNew", new_item_handler)
542 sbrowser.connect_to_signal("ItemRemove", remove_item_handler)
543+ sbrowser.connect_to_signal("Failure",
544+ lambda *a: logging.error("avahi error %r", a))

Subscribers

People subscribed via source and target branches