Hide keyboard shortcuts

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. 

6 

7# taken from the code I wrote for winswitch 

8 

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 

16 

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 

21 

22log = Logger("network", "mdns") 

23 

24 

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 

29 

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 

34 

35 iface = get_iface(host) 

36 log("get_iface(%s)=%s", host, iface) 

37 if iface is None: 

38 return avahi.IF_UNSPEC 

39 

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 

45 

46 

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 """ 

54 

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)) 

95 

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") 

107 

108 def stop(self): 

109 log("stopping: %s", self.publishers) 

110 for publisher in self.publishers: 

111 publisher.stop() 

112 

113 def update_txt(self, txt): 

114 for publisher in self.publishers: 

115 publisher.update_txt(txt) 

116 

117 

118class AvahiPublisher: 

119 

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 

134 

135 def iface(self): 

136 if self.interface>0: 

137 return "interface %i" % self.interface 

138 return "all interfaces" 

139 

140 def get_info(self) -> dict: 

141 return "%s %s:%s on %s" % (self.name, self.host, self.port, self.iface()) 

142 

143 def __repr__(self): 

144 return "AvahiPublisher(%s)" % self.get_info() 

145 

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()) 

159 

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 

181 

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() 

209 

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 

221 

222 

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) 

249 

250 

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() 

269 

270 

271if __name__ == "__main__": 

272 main()