/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: Björn Påhlsson
  • Date: 2010-09-02 18:53:38 UTC
  • mto: (237.7.1 mandos)
  • mto: This revision was merged to the branch mainline in revision 270.
  • Revision ID: belorn@fukt.bsnet.se-20100902185338-d1022sv517txn3sb
early commit to ease todays coding

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
class MandosClientPropertyCache(object):
 
44
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
45
    properties and calls a hook function when any of them are
 
46
    changed.
 
47
    """
 
48
    def __init__(self, proxy_object=None, *args, **kwargs):
 
49
        self.proxy = proxy_object # Mandos Client proxy object
 
50
        
 
51
        self.properties = dict()
 
52
        self.proxy.connect_to_signal(u"PropertyChanged",
 
53
                                     self.property_changed,
 
54
                                     client_interface,
 
55
                                     byte_arrays=True)
 
56
        
 
57
        self.properties.update(
 
58
            self.proxy.GetAll(client_interface,
 
59
                              dbus_interface = dbus.PROPERTIES_IFACE))
 
60
        super(MandosClientPropertyCache, self).__init__(
 
61
            *args, **kwargs)
 
62
    
 
63
    def property_changed(self, property=None, value=None):
 
64
        """This is called whenever we get a PropertyChanged signal
 
65
        It updates the changed property in the "properties" dict.
 
66
        """
 
67
        # Update properties dict with new value
 
68
        self.properties[property] = value
 
69
 
 
70
 
 
71
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
72
    """A Mandos Client which is visible on the screen.
 
73
    """
 
74
    
 
75
    def __init__(self, server_proxy_object=None, update_hook=None,
 
76
                 delete_hook=None, logger=None, *args, **kwargs):
 
77
        # Called on update
 
78
        self.update_hook = update_hook
 
79
        # Called on delete
 
80
        self.delete_hook = delete_hook
 
81
        # Mandos Server proxy object
 
82
        self.server_proxy_object = server_proxy_object
 
83
        # Logger
 
84
        self.logger = logger
 
85
        
 
86
        # The widget shown normally
 
87
        self._text_widget = urwid.Text(u"")
 
88
        # The widget shown when we have focus
 
89
        self._focus_text_widget = urwid.Text(u"")
 
90
        super(MandosClientWidget, self).__init__(
 
91
            update_hook=update_hook, delete_hook=delete_hook,
 
92
            *args, **kwargs)
 
93
        self.update()
 
94
        self.opened = False
 
95
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
96
                                     self.checker_completed,
 
97
                                     client_interface,
 
98
                                     byte_arrays=True)
 
99
        self.proxy.connect_to_signal(u"CheckerStarted",
 
100
                                     self.checker_started,
 
101
                                     client_interface,
 
102
                                     byte_arrays=True)
 
103
        self.proxy.connect_to_signal(u"GotSecret",
 
104
                                     self.got_secret,
 
105
                                     client_interface,
 
106
                                     byte_arrays=True)
 
107
        self.proxy.connect_to_signal(u"NeedApproval",
 
108
                                     self.need_approval,
 
109
                                     client_interface,
 
110
                                     byte_arrays=True)
 
111
        self.proxy.connect_to_signal(u"Rejected",
 
112
                                     self.rejected,
 
113
                                     client_interface,
 
114
                                     byte_arrays=True)
 
115
    
 
116
    def checker_completed(self, exitstatus, condition, command):
 
117
        if exitstatus == 0:
 
118
            #self.logger(u'Checker for client %s (command "%s")'
 
119
            #            u' was successful'
 
120
            #            % (self.properties[u"name"], command))
 
121
            return
 
122
        if os.WIFEXITED(condition):
 
123
            self.logger(u'Checker for client %s (command "%s")'
 
124
                        u' failed with exit code %s'
 
125
                        % (self.properties[u"name"], command,
 
126
                           os.WEXITSTATUS(condition)))
 
127
            return
 
128
        if os.WIFSIGNALED(condition):
 
129
            self.logger(u'Checker for client %s (command "%s")'
 
130
                        u' was killed by signal %s'
 
131
                        % (self.properties[u"name"], command,
 
132
                           os.WTERMSIG(condition)))
 
133
            return
 
134
        if os.WCOREDUMP(condition):
 
135
            self.logger(u'Checker for client %s (command "%s")'
 
136
                        u' dumped core'
 
137
                        % (self.properties[u"name"], command))
 
138
        self.logger(u'Checker for client %s completed mysteriously')
 
139
    
 
140
    def checker_started(self, command):
 
141
        #self.logger(u'Client %s started checker "%s"'
 
142
        #            % (self.properties[u"name"], unicode(command)))
 
143
        pass
 
144
    
 
145
    def got_secret(self):
 
146
        self.logger(u'Client %s received its secret'
 
147
                    % self.properties[u"name"])
 
148
    
 
149
    def need_approval(self, timeout, default):
 
150
        if not default:
 
151
            message = u'Client %s needs approval within %s seconds'
 
152
        else:
 
153
            message = u'Client %s will get its secret in %s seconds'
 
154
        self.logger(message
 
155
                    % (self.properties[u"name"], timeout/1000))
 
156
    
 
157
    def rejected(self, reason):
 
158
        self.logger(u'Client %s was rejected; reason: %s'
 
159
                    % (self.properties[u"name"], reason))
 
160
    
 
161
    def selectable(self):
 
162
        """Make this a "selectable" widget.
 
