/mandos/release

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/release

« back to all changes in this revision

Viewing changes to mandos-monitor

* mandos-ctl: Also show "LastApprovalRequest" property.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
# -*- mode: python; coding: utf-8 -*-
 
3
 
 
4
from __future__ import division, absolute_import, with_statement
 
5
 
 
6
import sys
 
7
import os
 
8
import signal
 
9
 
 
10
import datetime
 
11
 
 
12
import urwid.curses_display
 
13
import urwid
 
14
 
 
15
from dbus.mainloop.glib import DBusGMainLoop
 
16
import gobject
 
17
 
 
18
import dbus
 
19
 
 
20
import UserList
 
21
 
 
22
import locale
 
23
 
 
24
locale.setlocale(locale.LC_ALL, u'')
 
25
 
 
26
import logging
 
27
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
 
28
 
 
29
# Some useful constants
 
30
domain = 'se.bsnet.fukt'
 
31
server_interface = domain + '.Mandos'
 
32
client_interface = domain + '.Mandos.Client'
 
33
version = "1.0.15"
 
34
 
 
35
# Always run in monochrome mode
 
36
urwid.curses_display.curses.has_colors = lambda : False
 
37
 
 
38
# Urwid doesn't support blinking, but we want it.  Since we have no
 
39
# use for underline on its own, we make underline also always blink.
 
40
urwid.curses_display.curses.A_UNDERLINE |= (
 
41
    urwid.curses_display.curses.A_BLINK)
 
42
 
 
43
def isoformat_to_datetime(iso):
 
44
    "Parse an ISO 8601 date string to a datetime.datetime()"
 
45
    if not iso:
 
46
        return None
 
47
    d, t = iso.split(u"T", 1)
 
48
    year, month, day = d.split(u"-", 2)
 
49
    hour, minute, second = t.split(u":", 2)
 
50
    second, fraction = divmod(float(second), 1)
 
51
    return datetime.datetime(int(year),
 
52
                             int(month),
 
53
                             int(day),
 
54
                             int(hour),
 
55
                             int(minute),
 
56
                             int(second),           # Whole seconds
 
57
                             int(fraction*1000000)) # Microseconds
 
58
 
 
59
class MandosClientPropertyCache(object):
 
60
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
61
    properties and calls a hook function when any of them are
 
62
    changed.
 
63
    """
 
64
    def __init__(self, proxy_object=None, *args, **kwargs):
 
65
        self.proxy = proxy_object # Mandos Client proxy object
 
66
        
 
67
        self.properties = dict()
 
68
        self.proxy.connect_to_signal(u"PropertyChanged",
 
69
                                     self.property_changed,
 
70
                                     client_interface,
 
71
                                     byte_arrays=True)
 
72
        
 
73
        self.properties.update(
 
74
            self.proxy.GetAll(client_interface,
 
75
                              dbus_interface = dbus.PROPERTIES_IFACE))
 
76
 
 
77
        #XXX This break good super behaviour!
 
78
#        super(MandosClientPropertyCache, self).__init__(
 
79
#            *args, **kwargs)
 
80
    
 
81
    def property_changed(self, property=None, value=None):
 
82
        """This is called whenever we get a PropertyChanged signal
 
83
        It updates the changed property in the "properties" dict.
 
84
        """
 
85
        # Update properties dict with new value
 
86
        self.properties[property] = value
 
87
 
 
88
 
 
89
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
90
    """A Mandos Client which is visible on the screen.
 
