Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/net/mdns/avahi_publisher.py : 1%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# This file is part of Xpra.
3# Copyright (C) 2013-2019 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
7# taken from the code I wrote for winswitch
9import avahi
10import dbus
11try:
12 from dbus.exceptions import DBusException
13except ImportError:
14 #not available in all versions of the bindings?
15 DBusException = Exception
17from xpra.net.mdns import XPRA_MDNS_TYPE, SHOW_INTERFACE
18from xpra.dbus.common import init_system_bus
19from xpra.net.net_util import get_iface, if_nametoindex, if_indextoname
20from xpra.log import Logger
22log = Logger("network", "mdns")
25def get_interface_index(host):
26 log("get_interface_index(%s)", host)
27 if host in ("0.0.0.0", "", "*", "::"):
28 return avahi.IF_UNSPEC
30 if not if_nametoindex:
31 log.warn("Warning: cannot convert interface to index (if_nametoindex is missing)")
32 log.warn(" so returning 'IF_UNSPEC', avahi will publish on ALL interfaces")
33 return avahi.IF_UNSPEC
35 iface = get_iface(host)
36 log("get_iface(%s)=%s", host, iface)
37 if iface is None:
38 return avahi.IF_UNSPEC
40 index = if_nametoindex(iface)
41 log("if_nametoindex(%s)=%s", iface, index)
42 if iface is None:
43 return avahi.IF_UNSPEC
44 return index
47class AvahiPublishers:
48 """
49 Aggregates a number of AvahiPublisher(s).
50 This takes care of constructing the appropriate AvahiPublisher
51 with the interface index and port for the given list of (host,port)s to broadcast on,
52 and to convert the text dict into a TXT string.
53 """
55 def __init__(self, listen_on, service_name, service_type=XPRA_MDNS_TYPE, text_dict=None):
56 log("AvahiPublishers%s", (listen_on, service_name, service_type, text_dict))
57 self.publishers = []
58 try:
59 bus = init_system_bus()
60 except Exception as e:
61 log.warn("failed to connect to the system dbus: %s", e)
62 log.warn(" either start a dbus session or disable mdns support")
63 return
64 for host, port in listen_on:
65 iface_index = get_interface_index(host)
66 log("iface_index(%s)=%s", host, iface_index)
67 td = text_dict or {}
68 if SHOW_INTERFACE and if_indextoname and iface_index is not None:
69 td = text_dict.copy()
70 td["iface"] = if_indextoname(iface_index)
71 txt = []
72 if text_dict:
73 for k,v in text_dict.items():
74 txt.append("%s=%s" % (k,v))
75 fqdn = host
76 if host in ("0.0.0.0", "::"):
77 fqdn = ""
78 elif host:
79 try:
80 import socket
81 fqdn = socket.gethostbyaddr(host)[0]
82 log("gethostbyaddr(%s)=%s", host, fqdn)
83 if fqdn.find(".")<0:
84 fqdn = socket.getfqdn(host)
85 log("getfqdn(%s)=%s", host, fqdn)
86 if fqdn.find(".")<0:
87 if fqdn:
88 fqdn += ".local"
89 log("cannot find a fully qualified domain name for '%s', using: %s", host, fqdn)
90 except (OSError, IndexError):
91 log("failed to get hostbyaddr for '%s'", host, exc_info=True)
92 self.publishers.append(AvahiPublisher(bus, service_name, port,
93 service_type, domain="", host=fqdn,
94 text=txt, interface=iface_index))
96 def start(self):
97 log("avahi:starting: %s", self.publishers)
98 if not self.publishers:
99 return
100 all_err = True
101 for publisher in self.publishers:
102 if publisher.start():
103 all_err = False
104 if all_err:
105 log.warn(" to avoid this warning, disable mdns support ")
106 log.warn(" using the 'mdns=no' option")
108 def stop(self):
109 log("stopping: %s", self.publishers)
110 for publisher in self.publishers:
111 publisher.stop()
113 def update_txt(self, txt):
114 for publisher in self.publishers:
115 publisher.update_txt(txt)
118class AvahiPublisher:
120 def __init__(self, bus, name, port, stype=XPRA_MDNS_TYPE, domain="", host="", text=(), interface=avahi.IF_UNSPEC):
121 log("AvahiPublisher%s", (bus, name, port, stype, domain, host, text, interface))
122 self.bus = bus
123 self.name = name
124 self.stype = stype
125 self.domain = domain
126 if host=="::":
127 host = ""
128 self.host = host
129 self.port = port
130 self.text = avahi.string_array_to_txt_array(text)
131 self.interface = interface
132 self.server = None
133 self.group = None
135 def iface(self):
136 if self.interface>0:
137 return "interface %i" % self.interface
138 return "all interfaces"
140 def get_info(self) -> dict:
141 return "%s %s:%s on %s" % (self.name, self.host, self.port, self.iface())
143 def __repr__(self):
144 return "AvahiPublisher(%s)" % self.get_info()
146 def start(self):
147 try:
148 self.server = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER),
149 avahi.DBUS_INTERFACE_SERVER)
150 self.group = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.EntryGroupNew()),
151 avahi.DBUS_INTERFACE_ENTRY_GROUP)
152 except Exception as e:
153 log.warn("failed to connect to avahi's dbus interface: %s", e)
154 return False
155 log("avahi dbus server=%s", self.server)
156 log("avahi dbus group=%s", self.group)
157 self.server.connect_to_signal("StateChanged", self.server_state_changed)
158 return self.server_state_changed(self.server.GetState())
160 def server_state_changed(self, state, error=None):
161 log("server_state_changed(%s, %s) on %s", state, error, self.server)
162 if state == avahi.SERVER_COLLISION:
163 log.error("Error: mdns server name collision")
164 if error:
165 log.error(" %s", error)
166 log.error(" for name '%s' and port %i on %s", self.name, self.port, self.iface())
167 self.stop()
168 return False
169 if state == avahi.SERVER_RUNNING:
170 self.add_service()
171 return True
172 state_str = "unknown"
173 for const in ("INVALID", "REGISTERING", "COLLISION", "FAILURE"):
174 val = getattr(avahi, "SERVER_%s" % const, None)
175 if val is not None and val==state:
176 state_str = const
177 break
178 log.warn("Warning: mdns server state changed to '%s'", state_str)
179 log.warn(" for name '%s' and port %i on %s", self.name, self.port, self.iface())
180 return False
182 def add_service(self):
183 if not self.group:
184 return
185 try:
186 args = (self.interface, avahi.PROTO_UNSPEC, dbus.UInt32(0),
187 self.name, self.stype, self.domain, self.host,
188 dbus.UInt16(self.port), self.text)
189 log("calling %s%s", self.group, args)
190 self.group.AddService(*args)
191 self.group.Commit()
192 log("dbus service added")
193 except DBusException as e:
194 log("cannot add service", exc_info=True)
195 #use try+except as older versions may not have those modules?
196 message = e.get_dbus_message()
197 dbus_error_name = e.get_dbus_name()
198 log.error("Error starting publisher %s", self.get_info())
199 if dbus_error_name=="org.freedesktop.Avahi.CollisionError":
200 log.error(" another instance already claims this dbus name")
201 log.error(" %s", e)
202 log.error(" %s", message)
203 else:
204 for l in str(e).splitlines():
205 for x in l.split(":", 1):
206 if x:
207 log.error(" %s", x)
208 self.stop()
210 def stop(self):
211 group = self.group
212 log("%s.stop() group=%s", self, group)
213 if group:
214 self.group = None
215 try:
216 group.Reset()
217 except Exception as e:
218 log.error("Error stopping avahi publisher %s:", self)
219 log.error(" %s", e)
220 self.server = None
223 def update_txt(self, txt):
224 if not self.server:
225 log("update_txt(%s) ignored, already stopped", txt)
226 return
227 if not self.group:
228 log.warn("Warning: cannot update mdns record")
229 log.warn(" publisher has already been stopped")
230 return
231 #prevent avahi from choking on ints:
232 txt_strs = dict((k,str(v)) for k,v in txt.items())
233 def reply_handler(*args):
234 log("reply_handler%s", args)
235 log("update_txt(%s) done", txt)
236 def error_handler(*args):
237 log("error_handler%s", args)
238 log.warn("Warning: failed to update mDNS TXT record")
239 log.warn(" for name '%s'", self.name)
240 log.warn(" host=%s, port=%s", self.host, self.port)
241 log.warn(" with new data:")
242 for k,v in txt_strs.items():
243 log.warn(" * %s=%s", k, v)
244 txt_array = avahi.dict_to_txt_array(txt_strs)
245 self.group.UpdateServiceTxt(self.interface,
246 avahi.PROTO_UNSPEC, dbus.UInt32(0), self.name, self.stype, self.domain,
247 txt_array, reply_handler=reply_handler,
248 error_handler=error_handler)
251def main():
252 from gi.repository import GLib
253 import random
254 import signal
255 port = int(20000*random.random())+10000
256 host = ""
257 name = "test service"
258 bus = init_system_bus()
259 publisher = AvahiPublisher(bus, name, port, stype=XPRA_MDNS_TYPE, host=host, text=("somename=somevalue",))
260 assert publisher
261 def update_rec():
262 publisher.update_txt({b"hello" : b"world"})
263 GLib.timeout_add(5*1000, update_rec)
264 def start():
265 publisher.start()
266 GLib.idle_add(start)
267 signal.signal(signal.SIGTERM, exit)
268 GLib.MainLoop().run()
271if __name__ == "__main__":
272 main()