163
        This overrides the method from urwid.FlowWidget."""
 
164
        return True
 
165
    
 
166
    def rows(self, (maxcol,), focus=False):
 
167
        """How many rows this widget will occupy might depend on
 
168
        whether we have focus or not.
 
169
        This overrides the method from urwid.FlowWidget"""
 
170
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
171
    
 
172
    def current_widget(self, focus=False):
 
173
        if focus or self.opened:
 
174
            return self._focus_widget
 
175
        return self._widget
 
176
    
 
177
    def update(self):
 
178
        "Called when what is visible on the screen should be updated."
 
179
        # How to add standout mode to a style
 
180
        with_standout = { u"normal": u"standout",
 
181
                          u"bold": u"bold-standout",
 
182
                          u"underline-blink":
 
183
                              u"underline-blink-standout",
 
184
                          u"bold-underline-blink":
 
185
                              u"bold-underline-blink-standout",
 
186
                          }
 
187
        
 
188
        # Rebuild focus and non-focus widgets using current properties
 
189
        self._text = (u'%(name)s: %(enabled)s'
 
190
                      % { u"name": self.properties[u"name"],
 
191
                          u"enabled":
 
192
                              (u"enabled"
 
193
                               if self.properties[u"enabled"]
 
194
                               else u"DISABLED")})
 
195
        if not urwid.supports_unicode():
 
196
            self._text = self._text.encode("ascii", "replace")
 
197
        textlist = [(u"normal", self._text)]
 
198
        self._text_widget.set_text(textlist)
 
199
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
200
                                           text[1])
 
201
                                          if isinstance(text, tuple)
 
202
                                          else text
 
203
                                          for text in textlist])
 
204
        self._widget = self._text_widget
 
205
        self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
 
206
                                            "standout")
 
207
        # Run update hook, if any
 
208
        if self.update_hook is not None:
 
209
            self.update_hook()
 
210
    
 
211
    def delete(self):
 
212
        if self.delete_hook is not None:
 
213
            self.delete_hook(self)
 
214
    
 
215
    def render(self, (maxcol,), focus=False):
 
216
        """Render differently if we have focus.
 
217
        This overrides the method from urwid.FlowWidget"""
 
218
        return self.current_widget(focus).render((maxcol,),
 
219
                                                 focus=focus)
 
220
    
 
221
    def keypress(self, (maxcol,), key):
 
222
        """Handle keys.
 
223
        This overrides the method from urwid.FlowWidget"""
 
224
        if key == u"e" or key == u"+":
 
225
            self.proxy.Enable()
 
226
        elif key == u"d" or key == u"-":
 
227
            self.proxy.Disable()
 
228
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
229
            self.server_proxy_object.RemoveClient(self.proxy
 
230
                                                  .object_path)
 
231
        elif key == u"s":
 
232
            self.proxy.StartChecker()
 
233
        elif key == u"S":
 
234
            self.proxy.StopChecker()
 
235
        elif key == u"C":
 
236
            self.proxy.CheckedOK()
 
237
        # xxx
 
238
#         elif key == u"p" or key == "=":
 
239
#             self.proxy.pause()
 
240
#         elif key == u"u" or key == ":":
 
241
#             self.proxy.unpause()
 
242
#         elif key == u"RET":
 
243
#             self.open()
 
244
        elif key == u"+":
 
245
            self.proxy.Approve(True)
 
246
        elif key == u"-":
 
247
            self.proxy.Approve(False)
 
248
        else:
 
249
            return key
 
250
    
 
251
    def property_changed(self, property=None, value=None,
 
252
                         *args, **kwargs):
 
253
        """Call self.update() if old value is not new value.
 