91
    """
 
92
    
 
93
    def __init__(self, server_proxy_object=None, update_hook=None,
 
94
                 delete_hook=None, logger=None, *args, **kwargs):
 
95
        # Called on update
 
96
        self.update_hook = update_hook
 
97
        # Called on delete
 
98
        self.delete_hook = delete_hook
 
99
        # Mandos Server proxy object
 
100
        self.server_proxy_object = server_proxy_object
 
101
        # Logger
 
102
        self.logger = logger
 
103
        
 
104
        self._update_timer_callback_tag = None
 
105
        self._update_timer_callback_lock = 0
 
106
        self.last_checker_failed = False
 
107
        
 
108
        # The widget shown normally
 
109
        self._text_widget = urwid.Text(u"")
 
110
        # The widget shown when we have focus
 
111
        self._focus_text_widget = urwid.Text(u"")
 
112
        super(MandosClientWidget, self).__init__(
 
113
            update_hook=update_hook, delete_hook=delete_hook,
 
114
            *args, **kwargs)
 
115
        self.update()
 
116
        self.opened = False
 
117
        
 
118
        last_checked_ok = isoformat_to_datetime(self.properties
 
119
                                                [u"LastCheckedOK"])
 
120
        if last_checked_ok is None:
 
121
            self.last_checker_failed = True
 
122
        else:
 
123
            self.last_checker_failed = ((datetime.datetime.utcnow()
 
124
                                         - last_checked_ok)
 
125
                                        > datetime.timedelta
 
126
                                        (milliseconds=
 
127
                                         self.properties
 
128
                                         [u"Interval"]))
 
129
        
 
130
        if self.last_checker_failed:
 
131
            self.using_timer(True)
 
132
        
 
133
        if self.need_approval:
 
134
            self.using_timer(True)
 
135
        
 
136
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
137
                                     self.checker_completed,
 
138
                                     client_interface,
 
139
                                     byte_arrays=True)
 
140
        self.proxy.connect_to_signal(u"CheckerStarted",
 
141
                                     self.checker_started,
 
142
                                     client_interface,
 
143
                                     byte_arrays=True)
 
144
        self.proxy.connect_to_signal(u"GotSecret",
 
145
                                     self.got_secret,
 
146
                                     client_interface,
 
147
                                     byte_arrays=True)
 
148
        self.proxy.connect_to_signal(u"NeedApproval",
 
149
                                     self.need_approval,
 
150
                                     client_interface,
 
151
                                     byte_arrays=True)
 
152
        self.proxy.connect_to_signal(u"Rejected",
 
153
                                     self.rejected,
 
154
                                     client_interface,
 
155
                                     byte_arrays=True)
 
156
    
 
157
    def property_changed(self, property=None, value=None):
 
158
        super(self, MandosClientWidget).property_changed(property,
 
159
                                                         value)
 
160
        if property == u"ApprovalPending":
 
161
            using_timer(bool(value))
 
162
        
 
163
    def using_timer(self, flag):
 
164
        """Call this method with True or False when timer should be
 
165
        activated or deactivated.
 
166
        """
 
167
        old = self._update_timer_callback_lock
 
168
        if flag:
 
169
            self._update_timer_callback_lock += 1
 
170
        else:
 
171
            self._update_timer_callback_lock -= 1
 
172
        if old == 0 and self._update_timer_callback_lock:
 
173
            self._update_timer_callback_tag = (gobject.timeout_add
 
174
                                               (1000,
 
175
                                                self.update_timer))
 
176
        elif old and self._update_timer_callback_lock == 0:
 
177
            gobject.source_remove(self._update_timer_callback_tag)
 
178
            self._update_timer_callback_tag = None
 
179
    
 
180
    def checker_completed(self, exitstatus, condition, command):
 
181
        if exitstatus == 0:
 
182
            if self.last_checker_failed:
 
183
                self.last_checker_failed = False
 
184
                self.using_timer(False)
 
185
            #self.logger(u'Checker for client %s (command "%s")'
 
186
            #            u' was successful'
 
187
            #            % (self.properties[u"Name"], command))
 
188
            self.update()
 
189
            return
 
190
        # Checker failed
 
191
        if not self.last_checker_failed:
 
192
            self.last_checker_failed = True
 
193
            self.using_timer(True)
 
194
        if os.WIFEXITED(condition):
 
195
            self.logger(u'Checker for client %s (command "%s")'
 
196
                        u' failed with exit code %s'
 
197
                        % (self.properties[u"Name"], command,
 
198
                           os.WEXITSTATUS(condition)))
 
199
        elif os.WIFSIGNALED(condition):
 
200
            self.logger(u'Checker for client %s (command "%s")'
 
201
                        u' was killed by signal %s'
 
202
                        % (self.properties[u"Name"], command,
 
203
                           os.WTERMSIG(condition)))
 
204
        elif os.WCOREDUMP(condition):
 
205
            self.logger(u'Checker for client %s (command "%s")'
 
206
                        u' dumped core'
 
207
                        % (self.properties[u"Name"], command))
 
208
        else:
 
209
            self.logger(u'Checker for client %s completed'
 
210
                        u' mysteriously')
 
211
        self.update()
 
212
    
 
213
    def checker_started(self, command):
 
214
        #self.logger(u'Client %s started checker "%s"'
 
215
        #            % (self.properties[u"Name"], unicode(command)))
 
216
        pass
 
217
    
 
218
    def got_secret(self):
 
219
        self.last_checker_failed = False
 
220
        self.logger(u'Client %s received its secret'
 
221
                    % self.properties[u"Name"])
 
222
    
 
223
    def need_approval(self, timeout, default):
 
224
        if not default:
 
225
            message = u'Client %s needs approval within %s seconds'
 
226
        else:
 
227
            message = u'Client %s will get its secret in %s seconds'
 
228
        self.logger(message
 
229
                    % (self.properties[u"Name"], timeout/1000))
 
230
        self.using_timer(True)
 
231
    
 
232
    def rejected(self, reason):
 
233
        self.logger(u'Client %s was rejected; reason: %s'
 
234
                    % (self.properties[u"Name"], reason))
 
235
    
 
236
    def selectable(self):
 
237
        """Make this a "selectable" widget.
 
