37
30
urwid.curses_display.curses.A_UNDERLINE |= (
38
31
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
33
class MandosClientPropertyCache(object):
57
34
"""This wraps a Mandos Client D-Bus proxy object, caches the
58
35
properties and calls a hook function when any of them are
61
def __init__(self, proxy_object=None, *args, **kwargs):
38
def __init__(self, proxy_object=None, properties=None, *args,
40
# Type conversion mapping
42
dbus.ObjectPath: unicode,
44
dbus.Signature: unicode,
52
dbus.Dictionary: dict,
62
59
self.proxy = proxy_object # Mandos Client proxy object
64
self.properties = dict()
65
self.proxy.connect_to_signal(u"PropertyChanged",
61
if properties is None:
62
self.properties = dict()
64
self.properties = dict(self.convert_property(prop, val)
66
properties.iteritems())
67
self.proxy.connect_to_signal("PropertyChanged",
66
68
self.property_changed,
70
self.properties.update(
71
self.proxy.GetAll(client_interface,
72
dbus_interface = dbus.PROPERTIES_IFACE))
72
if properties is None:
73
self.properties.update(
74
self.convert_property(prop, val)
76
self.proxy.GetAll(client_interface,
78
dbus.PROPERTIES_IFACE).iteritems())
73
79
super(MandosClientPropertyCache, self).__init__(
74
proxy_object=proxy_object, *args, **kwargs)
80
proxy_object=proxy_object,
81
properties=properties, *args, **kwargs)
83
def convert_property(self, property, value):
84
"""This converts the arguments from a D-Bus signal, which are
85
D-Bus types, into normal Python types, using a conversion
86
function from "self.type_map".
88
property_name = unicode(property) # Always a dbus.String
89
if isinstance(value, dbus.UTF8String):
90
# Should not happen, but prepare for it anyway
91
value = dbus.String(str(value).decode("utf-8"))
93
convfunc = self.type_map[type(value)]
95
# Unknown type, return unmodified
96
return property_name, value
97
return property_name, convfunc(value)
76
98
def property_changed(self, property=None, value=None):
77
99
"""This is called whenever we get a PropertyChanged signal
78
100
It updates the changed property in the "properties" dict.
102
# Convert name and value
103
property_name, cvalue = self.convert_property(property, value)
80
104
# Update properties dict with new value
81
self.properties[property] = value
105
self.properties[property_name] = cvalue
84
108
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
88
112
def __init__(self, server_proxy_object=None, update_hook=None,
89
delete_hook=None, logger=None, *args, **kwargs):
113
delete_hook=None, *args, **kwargs):
90
114
# Called on update
91
115
self.update_hook = update_hook
92
116
# Called on delete
93
117
self.delete_hook = delete_hook
94
118
# Mandos Server proxy object
95
119
self.server_proxy_object = server_proxy_object
99
self._update_timer_callback_tag = None
100
self.last_checker_failed = False
102
121
# The widget shown normally
103
self._text_widget = urwid.Text(u"")
122
self._text_widget = urwid.Text("")
104
123
# The widget shown when we have focus
105
self._focus_text_widget = urwid.Text(u"")
124
self._focus_text_widget = urwid.Text("")
106
125
super(MandosClientWidget, self).__init__(
107
126
update_hook=update_hook, delete_hook=delete_hook,
110
129
self.opened = False
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
131
def selectable(self):
190
132
"""Make this a "selectable" widget.
216
158
# 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
159
self._text = (u'name="%(name)s", enabled=%(enabled)s'
240
161
if not urwid.supports_unicode():
241
162
self._text = self._text.encode("ascii", "replace")
242
textlist = [(u"normal", self._text)]
163
textlist = [(u"normal", u"BLÄRGH: "), (u"bold", self._text)]
243
164
self._text_widget.set_text(textlist)
244
165
self._focus_text_widget.set_text([(with_standout[text[0]],
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
225
class UserInterface(object):
325
226
"""This is the entire user interface - the whole screen
326
227
with boxes, lists of client widgets, etc.
328
def __init__(self, max_log_length=1000):
329
DBusGMainLoop(set_as_default=True)
230
DBusGMainLoop(set_as_default=True )
331
232
self.screen = urwid.curses_display.Screen()
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
360
254
self.screen.start()
362
256
self.size = self.screen.get_cols_rows()
364
258
self.clients = urwid.SimpleListWalker([])
365
259
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",
260
self.topwidget = urwid.LineBox(urwid.ListBox(self.clients))
261
#self.topwidget = urwid.ListBox(clients)
386
263
self.busname = domain + '.Mandos'
387
264
self.main_loop = gobject.MainLoop()
398
275
mandos_clients = dbus.Dictionary()
400
277
(self.mandos_serv
401
.connect_to_signal(u"ClientRemoved",
278
.connect_to_signal("ClientRemoved",
402
279
self.find_and_remove_client,
403
280
dbus_interface=server_interface,
404
281
byte_arrays=True))
405
282
(self.mandos_serv
406
.connect_to_signal(u"ClientAdded",
283
.connect_to_signal("ClientAdded",
407
284
self.add_new_client,
408
285
dbus_interface=server_interface,
409
286
byte_arrays=True))
411
.connect_to_signal(u"ClientNotFound",
412
self.client_not_found,
413
dbus_interface=server_interface,
415
for path, client in mandos_clients.iteritems():
287
for path, client in (mandos_clients.iteritems()):
416
288
client_proxy_object = self.bus.get_object(self.busname,
418
290
self.add_client(MandosClientWidget(server_proxy_object
298
=self.remove_client),
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
301
def find_and_remove_client(self, path, name):
483
302
"""Find an client from its object path and remove it.
548
361
def process_input(self, source, condition):
549
362
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
363
translations = { u"j": u"down",
569
375
elif key == u"window resize":
570
376
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
380
elif self.topwidget.selectable():
626
381
self.topwidget.keypress(self.size, key)