/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

  • Committer: Teddy Hogeborn
  • Date: 2014-07-16 01:41:23 UTC
  • mto: (237.7.272 trunk)
  • mto: This revision was merged to the branch mainline in revision 319.
  • Revision ID: teddy@recompile.se-20140716014123-kc87e8yoyqkzo24u
plugin-runner: Bug Fix: Fix some memory leaks.

* plugin-runner.c (main): Free the individual direntries.

Show diffs side-by-side

added added

removed removed

Lines of Context:
3
3
4
4
# Mandos Monitor - Control and monitor the Mandos server
5
5
6
 
# Copyright © 2009-2012 Teddy Hogeborn
7
 
# Copyright © 2009-2012 Björn Påhlsson
 
6
# Copyright © 2009-2014 Teddy Hogeborn
 
7
# Copyright © 2009-2014 Björn Påhlsson
8
8
9
9
# This program is free software: you can redistribute it and/or modify
10
10
# it under the terms of the GNU General Public License as published by
17
17
#     GNU General Public License for more details.
18
18
19
19
# You should have received a copy of the GNU General Public License
20
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
20
# along with this program.  If not, see
 
21
# <http://www.gnu.org/licenses/>.
21
22
22
23
# Contact the authors at <mandos@recompile.se>.
23
24
24
25
 
25
26
from __future__ import (division, absolute_import, print_function,
26
27
                        unicode_literals)
 
28
try:
 
29
    from future_builtins import *
 
30
except ImportError:
 
31
    pass
27
32
 
28
33
import sys
29
34
import os
30
 
import signal
31
35
 
32
36
import datetime
33
37
 
35
39
import urwid
36
40
 
37
41
from dbus.mainloop.glib import DBusGMainLoop
38
 
import gobject
 
42
try:
 
43
    import gobject
 
44
except ImportError:
 
45
    from gi.repository import GObject as gobject
39
46
 
40
47
import dbus
41
48
 
42
 
import UserList
43
 
 
44
49
import locale
45
50
 
 
51
if sys.version_info[0] == 2:
 
52
    str = unicode
 
53
 
46
54
locale.setlocale(locale.LC_ALL, '')
47
55
 
48
56
import logging
52
60
domain = 'se.recompile'
53
61
server_interface = domain + '.Mandos'
54
62
client_interface = domain + '.Mandos.Client'
55
 
version = "1.4.1"
56
 
 
57
 
# Always run in monochrome mode
58
 
urwid.curses_display.curses.has_colors = lambda : False
59
 
 
60
 
# Urwid doesn't support blinking, but we want it.  Since we have no
61
 
# use for underline on its own, we make underline also always blink.
62
 
urwid.curses_display.curses.A_UNDERLINE |= (
63
 
    urwid.curses_display.curses.A_BLINK)
 
63
version = "1.6.6"
64
64
 
65
65
def isoformat_to_datetime(iso):
66
66
    "Parse an ISO 8601 date string to a datetime.datetime()"
83
83
    properties and calls a hook function when any of them are
84
84
    changed.
85
85
    """
86
 
    def __init__(self, proxy_object=None, *args, **kwargs):
 
86
    def __init__(self, proxy_object=None, properties=None, **kwargs):
87
87
        self.proxy = proxy_object # Mandos Client proxy object
88
 
        
89
 
        self.properties = dict()
 
88
        self.properties = dict() if properties is None else properties
90
89
        self.property_changed_match = (
91
90
            self.proxy.connect_to_signal("PropertyChanged",
92
 
                                         self.property_changed,
 
91
                                         self._property_changed,
93
92
                                         client_interface,
94
93
                                         byte_arrays=True))
95
94
        
96
 
        self.properties.update(
97
 
            self.proxy.GetAll(client_interface,
98
 
                              dbus_interface = dbus.PROPERTIES_IFACE))
99
 
 
100
 
        #XXX This breaks good super behaviour
101
 
#        super(MandosClientPropertyCache, self).__init__(
102
 
#            *args, **kwargs)
 
95
        if properties is None:
 
96
            self.properties.update(
 
97
                self.proxy.GetAll(client_interface,
 
98
                                  dbus_interface
 
99
                                  = dbus.PROPERTIES_IFACE))
 
100
        
 
101
        super(MandosClientPropertyCache, self).__init__(**kwargs)
 
102
    
 
103
    def _property_changed(self, property, value):
 
104
        """Helper which takes positional arguments"""
 
105
        return self.property_changed(property=property, value=value)
103
106
    
104
107
    def property_changed(self, property=None, value=None):
105
108
        """This is called whenever we get a PropertyChanged signal
