~ubuntu-core-dev/ubuntu/lucid/apport/ubuntu

655.1.1 by mh21 at piware
First basic crude untested version.
1
#!/usr/bin/python
2
1369.1.310 by Martin Pitt
Update all copyright and description headers and consistently format them.
3
'''Command line Apport user interface.'''
4
5
# Copyright (C) 2007 - 2009 Canonical Ltd.
6
# Author: Michael Hofmann <mh21@piware.de>
7
# 
8
# This program is free software; you can redistribute it and/or modify it
9
# under the terms of the GNU General Public License as published by the
10
# Free Software Foundation; either version 2 of the License, or (at your
11
# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
12
# the full text of the license.
655.1.1 by mh21 at piware
First basic crude untested version.
13
655.1.5 by mh21 at piware
CLI Fixes:
14
# Web browser support:
15
#    w3m, lynx: do not work
16
#    elinks: works
17
1050 by Martin Pitt
* cli/apport-cli: Intercept SIGPIPE when calling sensible-pager, to avoid
18
import os.path, os, sys, subprocess, re, errno
1369.1.75 by Martin Pitt
apport-cli: Fix report saving in "bug report" mode. (LP: #353253)
19
import tty, termios, tempfile
655.1.10 by mh21 at piware
Control-C and wording fixes, shows date and time of crash.
20
from datetime import datetime
655.1.1 by mh21 at piware
First basic crude untested version.
21
1369.1.215 by Martin Pitt
Update usage of gettext to work around Python bug of gettext() not returning unicodes, but str. Fixes UnicodeDecodeErrors on translated --help output.
22
from apport import unicode_gettext as _
1369.18.1 by Marco Rodrigues
Add possibility of translation for some scripts
23
import apport.ui
655.1.1 by mh21 at piware
First basic crude untested version.
24
25
class CLIDialog:
26
    '''Command line dialog wrapper.'''
27
28
    def __init__(self, heading, text):
655.1.5 by mh21 at piware
CLI Fixes:
29
        self.heading = '\n*** ' + heading + '\n'
655.1.1 by mh21 at piware
First basic crude untested version.
30
        self.text = text
31
        self.keys = []
32
        self.buttons = []
33
        self.visible = False
34
1369.1.245 by Martin Pitt
apport-cli: Fix crash with non-ASCII characters in prompts.
35
    def raw_input_char(self, prompt):
1369.1.215 by Martin Pitt
Update usage of gettext to work around Python bug of gettext() not returning unicodes, but str. Fixes UnicodeDecodeErrors on translated --help output.
36
        '''raw_input, but read only one character'''
655.1.5 by mh21 at piware
CLI Fixes:
37
1369.1.245 by Martin Pitt
apport-cli: Fix crash with non-ASCII characters in prompts.
38
        print >> sys.stdout, prompt,
655.1.5 by mh21 at piware
CLI Fixes:
39
40
        file = sys.stdin.fileno()
41
        saved_attributes = termios.tcgetattr(file)
42
        attributes = termios.tcgetattr(file)
43
        attributes[3] = attributes[3] & ~(termios.ICANON)
44
        attributes[6][termios.VMIN] = 1
45
        attributes[6][termios.VTIME] = 0
46
        termios.tcsetattr(file, termios.TCSANOW, attributes)
47
48
        try:
49
            ch = str(sys.stdin.read(1))
50
        finally:
51
            termios.tcsetattr(file, termios.TCSANOW, saved_attributes)
52
53
        print
54
        return ch
55
655.1.1 by mh21 at piware
First basic crude untested version.
56
    def show(self):
57
        self.visible = True
58
        print self.heading
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
59
        if self.text:
60
            print self.text
655.1.1 by mh21 at piware
First basic crude untested version.
61
1369.4.9 by Martin Pitt
implement interactive hooks functions for CLI
62
    def run(self, prompt=None):
655.1.1 by mh21 at piware
First basic crude untested version.
63
        if not self.visible:
64
            self.show()
65
655.1.5 by mh21 at piware
CLI Fixes:
66
        print
655.1.10 by mh21 at piware
Control-C and wording fixes, shows date and time of crash.
67
        try:
68
            # Only one button
69
            if len (self.keys) <= 1:
70
                self.raw_input_char(_('Press any key to continue...'))
71
                return 0
72
            # Multiple choices
73
            while True:
1369.4.9 by Martin Pitt
implement interactive hooks functions for CLI
74
                if prompt is not None:
75
                    print prompt
76
                else:
77
                    print _('What would you like to do? Your options are:')
655.1.10 by mh21 at piware
Control-C and wording fixes, shows date and time of crash.
78
                for index, button in enumerate(self.buttons):
79
                    print '  %s: %s' % (self.keys[index], button)
80
81
                response = self.raw_input_char(_('Please choose (%s):') % ('/'.join(self.keys)))
82
                try:
83
                    return self.keys.index(response[0].upper()) + 1
84
                except ValueError:
85
                    pass
86
        except KeyboardInterrupt:
87
            print
88
            sys.exit(1)
655.1.1 by mh21 at piware
First basic crude untested version.
89
1369.4.9 by Martin Pitt
implement interactive hooks functions for CLI
90
    def addbutton(self, button, hotkey=None):
91
        if hotkey:
92
            self.keys.append(hotkey)
93
            self.buttons.append(button)
94
        else:
95
            self.keys.append(re.search('&(.)', button).group(1).upper())
96
            self.buttons.append(re.sub('&', '', button))
655.1.1 by mh21 at piware
First basic crude untested version.
97
        return len(self.keys)
98
99
100
class CLIProgressDialog(CLIDialog):
101
    '''Command line progress dialog wrapper.'''
102
103
    def __init__(self, heading, text):
104
        CLIDialog.__init__(self, heading, text)
655.1.5 by mh21 at piware
CLI Fixes:
105
        self.progresscount = 0
106
107
    def set(self, progress = None):
108
        self.progresscount = (self.progresscount + 1) % 5
109
        if self.progresscount:
110
            return
111
112
        if progress != None:
113
            sys.stdout.write('\r%u%%' % (progress * 100))
114
        else:
115
            sys.stdout.write('.')
655.1.1 by mh21 at piware
First basic crude untested version.
116
        sys.stdout.flush()
117
118
class CLIUserInterface(apport.ui.UserInterface):
119
    '''Command line Apport user interface'''
120
121
    def __init__(self):
122
        apport.ui.UserInterface.__init__(self)
123
124
    #
125
    # ui_* implementation of abstract UserInterface classes
126
    #
127
128
    def ui_present_crash(self, desktop_entry):
655.1.10 by mh21 at piware
Control-C and wording fixes, shows date and time of crash.
129
        date = datetime.strptime(self.report['Date'], '%a %b %d %H:%M:%S %Y')
655.1.1 by mh21 at piware
First basic crude untested version.
130
        # adapt dialog heading and label appropriately
131
        if desktop_entry:
132
            name = desktop_entry.getName()
133
        elif self.report.has_key('ExecutablePath'):
134
            name = os.path.basename(self.report['ExecutablePath'])
135
        else:
136
            name = self.cur_package
1096.1.3 by Daniel Hahler
cli/apport-cli: Fix UnboundLocalError in ui_present_crash, which rendered
137
        # translators: first %s: application name, second %s: date, third %s: time
138
        heading = _('%s closed unexpectedly on %s at %s.') % (name, date.date(), date.time())
655.1.1 by mh21 at piware
First basic crude untested version.
139
655.1.5 by mh21 at piware
CLI Fixes:
140
        dialog = CLIDialog(
655.1.1 by mh21 at piware
First basic crude untested version.
141
                heading,
142
                _('If you were not doing anything confidential (entering passwords or other\n'
143
                  'private information), you can help to improve the application by reporting\n'
144
                  'the problem.'))
145
        report = dialog.addbutton(_('&Report Problem...'))
146
        ignore = dialog.addbutton(_('Cancel and &ignore future crashes of this program version'))
147
        dialog.addbutton(_('&Cancel'))
148
149
        # show crash notification dialog
150
        response = dialog.run()
151
152
        if response == report:
153
            return {'action': 'report', 'blacklist': False}
154
        if response == ignore:
155
            return {'action': 'cancel', 'blacklist': True}
156
        # Fallback
157
        return {'action': 'cancel', 'blacklist': False}
158
159
    def ui_present_package_error(self):
160
        name = self.report['Package']
655.1.5 by mh21 at piware
CLI Fixes:
161
        dialog = CLIDialog(
655.1.1 by mh21 at piware
First basic crude untested version.
162
                _('The package "%s" failed to install or upgrade.') % name,
163
                _('You can help the developers to fix the package by reporting the problem.'))
164
        report = dialog.addbutton(_('&Report Problem...'))
165
        dialog.addbutton(_('&Cancel'))
166
1018 by martin at piware
* cli/apport-cli, ui_present_package_error(): Fix running of dialog, so that
167
        response = dialog.run()
655.1.1 by mh21 at piware
First basic crude untested version.
168
169
        if response == report:
170
            return 'report'
171
        # Fallback
172
        return 'cancel'
173
174
    def ui_present_kernel_error(self):
1254 by Martin Pitt
cli/apport-cli, qt4/apport-qt: Unify string with apport-gtk.
175
        message = _('Your system encountered a serious kernel problem.')
1251.1.1 by Andy Whitcroft
bin/apportcheckresume, bin/kernel_oops, cli/apport-cli, gtk/apport-gtk,
176
        annotation = ''
177
        if self.report.has_key('Annotation'):
178
            annotation += self.report['Annotation'] + '\n\n'
179
        annotation += _('You can help the developers to fix the problem by reporting it.')
180
181
        dialog = CLIDialog (message, annotation)
655.1.1 by mh21 at piware
First basic crude untested version.
182
        report = dialog.addbutton(_('&Report Problem...'))
183
        dialog.addbutton(_('&Cancel'))
184
185
        response = dialog.run()
186
187
        if response == report:
188
            return 'report'
189
        # Fallback
190
        return 'cancel'
191
1369.1.365 by Martin Pitt
apport-cli: Create the details string only if user wants to view details, and do not show files larger than 1MB. Thanks Scott Moser! (LP: #486122)
192
    def _get_details(self):
193
        '''Build report string for display.'''
655.1.2 by mh21 at piware
Pager support to view the sent report.
194
195
        details = ''
1369.1.365 by Martin Pitt
apport-cli: Create the details string only if user wants to view details, and do not show files larger than 1MB. Thanks Scott Moser! (LP: #486122)
196
        max_show = 1000000
1369.1.367 by Martin Pitt
Sort the report by key in the details view. (LP: #519416)
197
        for key in sorted(self.report):
655.1.2 by mh21 at piware
Pager support to view the sent report.
198
            details += key + ':'
979 by Martin Pitt
* apport/ui.py: Load crash report with keeping compressed binaries. This
199
            # string value
1369.1.365 by Martin Pitt
apport-cli: Create the details string only if user wants to view details, and do not show files larger than 1MB. Thanks Scott Moser! (LP: #486122)
200
            keylen = len(self.report[key])
1099 by Martin Pitt
* apport-{gtk,qt,cli}: Fix handling of file references added by package
201
            if not hasattr(self.report[key], 'gzipvalue') and \
202
                hasattr(self.report[key], 'isspace') and \
1369.1.365 by Martin Pitt
apport-cli: Create the details string only if user wants to view details, and do not show files larger than 1MB. Thanks Scott Moser! (LP: #486122)
203
                not self.report._is_binary(self.report[key]) and \
204
                keylen < max_show:
979 by Martin Pitt
* apport/ui.py: Load crash report with keeping compressed binaries. This
205
                lines = self.report[key].splitlines()
206
                if len(lines) <= 1:
207
                    details += ' ' + self.report[key] + '\n'
655.1.2 by mh21 at piware
Pager support to view the sent report.
208
                else:
979 by Martin Pitt
* apport/ui.py: Load crash report with keeping compressed binaries. This
209
                    details += '\n'
210
                    for line in lines:
211
                        details += '  ' + line + '\n'
1369.1.365 by Martin Pitt
apport-cli: Create the details string only if user wants to view details, and do not show files larger than 1MB. Thanks Scott Moser! (LP: #486122)
212
            elif keylen >= max_show:
213
                details += ' ' + (_('(%i bytes)') % keylen ) + '\n'
655.1.2 by mh21 at piware
Pager support to view the sent report.
214
            else:
979 by Martin Pitt
* apport/ui.py: Load crash report with keeping compressed binaries. This
215
                details += ' ' + _('(binary data)') + '\n'
655.1.2 by mh21 at piware
Pager support to view the sent report.
216
1369.1.365 by Martin Pitt
apport-cli: Create the details string only if user wants to view details, and do not show files larger than 1MB. Thanks Scott Moser! (LP: #486122)
217
        return details
218
219
220
    def ui_present_report_details(self, is_update):
221
        if is_update:
222
            dialog = CLIDialog (_('Send this data to the developers?'), None)
223
        else:
224
            dialog = CLIDialog (_('Send problem report to the developers?'), 
225
                    _('After the problem report has been sent, please fill out the form in the\n'
226
                      'automatically opened web browser.'))
227
655.1.2 by mh21 at piware
Pager support to view the sent report.
228
        # complete/reduced reports
764 by martin at piware
* gtk/apport-gtk, qt4/apport-qt, cli/apport-cli: Do not offer 'reduced
229
        if self.report.has_key('CoreDump') and self.report.has_useful_stacktrace():
655.1.2 by mh21 at piware
Pager support to view the sent report.
230
            complete = dialog.addbutton(_('&Send complete report (recommended; %s)') %
655.1.1 by mh21 at piware
First basic crude untested version.
231
                    self.format_filesize(self.get_complete_size()))
655.1.2 by mh21 at piware
Pager support to view the sent report.
232
            reduced = dialog.addbutton(_('Send &reduced report (slow Internet connection; %s)') %
655.1.1 by mh21 at piware
First basic crude untested version.
233
                    self.format_filesize(self.get_reduced_size()))
655.1.2 by mh21 at piware
Pager support to view the sent report.
234
        else:
235
            complete = dialog.addbutton(_('&Send report (%s)') %
236
                    self.format_filesize(self.get_complete_size()))
237
            reduced = None
238
655.1.1 by mh21 at piware
First basic crude untested version.
239
        view = dialog.addbutton(_('&View report'))
910 by martin at piware
* cli/apport-cli: Add option for keeping the report file without sending it,
240
        save = dialog.addbutton(_('&Keep report file for sending later or copying to somewhere else'))
241
        
655.1.1 by mh21 at piware
First basic crude untested version.
242
        dialog.addbutton(_('&Cancel'))
243
244
        while True:
245
            response = dialog.run()
246
247
            if response == complete:
248
                return 'full'
249
            if response == reduced:
250
                return 'reduced'
251
            if response == view:
1050 by Martin Pitt
* cli/apport-cli: Intercept SIGPIPE when calling sensible-pager, to avoid
252
                try:
253
                    subprocess.Popen(["/usr/bin/sensible-pager"],
254
                            stdin=subprocess.PIPE,
1369.1.365 by Martin Pitt
apport-cli: Create the details string only if user wants to view details, and do not show files larger than 1MB. Thanks Scott Moser! (LP: #486122)
255
                            close_fds=True).communicate(self._get_details())
1050 by Martin Pitt
* cli/apport-cli: Intercept SIGPIPE when calling sensible-pager, to avoid
256
                except IOError, e:
257
                    # ignore broken pipe (premature quit)
258
                    if e.errno == errno.EPIPE:
259
                        pass
260
                    else:
261
                        raise
655.1.1 by mh21 at piware
First basic crude untested version.
262
                continue
910 by martin at piware
* cli/apport-cli: Add option for keeping the report file without sending it,
263
            if response == save:
1369.1.75 by Martin Pitt
apport-cli: Fix report saving in "bug report" mode. (LP: #353253)
264
                # we do not already have a report file if we report a bug
265
                if not self.report_file:
1369.1.186 by Martin Pitt
apport-cli: Save reports with .apport extension instead of .txt. Thanks to Steve Beattie! (LP: #401983)
266
                    prefix = 'apport.'
267
                    if self.report.has_key('Package'):
268
                        prefix += self.report['Package'].split()[0] + '.'
269
                    (fd, self.report_file) = tempfile.mkstemp(prefix=prefix, suffix='.apport')
1369.1.75 by Martin Pitt
apport-cli: Fix report saving in "bug report" mode. (LP: #353253)
270
                    self.report.write(os.fdopen(fd, 'w'))
271
910 by martin at piware
* cli/apport-cli: Add option for keeping the report file without sending it,
272
                print _('Problem report file:'), self.report_file
273
                return 'cancel'
274
655.1.1 by mh21 at piware
First basic crude untested version.
275
            # Fallback
276
            return 'cancel'
277
278
    def ui_info_message(self, title, text):
655.1.5 by mh21 at piware
CLI Fixes:
279
        dialog = CLIDialog(title, text)
280
        dialog.addbutton(_('&Confirm'))
281
        dialog.run()
655.1.1 by mh21 at piware
First basic crude untested version.
282
283
    def ui_error_message(self, title, text):
655.1.5 by mh21 at piware
CLI Fixes:
284
        dialog = CLIDialog(_('Error: %s') % title, text)
285
        dialog.addbutton(_('&Confirm'))
286
        dialog.run()
655.1.1 by mh21 at piware
First basic crude untested version.
287
288
    def ui_start_info_collection_progress(self):
289
        self.progress = CLIProgressDialog (
290
                _('Collecting problem information'),
1008 by martin at piware
* cli/apport-cli, qt4/apport-qt: Fix typo 'send' -> 'sent'.
291
                _('The collected information can be sent to the developers to improve the\n'
707 by Martin Pitt
* cli/apport-cli, qt4/apport-qt: Fix bad grammar 'some minutes'.
292
                  'application. This might take a few minutes.'))
655.1.1 by mh21 at piware
First basic crude untested version.
293
        self.progress.show()
294
295
    def ui_pulse_info_collection_progress(self):
296
        self.progress.set()
297
298
    def ui_stop_info_collection_progress(self):
299
        print
300
301
    def ui_start_upload_progress(self):
302
        self.progress = CLIProgressDialog (
303
                _('Uploading problem information'),
329.1.42 by Kees Cook
english grammar fixup (thanks to Brian Murray)
304
                _('The collected information is being sent to the bug tracking system.\n'
707 by Martin Pitt
* cli/apport-cli, qt4/apport-qt: Fix bad grammar 'some minutes'.
305
                  'This might take a few minutes.'))
655.1.1 by mh21 at piware
First basic crude untested version.
306
        self.progress.show()
307
308
    def ui_set_upload_progress(self, progress):
655.1.5 by mh21 at piware
CLI Fixes:
309
        self.progress.set(progress)
655.1.1 by mh21 at piware
First basic crude untested version.
310
311
    def ui_stop_upload_progress(self):
655.1.5 by mh21 at piware
CLI Fixes:
312
        print
655.1.1 by mh21 at piware
First basic crude untested version.
313
1369.4.13 by Martin Pitt
introduce apport.ui.HookUI
314
    def ui_question_yesno(self, text):
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
315
        '''Show a yes/no question.
1369.4.4 by Martin Pitt
define new UI functions for interactive hooks
316
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
317
        Return True if the user selected "Yes", False if selected "No" or
318
        "None" on cancel/dialog closing.
1369.4.4 by Martin Pitt
define new UI functions for interactive hooks
319
        '''
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
320
        dialog = CLIDialog(text, None)
1369.4.9 by Martin Pitt
implement interactive hooks functions for CLI
321
        r_yes = dialog.addbutton('&Yes')
1369.4.16 by Martin Pitt
apport-cli: Fix totally broken yesno() implementation
322
        r_no = dialog.addbutton('&No')
323
        r_cancel = dialog.addbutton(_('&Cancel'))
324
        result = dialog.run()
325
        if result == r_yes:
326
            return True
327
        if result == r_no:
328
            return False
329
        assert result == r_cancel
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
330
        return None
1369.4.4 by Martin Pitt
define new UI functions for interactive hooks
331
1369.4.13 by Martin Pitt
introduce apport.ui.HookUI
332
    def ui_question_choice(self, text, options, multiple):
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
333
        '''Show an question with predefined choices.
1369.4.4 by Martin Pitt
define new UI functions for interactive hooks
334
335
        options is a list of strings to present. If multiple is True, they
336
        should be check boxes, if multiple is False they should be radio
337
        buttons.
338
339
        Return list of selected option indexes, or None if the user cancelled.
340
        If multiple == False, the list will always have one element.
341
        '''
1369.4.9 by Martin Pitt
implement interactive hooks functions for CLI
342
        result = []
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
343
        dialog = CLIDialog(text, None)
1369.4.9 by Martin Pitt
implement interactive hooks functions for CLI
344
345
        if multiple:
346
            while True:
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
347
                dialog = CLIDialog(text, None)
1369.4.9 by Martin Pitt
implement interactive hooks functions for CLI
348
                index = 0
349
                choice_index_map = {}
350
                for option in options:
351
                    if index not in result:
352
                        choice_index_map[dialog.addbutton(option, str(index+1))] = index
353
                    index += 1
354
                done = dialog.addbutton(_('&Done'))
355
                cancel = dialog.addbutton(_('&Cancel'))
356
357
                if result:
358
                    cur = ', '.join([str(r+1) for r in result])
359
                else:
360
                    cur = _('none')
361
                response = dialog.run(_('Selected: %s. Multiple choices:') % cur)
362
                if response == cancel:
363
                    return None
364
                if response == done:
365
                    break
366
                result.append(choice_index_map[response])
367
368
        else:
369
            # single choice (radio button)
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
370
            dialog = CLIDialog(text, None)
1369.4.9 by Martin Pitt
implement interactive hooks functions for CLI
371
            index = 1
372
            for option in options:
373
                dialog.addbutton(option, str(index))
374
                index += 1
375
376
            cancel = dialog.addbutton(_('&Cancel'))
377
            response = dialog.run(_('Choices:'))
378
            if response == cancel:
379
                return None
380
            result.append(response-1)
381
382
        return result
1369.4.4 by Martin Pitt
define new UI functions for interactive hooks
383
1369.4.13 by Martin Pitt
introduce apport.ui.HookUI
384
    def ui_question_file(self, text):
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
385
        '''Show a file selector dialog.
1369.4.4 by Martin Pitt
define new UI functions for interactive hooks
386
387
        Return path if the user selected a file, or None if cancelled.
388
        '''
1369.4.12 by Martin Pitt
rename hook UI functions, drop titles
389
        print '\n*** ', text
1369.4.9 by Martin Pitt
implement interactive hooks functions for CLI
390
        while True:
391
            print _('Path to file (Enter to cancel):'), ' ',
392
            f = sys.stdin.readline().strip()
393
            if not f:
394
                return None
395
            if not os.path.exists(f):
396
                print _('File does not exist.')
397
            elif os.path.isdir(f):
398
                print _('This is a directory.')
399
            else:
400
                return f
1369.4.4 by Martin Pitt
define new UI functions for interactive hooks
401
1369.1.241 by Martin Pitt
apport-cli: Print the URL and ask whether to open a browser. (LP: #286415)
402
    def open_url(self, url):
403
        text = '%s\n\n  %s\n\n%s' % (
404
            _('To continue, you must visit the following URL:'),
405
            url,
406
            _('You can launch a browser now, or copy this URL into a browser on another computer.'))
407
408
        answer = self.ui_question_choice(text, [_('Launch a browser now')], False)
1369.1.365 by Martin Pitt
apport-cli: Create the details string only if user wants to view details, and do not show files larger than 1MB. Thanks Scott Moser! (LP: #486122)
409
        if answer == [0]:
1369.1.241 by Martin Pitt
apport-cli: Print the URL and ask whether to open a browser. (LP: #286415)
410
            apport.ui.UserInterface.open_url(self, url)
411
655.1.1 by mh21 at piware
First basic crude untested version.
412
if __name__ == '__main__':
413
    app = CLIUserInterface()
1052 by Martin Pitt
* apport/ui.py, run_argv(): Add return code which indicates whether any
414
    if not app.run_argv():
415
        print >> sys.stderr, _('No pending crash reports. Try --help for more information.')