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
def isoformat_to_datetime(iso):
41
"Parse an ISO 8601 date string to a datetime.datetime()"
44
d, t = iso.split(u"T", 1)
45
year, month, day = d.split(u"-", 2)
46
hour, minute, second = t.split(u":", 2)
47
second, fraction = divmod(float(second), 1)
48
return datetime.datetime(int(year),
53
int(second), # Whole seconds
54
int(fraction*1000000)) # Microseconds
56
class MandosClientPropertyCache(object):
57
"""This wraps a Mandos Client D-Bus proxy object, caches the
58
properties and calls a hook function when any of them are
61
def __init__(self, proxy_object=None, *args, **kwargs):
62
self.proxy = proxy_object # Mandos Client proxy object
64
self.properties = dict()
65
self.proxy.connect_to_signal(u"PropertyChanged",
66
self.property_changed,
70
self.properties.update(
71
self.proxy.GetAll(client_interface,
72
dbus_interface = dbus.PROPERTIES_IFACE))
73
super(MandosClientPropertyCache, self).__init__(
74
proxy_object=proxy_object, *args, **kwargs)
76
def property_changed(self, property=None, value=None):
77
"""This is called whenever we get a PropertyChanged signal
78
It updates the changed property in the "properties" dict.
80
# Update properties dict with new value
81
self.properties[property] = value
84
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
85
"""A Mandos Client which is visible on the screen.
88
def __init__(self, server_proxy_object=None, update_hook=None,
89
delete_hook=None, logger=None, *args, **kwargs):
91
self.update_hook = update_hook
93
self.delete_hook = delete_hook
94
# Mandos Server proxy object
95
self.server_proxy_object = server_proxy_object
99
self._update_timer_callback_tag = None
100
self.last_checker_failed = False
102
# The widget shown normally
103
self._text_widget = urwid.Text(u"")
104
# The widget shown when we have focus
105
self._focus_text_widget = urwid.Text(u"")
106
super(MandosClientWidget, self).__init__(
107
update_hook=update_hook, delete_hook=delete_hook,
111
self.proxy.connect_to_signal(u"CheckerCompleted",
112
self.checker_completed,
115
self.proxy.connect_to_signal(u"CheckerStarted",
116
self.checker_started,
119
self.proxy.connect_to_signal(u"GotSecret",
123
self.proxy.connect_to_signal(u"Rejected",
127
last_checked_ok = isoformat_to_datetime(self.properties
129
if last_checked_ok is None:
130
self.last_checker_failed = True
132
self.last_checker_failed = ((datetime.datetime.utcnow()
136
self.properties["interval"]))
137
if self.last_checker_failed:
138
self._update_timer_callback_tag = (gobject.timeout_add
142
def checker_completed(self, exitstatus, condition, command):
144
if self.last_checker_failed:
145
self.last_checker_failed = False
146
gobject.source_remove(self._update_timer_callback_tag)
147
self._update_timer_callback_tag = None
148
self.logger(u'Checker for client %s (command "%s")'
150
% (self.properties[u"name"], command))
154
if not self.last_checker_failed:
155
self.last_checker_failed = True
156
self._update_timer_callback_tag = (gobject.timeout_add
159
if os.WIFEXITED(condition):
160
self.logger(u'Checker for client %s (command "%s")'
161
u' failed with exit code %s'
162
% (self.properties[u"name"], command,
163
os.WEXITSTATUS(condition)))
164
elif os.WIFSIGNALED(condition):
165
self.logger(u'Checker for client %s (command "%s")'
166
u' was killed by signal %s'
167
% (self.properties[u"name"], command,
168
os.WTERMSIG(condition)))
169
elif os.WCOREDUMP(condition):
170
self.logger(u'Checker for client %s (command "%s")'
172
% (self.properties[u"name"], command))
174
self.logger(u'Checker for client %s completed mysteriously')
177
def checker_started(self, command):
178
self.logger(u'Client %s started checker "%s"'
179
% (self.properties[u"name"], unicode(command)))
181
def got_secret(self):
182
self.logger(u'Client %s received its secret'
183
% self.properties[u"name"])
186
self.logger(u'Client %s was rejected'
187
% self.properties[u"name"])
189
def selectable(self):
190
"""Make this a "selectable" widget.
191
This overrides the method from urwid.FlowWidget."""
194
def rows(self, (maxcol,), focus=False):
195
"""How many rows this widget will occupy might depend on
196
whether we have focus or not.
197
This overrides the method from urwid.FlowWidget"""
198
return self.current_widget(focus).rows((maxcol,), focus=focus)
200
def current_widget(self, focus=False):
201
if focus or self.opened:
202
return self._focus_widget
206
"Called when what is visible on the screen should be updated."
207
# How to add standout mode to a style
208
with_standout = { u"normal": u"standout",
209
u"bold": u"bold-standout",
211
u"underline-blink-standout",
212
u"bold-underline-blink":
213
u"bold-underline-blink-standout",
216
# Rebuild focus and non-focus widgets using current properties
217
self._text = (u'%(name)s: %(enabled)s%(timer)s'
218
% { u"name": self.properties[u"name"],
221
if self.properties[u"enabled"]
223
u"timer": (unicode(datetime.timedelta
229
- isoformat_to_datetime
230
(max((self.properties
235
self.properties[u"last_enabled"]))))
236
if (self.last_checker_failed
240
if not urwid.supports_unicode():
241
self._text = self._text.encode("ascii", "replace")
242
textlist = [(u"normal", self._text)]
243
self._text_widget.set_text(textlist)
244
self._focus_text_widget.set_text([(with_standout[text[0]],
246
if isinstance(text, tuple)
248
for text in textlist])
249
self._widget = self._text_widget
250
self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
252
# Run update hook, if any
253
if self.update_hook is not None:
256
def update_timer(self):
259
return True # Keep calling this
262
if self._update_timer_callback_tag is not None:
263
gobject.source_remove(self._update_timer_callback_tag)
264
self._update_timer_callback_tag = None
265
if self.delete_hook is not None:
266
self.delete_hook(self)
268
def render(self, (maxcol,), focus=False):
269
"""Render differently if we have focus.
270
This overrides the method from urwid.FlowWidget"""
271
return self.current_widget(focus).render((maxcol,),
274
def keypress(self, (maxcol,), key):
276
This overrides the method from urwid.FlowWidget"""
277
if key == u"e" or key == u"+":
279
elif key == u"d" or key == u"-":
281
elif key == u"r" or key == u"_" or key == u"ctrl k":
282
self.server_proxy_object.RemoveClient(self.proxy
285
self.proxy.StartChecker()
287
self.proxy.StopChecker()
289
self.proxy.CheckedOK()
291
# elif key == u"p" or key == "=":
293
# elif key == u"u" or key == ":":
294
# self.proxy.unpause()
295
# elif key == u"RET":
300
def property_changed(self, property=None, value=None,
302
"""Call self.update() if old value is not new value.
303
This overrides the method from MandosClientPropertyCache"""
304
property_name = unicode(property)
305
old_value = self.properties.get(property_name)
306
super(MandosClientWidget, self).property_changed(
307
property=property, value=value, *args, **kwargs)
308
if self.properties.get(property_name) != old_value:
312
class ConstrainedListBox(urwid.ListBox):
313
"""Like a normal urwid.ListBox, but will consume all "up" or
314
"down" key presses, thus not allowing any containing widgets to
315
use them as an excuse to shift focus away from this widget.
317
def keypress(self, (maxcol, maxrow), key):
318
ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
319
if ret in (u"up", u"down"):
324
class UserInterface(object):
325
"""This is the entire user interface - the whole screen
326
with boxes, lists of client widgets, etc.
328
def __init__(self, max_log_length=1000):
329
DBusGMainLoop(set_as_default=True)
331
self.screen = urwid.curses_display.Screen()
333
self.screen.register_palette((
335
u"default", u"default", None),
337
u"default", u"default", u"bold"),
339
u"default", u"default", u"underline"),
341
u"default", u"default", u"standout"),
342
(u"bold-underline-blink",
343
u"default", u"default", (u"bold", u"underline")),
345
u"default", u"default", (u"bold", u"standout")),
346
(u"underline-blink-standout",
347
u"default", u"default", (u"underline", u"standout")),
348
(u"bold-underline-blink-standout",
349
u"default", u"default", (u"bold", u"underline",
353
if urwid.supports_unicode():
354
self.divider = u"─" # \u2500
355
#self.divider = u"━" # \u2501
357
#self.divider = u"-" # \u002d
358
self.divider = u"_" # \u005f
362
self.size = self.screen.get_cols_rows()
364
self.clients = urwid.SimpleListWalker([])
365
self.clients_dict = {}
367
# We will add Text widgets to this list
369
self.max_log_length = max_log_length
371
# We keep a reference to the log widget so we can remove it
372
# from the ListWalker without it getting destroyed
373
self.logbox = ConstrainedListBox(self.log)
375
# This keeps track of whether self.uilist currently has
376
# self.logbox in it or not
377
self.log_visible = True
378
self.log_wrap = u"any"
381
self.log_message_raw((u"bold",
382
u"Mandos Monitor version " + version))
383
self.log_message_raw((u"bold",
386
self.busname = domain + '.Mandos'
387
self.main_loop = gobject.MainLoop()
388
self.bus = dbus.SystemBus()
389
mandos_dbus_objc = self.bus.get_object(
390
self.busname, u"/", follow_name_owner_changes=True)
391
self.mandos_serv = dbus.Interface(mandos_dbus_objc,
395
mandos_clients = (self.mandos_serv
396
.GetAllClientsWithProperties())
397
except dbus.exceptions.DBusException:
398
mandos_clients = dbus.Dictionary()
401
.connect_to_signal(u"ClientRemoved",
402
self.find_and_remove_client,
403
dbus_interface=server_interface,
406
.connect_to_signal(u"ClientAdded",
408
dbus_interface=server_interface,
411
.connect_to_signal(u"ClientNotFound",
412
self.client_not_found,
413
dbus_interface=server_interface,
415
for path, client in mandos_clients.iteritems():
416
client_proxy_object = self.bus.get_object(self.busname,
418
self.add_client(MandosClientWidget(server_proxy_object
421
=client_proxy_object,
431
def client_not_found(self, fingerprint, address):
432
self.log_message((u"Client with address %s and fingerprint %s"
433
u" could not be found" % (address,
437
"""This rebuilds the User Interface.
438
Call this when the widget layout needs to change"""
440
#self.uilist.append(urwid.ListBox(self.clients))
441
self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
442
#header=urwid.Divider(),
444
footer=urwid.Divider(div_char=self.divider)))
446
self.uilist.append(self.logbox)
448
self.topwidget = urwid.Pile(self.uilist)
450
def log_message(self, message):
451
timestamp = datetime.datetime.now().isoformat()
452
self.log_message_raw(timestamp + u": " + message)
454
def log_message_raw(self, markup):
455
"""Add a log message to the log buffer."""
456
self.log.append(urwid.Text(markup, wrap=self.log_wrap))
457
if (self.max_log_length
458
and len(self.log) > self.max_log_length):
459
del self.log[0:len(self.log)-self.max_log_length-1]
460
self.logbox.set_focus(len(self.logbox.body.contents),
461
coming_from=u"above")
464
def toggle_log_display(self):
465
"""Toggle visibility of the log buffer."""
466
self.log_visible = not self.log_visible
468
self.log_message(u"Log visibility changed to: "
469
+ unicode(self.log_visible))
471
def change_log_display(self):
472
"""Change type of log display.
473
Currently, this toggles wrapping of text lines."""
474
if self.log_wrap == u"clip":
475
self.log_wrap = u"any"
477
self.log_wrap = u"clip"
478
for textwidget in self.log:
479
textwidget.set_wrap_mode(self.log_wrap)
480
self.log_message(u"Wrap mode: " + self.log_wrap)
482
def find_and_remove_client(self, path, name):
483
"""Find an client from its object path and remove it.
485
This is connected to the ClientRemoved signal from the
486
Mandos server object."""
488
client = self.clients_dict[path]
492
self.remove_client(client, path)
494
def add_new_client(self, path):
495
client_proxy_object = self.bus.get_object(self.busname, path)
496
self.add_client(MandosClientWidget(server_proxy_object
499
=client_proxy_object,
508
def add_client(self, client, path=None):
509
self.clients.append(client)
511
path = client.proxy.object_path
512
self.clients_dict[path] = client
513
self.clients.sort(None, lambda c: c.properties[u"name"])
516
def remove_client(self, client, path=None):
517
self.clients.remove(client)
519
path = client.proxy.object_path
520
del self.clients_dict[path]
521
if not self.clients_dict:
522
# Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
523
# is completely emptied, we need to recreate it.
524
self.clients = urwid.SimpleListWalker([])
529
"""Redraw the screen"""
530
canvas = self.topwidget.render(self.size, focus=True)
531
self.screen.draw_screen(self.size, canvas)
534
"""Start the main loop and exit when it's done."""
536
self._input_callback_tag = (gobject.io_add_watch
541
# Main loop has finished, we should close everything now
542
gobject.source_remove(self._input_callback_tag)
546
self.main_loop.quit()
548
def process_input(self, source, condition):
549
keys = self.screen.get_input()
550
translations = { u"ctrl n": u"down", # Emacs
551
u"ctrl p": u"up", # Emacs
552
u"ctrl v": u"page down", # Emacs
553
u"meta v": u"page up", # Emacs
554
u" ": u"page down", # less
555
u"f": u"page down", # less
556
u"b": u"page up", # less
562
key = translations[key]
563
except KeyError: # :-)
566
if key == u"q" or key == u"Q":
569
elif key == u"window resize":
570
self.size = self.screen.get_cols_rows()
572
elif key == u"\f": # Ctrl-L
574
elif key == u"l" or key == u"D":
575
self.toggle_log_display()
577
elif key == u"w" or key == u"i":
578
self.change_log_display()
580
elif key == u"?" or key == u"f1" or key == u"esc":
581
if not self.log_visible:
582
self.log_visible = True
584
self.log_message_raw((u"bold",
588
u"l: Log window toggle",
589
u"TAB: Switch window",
591
self.log_message_raw((u"bold",
597
u"s: Start new checker",
602
if self.topwidget.get_focus() is self.logbox:
603
self.topwidget.set_focus(0)
605
self.topwidget.set_focus(self.logbox)
607
#elif (key == u"end" or key == u"meta >" or key == u"G"
609
# pass # xxx end-of-buffer
610
#elif (key == u"home" or key == u"meta <" or key == u"g"
612
# pass # xxx beginning-of-buffer
613
#elif key == u"ctrl e" or key == u"$":
614
# pass # xxx move-end-of-line
615
#elif key == u"ctrl a" or key == u"^":
616
# pass # xxx move-beginning-of-line
617
#elif key == u"ctrl b" or key == u"meta (" or key == u"h":
619
#elif key == u"ctrl f" or key == u"meta )" or key == u"l":
622
# pass # scroll up log
624
# pass # scroll down log
625
elif self.topwidget.selectable():
626
self.topwidget.keypress(self.size, key)
634
ui.log_message(unicode(e))