108
111
        # Update properties dict with new value
109
112
        self.properties[property] = value
110
113
    
111
 
    def delete(self, *args, **kwargs):
 
114
    def delete(self):
112
115
        self.property_changed_match.remove()
113
 
        super(MandosClientPropertyCache, self).__init__(
114
 
            *args, **kwargs)
115
116
 
116
117
 
117
118
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
119
120
    """
120
121
    
121
122
    def __init__(self, server_proxy_object=None, update_hook=None,
122
 
                 delete_hook=None, logger=None, *args, **kwargs):
 
123
                 delete_hook=None, logger=None, **kwargs):
123
124
        # Called on update
124
125
        self.update_hook = update_hook
125
126
        # Called on delete
130
131
        self.logger = logger
131
132
        
132
133
        self._update_timer_callback_tag = None
133
 
        self._update_timer_callback_lock = 0
134
 
        self.last_checker_failed = False
135
134
        
136
135
        # The widget shown normally
137
136
        self._text_widget = urwid.Text("")
138
137
        # The widget shown when we have focus
139
138
        self._focus_text_widget = urwid.Text("")
140
 
        super(MandosClientWidget, self).__init__(
141
 
            update_hook=update_hook, delete_hook=delete_hook,
142
 
            *args, **kwargs)
 
139
        super(MandosClientWidget, self).__init__(**kwargs)
143
140
        self.update()
144
141
        self.opened = False
145
142
        
146
 
        last_checked_ok = isoformat_to_datetime(self.properties
147
 
                                                ["LastCheckedOK"])
148
 
        if last_checked_ok is None:
149
 
            self.last_checker_failed = True
150
 
        else:
151
 
            self.last_checker_failed = ((datetime.datetime.utcnow()
152
 
                                         - last_checked_ok)
153
 
                                        > datetime.timedelta
154
 
                                        (milliseconds=
155
 
                                         self.properties
156
 
                                         ["Interval"]))
157
 
        
158
 
        if self.last_checker_failed:
159
 
            self.using_timer(True)
160
 
        
161
 
        if self.need_approval:
162
 
            self.using_timer(True)
163
 
        
164
143
        self.match_objects = (
165
144
            self.proxy.connect_to_signal("CheckerCompleted",
166
145
                                         self.checker_completed,
182
161
                                         self.rejected,
183
162
                                         client_interface,
184
163
                                         byte_arrays=True))
185
 
        #self.logger('Created client %s' % (self.properties["Name"]))
 
164
        self.logger('Created client {0}'
 
165
                    .format(self.properties["Name"]), level=0)
186
166
    
187
 
    def property_changed(self, property=None, value=None):
188
 
        super(self, MandosClientWidget).property_changed(property,
189
 
                                                         value)
190
 
        if property == "ApprovalPending":
191
 
            using_timer(bool(value))
192
 
        
193
167
    def using_timer(self, flag):
194
168
        """Call this method with True or False when timer should be
195
169
        activated or deactivated.
