1#!@PYTHON@
2# -*-python-*-
3# This file is part of avahi.
4#
5# avahi is free software; you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as
7# published by the Free Software Foundation; either version 2 of the
8# License, or (at your option) any later version.
9#
10# avahi is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
13# License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with avahi; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18# USA.
19
20from __future__ import print_function
21
22import os, sys
23
24try:
25    import avahi, gettext, dbus, avahi.ServiceTypeDatabase
26    gettext.bindtextdomain(@GETTEXT_PACKAGE@, @LOCALEDIR@)
27    gettext.textdomain(@GETTEXT_PACKAGE@)
28    import gi
29    gi.require_version('Gtk', '3.0')
30    from gi.repository import Gtk, GObject
31    _ = gettext.gettext
32except ImportError as e:
33    print("Sorry, to use this tool you need to install Avahi, pygtk and python-dbus.\n Error: %s" % e)
34    sys.exit(1)
35except Exception as e:
36    print("Failed to initialize: %s" % e)
37    sys.exit(1)
38
39
40## !!NOTE!! ##
41# It's really important to do this, else you won't see any events
42##
43try:
44    from dbus import DBusException
45    from dbus.mainloop.glib import DBusGMainLoop
46    DBusGMainLoop(set_as_default=True)
47except ImportError as e:
48    pass
49
50service_type_browsers = {}
51service_browsers = {}
52
53def error_msg(msg):
54    d = Gtk.MessageDialog(parent=None, flags=Gtk.DialogFlags.MODAL,
55                          type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK)
56    d.set_markup(msg)
57    d.show_all()
58    d.run()
59    d.destroy()
60
61ui_dir = "@interfacesdir@"
62
63service_type_db = avahi.ServiceTypeDatabase.ServiceTypeDatabase()
64
65class Main_window:
66    def __init__(self, path="avahi-discover.ui", root="main_window", domain=None, **kwargs):
67        path = os.path.join(ui_dir, path)
68        Gtk.Window.set_default_icon_name("network-wired")
69        self.ui = Gtk.Builder()
70        self.ui.set_translation_domain("avahi")
71        self.ui.add_from_file(path)
72        self.ui.connect_signals(self)
73        self.tree_view = self.ui.get_object("tree_view")
74        self.info_label = self.ui.get_object("info_label")
75        self.new()
76
77    def on_tree_view_cursor_changed(self, widget, *args):
78        (model, iter) = widget.get_selection().get_selected()
79        stype = None
80        if iter is not None:
81            (name,interface,protocol,stype,domain) = self.treemodel.get(iter,1,2,3,4,5)
82        if stype == None:
83            self.info_label.set_markup(_("<i>No service currently selected.</i>"))
84            return
85        #Asynchronous resolving
86        self.server.ResolveService( int(interface), int(protocol), name, stype, domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), reply_handler=self.service_resolved, error_handler=self.print_error)
87
88    def protoname(self,protocol):
89        if protocol == avahi.PROTO_INET:
90            return "IPv4"
91        if protocol == avahi.PROTO_INET6:
92            return "IPv6"
93        return "n/a"
94
95    def siocgifname(self, interface):
96        if interface <= 0:
97            return "n/a"
98        else:
99            return self.server.GetNetworkInterfaceNameByIndex(interface)
100
101    def get_interface_name(self, interface, protocol):
102        if interface == avahi.IF_UNSPEC and protocol == avahi.PROTO_UNSPEC:
103            return "Wide Area"
104        else:
105            return str(self.siocgifname(interface)) + " " + str(self.protoname(protocol))
106
107    def service_resolved(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags):
108        print("Service data for service '%s' of type '%s' in domain '%s' on %i.%i:"
109                % (name, stype, domain, interface, protocol))
110
111        print("\tHost %s (%s), port %i, TXT data: %s"
112                % (host, address, port, str(avahi.txt_array_to_string_array(txt))))
113
114        self.update_label(interface, protocol, name, stype, domain, host, aprotocol, address, port, avahi.txt_array_to_string_array(txt))
115
116    def print_error(self, err):
117        error_label = "<b>Error:</b> %s" % (err)
118        self.info_label.set_markup(error_label)
119        print("Error:", str(err))
120
121    def lookup_type(self, stype):
122        global service_type_db
123
124        try:
125            return service_type_db[stype].decode('utf-8', 'replace')
126        except KeyError:
127            return stype
128
129    def new_service(self, interface, protocol, name, stype, domain, flags):
130        print("Found service '%s' of type '%s' in domain '%s' on %i.%i."
131                % (name, stype, domain, interface, protocol))
132
133        if (interface,protocol) not in self.zc_ifaces:
134            ifn = self.get_interface_name(interface, protocol)
135            self.zc_ifaces[(interface,protocol)] = self.insert_row(self.treemodel, None, ifn, None,interface,protocol,None,domain)
136
137        if (interface,protocol,domain) not in self.zc_domains:
138            self.zc_domains[(interface,protocol,domain)] = self.insert_row(self.treemodel, self.zc_ifaces[(interface,protocol)], domain,None,interface,protocol,None,domain)
139
140        if (interface,protocol,stype,domain) not in self.zc_types:
141            thisDomain = self.zc_domains[(interface,protocol,domain)]
142            self.zc_types[(interface,protocol,stype,domain)] = self.insert_row(self.treemodel, thisDomain, self.lookup_type(stype), name, interface,None,None,None)
143
144        treeiter = self.insert_row(self.treemodel,self.zc_types[(interface,protocol,stype,domain)], name, name, interface,protocol,stype,domain)
145        self.services_browsed[(interface, protocol, name, stype, domain)] = treeiter
146        # expand the tree of this path
147        self.tree_view.expand_to_path(self.treemodel.get_path(treeiter))
148
149    def remove_service(self, interface, protocol, name, stype, domain, flags):
150        print("Service '%s' of type '%s' in domain '%s' on %i.%i disappeared."
151                % (name, stype, domain, interface, protocol))
152        self.info_label.set_markup("")
153        treeiter=self.services_browsed[(interface, protocol, name, stype, domain)]
154        parent = self.treemodel.iter_parent(treeiter)
155        self.treemodel.remove(treeiter)
156        del self.services_browsed[(interface, protocol, name, stype, domain)]
157        if self.treemodel.iter_has_child(parent) == False:
158            treeiter=self.zc_types[(interface,protocol,stype,domain)]
159            parent = self.treemodel.iter_parent(treeiter)
160            self.treemodel.remove(treeiter)
161            del self.zc_types[(interface,protocol,stype,domain)]
162            if self.treemodel.iter_has_child(parent) == False:
163                treeiter=self.zc_domains[(interface,protocol,domain)]
164                parent = self.treemodel.iter_parent(treeiter)
165                self.treemodel.remove(treeiter)
166                del self.zc_domains[(interface,protocol,domain)]
167                if self.treemodel.iter_has_child(parent) == False:
168                    treeiter=self.zc_ifaces[(interface,protocol)]
169                    parent = self.treemodel.iter_parent(treeiter)
170                    self.treemodel.remove(treeiter)
171                    del self.zc_ifaces[(interface,protocol)]
172
173    def new_service_type(self, interface, protocol, stype, domain, flags):
174        global service_browsers
175
176        # Are we already browsing this domain for this type?
177        if (interface, protocol, stype, domain) in service_browsers:
178            return
179
180        print("Browsing for services of type '%s' in domain '%s' on %i.%i ..."
181                % (stype, domain, interface, protocol))
182
183        b = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.ServiceBrowserNew(interface, protocol, stype, domain, dbus.UInt32(0))),  avahi.DBUS_INTERFACE_SERVICE_BROWSER)
184        b.connect_to_signal('ItemNew', self.new_service)
185        b.connect_to_signal('ItemRemove', self.remove_service)
186
187        service_browsers[(interface, protocol, stype, domain)] = b
188
189    def browse_domain(self, interface, protocol, domain):
190        global service_type_browsers
191
192        # Are we already browsing this domain?
193        if (interface, protocol, domain) in service_type_browsers:
194            return
195
196        if self.stype is None:
197            print("Browsing domain '%s' on %i.%i ..." % (domain, interface, protocol))
198
199            try:
200                b = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.ServiceTypeBrowserNew(interface, protocol, domain, dbus.UInt32(0))),  avahi.DBUS_INTERFACE_SERVICE_TYPE_BROWSER)
201            except DBusException as e:
202                print(e)
203                error_msg("You should check that the avahi daemon is running.\n\nError : %s" % e)
204                sys.exit(0)
205
206            b.connect_to_signal('ItemNew', self.new_service_type)
207
208            service_type_browsers[(interface, protocol, domain)] = b
209        else:
210            self.new_service_type(interface, protocol, self.stype, domain)
211
212    def new_domain(self,interface, protocol, domain, flags):
213        if (interface,protocol) not in self.zc_ifaces:
214            ifn = self.get_interface_name(interface, protocol)
215            self.zc_ifaces[(interface,protocol)] = self.insert_row(self.treemodel, None, ifn,None,interface,protocol,None,domain)
216        if (interface,protocol,domain) not in self.zc_domains:
217            self.zc_domains[(interface,protocol,domain)] = self.insert_row(self.treemodel, self.zc_ifaces[(interface,protocol)], domain,None,interface,protocol,None,domain)
218        if domain != "local":
219            self.browse_domain(interface, protocol, domain)
220
221    def pair_to_dict(self, l):
222        res = dict()
223        for el in l:
224            if "=" not in el:
225                res[el]=''
226            else:
227                tmp = el.split('=',1)
228                if len(tmp[0]) > 0:
229                    res[tmp[0]] = tmp[1]
230        return res
231
232
233    def update_label(self,interface, protocol, name, stype, domain, host, aprotocol, address, port, txt):
234        if len(txt) != 0:
235            txts = ""
236            txtd = self.pair_to_dict(txt)
237            for k,v in txtd.items():
238                txts+="<b>" + _("TXT") + " <i>%s</i></b> = %s\n" % (k,v)
239        else:
240            txts = "<b>" + _("TXT Data:") + "</b> <i>" + _("empty") + "</i>"
241
242        infos = "<b>" + _("Service Type:") + "</b> %s\n"
243        infos += "<b>" + _("Service Name:") + "</b> %s\n"
244        infos += "<b>" + _("Domain Name:") + "</b> %s\n"
245        infos += "<b>" + _("Interface:") + "</b> %s %s\n"
246        infos += "<b>" + _("Address:") + "</b> %s/%s:%i\n%s"
247        infos = infos % (stype, name, domain, self.siocgifname(interface), self.protoname(protocol), host, address, port, txts.strip())
248        self.info_label.set_markup(infos)
249
250    def insert_row(self, model,parent,
251                   content, name, interface,protocol,stype,domain):
252        myiter=model.insert_after(parent,None)
253        model.set(myiter,0,content,1,name,2,str(interface),3,str(protocol),4,stype,5,domain)
254        return myiter
255
256    def new(self):
257        self.treemodel=Gtk.TreeStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING)
258        self.tree_view.set_model(self.treemodel)
259
260        #creating the columns headers
261        self.tree_view.set_headers_visible(False)
262        renderer=Gtk.CellRendererText()
263        column=Gtk.TreeViewColumn("",renderer, text=0)
264        column.set_resizable(True)
265        column.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY)
266        column.set_expand(True)
267        self.tree_view.append_column(column)
268
269        self.domain = None
270        self.stype = None
271        self.zc_ifaces = {}
272        self.zc_domains = {}
273        self.zc_types = {}
274        self.services_browsed = {}
275
276        try:
277            self.bus = dbus.SystemBus()
278            self.server = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER)
279        except Exception as e:
280            print("Failed to connect to Avahi Server (Is it running?): %s" % e)
281            sys.exit(1)
282
283        if self.domain is None:
284            # Explicitly browse .local
285            self.browse_domain(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "local")
286
287            # Browse for other browsable domains
288            db = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.DomainBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "", avahi.DOMAIN_BROWSER_BROWSE, dbus.UInt32(0))), avahi.DBUS_INTERFACE_DOMAIN_BROWSER)
289            db.connect_to_signal('ItemNew', self.new_domain)
290        else:
291            # Just browse the domain the user wants us to browse
292            self.browse_domain(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, self.domain)
293
294    def gtk_main_quit(self, *args):
295        Gtk.main_quit()
296
297def main():
298    main_window = Main_window()
299    # http://stackoverflow.com/a/16486080/434217
300    import signal
301    signal.signal(signal.SIGINT, signal.SIG_DFL)
302    Gtk.main()
303
304if __name__ == "__main__":
305    main()
306