2
# -*- mode: python; coding: utf-8 -*-
4
from __future__ import division, absolute_import, with_statement
12
import urwid.curses_display
15
from dbus.mainloop.glib import DBusGMainLoop
24
locale.setlocale(locale.LC_ALL, u'')
26
# Some useful constants
27
domain = 'se.bsnet.fukt'
28
server_interface = domain + '.Mandos'
29
client_interface = domain + '.Mandos.Client'
32
# Always run in monochrome mode
33
urwid.curses_display.curses.has_colors = lambda : False
35
# Urwid doesn't support blinking, but we want it. Since we have no
36
# use for underline on its own, we make underline also always blink.
37
urwid.curses_display.curses.A_UNDERLINE |= (
38
urwid.curses_display.curses.A_BLINK)
40
class MandosClientPropertyCache(object):
41
"""This wraps a Mandos Client D-Bus proxy object, caches the
42
properties and calls a hook function when any of them are
45
def __init__(self, proxy_object=None, *args, **kwargs):
46
self.proxy = proxy_object # Mandos Client proxy object
48
self.properties = dict()
49
self.proxy.connect_to_signal(u"PropertyChanged",
50
self.property_changed,
54
self.properties.update(
55
self.proxy.GetAll(client_interface,
56
dbus_interface = dbus.PROPERTIES_IFACE))
57
super(MandosClientPropertyCache, self).__init__(
58
proxy_object=proxy_object, *args, **kwargs)
60
def property_changed(self, property=None, value=None):
61
"""This is called whenever we get a PropertyChanged signal
62
It updates the changed property in the "properties" dict.
64
# Update properties dict with new value
65
self.properties[property] = value
68
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
69
"""A Mandos Client which is visible on the screen.
72
def __init__(self, server_proxy_object=None, update_hook=None,
73
delete_hook=None, logger=None, *args, **kwargs):
75
self.update_hook = update_hook
77
self.delete_hook = delete_hook
78
# Mandos Server proxy object
79
self.server_proxy_object = server_proxy_object
83
# The widget shown normally
84
self._text_widget = urwid.Text(u"")
85
# The widget shown when we have focus
86
self._focus_text_widget = urwid.Text(u"")
87
super(MandosClientWidget, self).__init__(
88
update_hook=update_hook, delete_hook=delete_hook,
92
self.proxy.connect_to_signal(u"CheckerCompleted",
93
self.checker_completed,
96
self.proxy.connect_to_signal(u"CheckerStarted",
100
self.proxy.connect_to_signal(u"GotSecret",
104
self.proxy.connect_to_signal(u"Rejected",
109
def checker_completed(self, exitstatus, condition, command):
111
self.logger(u'Checker for client %s (command "%s")'
113
% (self.properties[u"name"], command))
115
if os.WIFEXITED(condition):
116
self.logger(u'Checker for client %s (command "%s")'
117
u' failed with exit code %s'
118
% (self.properties[u"name"], command,
119
os.WEXITSTATUS(condition)))
121
if os.WIFSIGNALED(condition):
122
self.logger(u'Checker for client %s (command "%s")'
123
u' was killed by signal %s'
124
% (self.properties[u"name"], command,
125
os.WTERMSIG(condition)))
127
if os.WCOREDUMP(condition):
128
self.logger(u'Checker for client %s (command "%s")'
130
% (self.properties[u"name"], command))
131
self.logger(u'Checker for client %s completed mysteriously')
133
def checker_started(self, command):
134
self.logger(u'Client %s started checker "%s"'
135
% (self.properties[u"name"], unicode(command)))
137
def got_secret(self):
138
self.logger(u'Client %s received its secret'
139
% self.properties[u"name"])
142
self.logger(u'Client %s was rejected'
143
% self.properties[u"name"])
145
def selectable(self):
146
"""Make this a "selectable" widget.
147
This overrides the method from urwid.FlowWidget."""
150
def rows(self, (maxcol,), focus=False):
151
"""How many rows this widget will occupy might depend on
152
whether we have focus or not.
153
This overrides the method from urwid.FlowWidget"""
154
return self.current_widget(focus).rows((maxcol,), focus=focus)
156
def current_widget(self, focus=False):
157
if focus or self.opened:
158
return self._focus_widget
162
"Called when what is visible on the screen should be updated."
163
# How to add standout mode to a style
164
with_standout = { u"normal": u"standout",
165
u"bold": u"bold-standout",
167
u"underline-blink-standout",
168
u"bold-underline-blink":
169
u"bold-underline-blink-standout",
172
# Rebuild focus and non-focus widgets using current properties
173
self._text = (u'%(name)s: %(enabled)s'
174
% { u"name": self.properties[u"name"],
177
if self.properties[u"enabled"]
179
if not urwid.supports_unicode():
180
self._text = self._text.encode("ascii", "replace")
181
textlist = [(u"normal", self._text)]
182
self._text_widget.set_text(textlist)
183
self._focus_text_widget.set_text([(with_standout[text[0]],
185
if isinstance(text, tuple)
187
for text in textlist])
188
self._widget = self._text_widget
189
self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
191
# Run update hook, if any
192
if self.update_hook is not None:
196
if self.delete_hook is not None:
197
self.delete_hook(self)
199
def render(self, (maxcol,), focus=False):
200
"""Render differently if we have focus.
201
This overrides the method from urwid.FlowWidget"""
202
return self.current_widget(focus).render((maxcol,),
205
def keypress(self, (maxcol,), key):
207
This overrides the method from urwid.FlowWidget"""
208
if key == u"e" or key == u"+":
210
elif key == u"d" or key == u"-":
212
elif key == u"r" or key == u"_" or key == u"ctrl k":
213
self.server_proxy_object.RemoveClient(self.proxy
216
self.proxy.StartChecker()
218
self.proxy.StopChecker()
220
self.proxy.CheckedOK()
222
# elif key == u"p" or key == "=":
224
# elif key == u"u" or key == ":":
225
# self.proxy.unpause()
226
# elif key == u"RET":
231
def property_changed(self, property=None, value=None,
233
"""Call self.update() if old value is not new value.
234
This overrides the method from MandosClientPropertyCache"""
235
property_name = unicode(property)
236
old_value = self.properties.get(property_name)
237
super(MandosClientWidget, self).property_changed(
238
property=property, value=value, *args, **kwargs)
239
if self.properties.get(property_name) != old_value:
243
class ConstrainedListBox(urwid.ListBox):
244
"""Like a normal urwid.ListBox, but will consume all "up" or
245
"down" key presses, thus not allowing any containing widgets to
246
use them as an excuse to shift focus away from this widget.
248
def keypress(self, (maxcol, maxrow), key):
249
ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
250
if ret in (u"up", u"down"):
255
class UserInterface(object):
256
"""This is the entire user interface - the whole screen
257
with boxes, lists of client widgets, etc.
259
def __init__(self, max_log_length=1000):
260
DBusGMainLoop(set_as_default=True)
262
self.screen = urwid.curses_display.Screen()
264
self.screen.register_palette((
266
u"default", u"default", None),
268
u"default", u"default", u"bold"),
270
u"default", u"default", u"underline"),
272
u"default", u"default", u"standout"),
273
(u"bold-underline-blink",
274
u"default", u"default", (u"bold", u"underline")),
276
u"default", u"default", (u"bold", u"standout")),
277
(u"underline-blink-standout",
278
u"default", u"default", (u"underline", u"standout")),
279
(u"bold-underline-blink-standout",
280
u"default", u"default", (u"bold", u"underline",
284
if urwid.supports_unicode():
285
self.divider = u"─" # \u2500
286
#self.divider = u"━" # \u2501
288
#self.divider = u"-" # \u002d
289
self.divider = u"_" # \u005f
293
self.size = self.screen.get_cols_rows()
295
self.clients = urwid.SimpleListWalker([])
296
self.clients_dict = {}
298
# We will add Text widgets to this list
300
self.max_log_length = max_log_length
302
# We keep a reference to the log widget so we can remove it
303
# from the ListWalker without it getting destroyed
304
self.logbox = ConstrainedListBox(self.log)
306
# This keeps track of whether self.uilist currently has
307
# self.logbox in it or not
308
self.log_visible = True
309
self.log_wrap = u"any"
312
self.log_message_raw((u"bold",
313
u"Mandos Monitor version " + version))
314
self.log_message_raw((u"bold",
317
self.busname = domain + '.Mandos'
318
self.main_loop = gobject.MainLoop()
319
self.bus = dbus.SystemBus()
320
mandos_dbus_objc = self.bus.get_object(
321
self.busname, u"/", follow_name_owner_changes=True)
322
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
326
mandos_clients = (self.mandos_serv
327
.GetAllClientsWithProperties())
328
except dbus.exceptions.DBusException:
329
mandos_clients = dbus.Dictionary()
332
.connect_to_signal(u"ClientRemoved",
333
self.find_and_remove_client,
334
dbus_interface=server_interface,
337
.connect_to_signal(u"ClientAdded",
339
dbus_interface=server_interface,
342
.connect_to_signal(u"ClientNotFound",
343
self.client_not_found,
344
dbus_interface=server_interface,
346
for path, client in mandos_clients.iteritems():
347
client_proxy_object = self.bus.get_object(self.busname,
349
self.add_client(MandosClientWidget(server_proxy_object
352
=client_proxy_object,
362
def client_not_found(self, fingerprint, address):
363
self.log_message((u"Client with address %s and fingerprint %s"
364
u" could not be found" % (address,
368
"""This rebuilds the User Interface.
369
Call this when the widget layout needs to change"""
371
#self.uilist.append(urwid.ListBox(self.clients))
372
self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
373
#header=urwid.Divider(),
375
footer=urwid.Divider(div_char=self.divider)))
377
self.uilist.append(self.logbox)
379
self.topwidget = urwid.Pile(self.uilist)
381
def log_message(self, message):
382
timestamp = datetime.datetime.now().isoformat()
383
self.log_message_raw(timestamp + u": " + message)
385
def log_message_raw(self, markup):
386
"""Add a log message to the log buffer."""
387
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
388
if (self.max_log_length
389
and len(self.log) > self.max_log_length):
390
del self.log[0:len(self.log)-self.max_log_length-1]
391
self.logbox.set_focus(len(self.logbox.body.contents),
392
coming_from=u"above")
395
def toggle_log_display(self):
396
"""Toggle visibility of the log buffer."""
397
self.log_visible = not self.log_visible
399
self.log_message(u"Log visibility changed to: "
400
+ unicode(self.log_visible))
402
def change_log_display(self):
403
"""Change type of log display.
404
Currently, this toggles wrapping of text lines."""
405
if self.log_wrap == u"clip":
406
self.log_wrap = u"any"
408
self.log_wrap = u"clip"
409
for textwidget in self.log:
410
textwidget.set_wrap_mode(self.log_wrap)
411
self.log_message(u"Wrap mode: " + self.log_wrap)
413
def find_and_remove_client(self, path, name):
414
"""Find an client from its object path and remove it.
416
This is connected to the ClientRemoved signal from the
417
Mandos server object."""
419
client = self.clients_dict[path]
423
self.remove_client(client, path)
425
def add_new_client(self, path):
426
client_proxy_object = self.bus.get_object(self.busname, path)
427
self.add_client(MandosClientWidget(server_proxy_object
430
=client_proxy_object,
439
def add_client(self, client, path=None):
440
self.clients.append(client)
442
path = client.proxy.object_path
443
self.clients_dict[path] = client
444
self.clients.sort(None, lambda c: c.properties[u"name"])
447
def remove_client(self, client, path=None):
448
self.clients.remove(client)
450
path = client.proxy.object_path
451
del self.clients_dict[path]
452
if not self.clients_dict:
453
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
454
# is completely emptied, we need to recreate it.
455
self.clients = urwid.SimpleListWalker([])
460
"""Redraw the screen"""
461
canvas = self.topwidget.render(self.size, focus=True)
462
self.screen.draw_screen(self.size, canvas)
465
"""Start the main loop and exit when it's done."""
467
self._input_callback_tag = (gobject.io_add_watch
472
# Main loop has finished, we should close everything now
473
gobject.source_remove(self._input_callback_tag)
477
self.main_loop.quit()
479
def process_input(self, source, condition):
480
keys = self.screen.get_input()
481
translations = { u"ctrl n": u"down", # Emacs
482
u"ctrl p": u"up", # Emacs
483
u"ctrl v": u"page down", # Emacs
484
u"meta v": u"page up", # Emacs
485
u" ": u"page down", # less
486
u"f": u"page down", # less
487
u"b": u"page up", # less
493
key = translations[key]
494
except KeyError: # :-)
497
if key == u"q" or key == u"Q":
500
elif key == u"window resize":
501
self.size = self.screen.get_cols_rows()
503
elif key == u"\f": # Ctrl-L
505
elif key == u"l" or key == u"D":
506
self.toggle_log_display()
508
elif key == u"w" or key == u"i":
509
self.change_log_display()
511
elif key == u"?" or key == u"f1" or key == u"esc":
512
if not self.log_visible:
513
self.log_visible = True
515
self.log_message_raw((u"bold",
519
u"l: Log window toggle",
520
u"TAB: Switch window",
522
self.log_message_raw((u"bold",
528
u"s: Start new checker",
533
if self.topwidget.get_focus() is self.logbox:
534
self.topwidget.set_focus(0)
536
self.topwidget.set_focus(self.logbox)
538
#elif (key == u"end" or key == u"meta >" or key == u"G"
540
# pass # xxx end-of-buffer
541
#elif (key == u"home" or key == u"meta <" or key == u"g"
543
# pass # xxx beginning-of-buffer
544
#elif key == u"ctrl e" or key == u"$":
545
# pass # xxx move-end-of-line
546
#elif key == u"ctrl a" or key == u"^":
547
# pass # xxx move-beginning-of-line
548
#elif key == u"ctrl b" or key == u"meta (" or key == u"h":
550
#elif key == u"ctrl f" or key == u"meta )" or key == u"l":
553
# pass # scroll up log
555
# pass # scroll down log
556
elif self.topwidget.selectable():
557
self.topwidget.keypress(self.size, key)
565
ui.log_message(unicode(e))