196
170
        """
197
 
        old = self._update_timer_callback_lock
198
 
        if flag:
199
 
            self._update_timer_callback_lock += 1
200
 
        else:
201
 
            self._update_timer_callback_lock -= 1
202
 
        if old == 0 and self._update_timer_callback_lock:
 
171
        if flag and self._update_timer_callback_tag is None:
203
172
            # Will update the shown timer value every second
204
173
            self._update_timer_callback_tag = (gobject.timeout_add
205
174
                                               (1000,
206
175
                                                self.update_timer))
207
 
        elif old and self._update_timer_callback_lock == 0:
 
176
        elif not (flag or self._update_timer_callback_tag is None):
208
177
            gobject.source_remove(self._update_timer_callback_tag)
209
178
            self._update_timer_callback_tag = None
210
179
    
211
180
    def checker_completed(self, exitstatus, condition, command):
212
181
        if exitstatus == 0:
213
 
            if self.last_checker_failed:
214
 
                self.last_checker_failed = False
215
 
                self.using_timer(False)
216
 
            #self.logger('Checker for client %s (command "%s")'
217
 
            #            ' was successful'
218
 
            #            % (self.properties["Name"], command))
 
182
            self.logger('Checker for client {0} (command "{1}")'
 
183
                        ' succeeded'.format(self.properties["Name"],
 
184
                                            command), level=0)
219
185
            self.update()
220
186
            return
221
187
        # Checker failed
222
 
        if not self.last_checker_failed:
223
 
            self.last_checker_failed = True
224
 
            self.using_timer(True)
225
188
        if os.WIFEXITED(condition):
226
 
            self.logger('Checker for client %s (command "%s")'
227
 
                        ' failed with exit code %s'
228
 
                        % (self.properties["Name"], command,
229
 
                           os.WEXITSTATUS(condition)))
 
189
            self.logger('Checker for client {0} (command "{1}")'
 
190
                        ' failed with exit code {2}'
 
191
                        .format(self.properties["Name"], command,
 
192
                                os.WEXITSTATUS(condition)))
230
193
        elif os.WIFSIGNALED(condition):
231
 
            self.logger('Checker for client %s (command "%s")'
232
 
                        ' was killed by signal %s'
233
 
                        % (self.properties["Name"], command,
234
 
                           os.WTERMSIG(condition)))
 
194
            self.logger('Checker for client {0} (command "{1}") was'
 
195
                        ' killed by signal {2}'
 
196
                        .format(self.properties["Name"], command,
 
197
                                os.WTERMSIG(condition)))
235
198
        elif os.WCOREDUMP(condition):
236
 
            self.logger('Checker for client %s (command "%s")'
 
199
            self.logger('Checker for client {0} (command "{1}")'
237
200
                        ' dumped core'
238
 
                        % (self.properties["Name"], command))
 
201
                        .format(self.properties["Name"], command))
239
202
        else:
240
 
            self.logger('Checker for client %s completed'
241
 
                        ' mysteriously')
 
203
            self.logger('Checker for client {0} completed'
 
204
                        ' mysteriously'
 
205
                        .format(self.properties["Name"]))
242
206
        self.update()
243
207
    
244
208
    def checker_started(self, command):
245
 
        """Server signals that a checker started. This could be useful
246
 
           to log in the future. """
247
 
        #self.logger('Client %s started checker "%s"'
248
 
        #            % (self.properties["Name"], unicode(command)))
249
 
        pass
 
209
        """Server signals that a checker started."""
 
210
        self.logger('Client {0} started checker "{1}"'
 
211
                    .format(self.properties["Name"],
 
212
                            command), level=0)
250
213
    
251
214
    def got_secret(self):
252
 
        self.last_checker_failed = False
253
 
        self.logger('Client %s received its secret'
254
 
                    % self.properties["Name"])
 
215
        self.logger('Client {0} received its secret'
 
216
                    .format(self.properties["Name"]))
255
217
    
256
218
    def need_approval(self, timeout, default):
257
219
        if not default:
258
 
            message = 'Client %s needs approval within %s seconds'
 
220
            message = 'Client {0} needs approval within {1} seconds'
259
221
        else:
260
 
            message = 'Client %s will get its secret in %s seconds'
261
 
        self.logger(message
262
 
                    % (self.properties["Name"], timeout/1000))
263
 
        self.using_timer(True)
 
222
            message = 'Client {0} will get its secret in {1} seconds'
 
223
        self.logger(message.format(self.properties["Name"],
 
224
                                   timeout/1000))
264
225
    
265
226
    def rejected(self, reason):
266
 
        self.logger('Client %s was rejected; reason: %s'
267
 
                    % (self.properties["Name"], reason))
 
227
        self.logger('Client {0} was rejected; reason: {1}'
 
228
                    .format(self.properties["Name"], reason))
268
229
    
269
230
    def selectable(self):
270
231
        """Make this a "selectable" widget.
292
253
                          "bold-underline-blink":
293
254
                              "bold-underline-blink-standout",
294
255
                          }
295
 
 
 
256
        
296
257
        # Rebuild focus and non-focus widgets using current properties
297
 
 
 
258
        
298
259
        # Base part of a client. Name!