254
        This overrides the method from MandosClientPropertyCache"""
 
255
        property_name = unicode(property)
 
256
        old_value = self.properties.get(property_name)
 
257
        super(MandosClientWidget, self).property_changed(
 
258
            property=property, value=value, *args, **kwargs)
 
259
        if self.properties.get(property_name) != old_value:
 
260
            self.update()
 
261
 
 
262
 
 
263
class ConstrainedListBox(urwid.ListBox):
 
264
    """Like a normal urwid.ListBox, but will consume all "up" or
 
265
    "down" key presses, thus not allowing any containing widgets to
 
266
    use them as an excuse to shift focus away from this widget.
 
267
    """
 
268
    def keypress(self, (maxcol, maxrow), key):
 
269
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
 
270
        if ret in (u"up", u"down"):
 
271
            return
 
272
        return ret
 
273
 
 
274
 
 
275
class UserInterface(object):
 
276
    """This is the entire user interface - the whole screen
 
277
    with boxes, lists of client widgets, etc.
 
278
    """
 
279
    def __init__(self, max_log_length=1000):
 
280
        DBusGMainLoop(set_as_default=True)
 
281
        
 
282
        self.screen = urwid.curses_display.Screen()
 
283
        
 
284
        self.screen.register_palette((
 
285
                (u"normal",
 
286
                 u"default", u"default", None),
 
287
                (u"bold",
 
288
                 u"default", u"default", u"bold"),
 
289
                (u"underline-blink",
 
290
                 u"default", u"default", u"underline"),
 
291
                (u"standout",
 
292
                 u"default", u"default", u"standout"),
 
293
                (u"bold-underline-blink",
 
294
                 u"default", u"default", (u"bold", u"underline")),
 
295
                (u"bold-standout",
 
296
                 u"default", u"default", (u"bold", u"standout")),
 
297
                (u"underline-blink-standout",
 
298
                 u"default", u"default", (u"underline", u"standout")),
 
299
                (u"bold-underline-blink-standout",
 
300
                 u"default", u"default", (u"bold", u"underline",
 
301
                                          u"standout")),
 
302
                ))
 
303
        
 
304
        if urwid.supports_unicode():
 
305
            self.divider = u"─" # \u2500
 
306
            #self.divider = u"━" # \u2501
 
307
        else:
 
308
            #self.divider = u"-" # \u002d
 
309
            self.divider = u"_" # \u005f
 
310
        
 
311
        self.screen.start()
 
312
        
 
313
        self.size = self.screen.get_cols_rows()
 
314
        
 
315
        self.clients = urwid.SimpleListWalker([])
 
316
        self.clients_dict = {}
 
317
        
 
318
        # We will add Text widgets to this list
 
319
        self.log = []
 
320
        self.max_log_length = max_log_length
 
321
        
 
322
        # We keep a reference to the log widget so we can remove it
 
323
        # from the ListWalker without it getting destroyed
 
324
        self.logbox = ConstrainedListBox(self.log)
 
325
        
 
326
        # This keeps track of whether self.uilist currently has
 
327
        # self.logbox in it or not
 
328
        self.log_visible = True
 
329
        self.log_wrap = u"any"
 
330
        
 
331
        self.rebuild()
 
332
        self.log_message_raw((u"bold",
 
333
                              u"Mandos Monitor version " + version))
 
334
        self.log_message_raw((u"bold",
 
335
                              u"q: Quit  ?: Help"))
 
336
        
 
337
        self.busname = domain + '.Mandos'
 
338
        self.main_loop = gobject.MainLoop()
 
339
        self.bus = dbus.SystemBus()
 
340
        mandos_dbus_objc = self.bus.get_object(
 
341
            self.busname, u"/", follow_name_owner_changes=True)
 
342
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
343
                                          dbus_interface
 
344
                                          = server_interface)
 
345
        try:
 
346
            mandos_clients = (self.mandos_serv
 
347
                              .GetAllClientsWithProperties())
 
348
        except dbus.exceptions.DBusException:
 
349
            mandos_clients = dbus.Dictionary()
 
350
        
 
351
        (self.mandos_serv
 
352
         .connect_to_signal(u"ClientRemoved",
 
353
                            self.find_and_remove_client,
 
354
                            dbus_interface=server_interface,
 
355
                            byte_arrays=True))
 
356
        (self.mandos_serv
 
357
         .connect_to_signal(u"ClientAdded",
 
358
                            self.add_new_client,
 
359
                            dbus_interface=server_interface,
 
360
                            byte_arrays=True))
 
361
        (self.mandos_serv
 
362
         .connect_to_signal(u"ClientNotFound",
 
363
                            self.client_not_found,
 
364
                            dbus_interface=server_interface,
 
365
                            byte_arrays=True))
 
366
        for path, client in mandos_clients.iteritems():
 
367
            client_proxy_object = self.bus.get_object(self.busname,
 
368
                                                      path)
 
369
            self.add_client(MandosClientWidget(server_proxy_object
 
370
                                               =self.mandos_serv,
 
371
                                               proxy_object
 
372
                                               =client_proxy_object,
 
373
                                               properties=client,
 
374
                                               update_hook
 
375
                                               =self.refresh,
 
376
                                               delete_hook
 
377
                                               =self.remove_client,
 
378
                                               logger
 
379
                                               =self.log_message),
 
380
                            path=path)
 
381
    
 
382
    def client_not_found(self, fingerprint, address):
 
383
        self.log_message((u"Client with address %s and fingerprint %s"
 
384
                          u" could not be found" % (address,
 
385
                                                    fingerprint)))
 
386
    
 
387
    def rebuild(self):
 
388
        """This rebuilds the User Interface.
 