238
        This overrides the method from urwid.FlowWidget."""
 
239
        return True
 
240
    
 
241
    def rows(self, (maxcol,), focus=False):
 
242
        """How many rows this widget will occupy might depend on
 
243
        whether we have focus or not.
 
244
        This overrides the method from urwid.FlowWidget"""
 
245
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
246
    
 
247
    def current_widget(self, focus=False):
 
248
        if focus or self.opened:
 
249
            return self._focus_widget
 
250
        return self._widget
 
251
    
 
252
    def update(self):
 
253
        "Called when what is visible on the screen should be updated."
 
254
        # How to add standout mode to a style
 
255
        with_standout = { u"normal": u"standout",
 
256
                          u"bold": u"bold-standout",
 
257
                          u"underline-blink":
 
258
                              u"underline-blink-standout",
 
259
                          u"bold-underline-blink":
 
260
                              u"bold-underline-blink-standout",
 
261
                          }
 
262
 
 
263
        # Rebuild focus and non-focus widgets using current properties
 
264
 
 
265
        # Base part of a client. Name!
 
266
        base = (u'%(name)s: '
 
267
                      % {u"name": self.properties[u"Name"]})
 
268
        if not self.properties[u"Enabled"]:
 
269
            message = u"DISABLED"
 
270
        elif self.properties[u"ApprovalPending"]:
 
271
            timeout = datetime.timedelta(milliseconds
 
272
                                         = self.properties
 
273
                                         [u"ApprovalDelay"])
 
274
            last_approval_request = isoformat_to_datetime(
 
275
                self.properties[u"LastApprovalRequest"])
 
276
            if last_approval_request is not None:
 
277
                timer = timeout - (datetime.datetime.utcnow()
 
278
                                   - last_approval_request)
 
279
            else:
 
280
                timer = datetime.timedelta()
 
281
            if self.properties[u"ApprovedByDefault"]:
 
282
                message = u"Approval in %s. (d)eny?"
 
283
            else:
 
284
                message = u"Denial in %s. (a)pprove?"
 
285
            message = message % unicode(timer).rsplit(".", 1)[0]
 
286
        elif self.last_checker_failed:
 
287
            timeout = datetime.timedelta(milliseconds
 
288
                                         = self.properties
 
289
                                         [u"Timeout"])
 
290
            last_ok = isoformat_to_datetime(
 
291
                max((self.properties[u"LastCheckedOK"]
 
292
                     or self.properties[u"Created"]),
 
293
                    self.properties[u"LastEnabled"]))
 
294
            timer = timeout - (datetime.datetime.utcnow() - last_ok)
 
295
            message = (u'A checker has failed! Time until client'
 
296
                       u' gets disabled: %s'
 
297
                           % unicode(timer).rsplit(".", 1)[0])
 
298
        else:
 
299
            message = u"enabled"
 
300
        self._text = "%s%s" % (base, message)
 
301
            
 
302
        if not urwid.supports_unicode():
 
303
            self._text = self._text.encode("ascii", "replace")
 
304
        textlist = [(u"normal", self._text)]
 
305
        self._text_widget.set_text(textlist)
 
306
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
307
                                           text[1])
 
308
                                          if isinstance(text, tuple)
 
309
                                          else text
 
310
                                          for text in textlist])
 
311
        self._widget = self._text_widget
 
312
        self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
 
313
                                            "standout")
 
314
        # Run update hook, if any
 
315
        if self.update_hook is not None:
 
316
            self.update_hook()
 
317
    
 
318
    def update_timer(self):
 
319
        "called by gobject"
 
320
        self.update()
 
321
        return True             # Keep calling this
 
322
    
 
323
    def delete(self):
 
324
        if self._update_timer_callback_tag is not None:
 
325
            gobject.source_remove(self._update_timer_callback_tag)
 
326
            self._update_timer_callback_tag = None
 
327
        if self.delete_hook is not None:
 
328
            self.delete_hook(self)
 
329
    
 
330
    def render(self, (maxcol,), focus=False):
 
331
        """Render differently if we have focus.
 
