HasRun signal
[opensuse:fwzs.git] / fwzsapp.py
1 #!/usr/bin/python
2 #
3 # fwszapp - fwzs tray applet
4 # Copyright (C) 2009 SUSE LINUX Products GmbH
5 #
6 # Author:     Ludwig Nussel
7
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License
10 # version 2 as published by the Free Software Foundation.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21 ICONDIR = '.' # +++automake+++
22 BINDIR  = '.' # +++automake+++
23
24 import glib
25 import gtk
26 import sys
27 from traceback import print_exc
28 import os
29 import ConfigParser
30
31 import dbus
32 import dbus.mainloop.glib
33
34 import gettext
35 import locale
36
37 gettext.install('fwzsapp')
38
39 icon_green = ICONDIR + '/firewall.png'
40 icon_yellow = ICONDIR + '/firewall_y.png'
41 icon_red = ICONDIR + '/firewall_x.png'
42 icon_grey = ICONDIR + '/firewall_g.png'
43
44 xdg_configdir = os.path.expanduser(os.getenv('XDG_CONFIG_HOME', '~/.config'))
45
46 txt_no_zones_found_on = _("No zones found but Firewall is running.\nFwzs is probably not supported.")
47 txt_not_running = _("The firewall is not running.")
48 txt_service_not_running = _("zoneswitcher service not running or broken")
49
50 class DesktopAutoStart:
51     def __init__(self):
52         self.file = xdg_configdir + '/autostart/fwzs.desktop'
53
54     def is_enabled(self):
55         if os.access(self.file, os.F_OK):
56             return True
57         return False
58
59     def enable(self, enable=None):
60         if enable == None or enable:
61             script = sys.argv[0]
62             if script and (script[0:1] != '/' or not os.access(script, os.X_OK)):
63                 print "enable autostart not possible"
64                 return False
65
66             try:
67                 dir = os.path.dirname(self.file)
68                 if not os.access(dir, os.F_OK):
69                     os.makedirs(dir)
70                 out = open(self.file, "w")
71                 out.write("""[Desktop Entry]
72 Name=Firewall Zone Switcher
73 Comment=System Tray applet that allows to switch firewall zones
74 Terminal=false
75 Type=Application
76 StartupNotify=false
77 """)
78                 out.write("Exec="+script+" --tray --delay=5\n")
79                 out.close()
80                 return True
81
82             except Exception, e:
83                 print e
84             return False
85         else:
86             return self.disable()
87
88     def disable(self):
89         try:
90             if os.access(self.file, os.F_OK):
91                 os.remove(self.file)
92             return True
93         except Exception, e:
94             print e
95         return False
96
97 class Config:
98
99     def __init__(self):
100         self.file = xdg_configdir + '/fwzs/config'
101         self.config = ConfigParser.RawConfigParser()
102         self.config.read(self.file)
103
104     def getbool(self, section, option, default=None):
105         if self.config.has_option(section, option):
106             v = self.config.get(section, option)
107             if v == 'True':
108                 return True
109             return False
110         return default
111
112     def set(self, section, option, value):
113         if not self.config.has_section(section):
114             self.config.add_section(section)
115         self.config.set(section, option, str(value))
116
117     def save(self):
118         try:
119             dir = os.path.dirname(self.file)
120             if not os.access(dir, os.F_OK):
121                 os.makedirs(dir)
122             out = open(self.file, "wb")
123             self.config.write(out)
124         except Exception, e:
125             print e
126
127
128 class SettingsDialog:
129
130     def __init__(self, parent, app):
131         self.app = app
132         dialog = gtk.Dialog(_("Settings"), parent, gtk.DIALOG_MODAL,
133                 (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
134         dialog.set_icon_from_file(icon_green)
135         v = gtk.VBox()
136         v.set_border_width(6)
137
138         self.trayiconcb = gtk.CheckButton(_("System Tray Icon"))
139         if app.config.getbool('general', 'systray', False):
140             self.trayiconcb.set_active(True)
141         v.pack_start(self.trayiconcb)
142
143         self.autostartcb = gtk.CheckButton(_("Start on Log-in"))
144         if DesktopAutoStart().is_enabled():
145             self.autostartcb.set_active(True)
146         v.pack_start(self.autostartcb)
147
148         v.show_all()
149         dialog.get_child().pack_start(v)
150
151         dialog.show()
152         dialog.connect('response', lambda dialog, id: self.response(dialog, id))
153
154     def response(self, dialog, id):
155         dialog.destroy()
156
157         if id != gtk.RESPONSE_ACCEPT:
158             return
159
160         v = self.trayiconcb.get_active()
161         self.app.config.set('general', 'systray', v)
162         if v:
163             self.app.icon.show()
164         else:
165             self.app.icon.hide()
166
167         v = self.autostartcb.get_active()
168         DesktopAutoStart().enable(v)
169
170         self.app.config.save()
171
172 #    def __del__(self):
173 #       print "destruct"
174
175 class ChangeZoneDialog:
176
177     def __init__(self, parent, app, iface):
178         self.app = app
179         self.selection = None
180         zones = app.iface.Zones()
181         ifaces = app.iface.Interfaces()
182         if not zones or not ifaces:
183             app.error_dialog(_("Can't get list of interfaces or zones"))
184             return
185
186         dialog = gtk.Dialog(_("Choose Zone for %s") % iface, parent, gtk.DIALOG_MODAL,
187                 (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
188         dialog.set_icon_from_file(icon_green)
189         vbox = dialog.get_child()
190         v = gtk.VBox()
191         v.set_border_width(6)
192         group = None
193         for z in zones:
194             txt = zones[z]['desc']
195             if not txt:
196                 txt = z
197             rb = gtk.RadioButton(group, txt)
198             group = rb
199             if z == ifaces[iface]:
200                 rb.set_active(True)
201                 self.selection = (iface, z)
202             rb.connect('toggled', lambda *args: self._zone_selected(*args), iface, z)
203             v.pack_start(rb, False, False)
204             rb.show()
205
206         v.show_all()
207         vbox.pack_start(v, True, True)
208         dialog.show()
209         dialog.connect('response', lambda dialog, id: self.change_zone_dialog_response(dialog, id))
210
211     def _zone_selected(self, item, iface, zone):
212         if not item.get_active():
213             return
214
215         self.selection = (iface, zone)
216
217     def change_zone_dialog_response(self, dialog, id):
218         dialog.destroy()
219         if id != gtk.RESPONSE_ACCEPT:
220             return
221
222         if not self.selection:
223             print "error: no active item"
224
225         self.app.set_zone(self.selection[0], self.selection[1])
226
227 class StatusIcon:
228
229     def __init__(self, app):
230         self.icon = None
231         self.app = app
232         self.iconfile = icon_grey
233
234     def show(self):
235         if not self.icon:
236             self.icon = gtk.status_icon_new_from_file(self.iconfile)
237             self.icon.connect('popup-menu', lambda i, eb, et: self.show_menu(i, eb, et))
238             self.icon.connect('activate', lambda *args: self.app.toggle_overview_dialog())
239         else:
240             self.icon.set_visible(True)
241
242     def isshown(self):
243         if self.icon and self.icon.get_visible():
244             return True
245         return False
246
247     def hide(self):
248         # no idea how to destroy it, so hide it
249         if self.icon:
250             self.icon.set_visible(False)
251
252     def _set_icon(self, file):
253         
254         if self.iconfile != file:
255             self.iconfile = file
256             if self.app.overview_dialog:
257                 self.app.overview_dialog.set_contents()
258
259             if self.icon:
260                 self.icon.set_from_file(self.iconfile)
261
262     def update(self):
263         if self.app.running == None:
264             self._set_icon(icon_grey)
265         elif self.app.running == True:
266             self._set_icon(icon_green)
267         else:
268             self._set_icon(icon_red)
269         #self._set_icon(icon_yellow)
270
271     def _menu_error(self, menu, txt):
272         item = gtk.MenuItem(txt)
273         item.set_sensitive(False)
274         item.show()
275         menu.append(item)
276
277     def _change_zone(self, item, iface, zone):
278         if not item.get_active():
279             return
280
281         self.app.set_zone(iface, zone)
282
283     def show_menu(self, icon, event_button, event_time):
284
285         menu = gtk.Menu()
286         self.app.check_status()
287
288         if(self.app.running == None):
289             item = gtk.MenuItem(txt_service_not_running)
290             item.set_sensitive(False)
291             item.show()
292             menu.append(item)
293         
294         else:
295             zones = self.app.iface.Zones()
296
297             if zones and self.app.running == True:
298                 ifaces = self.app.iface.Interfaces()
299
300                 if ifaces:
301                     item = gtk.MenuItem(_("Firewall interfaces"))
302                     item.set_sensitive(False)
303                     item.show()
304                     menu.append(item)
305
306                     for i in ifaces:
307                         item = gtk.MenuItem(i)
308                         item.show()
309                         menu.append(item)
310                         group = None
311                         submenu = gtk.Menu()
312                         for z in zones:
313                             txt = zones[z]['desc']
314                             if not txt:
315                                 txt = z
316                             zitem = gtk.RadioMenuItem(group, txt)
317                             group = zitem
318                             if z == ifaces[i]:
319                                 zitem.set_active(True)
320                             zitem.connect('toggled', lambda *args: self._change_zone(*args), i, z)
321                             zitem.show()
322                             submenu.append(zitem)
323                         item.set_submenu(submenu)
324
325                 else:
326                     item = gtk.MenuItem(_("No interfaces found."))
327                     item.set_sensitive(False)
328                     item.show()
329                     menu.append(item)
330
331             else:
332                 if self.app.running == True:
333                     self._menu_error(menu, txt_no_zones_found_on)
334                 else:
335                     self._menu_error(menu, txt_not_running)
336
337             item = gtk.MenuItem(_("Run Firewall"))
338             item.connect('activate', lambda *args: self.app.run_firewall())
339             item.show()
340             menu.append(item)
341
342         item = gtk.SeparatorMenuItem()
343         item.show()
344         menu.append(item)
345
346         item = gtk.MenuItem(_("Quit"))
347         item.connect('activate', lambda *args: gtk.main_quit())
348         item.show()
349         menu.append(item)
350
351         menu.popup(None, None,
352             gtk.status_icon_position_menu, event_button,
353             event_time, icon)
354
355 class OverviewDialog:
356
357     def __init__(self, app):
358
359         self.app = app
360         closebutton = gtk.STOCK_QUIT
361         self.content = None
362         self.ifaces = None
363         self.zones = None
364         if app.icon.isshown():
365             closebutton = gtk.STOCK_CLOSE
366         dialog = gtk.Dialog(_("Firewall Zone Switcher"), None, 0, ( closebutton, gtk.RESPONSE_CANCEL ))
367         dialog.set_default_size(400, 250)
368         dialog.set_icon_from_file(icon_green)
369
370         dialog.connect('response', lambda dialog, id: self.response(id))
371         self.dialog = dialog
372
373         self.set_contents()
374
375         dialog.show()
376
377     def create_button_area(self, dialog):
378
379         vbox = gtk.VBox()
380         vbox.set_border_width(3)
381
382         if(not self.app.bus):
383             w = gtk.Label("DBus not running")
384             vbox.pack_start(w)
385
386         elif(self.app.running == None):
387             w = gtk.Label(txt_service_not_running)
388             vbox.pack_start(w)
389         else:
390             try:
391                 self.zones = self.app.iface.Zones()
392             except Exception, e:
393                 print e
394                 pass
395
396             if not self.zones or not self.app.running:
397                 vbox.set_border_width(6)
398                 vbox.set_spacing(6)
399                 if self.app.running == True:
400                     w = gtk.Label(txt_no_zones_found_on)
401                 else:
402                     w = gtk.Label(txt_not_running)
403                 vbox.pack_start(w, False, False)
404                 w = gtk.Button(_("Run Firewall"))
405                 w.connect('clicked', lambda *args: self.app.run_firewall())
406                 vbox.pack_start(w, False, False)
407             else:
408                 self.ifaces = self.app.iface.Interfaces()
409
410                 if self.ifaces:
411                     for i in self.ifaces:
412                         z = self.ifaces[i]
413                         txt = self.make_label(i, z)
414                         w = gtk.Button(txt)
415                         w.connect('clicked', lambda button, i: ChangeZoneDialog(dialog, self.app, i), str(i))
416                         self.dialog.set_data(i, w)
417                         vbox.pack_start(w, False, False)
418
419         return vbox
420
421     def make_label(self, i, z):
422         if z:
423             if 'desc' in self.zones[z] and self.zones[z]['desc'] != '':
424                 z = self.zones[z]['desc']
425         else:
426             z = _("Unknown")
427         txt = '%s - %s' % (i, z)
428         return txt
429
430     def zone_changed(self, iface, zone):
431         if not self.dialog:
432             return
433
434         self.ifaces[iface] = zone
435         button = self.dialog.get_data(iface)
436         txt = self.make_label(iface, zone)
437         if button:
438             button.set_label(txt)
439
440     def set_contents(self):
441
442         dialog = self.dialog
443
444         vbox = gtk.VBox()
445         vbox.set_border_width(3)
446
447         h = gtk.HBox()
448         i = gtk.image_new_from_file(self.app.icon.iconfile)
449         i.set_alignment(1, 0.5)
450         l = gtk.Label(_("Firewall Zone Switcher"))
451         l.set_alignment(0, 0.5)
452         h.pack_start(i, True, True)
453         h.pack_start(l, True, True)
454         vbox.pack_start(h, False, False)
455
456         frame = gtk.Frame(_("Interfaces"))
457         frame.set_border_width(3)
458         frame.add(self.create_button_area(dialog))
459         vbox.pack_start(frame, True, True, 0)
460
461         b = gtk.Button(_("Settings..."))
462         b.connect('clicked', lambda button, *args: SettingsDialog(dialog, self.app))
463         h = gtk.HBox()
464         h.pack_start(b, False, False, 0)
465         vbox.pack_start(h, False, False, 0)
466
467         vbox.show_all()
468
469         if self.content:
470             dialog.get_child().remove(self.content)
471         self.content = vbox
472         dialog.get_child().pack_start(self.content)
473
474     def response(self, id):
475         self.dialog.destroy()
476         self.dialog = None
477         self.app.overview_dialog = None
478
479         if not self.app.icon.isshown():
480             gtk.main_quit()
481
482     def cancel(self):
483         self.dialog.response(gtk.RESPONSE_CANCEL)
484
485 class fwzsApp:
486
487     def __init__(self, trayonly=False, delay=0):
488         self.bus = self.obj = self.iface = None
489         self.config = Config()
490         self.icon = StatusIcon(self)
491         self.overview_dialog = None
492         self.running = None;
493         self.signalreceivers = []
494
495         if delay:
496             glib.timeout_add_seconds(delay, self.startup_timer)
497         else:
498             self.check_status()
499
500         if trayonly or self.config.getbool('general', 'systray', False):
501             self.icon.show()
502
503         if not trayonly:
504             self.overview_dialog = OverviewDialog(self)
505
506     def startup_timer(self):
507         if not self.bus:
508             self.check_status()
509         return False
510
511     def nameowner_changed_handler(self, name, old, new):
512         if name != 'org.opensuse.zoneswitcher':
513             return
514         
515         if(not new and old):
516             self.obj = self.iface = None
517             self.running = None
518             for sig in self.signalreceivers:
519                 sig.remove()
520             self.signalreceivers = []
521             self.icon.update()
522
523         elif(not old and new):
524             self.check_status()
525             self._connect_signals(new)
526
527     def _connect_signals(self, sender):
528             sig = self.bus.add_signal_receiver(
529                     lambda iface, zone: self._zone_changed_receive(iface, zone),
530                         dbus_interface='org.opensuse.zoneswitcher',
531                         bus_name = sender, signal_name='ZoneChanged')
532             self.signalreceivers.append(sig)
533             sig = self.bus.add_signal_receiver(
534                     lambda: self._has_run_received(),
535                         dbus_interface='org.opensuse.zoneswitcher',
536                         bus_name = sender, signal_name='HasRun')
537             self.signalreceivers.append(sig)
538
539     def _zone_changed_receive(self, iface, zone):
540         print "got zone change: ", iface, zone
541         if self.overview_dialog:
542             self.overview_dialog.zone_changed(iface, zone)
543
544     def _has_run_received(self):
545         print "got HasRun"
546         if self.overview_dialog:
547             self.overview_dialog.set_contents()
548
549     def catchall_handler(self, *args, **kwargs):
550         print "args: ", args
551         print "kwargs: ", kwargs
552
553     def bus_disconnected(self):
554         print "bus disconnected"
555         self.icon.grey()
556
557     def check_status(self):
558         try: 
559             self.getzsiface()
560             try: 
561                 if self.iface.Status():
562                     self.running = True;
563                     self.icon.update()
564                     return
565                 else:
566                     self.running = False;
567                     self.icon.update()
568                     return
569             except Exception, e:
570                 print e
571
572         except:
573             pass
574
575         self.running = None
576         self.icon.update()
577
578     def getzsiface(self):
579         if(not self.bus):
580             try:
581                 self.bus = dbus.SystemBus()
582
583                 self.bus.call_on_disconnection(lambda arg: self.bus_disconnected())
584
585                 self.bus.set_exit_on_disconnect(False)
586
587 #               self.bus.add_signal_receiver(
588 #                       lambda *args: self.catchall_handler(args))
589
590                 self.bus.add_signal_receiver(
591                         lambda name, old, new: self.nameowner_changed_handler(name, old, new),
592                         dbus_interface='org.freedesktop.DBus',
593                         signal_name='NameOwnerChanged')
594
595             except dbus.DBusException, e:
596                 print "can't connect to bus:", str(e)
597                 self.bus = self.obj = self.iface = None
598                 return None
599
600         if(not (self.obj and self.iface)):
601             try:
602                 self.obj = self.bus.get_object("org.opensuse.zoneswitcher",
603                                                "/org/opensuse/zoneswitcher0")
604
605                 self.iface = dbus.Interface(self.obj, "org.opensuse.zoneswitcher")
606
607                 l = locale.getlocale(locale.LC_MESSAGES)
608                 if l[0]:
609                     self.iface.setLang(l[0])
610
611                 self._connect_signals(self.obj.bus_name)
612
613                 #print self.obj.Introspect(dbus_interface="org.freedesktop.DBus.Introspectable")
614
615             except dbus.DBusException, e:
616                 self.obj = self.iface = None
617                 print "can't connect to zoneswitcher:", e
618                 return None
619
620         return self.iface
621
622     def error_dialog(self, msg, title=_("Firewall Error")):
623         d = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, msg)
624         d.set_title(title)
625         d.run()
626         d.destroy()
627
628     def set_zone(self, iface, zone):
629         repeat = True
630         while repeat:
631             repeat = False
632             try:
633                 self.iface.setZone(iface, zone)
634                 self.run_firewall()
635             except dbus.DBusException, e:
636                 if e.get_dbus_name() == 'org.freedesktop.PolicyKit.NotPrivilegedException':
637                     if self.polkitauth(Exception.__str__(e)):
638                         repeat = True
639                 else:
640                     self.error_dialog(str(e))
641             except Exception, e:
642                 self.error_dialog(str(e))
643                 return
644
645     def run_firewall(self):
646         ret = False
647         repeat = True
648         while repeat:
649             repeat = False
650             try:
651                 ret = self.iface.Run()
652             except dbus.DBusException, e:
653                 if e.get_dbus_name() == 'org.freedesktop.PolicyKit.NotPrivilegedException':
654                     if self.polkitauth(Exception.__str__(e)):
655                         repeat = True
656                 else:
657                     self.error_dialog(str(e))
658             except Exception, e:
659                 self.error_dialog(str(e))
660
661         self.check_status()
662         return ret
663
664     def toggle_overview_dialog(self):
665         if self.overview_dialog:
666             self.overview_dialog.cancel()
667         else:
668             self.check_status()
669             self.overview_dialog = OverviewDialog(self)
670
671     def polkitauth(self, action):
672         ok = False
673         try:
674             agent = dbus.SessionBus().get_object("org.freedesktop.PolicyKit.AuthenticationAgent", "/")
675             ok = agent.ObtainAuthorization(action, dbus.UInt32(0), dbus.UInt32(os.getpid()));
676         except dbus.DBusException, e:
677             if e.get_dbus_name() == 'org.freedesktop.DBus.Error.ServiceUnknown':
678                 self.error_dialog(_("The PolicyKit Authentication Agent is not available.\nTry installing 'PolicyKit-gnome'."))
679             else:
680                 raise
681         return ok
682
683 if __name__ == '__main__':
684     from optparse import OptionParser
685
686     parser = OptionParser(usage="%prog [options]")
687     parser.add_option('--tray', dest="tray", action='store_true',
688             default=False, help="start as system tray icon")
689     parser.add_option('--delay', dest="delay", metavar='N',
690             action='store', type='int', default=0,
691             help="when started in system tray, delay status query N seconds")
692
693     (opts, args) = parser.parse_args()
694
695     dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
696     app = fwzsApp(trayonly = opts.tray, delay=opts.delay);
697     gtk.main()
698
699 # vim: sw=4 ts=8 noet