gstlal  1.4.1
servicediscovery.py
1 # Copyright (C) 2012--2014,2016--2018 Kipp Cannon
2 #
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License as published by the
5 # Free Software Foundation; either version 2 of the License, or (at your
6 # option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 # Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 
17 
18 #
19 # =============================================================================
20 #
21 # Preamble
22 #
23 # =============================================================================
24 #
25 
26 
27 import avahi
28 
29 
30 from gi.repository import Gio
31 
32 
33 __all__ = ["DEFAULT_SERVICE_TYPE", "DEFAULT_SERVICE_DOMAIN", "Publisher", "Listener", "ServiceBrowser"]
34 
35 
36 __author__ = "Kipp Cannon <kipp.cannon@ligo.org>"
37 __version__ = "FIXME"
38 __date__ = "FIXME"
39 
40 
41 #
42 # =============================================================================
43 #
44 # HTTP Service Metadata
45 #
46 # =============================================================================
47 #
48 
49 
50 DEFAULT_SERVICE_TYPE = "_http._tcp"
51 DEFAULT_SERVICE_DOMAIN = "gw.local"
52 
53 
54 #
55 # =============================================================================
56 #
57 # Service Publishing
58 #
59 # =============================================================================
60 #
61 
62 
63 class Service(object):
64  """
65  Add a service to a group, and allow its properties to be updated
66  later.
67  """
68  @staticmethod
69  def properties_to_txt_array(properties):
70  if properties is None:
71  properties = {}
72  elif any("=" in key for key in properties):
73  raise ValueError("'=' not permitted in property keys")
74  return avahi.dict_to_txt_array(properties)
75 
76 
77  def __init__(self, group, sname, port, stype = None, sdomain = None, host = None, properties = None):
78  """
79  Add a service to the collection of services currently
80  advertised. sname and port specify the service name and
81  the port number on which the service can be found. stype
82  and sdomain set the service type and service domain; if
83  not set the module-level symbols DEFAULT_SERVICE_TYPE and
84  DEFAULT_SERVICE_DOMAIN are used, respectively.
85 
86  Avahi is asked to advertise the service on all network
87  interfaces to which it is connected. If host is "" (the
88  default) then on each interface avahi will use the host
89  name corresponding to that network interface (as determined
90  by itself). This is a convenient way to ensure the service
91  is advertised on each interface with a host name that
92  exists on that interface's network.
93 
94  properties is a dictionary of name-value pairs all of which
95  are strings. "=" is not allowed in any of the names.
96  """
97  #
98  # this information will be needed to make updates
99  #
100 
101  self.group = group
102  self.sname = sname
103  self.stype = stype if stype is not None else DEFAULT_SERVICE_TYPE
104  self.sdomain = sdomain if sdomain is not None else DEFAULT_SERVICE_DOMAIN
105  if self.sdomain.split(".")[-1] != "local":
106  raise ValueError("sdomain must end in 'local': %s" % self.sdomain)
107 
108  #
109  # add the service to the avahi service group
110  #
111 
112  group.AddService(
113  "(iiussssqaay)",
114  avahi.IF_UNSPEC, # interface
115  avahi.PROTO_INET, # protocol
116  0, # flags
117  sname, # service name
118  self.stype, # service type
119  self.sdomain, # service domain
120  host if host is not None else "", # host name
121  port, # port
122  self.properties_to_txt_array(properties) # text/description
123  )
124 
125 
126  def set_properties(self, properties = None):
127  """
128  properties is a dictionary of name-value pairs all of which
129  are strings. "=" is not allowed in any of the names.
130  """
131  self.group.UpdateServiceTxt(
132  "(iiusssaay)",
133  avahi.IF_UNSPEC, # interface
134  avahi.PROTO_INET, # protocol
135  0, # flags
136  self.sname, # service name
137  self.stype, # service type
138  self.sdomain, # service domain
139  self.properties_to_txt_array(properties) # text/description
140  )
141 
142 
143 class Publisher(object):
144  """
145  Glue code to connect to the avahi daemon through dbus and manage
146  the advertisement of services.
147  """
148  def __enter__(self):
149  bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
150  server = Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None, avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER, avahi.DBUS_INTERFACE_SERVER, None)
151  group_path = server.EntryGroupNew("()")
152  self.group = Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None, avahi.DBUS_NAME, group_path, avahi.DBUS_INTERFACE_ENTRY_GROUP, None)
153  return self
154 
155  def add_service(self, sname, port, stype = None, sdomain = None, host = None, properties = None, commit = True):
156  """
157  See the Service class for the meaning of the arguments.
158 
159  If commit is True (the default), then the new service is
160  advertised immediately along with all other previously
161  unadvertised services; otherwise the calling code is
162  responsible for calling the .commit() method itself.
163  """
164  service = Service(self.group, sname, port, stype, sdomain, host, properties)
165  if commit:
166  self.commit()
167  return service
168 
169  def commit(self):
170  self.group.Commit("()")
171 
172  def __exit__(self, exc_type, exc_value, traceback):
173  """
174  Unpublish all services.
175  """
176  self.group.Reset("()")
177 
178 
179 #
180 # =============================================================================
181 #
182 # Service Discovery
183 #
184 # =============================================================================
185 #
186 
187 
188 class Listener(object):
189  """
190  Parent class for Listener implementations. Each method corresponds
191  to an event type. Subclasses override the desired methods with the
192  code to be invoked upon those events. The default methods are all
193  no-ops. An instance of a Listener implementation is required to
194  initialize a ServiceBrowser.
195  """
196  def add_service(self, sname, stype, sdomain, host, port, properties):
197  pass
198 
199  def remove_service(self, sname, stype, sdomain):
200  pass
201 
202  def all_for_now(self):
203  pass
204 
205  def failure(self, *args):
206  pass
207 
208 
209 class ServiceBrowser(object):
210  """
211  Glue code to connect a Listener implementation to the avahi daemon
212  through dbus.
213  """
214  def __init__(self, listener, stype = DEFAULT_SERVICE_TYPE, sdomain = DEFAULT_SERVICE_DOMAIN, ignore_local = False):
215  """
216  Connects to the avahi daemon through dbus, requests an
217  avahi ServiceBrowser instance from the daemon configured to
218  browse for the given service type and domain, then connects
219  signal handlers that forward information from avahi to the
220  methods of a Listener instance.
221 
222  listener is an instance of a subclass of Listener (or any
223  other object that provides the required methods to be used
224  as call-backs).
225 
226  if ignore_local is True then services discovered on the
227  local machine itself will be ignored (the default is False,
228  all discovered services are reported to the Listener).
229  """
230  self.listener = listener
231  self.ignore_local = ignore_local
232  bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
233  self.server = Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None, avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER, avahi.DBUS_INTERFACE_SERVER, None)
234  browser_path = self.server.ServiceBrowserNew(
235  "(iissu)",
236  avahi.IF_UNSPEC, # interface
237  avahi.PROTO_UNSPEC, # protocol
238  stype, # service type
239  sdomain, # service domain
240  0 # flags
241  )
242  bus.signal_subscribe(None, None, "ItemNew", browser_path, None, Gio.DBusSignalFlags.NONE, self.itemnew_handler, None)
243  bus.signal_subscribe(None, None, "ItemRemove", browser_path, None, Gio.DBusSignalFlags.NONE, self.itemremove_handler, None)
244  bus.signal_subscribe(None, None, "AllForNow", browser_path, None, Gio.DBusSignalFlags.NONE, self.allfornow_handler, None)
245  bus.signal_subscribe(None, None, "Failure", browser_path, None, Gio.DBusSignalFlags.NONE, self.failure_handler, None)
246 
247  def itemnew_handler(self, bus, sender_name, object_path, interface_name, signal_name, (interface, protocol, sname, stype, sdomain, flags), data):
248  """
249  Internal ItemNew signal handler. Forwards the essential
250  information to the Listener's .add_service() method.
251  """
252  if self.ignore_local and (flags & avahi.LOOKUP_RESULT_LOCAL):
253  # local service (on this machine)
254  return
255  interface, protocol, sname, stype, sdomain, host, aprotocol, address, port, txt, flags = self.server.ResolveService(
256  "(iisssiu)",
257  interface,
258  protocol,
259  sname,
260  stype,
261  sdomain,
262  avahi.PROTO_UNSPEC,
263  0
264  )
265  self.listener.add_service(sname, stype, sdomain, host, port, dict(s.split("=", 1) for s in avahi.txt_array_to_string_array(txt)))
266 
267  def itemremove_handler(self, bus, sender_name, object_path, interface_name, signal_name, (interface, protocol, sname, stype, sdomain, flags), data):
268  """
269  Internal ItemRemove signal handler. Forwards the essential
270  information to the Listener's .remove_service() method.
271  """
272  if self.ignore_local and (flags & avahi.LOOKUP_RESULT_LOCAL):
273  # local service (on this machine)
274  return
275  self.listener.remove_service(sname, stype, sdomain)
276 
277  def allfornow_handler(self, bus, sender_name, object_path, interface_name, signal_name, parameters, data):
278  """
279  Internal AllForNow signal handler. Forwards the essential
280  information to the Listener's .all_for_now() method.
281  """
282  self.listener.all_for_now()
283 
284  def failure_handler(self, bus, sender_name, object_path, interface_name, signal_name, parameters, data):
285  """
286  Internal Failure signal handler. Forwards the essential
287  information to the Listener's .failure() method.
288  """
289  self.listener.failure(*parameters)
290 
291 
292 #
293 # =============================================================================
294 #
295 # Demo
296 #
297 # =============================================================================
298 #
299 
300 
301 if __name__ == "__main__":
302  #
303  # usage:
304  #
305  # python /path/to/servicediscovery.py [publish]
306  #
307  # if publish is given on the command line then a service is
308  # published, otherwise a browser is started and discovered services
309  # are printed
310  #
311 
312  from gi.repository import GLib
313  import sys
314 
315  if sys.argv[-1] == "publish":
316  #
317  # publish a service
318  #
319 
320  with Publisher() as publisher:
321  publisher.add_service(
322  sname = "My Test Service",
323  port = 3456,
324  properties = {
325  "version": "0.10",
326  "a": "test value",
327  "b": "another value"
328  }
329  )
330  raw_input("Service published. Press return to unpublish and quit.\n")
331  else:
332  #
333  # browse for services
334  #
335 
337  def print_msg(self, action, sname, stype, sdomain, host, port, properties):
338  print >>sys.stderr, "Service \"%s\" %s" % (sname, action)
339  print >>sys.stderr, "\tType is \"%s\"" % stype
340  print >>sys.stderr, "\tDomain is \"%s\"" % sdomain
341  print >>sys.stderr, "\tHost is \"%s\"" % host
342  print >>sys.stderr, "\tPort is %s" % port
343  print >>sys.stderr, "\tProperties are %s\n" % properties
344  def add_service(self, sname, stype, sdomain, host, port, properties):
345  self.print_msg("added", sname, stype, sdomain, host, port, properties)
346  def remove_service(self, sname, stype, sdomain):
347  self.print_msg("removed", sname, stype, sdomain, None, None, None)
348 
349  def all_for_now(self):
350  print >>sys.stderr, "All for now\n"
351 
352  def failure(self, *args):
353  print >>sys.stderr, "failure", args
354  mainloop = GLib.MainLoop()
355  browser = ServiceBrowser(MyListener())
356  print "Browsing for services. Press CTRL-C to quit.\n"
357  mainloop.run()
def add_service(self, sname, port, stype=None, sdomain=None, host=None, properties=None, commit=True)
def itemnew_handler(self, bus, sender_name, object_path, interface_name, signal_name, interface, protocol, sname, stype, sdomain, flags, data)
def allfornow_handler(self, bus, sender_name, object_path, interface_name, signal_name, parameters, data)
def failure_handler(self, bus, sender_name, object_path, interface_name, signal_name, parameters, data)
def __init__(self, listener, stype=DEFAULT_SERVICE_TYPE, sdomain=DEFAULT_SERVICE_DOMAIN, ignore_local=False)
def set_properties(self, properties=None)
def __init__(self, group, sname, port, stype=None, sdomain=None, host=None, properties=None)
def itemremove_handler(self, bus, sender_name, object_path, interface_name, signal_name, interface, protocol, sname, stype, sdomain, flags, data)
def __exit__(self, exc_type, exc_value, traceback)
def print_msg(self, action, sname, stype, sdomain, host, port, properties)