332
        This overrides the method from urwid.FlowWidget"""
 
333
        return self.current_widget(focus).render((maxcol,),
 
334
                                                 focus=focus)
 
335
    
 
336
    def keypress(self, (maxcol,), key):
 
337
        """Handle keys.
 
338
        This overrides the method from urwid.FlowWidget"""
 
339
        if key == u"+":
 
340
            self.proxy.Enable(dbus_interface = client_interface)
 
341
        elif key == u"-":
 
342
            self.proxy.Disable(dbus_interface = client_interface)
 
343
        elif key == u"a":
 
344
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
 
345
                               dbus_interface = client_interface)
 
346
        elif key == u"d":
 
347
            self.proxy.Approve(dbus.Boolean(False, variant_level=1),
 
348
                                  dbus_interface = client_interface)
 
349
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
350
            self.server_proxy_object.RemoveClient(self.proxy
 
351
                                                  .object_path)
 
352
        elif key == u"s":
 
353
            self.proxy.StartChecker(dbus_interface = client_interface)
 
354
        elif key == u"S":
 
355
            self.proxy.StopChecker(dbus_interface = client_interface)
 
356
        elif key == u"C":
 
357
            self.proxy.CheckedOK(dbus_interface = client_interface)
 
358
        # xxx
 
359
#         elif key == u"p" or key == "=":
 
360
#             self.proxy.pause()
 
361
#         elif key == u"u" or key == ":":
 
362
#             self.proxy.unpause()
 
363
#         elif key == u"RET":
 
364
#             self.open()
 
365
        else:
 
366
            return key
 
367
    
 
368
    def property_changed(self, property=None, value=None,
 
369
                         *args, **kwargs):
 
370
        """Call self.update() if old value is not new value.
 
371
        This overrides the method from MandosClientPropertyCache"""
 
372
        property_name = unicode(property)
 
373
        old_value = self.properties.get(property_name)
 
374
        super(MandosClientWidget, self).property_changed(
 
375
            property=property, value=value, *args, **kwargs)
 
376
        if self.properties.get(property_name) != old_value:
 
377
            self.update()
 
378
 
 
379
 
 
380
class ConstrainedListBox(urwid.ListBox):
 
381
    """Like a normal urwid.ListBox, but will consume all "up" or
 
382
    "down" key presses, thus not allowing any containing widgets to
 
383
    use them as an excuse to shift focus away from this widget.
 
384
    """
 
385
    def keypress(self, (maxcol, maxrow), key):
 
386
        ret = super(ConstrainedListBox, self).keypress((maxcol,
 
387
                                                        maxrow), key)
 
388
        if ret in (u"up", u"down"):
 
389
            return
 
390
        return ret
 
391
 
 
392
 
 
393
class UserInterface(object):
 
394
    """This is the entire user interface - the whole screen
 
395
    with boxes, lists of client widgets, etc.
 