389
        Call this when the widget layout needs to change"""
 
390
        self.uilist = []
 
391
        #self.uilist.append(urwid.ListBox(self.clients))
 
392
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
393
                                       #header=urwid.Divider(),
 
394
                                       header=None,
 
395
                                       footer=urwid.Divider(div_char=self.divider)))
 
396
        if self.log_visible:
 
397
            self.uilist.append(self.logbox)
 
398
            pass
 
399
        self.topwidget = urwid.Pile(self.uilist)
 
400
    
 
401
    def log_message(self, message):
 
402
        timestamp = datetime.datetime.now().isoformat()
 
403
        self.log_message_raw(timestamp + u": " + message)
 
404
    
 
405
    def log_message_raw(self, markup):
 
406
        """Add a log message to the log buffer."""
 
407
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
408
        if (self.max_log_length
 
409
            and len(self.log) > self.max_log_length):
 
410
            del self.log[0:len(self.log)-self.max_log_length-1]
 
411
        self.logbox.set_focus(len(self.logbox.body.contents),
 
412
                              coming_from=u"above")
 
413
        self.refresh()
 
414
    
 
415
    def toggle_log_display(self):
 
416
        """Toggle visibility of the log buffer."""
 
417
        self.log_visible = not self.log_visible
 
418
        self.rebuild()
 
419
        self.log_message(u"Log visibility changed to: "
 
420
                         + unicode(self.log_visible))
 
421
    
 
422
    def change_log_display(self):
 
423
        """Change type of log display.
 
424
        Currently, this toggles wrapping of text lines."""
 
425
        if self.log_wrap == u"clip":
 
426
            self.log_wrap = u"any"
 
427
        else:
 
428
            self.log_wrap = u"clip"
 
429
        for textwidget in self.log:
 
430
            textwidget.set_wrap_mode(self.log_wrap)
 
431
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
432
    
 
433
    def find_and_remove_client(self, path, name):
 
434
        """Find an client from its object path and remove it.
 
435
        
 
436
        This is connected to the ClientRemoved signal from the
 
437
        Mandos server object."""
 
438
        try:
 
439
            client = self.clients_dict[path]
 
440
        except KeyError:
 
441
            # not found?
 
442
            return
 
443
        self.remove_client(client, path)
 
444
    
 
445
    def add_new_client(self, path):
 
446
        client_proxy_object = self.bus.get_object(self.busname, path)
 
447
        self.add_client(MandosClientWidget(server_proxy_object
 
448
                                           =self.mandos_serv,
 
449
                                           proxy_object
 
450
                                           =client_proxy_object,
 
451
                                           update_hook
 
452
                                           =self.refresh,
 
453
                                           delete_hook
 
454
                                           =self.remove_client,
 
455
                                           logger
 
456
                                           =self.log_message),
 
457
                        path=path)
 
458
    
 
459
    def add_client(self, client, path=None):
 
460
        self.clients.append(client)
 
461
        if path is None:
 
462
            path = client.proxy.object_path
 
463
        self.clients_dict[path] = client
 
464
        self.clients.sort(None, lambda c: c.properties[u"name"])
 
465
        self.refresh()
 
466
    
 
467
    def remove_client(self, client, path=None):
 
468
        self.clients.remove(client)
 
469
        if path is None:
 
470
            path = client.proxy.object_path
 
471
        del self.clients_dict[path]
 
472
        if not self.clients_dict:
 
473
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
474
            # is completely emptied, we need to recreate it.
 
475
            self.clients = urwid.SimpleListWalker([])
 
476
            self.rebuild()
 
477
        self.refresh()
 
478
    
 
479
    def refresh(self):
 
480
        """Redraw the screen"""
 
481
        canvas = self.topwidget.render(self.size, focus=True)
 
482
        self.screen.draw_screen(self.size, canvas)
 
483
    
 
484
    def run(self):
 
485
        """Start the main loop and exit when it's done."""
 