299
 
        base = ('%(name)s: '
300
 
                      % {"name": self.properties["Name"]})
 
260
        base = '{name}: '.format(name=self.properties["Name"])
301
261
        if not self.properties["Enabled"]:
302
262
            message = "DISABLED"
 
263
            self.using_timer(False)
303
264
        elif self.properties["ApprovalPending"]:
304
265
            timeout = datetime.timedelta(milliseconds
305
266
                                         = self.properties
307
268
            last_approval_request = isoformat_to_datetime(
308
269
                self.properties["LastApprovalRequest"])
309
270
            if last_approval_request is not None:
310
 
                timer = timeout - (datetime.datetime.utcnow()
311
 
                                   - last_approval_request)
 
271
                timer = max(timeout - (datetime.datetime.utcnow()
 
272
                                       - last_approval_request),
 
273
                            datetime.timedelta())
312
274
            else:
313
275
                timer = datetime.timedelta()
314
276
            if self.properties["ApprovedByDefault"]:
315
 
                message = "Approval in %s. (d)eny?"
 
277
                message = "Approval in {0}. (d)eny?"
316
278
            else:
317
 
                message = "Denial in %s. (a)pprove?"
318
 
            message = message % unicode(timer).rsplit(".", 1)[0]
319
 
        elif self.last_checker_failed:
320
 
            # When checker has failed, print a timer until client expires
 
279
                message = "Denial in {0}. (a)pprove?"
 
280
            message = message.format(str(timer).rsplit(".", 1)[0])
 
281
            self.using_timer(True)
 
282
        elif self.properties["LastCheckerStatus"] != 0:
 
283
            # When checker has failed, show timer until client expires
321
284
            expires = self.properties["Expires"]
322
285
            if expires == "":
323
286
                timer = datetime.timedelta(0)
324
287
            else:
325
 
                expires = datetime.datetime.strptime(expires,
326
 
                                                     '%Y-%m-%dT%H:%M:%S.%f')
327
 
                timer = expires - datetime.datetime.utcnow()
 
288
                expires = (datetime.datetime.strptime
 
289
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
 
290
                timer = max(expires - datetime.datetime.utcnow(),
 
291
                            datetime.timedelta())
328
292
            message = ('A checker has failed! Time until client'
329
 
                       ' gets disabled: %s'
330
 
                           % unicode(timer).rsplit(".", 1)[0])
 
293
                       ' gets disabled: {0}'
 
294
                       .format(str(timer).rsplit(".", 1)[0]))
 
295
            self.using_timer(True)
331
296
        else:
332
297
            message = "enabled"
333
 
        self._text = "%s%s" % (base, message)
334
 
            
 
298
            self.using_timer(False)
 
299
        self._text = "{0}{1}".format(base, message)
 
300
        
335
301
        if not urwid.supports_unicode():
336
302
            self._text = self._text.encode("ascii", "replace")
337
303
        textlist = [("normal", self._text)]
354
320
        self.update()
355
321
        return True             # Keep calling this
356
322
    
357
 
    def delete(self, *args, **kwargs):
 
323
    def delete(self, **kwargs):
358
324
        if self._update_timer_callback_tag is not None:
359
325
            gobject.source_remove(self._update_timer_callback_tag)
360
326
            self._update_timer_callback_tag = None
363
329
        self.match_objects = ()
364
330
        if self.delete_hook is not None:
365
331
            self.delete_hook(self)
366
 
        return super(MandosClientWidget, self).delete(*args, **kwargs)
 
332
        return super(MandosClientWidget, self).delete(**kwargs)
367
333
    
368
334
    def render(self, maxcolrow, focus=False):
369
335
        """Render differently if we have focus.
411
377
        else:
412
378
            return key
413
379
    
414
 
    def property_changed(self, property=None, value=None,
415
 
                         *args, **kwargs):
 
380
    def property_changed(self, property=None, **kwargs):
416
381
        """Call self.update() if old value is not new value.
417
382
        This overrides the method from MandosClientPropertyCache"""
418
 
        property_name = unicode(property)
 
383
        property_name = str(property)
419
384
        old_value = self.properties.get(property_name)
420
385
        super(MandosClientWidget, self).property_changed(
421
 
            property=property, value=value, *args, **kwargs)
 
386
            property=property, **kwargs)
422
387
        if self.properties.get(property_name) != old_value:
423
388
            self.update()
424
389
 
428
393
    "down" key presses, thus not allowing any containing widgets to
429
394
    use them as an excuse to shift focus away from this widget.
430
395
    """
431
 
    def keypress(self, maxcolrow, key):
432
 
        ret = super(ConstrainedListBox, self).keypress(maxcolrow, key)
 
396
    def keypress(self, *args, **kwargs):
 
397
        ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
433
398
        if ret in ("up", "down"):
434
399
            return
435
400
        return ret
439
404
    """This is the entire user interface - the whole screen
440
405
    with boxes, lists of client widgets, etc.
441
406
    """
442
 
    def __init__(self, max_log_length=1000):
 
407
    def __init__(self, max_log_length=1000, log_level=1):
443
408
        DBusGMainLoop(set_as_default=True)
444
409
        
445
410
        self.screen = urwid.curses_display.Screen()
448
413
                ("normal",
449
414
                 "default", "default", None),
450
415
                ("bold",
451
 
                 "default", "default", "bold"),
 
416
                 "bold", "default", "bold"),