396
    """
 
397
    def __init__(self, max_log_length=1000):
 
398
        DBusGMainLoop(set_as_default=True)
 
399
        
 
400
        self.screen = urwid.curses_display.Screen()
 
401
        
 
402
        self.screen.register_palette((
 
403
                (u"normal",
 
404
                 u"default", u"default", None),
 
405
                (u"bold",
 
406
                 u"default", u"default", u"bold"),
 
407
                (u"underline-blink",
 
408
                 u"default", u"default", u"underline"),
 
409
                (u"standout",
 
410
                 u"default", u"default", u"standout"),
 
411
                (u"bold-underline-blink",
 
412
                 u"default", u"default", (u"bold", u"underline")),
 
413
                (u"bold-standout",
 
414
                 u"default", u"default", (u"bold", u"standout")),
 
415
                (u"underline-blink-standout",
 
416
                 u"default", u"default", (u"underline", u"standout")),
 
417
                (u"bold-underline-blink-standout",
 
418
                 u"default", u"default", (u"bold", u"underline",
 
419
                                          u"standout")),
 
420
                ))
 
421
        
 
422
        if urwid.supports_unicode():
 
423
            self.divider = u"─" # \u2500
 
424
            #self.divider = u"━" # \u2501
 
425
        else:
 
426
            #self.divider = u"-" # \u002d
 
427
            self.divider = u"_" # \u005f
 
428
        
 
429
        self.screen.start()
 
430
        
 
431
        self.size = self.screen.get_cols_rows()
 
432
        
 
433
        self.clients = urwid.SimpleListWalker([])
 
434
        self.clients_dict = {}
 
435
        
 
436
        # We will add Text widgets to this list
 
437
        self.log = []
 
438
        self.max_log_length = max_log_length
 
439
        
 
440
        # We keep a reference to the log widget so we can remove it
 
441
        # from the ListWalker without it getting destroyed
 
442
        self.logbox = ConstrainedListBox(self.log)
 
443
        
 
444
        # This keeps track of whether self.uilist currently has
 
445
        # self.logbox in it or not
 
446
        self.log_visible = True
 
447
        self.log_wrap = u"any"
 
448
        
 
449
        self.rebuild()
 
450
        self.log_message_raw((u"bold",
 
451
                              u"Mandos Monitor version " + version))
 
452
        self.log_message_raw((u"bold",
 
453
                              u"q: Quit  ?: Help"))
 
454
        
 
455
        self.busname = domain + '.Mandos'
 
456
        self.main_loop = gobject.MainLoop()
 
457
        self.bus = dbus.SystemBus()
 
458
        mandos_dbus_objc = self.bus.get_object(
 
459
            self.busname, u"/", follow_name_owner_changes=True)
 
460
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
461
                                          dbus_interface
 
462
                                          = server_interface)
 
463
        try:
 
464
            mandos_clients = (self.mandos_serv
 
465
                              .GetAllClientsWithProperties())
 
466
        except dbus.exceptions.DBusException:
 
467
            mandos_clients = dbus.Dictionary()
 
468
        
 
469
        (self.mandos_serv
 
470
         .connect_to_signal(u"ClientRemoved",
 
471
                            self.find_and_remove_client,
 
472
                            dbus_interface=server_interface,
 
473
                            byte_arrays=True))
 
474
        (self.mandos_serv
 
475
         .connect_to_signal(u"ClientAdded",
 
476
                            self.add_new_client,
 
477
                            dbus_interface=server_interface,
 
478
                            byte_arrays=True))
 
479
        (self.mandos_serv
 
480
         .connect_to_signal(u"ClientNotFound",
 
481
                            self.client_not_found,
 
482
                            dbus_interface=server_interface,
 
483
                            byte_arrays=True))
 
484
        for path, client in mandos_clients.iteritems():
 
485
            client_proxy_object = self.bus.get_object(self.busname,
 
486
                                                      path)
 
487
            self.add_client(MandosClientWidget(server_proxy_object
 
488
                                               =self.mandos_serv,
 
489
                                               proxy_object
 
490
                                               =client_proxy_object,
 
491
                                               properties=client,
 
492
                                               update_hook
 
493
                                               =self.refresh,
 
494
                                               delete_hook
 
495
                                               =self.remove_client,
 
496
                                               logger
 
497
                                               =self.log_message),
 
498
                            path=path)
 
499
    
 
500
    def client_not_found(self, fingerprint, address):
 
501
        self.log_message((u"Client with address %s and fingerprint %s"
 
502
                          u" could not be found" % (address,
 
503
                                                    fingerprint)))
 
504
    
 
505
    def rebuild(self):
 
506
        """This rebuilds the User Interface.
 
