1# Copyright 2014 Christoph Reiter <reiter.christoph@gmail.com> 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7 8from contextlib import contextmanager 9 10from gi.repository import GLib 11from gi.repository import Gio 12 13from quodlibet.util import print_d 14 15 16def alternative_service_name(name): 17 if "#" in name: 18 name, num = name.rsplit("#", 1) 19 num = int(num) 20 else: 21 num = 1 22 return "%s#%d" % (name, num + 1) 23 24 25class AvahiPublishFlags(object): 26 NONE = 0 27 UNIQUE = 1 << 0 28 NO_PROBE = 1 << 1 29 NO_ANNOUNCE = 1 << 2 30 ALLOW_MULTIPLE = 1 << 3 31 NO_REVERSE = 1 << 4 32 NO_COOKIE = 1 << 5 33 UPDATE = 1 << 6 34 USE_WIDE_AREA = 1 << 7 35 USE_MULTICAST = 1 << 8 36 37 38class AvahiEntryGroupState(object): 39 UNCOMMITED = 0 40 REGISTERING = 1 41 ESTABLISHED = 2 42 COLLISION = 3 43 FAILURE = 4 44 45 46class AvahiServerState(object): 47 INVALID = 0 48 REGISTERING = 1 49 RUNNING = 2 50 COLLISION = 3 51 FAILURE = 4 52 53 54class AvahiProtocol(object): 55 INET = 0 56 INET6 = 1 57 UNSPEC = -1 58 59 60AVAHI_IF_UNSPEC = -1 61 62 63class AvahiError(Exception): 64 pass 65 66 67@contextmanager 68def ignored(*exceptions): 69 try: 70 yield 71 except exceptions: 72 pass 73 74 75class AvahiService(object): 76 """Register a single network service using zeroconf/avahi 77 78 service = AvahiService() 79 service.register("foo", 4242, "_mpd._tcp") 80 service.register("foo", 2424, "_mpd._tcp") 81 service.unregister() 82 83 http://avahi.org/download/doxygen/ 84 """ 85 86 DBUS_NAME = "org.freedesktop.Avahi" 87 DBUS_PATH_SERVER = "/" 88 DBUS_INTERFACE_ENTRY_GROUP = "org.freedesktop.Avahi.EntryGroup" 89 DBUS_INTERFACE_SERVER = "org.freedesktop.Avahi.Server" 90 91 def register(self, name, port, stype): 92 """Register the service with avahi. 93 94 Can be called multiple times and will update the service entry 95 each time. In case Avahi isn't running or ready, the service 96 will be registered when it is ready. 97 98 Can raise AvahiError 99 """ 100 101 try: 102 GLib.Variant('q', port) # guint16 103 except OverflowError as e: 104 raise AvahiError(e) 105 106 self.name = name 107 self._real_name = name 108 self.port = port 109 self.stype = stype 110 111 try: 112 bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) 113 if not self._watch: 114 self._watch = Gio.bus_watch_name_on_connection( 115 bus, self.DBUS_NAME, Gio.BusNameWatcherFlags.NONE, 116 self._owner_appeared, self._owner_vanished) 117 else: 118 self._try_update_service() 119 except GLib.Error as e: 120 raise AvahiError(e) 121 122 def unregister(self): 123 """Unregister the service. 124 125 Can be called multiple times. In case you want to update, 126 call register() with new data instead. 127 128 Will not raise. 129 """ 130 131 if self._watch: 132 with ignored(GLib.Error): 133 Gio.bus_unwatch_name(self._watch) 134 self._watch = None 135 136 self._remove_server() 137 138 def __init__(self): 139 self.name = None 140 self.stype = None 141 self.port = None 142 143 self._group = None 144 self._group_id = None 145 self._server = None 146 self._server_id = None 147 self._watch = None 148 self._real_name = None 149 self._last_server_state = None 150 151 def _on_group_signal(self, proxy, sender, signal, *args): 152 if signal == 'StateChanged': 153 self._group_state_change(args[0]) 154 155 def _group_state_change(self, state, *args): 156 if state == AvahiEntryGroupState.COLLISION: 157 self._real_name = alternative_service_name(self._real_name) 158 self._try_update_service() 159 160 def _group_add_service_and_commit(self, group, flags): 161 print_d("name=%s, flags=%x, stype=%s, port=%d" % ( 162 self._real_name, flags, self.stype, self.port)) 163 group.AddService('(iiussssqaay)', 164 AVAHI_IF_UNSPEC, AvahiProtocol.UNSPEC, flags, 165 self._real_name, self.stype, '', '', self.port, []) 166 group.Commit() 167 168 def _add_service(self): 169 assert not self._group 170 assert not self._group_id 171 172 try: 173 bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) 174 server = Gio.DBusProxy.new_sync( 175 bus, Gio.DBusProxyFlags.NONE, None, self.DBUS_NAME, 176 self.DBUS_PATH_SERVER, self.DBUS_INTERFACE_SERVER, None) 177 178 group_path = server.EntryGroupNew() 179 group = Gio.DBusProxy.new_sync( 180 bus, Gio.DBusProxyFlags.NONE, None, self.DBUS_NAME, 181 group_path, self.DBUS_INTERFACE_ENTRY_GROUP, None) 182 183 self._group_id = group.connect('g-signal', self._on_group_signal) 184 185 self._group_add_service_and_commit(group, AvahiPublishFlags.NONE) 186 self._group = group 187 except GLib.Error: 188 self._remove_service() 189 190 def _try_update_service(self): 191 if not self._group: 192 return 193 assert self._group_id 194 195 try: 196 group = self._group 197 # XXX: http://markmail.org/message/b5d5wa2tdcplxpk2 198 # It's "documented" that Reset() shouldn't be called in this case 199 # but it doesn't work otherwise... 200 group.Reset() 201 202 self._group_add_service_and_commit(group, AvahiPublishFlags.UPDATE) 203 except GLib.Error: 204 self._remove_service() 205 206 def _remove_service(self): 207 if self._group: 208 if self._group_id: 209 with ignored(GLib.Error): 210 self._group.disconnect(self._group_id) 211 self._group_id = None 212 213 with ignored(GLib.Error): 214 self._group.Free() 215 self._group = None 216 217 def _remove_server(self): 218 if self._server: 219 if self._server_id: 220 with ignored(GLib.Error): 221 self._server.disconnect(self._server_id) 222 self._server_id = None 223 self._server = None 224 225 self._last_server_state = None 226 227 self._remove_service() 228 229 def _add_server(self): 230 assert not self._server_id 231 232 try: 233 server = Gio.DBusProxy.new_for_bus_sync( 234 Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None, 235 self.DBUS_NAME, self.DBUS_PATH_SERVER, 236 self.DBUS_INTERFACE_SERVER, None) 237 self._server_id = server.connect('g-signal', 238 self._on_server_signal) 239 self._server_state_changed(server.GetState()) 240 self._server = server 241 except GLib.Error: 242 self._remove_server() 243 244 def _on_server_signal(self, proxy, sender, signal, *args): 245 if signal == 'StateChanged': 246 self._server_state_changed(args[0]) 247 248 def _server_state_changed(self, state, *args): 249 # protect from calling this twice in a row for the same state 250 # because we have to call this manually on start and can't 251 # be sure if the signal fires as well 252 if state == self._last_server_state: 253 return 254 self._last_server_state = state 255 256 if state == AvahiServerState.RUNNING: 257 self._add_service() 258 elif state in (AvahiServerState.COLLISION, 259 AvahiServerState.REGISTERING): 260 self._remove_service() 261 262 def _owner_appeared(self, bus, name, owner): 263 self._add_server() 264 265 def _owner_vanished(self, bus, owner): 266 self._remove_server() 267