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