486
        self.refresh()
 
487
        self._input_callback_tag = (gobject.io_add_watch
 
488
                                    (sys.stdin.fileno(),
 
489
                                     gobject.IO_IN,
 
490
                                     self.process_input))
 
491
        self.main_loop.run()
 
492
        # Main loop has finished, we should close everything now
 
493
        gobject.source_remove(self._input_callback_tag)
 
494
        self.screen.stop()
 
495
    
 
496
    def stop(self):
 
497
        self.main_loop.quit()
 
498
    
 
499
    def process_input(self, source, condition):
 
500
        keys = self.screen.get_input()
 
501
        translations = { u"ctrl n": u"down",      # Emacs
 
502
                         u"ctrl p": u"up",        # Emacs
 
503
                         u"ctrl v": u"page down", # Emacs
 
504
                         u"meta v": u"page up",   # Emacs
 
505
                         u" ": u"page down",      # less
 
506
                         u"f": u"page down",      # less
 
507
                         u"b": u"page up",        # less
 
508
                         u"j": u"down",           # vi
 
509
                         u"k": u"up",             # vi
 
510
                         }
 
511
        for key in keys:
 
512
            try:
 
513
                key = translations[key]
 
514
            except KeyError:    # :-)
 
515
                pass
 
516
            
 
517
            if key == u"q" or key == u"Q":
 
518
                self.stop()
 
519
                break
 
520
            elif key == u"window resize":
 
521
                self.size = self.screen.get_cols_rows()
 
522
                self.refresh()
 
523
            elif key == u"\f":  # Ctrl-L
 
524
                self.refresh()
 
525
            elif key == u"l" or key == u"D":
 
526
                self.toggle_log_display()
 
527
                self.refresh()
 
528
            elif key == u"w" or key == u"i":
 
529
                self.change_log_display()
 
530
                self.refresh()
 
531
            elif key == u"?" or key == u"f1" or key == u"esc":
 
532
                if not self.log_visible:
 
533
                    self.log_visible = True
 
534
                    self.rebuild()
 
535
                self.log_message_raw((u"bold",
 
536
                                      u"  ".
 
537
                                      join((u"q: Quit",
 
538
                                            u"?: Help",
 
539
                                            u"l: Log window toggle",
 
540
                                            u"TAB: Switch window",
 
541
                                            u"w: Wrap (log)"))))
 
542
                self.log_message_raw((u"bold",
 
543
                                      u"  "
 
544
                                      .join((u"Clients:",
 
545
                                             u"e: Enable",
 
546
                                             u"d: Disable",
 
547
                                             u"r: Remove",
 
548
                                             u"s: Start new checker",
 
549
                                             u"S: Stop checker",
 
550
                                             u"C: Checker OK"))))
 
551
                self.refresh()
 
552
            elif key == u"tab":
 
553
                if self.topwidget.get_focus() is self.logbox:
 
554
                    self.topwidget.set_focus(0)
 
555
                else:
 
556
                    self.topwidget.set_focus(self.logbox)
 
557
                self.refresh()
 
558
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
559
            #      or key == u">"):
 
560
            #    pass            # xxx end-of-buffer
 
561
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
562
            #      or key == u"<"):
 
563
            #    pass            # xxx beginning-of-buffer
 
564
            #elif key == u"ctrl e" or key == u"$":
 
565
            #    pass            # xxx move-end-of-line
 
566
            #elif key == u"ctrl a" or key == u"^":
 
567
            #    pass            # xxx move-beginning-of-line
 
568
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
569
            #    pass            # xxx left
 
570
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
571
            #    pass            # xxx right
 
572
            #elif key == u"a":
 
573
            #    pass            # scroll up log
 
574
            #elif key == u"z":
 
575
            #    pass            # scroll down log
 
576
            elif self.topwidget.selectable():
 
577
                self.topwidget.keypress(self.size, key)
 
578
                self.refresh()
 
579
        return True
 
580
 
 
581
ui = UserInterface()
 
582
try:
 
583
    ui.run()
 
584
except Exception, e:
 
585
    ui.log_message(unicode(e))
 
586
    ui.screen.stop()
 
587
    raise