507
        Call this when the widget layout needs to change"""
 
508
        self.uilist = []
 
509
        #self.uilist.append(urwid.ListBox(self.clients))
 
510
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.
 
511
                                                          clients),
 
512
                                       #header=urwid.Divider(),
 
513
                                       header=None,
 
514
                                       footer=
 
515
                                       urwid.Divider(div_char=
 
516
                                                     self.divider)))
 
517
        if self.log_visible:
 
518
            self.uilist.append(self.logbox)
 
519
            pass
 
520
        self.topwidget = urwid.Pile(self.uilist)
 
521
    
 
522
    def log_message(self, message):
 
523
        timestamp = datetime.datetime.now().isoformat()
 
524
        self.log_message_raw(timestamp + u": " + message)
 
525
    
 
526
    def log_message_raw(self, markup):
 
527
        """Add a log message to the log buffer."""
 
528
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
529
        if (self.max_log_length
 
530
            and len(self.log) > self.max_log_length):
 
531
            del self.log[0:len(self.log)-self.max_log_length-1]
 
532
        self.logbox.set_focus(len(self.logbox.body.contents),
 
533
                              coming_from=u"above")
 
534
        self.refresh()
 
535
    
 
536
    def toggle_log_display(self):
 
537
        """Toggle visibility of the log buffer."""
 
538
        self.log_visible = not self.log_visible
 
539
        self.rebuild()
 
540
        #self.log_message(u"Log visibility changed to: "
 
541
        #                 + unicode(self.log_visible))
 
542
    
 
543
    def change_log_display(self):
 
544
        """Change type of log display.
 
545
        Currently, this toggles wrapping of text lines."""
 
546
        if self.log_wrap == u"clip":
 
547
            self.log_wrap = u"any"
 
548
        else:
 
549
            self.log_wrap = u"clip"
 
550
        for textwidget in self.log:
 
551
            textwidget.set_wrap_mode(self.log_wrap)
 
552
        #self.log_message(u"Wrap mode: " + self.log_wrap)
 
553
    
 
554
    def find_and_remove_client(self, path, name):
 
555
        """Find an client from its object path and remove it.
 
556
        
 
557
        This is connected to the ClientRemoved signal from the
 
558
        Mandos server object."""
 
559
        try:
 
560
            client = self.clients_dict[path]
 
561
        except KeyError:
 
562
            # not found?
 
563
            return
 
564
        self.remove_client(client, path)
 
565
    
 
566
    def add_new_client(self, path):
 
567
        client_proxy_object = self.bus.get_object(self.busname, path)
 
568
        self.add_client(MandosClientWidget(server_proxy_object
 
569
                                           =self.mandos_serv,
 
570
                                           proxy_object
 
571
                                           =client_proxy_object,
 
572
                                           update_hook
 
573
                                           =self.refresh,
 
574
                                           delete_hook
 
575
                                           =self.remove_client,
 
576
                                           logger
 
577
                                           =self.log_message),
 
578
                        path=path)
 
579
    
 
580
    def add_client(self, client, path=None):
 
581
        self.clients.append(client)
 
582
        if path is None:
 
583
            path = client.proxy.object_path
 
584
        self.clients_dict[path] = client
 
585
        self.clients.sort(None, lambda c: c.properties[u"Name"])
 
586
        self.refresh()
 
587
    
 
588
    def remove_client(self, client, path=None):
 
589
        self.clients.remove(client)
 
590
        if path is None:
 
591
            path = client.proxy.object_path
 
592
        del self.clients_dict[path]
 
593
        if not self.clients_dict:
 
594
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
595
            # is completely emptied, we need to recreate it.
 
596
            self.clients = urwid.SimpleListWalker([])
 
597
            self.rebuild()
 
598
        self.refresh()
 
599
    
 
600
    def refresh(self):
 
601
        """Redraw the screen"""
 
602
        canvas = self.topwidget.render(self.size, focus=True)
 
603
        self.screen.draw_screen(self.size, canvas)
 
604
    
 
605
    def run(self):
 
606
        """Start the main loop and exit when it's done."""
 
607
        self.refresh()
 
