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
22
# Some useful constants
23
domain = 'se.bsnet.fukt'
24
server_interface = domain + '.Mandos'
25
client_interface = domain + '.Mandos.Client'
28
# Always run in monochrome mode
29
urwid.curses_display.curses.has_colors = lambda : False
31
# Urwid doesn't support blinking, but we want it. Since we have no
32
# use for underline on its own, we make underline also always blink.
33
urwid.curses_display.curses.A_UNDERLINE |= (
34
urwid.curses_display.curses.A_BLINK)
36
class MandosClientPropertyCache(object):
37
"""This wraps a Mandos Client D-Bus proxy object, caches the
38
properties and calls a hook function when any of them are
41
def __init__(self, proxy_object=None, properties=None, *args,
43
self.proxy = proxy_object # Mandos Client proxy object
45
if properties is None:
46
self.properties = dict()
48
self.properties = properties
49
self.proxy.connect_to_signal(u"PropertyChanged",
50
self.property_changed,
54
if properties is None:
55
self.properties.update(self.proxy.GetAll(client_interface,
57
dbus.PROPERTIES_IFACE))
58
super(MandosClientPropertyCache, self).__init__(
59
proxy_object=proxy_object,
60
properties=properties, *args, **kwargs)
62
def property_changed(self, property=None, value=None):
63
"""This is called whenever we get a PropertyChanged signal
64
It updates the changed property in the "properties" dict.
66
# Update properties dict with new value
67
self.properties[property] = value
70
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
71
"""A Mandos Client which is visible on the screen.
74
def __init__(self, server_proxy_object=None, update_hook=None,
75
delete_hook=None, logger=None, *args, **kwargs):
77
self.update_hook = update_hook
79
self.delete_hook = delete_hook
80
# Mandos Server proxy object
81
self.server_proxy_object = server_proxy_object
85
# The widget shown normally
86
self._text_widget = urwid.Text(u"")
87
# The widget shown when we have focus
88
self._focus_text_widget = urwid.Text(u"")
89
super(MandosClientWidget, self).__init__(
90
update_hook=update_hook, delete_hook=delete_hook,
94
self.proxy.connect_to_signal(u"CheckerCompleted",
95
self.checker_completed,
98
self.proxy.connect_to_signal(u"CheckerStarted",
102
self.proxy.connect_to_signal(u"GotSecret",
106
self.proxy.connect_to_signal(u"Rejected",
111
def checker_completed(self, exitstatus, condition, command):
113
self.logger(u'Checker for client %s (command "%s")'
115
% (self.properties[u"name"], command))
117
if os.WIFEXITED(condition):
118
self.logger(u'Checker for client %s (command "%s")'
119
u' failed with exit code %s'
120
% (self.properties[u"name"], command,
121
os.WEXITSTATUS(condition)))
123
if os.WIFSIGNALED(condition):
124
self.logger(u'Checker for client %s (command "%s")'
125
u' was killed by signal %s'
126
% (self.properties[u"name"], command,
127
os.WTERMSIG(condition)))
129
if os.WCOREDUMP(condition):
130
self.logger(u'Checker for client %s (command "%s")'
132
% (self.properties[u"name"], command))
133
self.logger(u'Checker for client %s completed mysteriously')
135
def checker_started(self, command):
136
self.logger(u'Client %s started checker "%s"'
137
% (self.properties[u"name"], unicode(command)))
139
def got_secret(self):
140
self.logger(u'Client %s received its secret'
141
% self.properties[u"name"])
144
self.logger(u'Client %s was rejected'
145
% self.properties[u"name"])
147
def selectable(self):
148
"""Make this a "selectable" widget.
149
This overrides the method from urwid.FlowWidget."""
152
def rows(self, (maxcol,), focus=False):
153
"""How many rows this widget will occupy might depend on
154
whether we have focus or not.
155
This overrides the method from urwid.FlowWidget"""
156
return self.current_widget(focus).rows((maxcol,), focus=focus)
158
def current_widget(self, focus=False):
159
if focus or self.opened:
160
return self._focus_widget
164
"Called when what is visible on the screen should be updated."
165
# How to add standout mode to a style
166
with_standout = { u"normal": u"standout",
167
u"bold": u"bold-standout",
169
u"underline-blink-standout",
170
u"bold-underline-blink":
171
u"bold-underline-blink-standout",
174
# Rebuild focus and non-focus widgets using current properties
175
self._text = (u'%(name)s: %(enabled)s'
176
% { u"name": self.properties[u"name"],
179
if self.properties[u"enabled"]
181
if not urwid.supports_unicode():
182
self._text = self._text.encode("ascii", "replace")
183
textlist = [(u"normal", self._text)]
184
self._text_widget.set_text(textlist)
185
self._focus_text_widget.set_text([(with_standout[text[0]],
187
if isinstance(text, tuple)
189
for text in textlist])
190
self._widget = self._text_widget
191
self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
193
# Run update hook, if any
194
if self.update_hook is not None:
198
if self.delete_hook is not None:
199
self.delete_hook(self)
201
def render(self, (maxcol,), focus=False):
202
"""Render differently if we have focus.
203
This overrides the method from urwid.FlowWidget"""
204
return self.current_widget(focus).render((maxcol,),
207
def keypress(self, (maxcol,), key):
209
This overrides the method from urwid.FlowWidget"""
210
if key == u"e" or key == u"+":
212
elif key == u"d" or key == u"-":
214
elif key == u"r" or key == u"_" or key == u"ctrl k":
215
self.server_proxy_object.RemoveClient(self.proxy
218
self.proxy.StartChecker()
220
self.proxy.StopChecker()
222
self.proxy.CheckedOK()
224
# elif key == u"p" or key == "=":
226
# elif key == u"u" or key == ":":
227
# self.proxy.unpause()
228
# elif key == u"RET":
233
def property_changed(self, property=None, value=None,
235
"""Call self.update() if old value is not new value.
236
This overrides the method from MandosClientPropertyCache"""
237
property_name = unicode(property)
238
old_value = self.properties.get(property_name)
239
super(MandosClientWidget, self).property_changed(
240
property=property, value=value, *args, **kwargs)
241
if self.properties.get(property_name) != old_value:
245
class ConstrainedListBox(urwid.ListBox):
246
"""Like a normal urwid.ListBox, but will consume all "up" or
247
"down" key presses, thus not allowing any containing widgets to
248
use them as an excuse to shift focus away from this widget.
250
def keypress(self, (maxcol, maxrow), key):
251
ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
252
if ret in (u"up", u"down"):
257
class UserInterface(object):
258
"""This is the entire user interface - the whole screen
259
with boxes, lists of client widgets, etc.
261
def __init__(self, max_log_length=1000):
262
DBusGMainLoop(set_as_default=True)
264
self.screen = urwid.curses_display.Screen()
266
self.screen.register_palette((
268
u"default", u"default", None),
270
u"default", u"default", u"bold"),
272
u"default", u"default", u"underline"),
274
u"default", u"default", u"standout"),
275
(u"bold-underline-blink",
276
u"default", u"default", (u"bold", u"underline")),
278
u"default", u"default", (u"bold", u"standout")),
279
(u"underline-blink-standout",
280
u"default", u"default", (u"underline", u"standout")),
281
(u"bold-underline-blink-standout",
282
u"default", u"default", (u"bold", u"underline",
286
if urwid.supports_unicode():
287
self.divider = u"─" # \u2500
288
#self.divider = u"━" # \u2501
290
#self.divider = u"-" # \u002d
291
self.divider = u"_" # \u005f
295
self.size = self.screen.get_cols_rows()
297
self.clients = urwid.SimpleListWalker([])
298
self.clients_dict = {}
300
# We will add Text widgets to this list
302
self.max_log_length = max_log_length
304
# We keep a reference to the log widget so we can remove it
305
# from the ListWalker without it getting destroyed
306
self.logbox = ConstrainedListBox(self.log)
308
# This keeps track of whether self.uilist currently has
309
# self.logbox in it or not
310
self.log_visible = True
311
self.log_wrap = u"any"
314
self.log_message_raw((u"bold",
315
u"Mandos Monitor version " + version))
316
self.log_message_raw((u"bold",
319
self.busname = domain + '.Mandos'
320
self.main_loop = gobject.MainLoop()
321
self.bus = dbus.SystemBus()
322
mandos_dbus_objc = self.bus.get_object(
323
self.busname, u"/", follow_name_owner_changes=True)
324
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
328
mandos_clients = (self.mandos_serv
329
.GetAllClientsWithProperties())
330
except dbus.exceptions.DBusException:
331
mandos_clients = dbus.Dictionary()
334
.connect_to_signal(u"ClientRemoved",
335
self.find_and_remove_client,
336
dbus_interface=server_interface,
339
.connect_to_signal(u"ClientAdded",
341
dbus_interface=server_interface,
344
.connect_to_signal(u"ClientNotFound",
345
self.client_not_found,
346
dbus_interface=server_interface,
348
for path, client in mandos_clients.iteritems():
349
client_proxy_object = self.bus.get_object(self.busname,
351
self.add_client(MandosClientWidget(server_proxy_object
354
=client_proxy_object,
364
def client_not_found(self, fingerprint, address):
365
self.log_message((u"Client with address %s and fingerprint %s"
366
u" could not be found" % (address,
370
"""This rebuilds the User Interface.
371
Call this when the widget layout needs to change"""
373
#self.uilist.append(urwid.ListBox(self.clients))
374
self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
375
#header=urwid.Divider(),
377
footer=urwid.Divider(div_char=self.divider)))
379
self.uilist.append(self.logbox)
381
self.topwidget = urwid.Pile(self.uilist)
383
def log_message(self, message):
384
timestamp = datetime.datetime.now().isoformat()
385
self.log_message_raw(timestamp + u": " + message)
387
def log_message_raw(self, markup):
388
"""Add a log message to the log buffer."""
389
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
390
if (self.max_log_length
391
and len(self.log) > self.max_log_length):
392
del self.log[0:len(self.log)-self.max_log_length-1]
393
self.logbox.set_focus(len(self.logbox.body.contents),
394
coming_from=u"above")
397
def toggle_log_display(self):
398
"""Toggle visibility of the log buffer."""
399
self.log_visible = not self.log_visible
401
self.log_message(u"Log visibility changed to: "
402
+ unicode(self.log_visible))
404
def change_log_display(self):
405
"""Change type of log display.
406
Currently, this toggles wrapping of text lines."""
407
if self.log_wrap == u"clip":
408
self.log_wrap = u"any"
410
self.log_wrap = u"clip"
411
for textwidget in self.log:
412
textwidget.set_wrap_mode(self.log_wrap)
413
self.log_message(u"Wrap mode: " + self.log_wrap)
415
def find_and_remove_client(self, path, name):
416
"""Find an client from its object path and remove it.
418
This is connected to the ClientRemoved signal from the
419
Mandos server object."""
421
client = self.clients_dict[path]
425
self.remove_client(client, path)
427
def add_new_client(self, path, properties):
428
client_proxy_object = self.bus.get_object(self.busname, path)
429
self.add_client(MandosClientWidget(server_proxy_object
432
=client_proxy_object,
433
properties=properties,
437
=self.remove_client),
440
def add_client(self, client, path=None):
441
self.clients.append(client)
443
path = client.proxy.object_path
444
self.clients_dict[path] = client
445
self.clients.sort(None, lambda c: c.properties[u"name"])
448
def remove_client(self, client, path=None):
449
self.clients.remove(client)
451
path = client.proxy.object_path
452
del self.clients_dict[path]
453
if not self.clients_dict:
454
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
455
# is completely emptied, we need to recreate it.
456
self.clients = urwid.SimpleListWalker([])
461
"""Redraw the screen"""
462
canvas = self.topwidget.render(self.size, focus=True)
463
self.screen.draw_screen(self.size, canvas)
466
"""Start the main loop and exit when it's done."""
468
self._input_callback_tag = (gobject.io_add_watch
473
# Main loop has finished, we should close everything now
474
gobject.source_remove(self._input_callback_tag)
478
self.main_loop.quit()
480
def process_input(self, source, condition):
481
keys = self.screen.get_input()
482
translations = { u"ctrl n": u"down", # Emacs
483
u"ctrl p": u"up", # Emacs
484
u"ctrl v": u"page down", # Emacs
485
u"meta v": u"page up", # Emacs
486
u" ": u"page down", # less
487
u"f": u"page down", # less
488
u"b": u"page up", # less
494
key = translations[key]
495
except KeyError: # :-)
498
if key == u"q" or key == u"Q":
501
elif key == u"window resize":
502
self.size = self.screen.get_cols_rows()
504
elif key == u"\f": # Ctrl-L
506
elif key == u"l" or key == u"D":
507
self.toggle_log_display()
509
elif key == u"w" or key == u"i":
510
self.change_log_display()
512
elif key == u"?" or key == u"f1" or key == u"esc":
513
if not self.log_visible:
514
self.log_visible = True
516
self.log_message_raw((u"bold",
520
u"l: Log window toggle",
521
u"TAB: Switch window",
523
self.log_message_raw((u"bold",
529
u"s: Start new checker",
534
if self.topwidget.get_focus() is self.logbox:
535
self.topwidget.set_focus(0)
537
self.topwidget.set_focus(self.logbox)
539
#elif (key == u"end" or key == u"meta >" or key == u"G"
541
# pass # xxx end-of-buffer
542
#elif (key == u"home" or key == u"meta <" or key == u"g"
544
# pass # xxx beginning-of-buffer
545
#elif key == u"ctrl e" or key == u"$":
546
# pass # xxx move-end-of-line
547
#elif key == u"ctrl a" or key == u"^":
548
# pass # xxx move-beginning-of-line
549
#elif key == u"ctrl b" or key == u"meta (" or key == u"h":
551
#elif key == u"ctrl f" or key == u"meta )" or key == u"l":
554
# pass # scroll up log
556
# pass # scroll down log
557
elif self.topwidget.selectable():
558
self.topwidget.keypress(self.size, key)