452
417
                ("underline-blink",
453
 
                 "default", "default", "underline"),
 
418
                 "underline,blink", "default", "underline,blink"),
454
419
                ("standout",
455
 
                 "default", "default", "standout"),
 
420
                 "standout", "default", "standout"),
456
421
                ("bold-underline-blink",
457
 
                 "default", "default", ("bold", "underline")),
 
422
                 "bold,underline,blink", "default", "bold,underline,blink"),
458
423
                ("bold-standout",
459
 
                 "default", "default", ("bold", "standout")),
 
424
                 "bold,standout", "default", "bold,standout"),
460
425
                ("underline-blink-standout",
461
 
                 "default", "default", ("underline", "standout")),
 
426
                 "underline,blink,standout", "default",
 
427
                 "underline,blink,standout"),
462
428
                ("bold-underline-blink-standout",
463
 
                 "default", "default", ("bold", "underline",
464
 
                                          "standout")),
 
429
                 "bold,underline,blink,standout", "default",
 
430
                 "bold,underline,blink,standout"),
465
431
                ))
466
432
        
467
433
        if urwid.supports_unicode():
482
448
        self.log = []
483
449
        self.max_log_length = max_log_length
484
450
        
 
451
        self.log_level = log_level
 
452
        
485
453
        # We keep a reference to the log widget so we can remove it
486
454
        # from the ListWalker without it getting destroyed
487
455
        self.logbox = ConstrainedListBox(self.log)
501
469
        self.main_loop = gobject.MainLoop()
502
470
    
503
471
    def client_not_found(self, fingerprint, address):
504
 
        self.log_message(("Client with address %s and fingerprint %s"
505
 
                          " could not be found" % (address,
506
 
                                                    fingerprint)))
 
472
        self.log_message("Client with address {0} and fingerprint"
 
473
                         " {1} could not be found"
 
474
                         .format(address, fingerprint))
507
475
    
508
476
    def rebuild(self):
509
477
        """This rebuilds the User Interface.
521
489
            self.uilist.append(self.logbox)
522
490
        self.topwidget = urwid.Pile(self.uilist)
523
491
    
524
 
    def log_message(self, message):
 
492
    def log_message(self, message, level=1):
 
493
        """Log message formatted with timestamp"""
 
494
        if level < self.log_level:
 
495
            return
525
496
        timestamp = datetime.datetime.now().isoformat()
526
 
        self.log_message_raw(timestamp + ": " + message)
 
497
        self.log_message_raw("{0}: {1}".format(timestamp, message),
 
498
                             level=level)
527
499
    
528
 
    def log_message_raw(self, markup):
 
500
    def log_message_raw(self, markup, level=1):
529
501
        """Add a log message to the log buffer."""
 
502
        if level < self.log_level:
 
503
            return
530
504
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
531
505
        if (self.max_log_length
532
506
            and len(self.log) > self.max_log_length):
539
513
        """Toggle visibility of the log buffer."""
540
514
        self.log_visible = not self.log_visible
541
515
        self.rebuild()
542
 
        #self.log_message("Log visibility changed to: "
543
 
        #                 + unicode(self.log_visible))
 
516
        self.log_message("Log visibility changed to: {0}"
 
517
                         .format(self.log_visible), level=0)
544
518
    
545
519
    def change_log_display(self):
546
520
        """Change type of log display.