608
        self._input_callback_tag = (gobject.io_add_watch
 
609
                                    (sys.stdin.fileno(),
 
610
                                     gobject.IO_IN,
 
611
                                     self.process_input))
 
612
        self.main_loop.run()
 
613
        # Main loop has finished, we should close everything now
 
614
        gobject.source_remove(self._input_callback_tag)
 
615
        self.screen.stop()
 
616
    
 
617
    def stop(self):
 
618
        self.main_loop.quit()
 
619
    
 
620
    def process_input(self, source, condition):
 
621
        keys = self.screen.get_input()
 
622
        translations = { u"ctrl n": u"down",      # Emacs
 
623
                         u"ctrl p": u"up",        # Emacs
 
624
                         u"ctrl v": u"page down", # Emacs
 
625
                         u"meta v": u"page up",   # Emacs
 
626
                         u" ": u"page down",      # less
 
627
                         u"f": u"page down",      # less
 
628
                         u"b": u"page up",        # less
 
629
                         u"j": u"down",           # vi
 
630
                         u"k": u"up",             # vi
 
631
                         }
 
632
        for key in keys:
 
633
            try:
 
634
                key = translations[key]
 
635
            except KeyError:    # :-)
 
636
                pass
 
637
            
 
638
            if key == u"q" or key == u"Q":
 
639
                self.stop()
 
640
                break
 
641
            elif key == u"window resize":
 
642
                self.size = self.screen.get_cols_rows()
 
643
                self.refresh()
 
644
            elif key == u"\f":  # Ctrl-L
 
645
                self.refresh()
 
646
            elif key == u"l" or key == u"D":
 
647
                self.toggle_log_display()
 
648
                self.refresh()
 
649
            elif key == u"w" or key == u"i":
 
650
                self.change_log_display()
 
651
                self.refresh()
 
652
            elif key == u"?" or key == u"f1" or key == u"esc":
 
653
                if not self.log_visible:
 
654
                    self.log_visible = True
 
655
                    self.rebuild()
 
656
                self.log_message_raw((u"bold",
 
657
                                      u"  ".
 
658
                                      join((u"q: Quit",
 
659
                                            u"?: Help",
 
660
                                            u"l: Log window toggle",
 
661
                                            u"TAB: Switch window",
 
662
                                            u"w: Wrap (log)"))))
 
663
                self.log_message_raw((u"bold",
 
664
                                      u"  "
 
665
                                      .join((u"Clients:",
 
666
                                             u"+: Enable",
 
667
                                             u"-: Disable",
 
668
                                             u"r: Remove",
 
669
                                             u"s: Start new checker",
 
670
                                             u"S: Stop checker",
 
671
                                             u"C: Checker OK",
 
672
                                             u"a: Approve",
 
673
                                             u"d: Deny"))))
 
674
                self.refresh()
 
675
            elif key == u"tab":
 
676
                if self.topwidget.get_focus() is self.logbox:
 
677
                    self.topwidget.set_focus(0)
 
678
                else:
 
679
                    self.topwidget.set_focus(self.logbox)
 
680
                self.refresh()
 
681
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
682
            #      or key == u">"):
 
683
            #    pass            # xxx end-of-buffer
 
684
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
685
            #      or key == u"<"):
 
686
            #    pass            # xxx beginning-of-buffer
 
687
            #elif key == u"ctrl e" or key == u"$":
 
688
            #    pass            # xxx move-end-of-line
 
689
            #elif key == u"ctrl a" or key == u"^":
 
690
            #    pass            # xxx move-beginning-of-line
 
691
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
692
            #    pass            # xxx left
 
693
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
694
            #    pass            # xxx right
 
695
            #elif key == u"a":
 
696
            #    pass            # scroll up log
 
697
            #elif key == u"z":
 
698
            #    pass            # scroll down log
 
699
            elif self.topwidget.selectable():
 
700
                self.topwidget.keypress(self.size, key)
 
701
                self.refresh()
 
702
        return True
 
703
 
 
704
ui = UserInterface()
 
705
try:
 
706
    ui.run()
 
707
except KeyboardInterrupt:
 
708
    ui.screen.stop()
 
709
except Exception, e:
 
710
    ui.log_message(unicode(e))
 
711
    ui.screen.stop()
 
712
    raise