gstlal 1.13.0
Loading...
Searching...
No Matches
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
27import avahi
28
29
30from 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
50DEFAULT_SERVICE_TYPE = "_http._tcp"
51DEFAULT_SERVICE_DOMAIN = "gw.local"
52
53
54#
55# =============================================================================
56#
57# Service Publishing
58#
59# =============================================================================
60#
61
62
63class 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
143class 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
188class 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
209class 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, service_config, data):
248 """
249 Internal ItemNew signal handler. Forwards the essential
250 information to the Listener's .add_service() method.
251 """
252 interface, protocol, sname, stype, sdomain, flags = service_config
253 if self.ignore_local and (flags & avahi.LOOKUP_RESULT_LOCAL):
254 # local service (on this machine)
255 return
256 interface, protocol, sname, stype, sdomain, host, aprotocol, address, port, txt, flags = self.server.ResolveService(
257 "(iisssiu)",
258 interface,
259 protocol,
260 sname,
261 stype,
262 sdomain,
263 avahi.PROTO_UNSPEC,
264 0
265 )
266 self.listener.add_service(sname, stype, sdomain, host, port, dict(s.split("=", 1) for s in avahi.txt_array_to_string_array(txt)))
267
268 def itemremove_handler(self, bus, sender_name, object_path, interface_name, signal_name, service_config, data):
269 """
270 Internal ItemRemove signal handler. Forwards the essential
271 information to the Listener's .remove_service() method.
272 """
273 interface, protocol, sname, stype, sdomain, flags = service_config
274 if self.ignore_local and (flags & avahi.LOOKUP_RESULT_LOCAL):
275 # local service (on this machine)
276 return
277 self.listener.remove_service(sname, stype, sdomain)
278
279 def allfornow_handler(self, bus, sender_name, object_path, interface_name, signal_name, parameters, data):
280 """
281 Internal AllForNow signal handler. Forwards the essential
282 information to the Listener's .all_for_now() method.
283 """
284 self.listener.all_for_now()
285
286 def failure_handler(self, bus, sender_name, object_path, interface_name, signal_name, parameters, data):
287 """
288 Internal Failure signal handler. Forwards the essential
289 information to the Listener's .failure() method.
290 """
291 self.listener.failure(*parameters)
292
293
294#
295# =============================================================================
296#
297# Demo
298#
299# =============================================================================
300#
301
302
303if __name__ == "__main__":
304 #
305 # usage:
306 #
307 # python /path/to/servicediscovery.py [publish]
308 #
309 # if publish is given on the command line then a service is
310 # published, otherwise a browser is started and discovered services
311 # are printed
312 #
313
314 from gi.repository import GLib
315 import sys
316
317 if sys.argv[-1] == "publish":
318 #
319 # publish a service
320 #
321
322 with Publisher() as publisher:
323 publisher.add_service(
324 sname = "My Test Service",
325 port = 3456,
326 properties = {
327 "version": "0.10",
328 "a": "test value",
329 "b": "another value"
330 }
331 )
332 raw_input("Service published. Press return to unpublish and quit.\n")
333 else:
334 #
335 # browse for services
336 #
337
339 def print_msg(self, action, sname, stype, sdomain, host, port, properties):
340 print("Service \"%s\" %s" % (sname, action), file=sys.stderr)
341 print("\tType is \"%s\"" % stype, file=sys.stderr)
342 print("\tDomain is \"%s\"" % sdomain, file=sys.stderr)
343 print("\tHost is \"%s\"" % host, file=sys.stderr)
344 print("\tPort is %s" % port, file=sys.stderr)
345 print("\tProperties are %s\n" % properties, file=sys.stderr)
346 def add_service(self, sname, stype, sdomain, host, port, properties):
347 self.print_msg("added", sname, stype, sdomain, host, port, properties)
348 def remove_service(self, sname, stype, sdomain):
349 self.print_msg("removed", sname, stype, sdomain, None, None, None)
350
351 def all_for_now(self):
352 print("All for now\n", file=sys.stderr)
353
354 def failure(self, *args):
355 print("failure", args, file=sys.stderr)
356 mainloop = GLib.MainLoop()
357 browser = ServiceBrowser(MyListener())
358 print("Browsing for services. Press CTRL-C to quit.\n")
359 mainloop.run()
print_msg(self, action, sname, stype, sdomain, host, port, properties)
__exit__(self, exc_type, exc_value, traceback)
add_service(self, sname, port, stype=None, sdomain=None, host=None, properties=None, commit=True)
__init__(self, listener, stype=DEFAULT_SERVICE_TYPE, sdomain=DEFAULT_SERVICE_DOMAIN, ignore_local=False)
set_properties(self, properties=None)
__init__(self, group, sname, port, stype=None, sdomain=None, host=None, properties=None)