551
525
            self.log_wrap = "clip"
552
526
        for textwidget in self.log:
553
527
            textwidget.set_wrap_mode(self.log_wrap)
554
 
        #self.log_message("Wrap mode: " + self.log_wrap)
 
528
        self.log_message("Wrap mode: {0}".format(self.log_wrap),
 
529
                         level=0)
555
530
    
556
531
    def find_and_remove_client(self, path, name):
557
532
        """Find a client by its object path and remove it.
562
537
            client = self.clients_dict[path]
563
538
        except KeyError:
564
539
            # not found?
565
 
            self.log_message("Unknown client %r (%r) removed", name,
566
 
                             path)
 
540
            self.log_message("Unknown client {0!r} ({1!r}) removed"
 
541
                             .format(name, path))
567
542
            return
568
543
        client.delete()
569
544
    
586
561
        if path is None:
587
562
            path = client.proxy.object_path
588
563
        self.clients_dict[path] = client
589
 
        self.clients.sort(None, lambda c: c.properties["Name"])
 
564
        self.clients.sort(key=lambda c: c.properties["Name"])
590
565
        self.refresh()
591
566
    
592
567
    def remove_client(self, client, path=None):
594
569
        if path is None:
595
570
            path = client.proxy.object_path
596
571
        del self.clients_dict[path]
597
 
        if not self.clients_dict:
598
 
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
599
 
            # is completely emptied, we need to recreate it.
600
 
            self.clients = urwid.SimpleListWalker([])
601
 
            self.rebuild()
602
572
        self.refresh()
603
573
    
604
574
    def refresh(self):
617
587
        try:
618
588
            mandos_clients = (self.mandos_serv
619
589
                              .GetAllClientsWithProperties())
 
590
            if not mandos_clients:
 
591
                self.log_message_raw(("bold", "Note: Server has no clients."))
620
592
        except dbus.exceptions.DBusException:
 
593
            self.log_message_raw(("bold", "Note: No Mandos server running."))
621
594
            mandos_clients = dbus.Dictionary()
622
595
        
623
596
        (self.mandos_serv
635
608
                            self.client_not_found,
636
609
                            dbus_interface=server_interface,
637
610
                            byte_arrays=True))
638
 
        for path, client in mandos_clients.iteritems():
 
611
        for path, client in mandos_clients.items():
639
612
            client_proxy_object = self.bus.get_object(self.busname,
640
613
                                                      path)
641
614
            self.add_client(MandosClientWidget(server_proxy_object
650
623
                                               logger
651
624
                                               =self.log_message),
652
625
                            path=path)
653
 
 
 
626
        
654
627
        self.refresh()
655
628
        self._input_callback_tag = (gobject.io_add_watch
656
629
                                    (sys.stdin.fileno(),
706
679
                                            "?: Help",
707
680
                                            "l: Log window toggle",
708
681
                                            "TAB: Switch window",
709
 
                                            "w: Wrap (log)"))))
 
682
                                            "w: Wrap (log lines)",
 
683
                                            "v: Toggle verbose log",
 
684
                                            ))))
710
685
                self.log_message_raw(("bold",
711
686
                                      "  "
712
687
                                      .join(("Clients:",
725
700
                else:
726
701
                    self.topwidget.set_focus(self.logbox)
727
702
                self.refresh()
 
703
            elif key == "v":
 
704
                if self.log_level == 0:
 
705
                    self.log_level = 1
 
706
                    self.log_message("Verbose mode: Off")
 
707
                else:
 
708
                    self.log_level = 0
 
709
                    self.log_message("Verbose mode: On")
728
710
            #elif (key == "end" or key == "meta >" or key == "G"
729
711
            #      or key == ">"):
730
712
            #    pass            # xxx end-of-buffer
753
735
    ui.run()
754
736
except KeyboardInterrupt:
755
737
    ui.screen.stop()
756
 
except Exception, e:
757
 
    ui.log_message(unicode(e))
 
738
except Exception as e:
 
739
    ui.log_message(str(e))
758
740
    ui.screen.stop()
759
741
    raise