Back to index

apport  2.4
ui.py
Go to the documentation of this file.
00001 '''Abstract Apport user interface.
00002 
00003 This encapsulates the workflow and common code for any user interface
00004 implementation (like GTK, Qt, or CLI).
00005 '''
00006 
00007 # Copyright (C) 2007 - 2011 Canonical Ltd.
00008 # Author: Martin Pitt <martin.pitt@ubuntu.com>
00009 #
00010 # This program is free software; you can redistribute it and/or modify it
00011 # under the terms of the GNU General Public License as published by the
00012 # Free Software Foundation; either version 2 of the License, or (at your
00013 # option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
00014 # the full text of the license.
00015 
00016 __version__ = '2.4'
00017 
00018 import glob, sys, os.path, optparse, traceback, locale, gettext
00019 import errno, zlib
00020 import subprocess, threading, webbrowser
00021 import signal
00022 import time
00023 
00024 import apport, apport.fileutils, apport.REThread
00025 
00026 from apport.crashdb import get_crashdb, NeedsCredentials
00027 from apport import unicode_gettext as _
00028 
00029 if sys.version_info.major == 2:
00030     from ConfigParser import ConfigParser
00031     ConfigParser  # pyflakes
00032 else:
00033     from configparser import ConfigParser
00034 
00035 
00036 def excstr(exception):
00037     '''Return exception message as unicode.'''
00038 
00039     if sys.version_info.major == 2:
00040         return str(exception).decode(locale.getpreferredencoding(), 'replace')
00041     return str(exception)
00042 
00043 symptom_script_dir = os.environ.get('APPORT_SYMPTOMS_DIR',
00044                                     '/usr/share/apport/symptoms')
00045 PF_KTHREAD = 0x200000
00046 
00047 
00048 def thread_collect_info(report, reportfile, package, ui, symptom_script=None,
00049                         ignore_uninstalled=False):
00050     '''Collect information about report.
00051 
00052     Encapsulate calls to add_*_info() and update given report, so that this
00053     function is suitable for threading.
00054 
00055     ui must be a HookUI instance, it gets passed to add_hooks_info().
00056 
00057     If reportfile is not None, the file is written back with the new data.
00058 
00059     If symptom_script is given, it will be run first (for run_symptom()).
00060     '''
00061     report.add_gdb_info()
00062     report.add_os_info()
00063 
00064     if symptom_script:
00065         symb = {}
00066         try:
00067             with open(symptom_script) as f:
00068                 exec(compile(f.read(), symptom_script, 'exec'), symb)
00069             package = symb['run'](report, ui)
00070             if not package:
00071                 apport.error('symptom script %s did not determine the affected package', symptom_script)
00072                 return
00073             report['Symptom'] = os.path.splitext(os.path.basename(symptom_script))[0]
00074         except StopIteration:
00075             sys.exit(0)
00076         except:
00077             apport.error('symptom script %s crashed:', symptom_script)
00078             traceback.print_exc()
00079             sys.exit(0)
00080 
00081     if not package:
00082         if 'ExecutablePath' in report:
00083             package = apport.fileutils.find_file_package(report['ExecutablePath'])
00084         else:
00085             raise KeyError('called without a package, and report does not have ExecutablePath')
00086     try:
00087         report.add_package_info(package)
00088     except ValueError:
00089         # this happens if we are collecting information on an uninstalled
00090         # package
00091         if not ignore_uninstalled:
00092             raise
00093     except SystemError as e:
00094         report['UnreportableReason'] = excstr(e)
00095         return
00096 
00097     if report.add_hooks_info(ui):
00098         sys.exit(0)
00099 
00100     # check package origin; we do that after adding hooks, so that hooks have
00101     # the chance to set a third-party CrashDB.
00102     try:
00103         if 'CrashDB' not in report and 'APPORT_DISABLE_DISTRO_CHECK' not in os.environ:
00104             if 'Package' not in report:
00105                 report['UnreportableReason'] = _('This package does not seem to be installed correctly')
00106             elif not apport.packaging.is_distro_package(report['Package'].split()[0]):
00107                 #TRANS: %s is the name of the operating system
00108                 report['UnreportableReason'] = _(
00109                     'This is not an official %s package. Please remove any third party package and try again.') % report['DistroRelease'].split()[0]
00110     except ValueError:
00111         # this happens if we are collecting information on an uninstalled
00112         # package
00113         if not ignore_uninstalled:
00114             raise
00115 
00116     # add title
00117     if 'Title' not in report:
00118         title = report.standard_title()
00119         if title:
00120             report['Title'] = title
00121 
00122     # check obsolete packages
00123     if report['ProblemType'] == 'Crash' and 'APPORT_IGNORE_OBSOLETE_PACKAGES' not in os.environ:
00124         old_pkgs = report.obsolete_packages()
00125         if old_pkgs:
00126             report['UnreportableReason'] = _('You have some obsolete package \
00127 versions installed. Please upgrade the following packages and check if the \
00128 problem still occurs:\n\n%s') % ', '.join(old_pkgs)
00129 
00130     # disabled: if we have a SIGABRT without an assertion message, declare as unreportable
00131     #if report.get('Signal') == '6' and 'AssertionMessage' not in report:
00132     #    report['UnreportableReason'] = _('The program crashed on an assertion failure, but the message could not be retrieved. Apport does not support reporting these crashes.')
00133 
00134     if reportfile:
00135         try:
00136             with open(reportfile, 'ab') as f:
00137                 os.chmod(reportfile, 0)
00138                 report.write(f, only_new=True)
00139         except IOError as e:
00140             # this should happen very rarely; presumably a new crash report is
00141             # being generated by a background apport instance (which will set
00142             # the file to permissions zero while writing), while the first
00143             # report is being processed
00144             apport.error('Cannot update %s: %s' % (reportfile, e))
00145 
00146         apport.fileutils.mark_report_seen(reportfile)
00147         os.chmod(reportfile, 0o640)
00148 
00149 
00150 class UserInterface:
00151     '''Apport user interface API.
00152 
00153     This provides an abstract base class for encapsulating the workflow and
00154     common code for any user interface implementation (like GTK, Qt, or CLI).
00155 
00156     A concrete subclass must implement all the abstract ui_* methods.
00157     '''
00158     def __init__(self):
00159         '''Initialize program state and parse command line options.'''
00160 
00161         self.gettext_domain = 'apport'
00162         self.report = None
00163         self.report_file = None
00164         self.cur_package = None
00165 
00166         try:
00167             self.crashdb = get_crashdb(None)
00168         except ImportError as e:
00169             # this can happen while upgrading python packages
00170             apport.fatal('Could not import module, is a package upgrade in progress? Error: %s', str(e))
00171         except KeyError:
00172             apport.fatal('/etc/apport/crashdb.conf is damaged: No default database')
00173 
00174         gettext.textdomain(self.gettext_domain)
00175         self.parse_argv()
00176 
00177     #
00178     # main entry points
00179     #
00180 
00181     def run_crashes(self):
00182         '''Present all currently pending crash reports.
00183 
00184         Ask the user what to do about them, and offer to file bugs for them.
00185 
00186         Return True if at least one crash report was processed, False
00187         otherwise.
00188         '''
00189         result = False
00190 
00191         if os.geteuid() == 0:
00192             reports = apport.fileutils.get_new_system_reports()
00193         else:
00194             reports = apport.fileutils.get_new_reports()
00195         for f in reports:
00196             if not self.load_report(f):
00197                 continue
00198             if self.report['ProblemType'] == 'Hang':
00199                 self.finish_hang(f)
00200             else:
00201                 self.run_crash(f)
00202             result = True
00203 
00204         return result
00205 
00206     def run_crash(self, report_file, confirm=True):
00207         '''Present and report a particular crash.
00208 
00209         If confirm is True, ask the user what to do about it, and offer to file
00210         a bug for it.
00211 
00212         If confirm is False, the user will not be asked, and the crash is
00213         reported right away.
00214         '''
00215         self.report_file = report_file
00216 
00217         try:
00218             try:
00219                 apport.fileutils.mark_report_seen(report_file)
00220             except OSError:
00221                 # not there any more? no problem, then it won't be regarded as
00222                 # "seen" any more anyway
00223                 pass
00224             if not self.report and not self.load_report(report_file):
00225                 return
00226 
00227             if 'Ignore' in self.report:
00228                 return
00229 
00230             # check for absent CoreDumps (removed if they exceed size limit)
00231             if self.report.get('ProblemType') == 'Crash' and 'Signal' in self.report and 'CoreDump' not in self.report and 'Stacktrace' not in self.report:
00232                 subject = os.path.basename(self.report.get('ExecutablePath', _('unknown program')))
00233                 heading = _('Sorry, the program "%s" closed unexpectedly') % subject
00234                 self.ui_error_message(
00235                     _('Problem in %s') % subject,
00236                     '%s\n\n%s' % (heading, _('Your computer does not have '
00237                     'enough free memory to automatically analyze the problem '
00238                     'and send a report to the developers.')))
00239                 return
00240 
00241             allowed_to_report = apport.fileutils.allowed_to_report()
00242             response = self.ui_present_report_details(allowed_to_report)
00243             if response['report'] or response['examine']:
00244                 try:
00245                     if 'Dependencies' not in self.report:
00246                         self.collect_info()
00247                 except (IOError, zlib.error) as e:
00248                     # can happen with broken core dumps
00249                     self.report = None
00250                     self.ui_error_message(
00251                         _('Invalid problem report'), '%s\n\n%s' % (
00252                             _('This problem report is damaged and cannot be processed.'),
00253                             repr(e)))
00254                     self.ui_shutdown()
00255                     return
00256                 except ValueError:  # package does not exist
00257                     self.ui_error_message(_('Invalid problem report'),
00258                                           _('The report belongs to a package that is not installed.'))
00259                     self.ui_shutdown()
00260                     return
00261                 except Exception as e:
00262                     apport.error(repr(e))
00263                     self.ui_error_message(_('Invalid problem report'),
00264                                           _('An error occurred while attempting to process this'
00265                                             ' problem report:') + '\n\n' + str(e))
00266                     self.ui_shutdown()
00267                     return
00268 
00269             if self.report is None:
00270                 # collect() does that on invalid reports
00271                 return
00272 
00273             if response['examine']:
00274                 self.examine()
00275                 return
00276             if response['restart']:
00277                 self.restart()
00278             if response['blacklist']:
00279                 self.report.mark_ignore()
00280             if not response['report']:
00281                 return
00282 
00283             apport.fileutils.mark_report_upload(report_file)
00284             # We check for duplicates and unreportable crashes here, rather
00285             # than before we show the dialog, as we want to submit these to the
00286             # crash database, but not Launchpad.
00287             if self.crashdb.accepts(self.report):
00288                 # FIXME: This behaviour is not really correct, but necessary as
00289                 # long as we only support a single crashdb and have whoopsie
00290                 # hardcoded. Once we have multiple crash dbs, we need to check
00291                 # accepts() earlier, and not even present the data if none of
00292                 # the DBs wants the report. See LP#957177 for details.
00293                 if self.handle_duplicate():
00294                     return
00295                 if self.check_unreportable():
00296                     return
00297                 self.file_report()
00298         except IOError as e:
00299             # fail gracefully if file is not readable for us
00300             if e.errno in (errno.EPERM, errno.EACCES):
00301                 self.ui_error_message(_('Invalid problem report'),
00302                                       _('You are not allowed to access this problem report.'))
00303                 sys.exit(1)
00304             elif e.errno == errno.ENOSPC:
00305                 self.ui_error_message(_('Error'),
00306                                       _('There is not enough disk space available to process this report.'))
00307                 sys.exit(1)
00308             else:
00309                 self.ui_error_message(_('Invalid problem report'), e.strerror)
00310                 sys.exit(1)
00311         except OSError as e:
00312             # fail gracefully on ENOMEM
00313             if e.errno == errno.ENOMEM:
00314                 apport.fatal('Out of memory, aborting')
00315             else:
00316                 raise
00317 
00318     def finish_hang(self, f):
00319         '''Finish processing a hanging application after the core pipe handler
00320         has handed the report back.
00321 
00322         This will signal to whoopsie that the report needs to be uploaded.
00323         '''
00324         apport.fileutils.mark_report_upload(f)
00325         apport.fileutils.mark_report_seen(f)
00326 
00327     def run_hang(self, pid):
00328         '''Report an application hanging.
00329 
00330         This will first present a dialog containing the information it can
00331         collect from the running application (everything but the trace) with
00332         the option of terminating or restarting the application, optionally
00333         reporting that this error occurred.
00334 
00335         A SIGABRT will then be sent to the process and a series of
00336         noninteractive processes will collect the remaining information and
00337         mark the report for uploading.
00338         '''
00339         self.report = apport.Report('Hang')
00340         self.report.add_proc_info(pid)
00341         self.report.add_package_info()
00342         path = self.report.get('ExecutablePath', '')
00343         self.cur_package = apport.fileutils.find_file_package(path)
00344         self.report.add_os_info()
00345         allowed_to_report = apport.fileutils.allowed_to_report()
00346         response = self.ui_present_report_details(allowed_to_report,
00347                                                   modal_for=pid)
00348         if response['report']:
00349             apport.fileutils.mark_hanging_process(self.report, pid)
00350             os.kill(int(pid), signal.SIGABRT)
00351         else:
00352             os.kill(int(pid), signal.SIGKILL)
00353 
00354         if response['restart']:
00355             self.wait_for_pid(pid)
00356             self.restart()
00357 
00358     def wait_for_pid(self, pid):
00359         '''waitpid() does not work for non-child processes. Query the process
00360         state in a loop, waiting for "no such process."
00361         '''
00362         while True:
00363             try:
00364                 os.kill(int(pid), 0)
00365             except OSError as e:
00366                 if e.errno == errno.ESRCH:
00367                     break
00368                 else:
00369                     raise
00370             time.sleep(1)
00371 
00372     def kill_segv(self, pid):
00373         os.kill(int(pid), signal.SIGSEGV)
00374 
00375     def run_report_bug(self, symptom_script=None):
00376         '''Report a bug.
00377 
00378         If a pid is given on the command line, the report will contain runtime
00379         debug information. Either a package or a pid must be specified; if none
00380         is given, show a list of symptoms.
00381 
00382         If a symptom script is given, this will be run first (used by
00383         run_symptom()).
00384         '''
00385         if not self.options.package and not self.options.pid and \
00386                 not symptom_script:
00387             if self.run_symptoms():
00388                 return True
00389             else:
00390                 self.ui_error_message(_('No package specified'),
00391                                       _('You need to specify a package or a PID. See --help for more information.'))
00392             return False
00393 
00394         self.report = apport.Report('Bug')
00395 
00396         # if PID is given, add info
00397         if self.options.pid:
00398             try:
00399                 with open('/proc/%s/stat' % self.options.pid) as f:
00400                     stat = f.read().split()
00401                 flags = int(stat[8])
00402                 if flags & PF_KTHREAD:
00403                     # this PID is a kernel thread
00404                     self.options.package = 'linux'
00405                 else:
00406                     self.report.add_proc_info(self.options.pid)
00407             except (ValueError, IOError):
00408                 self.ui_error_message(_('Invalid PID'),
00409                                       _('The specified process ID does not belong to a program.'))
00410                 return False
00411             except OSError as e:
00412                 # silently ignore nonexisting PIDs; the user must not close the
00413                 # application prematurely
00414                 if e.errno == errno.ENOENT:
00415                     return False
00416                 elif e.errno == errno.EACCES:
00417                     self.ui_error_message(_('Permission denied'),
00418                                           _('The specified process does not belong to you. Please run this program as the process owner or as root.'))
00419                     return False
00420                 else:
00421                     raise
00422         else:
00423             self.report.add_proc_environ()
00424 
00425         if self.options.package:
00426             self.options.package = self.options.package.strip()
00427         # "Do what I mean" for filing against "linux"
00428         if self.options.package == 'linux':
00429             self.cur_package = apport.packaging.get_kernel_package()
00430         else:
00431             self.cur_package = self.options.package
00432 
00433         try:
00434             self.collect_info(symptom_script)
00435         except ValueError as e:
00436             if str(e) == 'package does not exist':
00437                 if not self.cur_package:
00438                     self.ui_error_message(_('Invalid problem report'),
00439                                           _('Symptom script %s did not determine an affected package') % symptom_script)
00440                 else:
00441                     self.ui_error_message(_('Invalid problem report'),
00442                                           _('Package %s does not exist') % self.cur_package)
00443                 return False
00444             else:
00445                 raise
00446 
00447         if self.check_unreportable():
00448             return
00449 
00450         self.add_extra_tags()
00451 
00452         if self.handle_duplicate():
00453             return True
00454 
00455         # not useful for bug reports, and has potentially sensitive information
00456         try:
00457             del self.report['ProcCmdline']
00458         except KeyError:
00459             pass
00460 
00461         if self.options.save:
00462             try:
00463                 with open(os.path.expanduser(self.options.save), 'wb') as f:
00464                     self.report.write(f)
00465             except (IOError, OSError) as e:
00466                 self.ui_error_message(_('Cannot create report'), excstr(e))
00467         else:
00468             # show what's being sent
00469             allowed_to_report = apport.fileutils.allowed_to_report()
00470             response = self.ui_present_report_details(allowed_to_report)
00471             if response['report']:
00472                 self.file_report()
00473 
00474         return True
00475 
00476     def run_update_report(self):
00477         '''Update an existing bug with locally collected information.'''
00478 
00479         # avoid irrelevant noise
00480         if not self.crashdb.can_update(self.options.update_report):
00481             self.ui_error_message(_('Updating problem report'),
00482                                   _('You are not the reporter or subscriber of this '
00483                                     'problem report, or the report is a duplicate or already '
00484                                     'closed.\n\nPlease create a new report using "apport-bug".'))
00485             return False
00486 
00487         is_reporter = self.crashdb.is_reporter(self.options.update_report)
00488 
00489         if not is_reporter:
00490             r = self.ui_question_yesno(
00491                 _('You are not the reporter of this problem report. It '
00492                   'is much easier to mark a bug as a duplicate of another '
00493                   'than to move your comments and attachments to a new bug.\n\n'
00494                   'Subsequently, we recommend that you file a new bug report '
00495                   'using "apport-bug" and make a comment in this bug about '
00496                   'the one you file.\n\n'
00497                   'Do you really want to proceed?'))
00498             if not r:
00499                 return False
00500 
00501         # list of affected source packages
00502         self.report = apport.Report('Bug')
00503         if self.options.package:
00504             pkgs = [self.options.package.strip()]
00505         else:
00506             pkgs = self.crashdb.get_affected_packages(self.options.update_report)
00507 
00508         info_collected = False
00509         for p in pkgs:
00510             #print('Collecting apport information for source package %s...' % p)
00511             self.cur_package = p
00512             self.report['SourcePackage'] = p
00513             self.report['Package'] = p  # no way to find this out
00514 
00515             # we either must have the package installed or a source package hook
00516             # available to collect sensible information
00517             try:
00518                 apport.packaging.get_version(p)
00519             except ValueError:
00520                 if not os.path.exists(os.path.join(apport.report._hook_dir, 'source_%s.py' % p)):
00521                     print('Package %s not installed and no hook available, ignoring' % p)
00522                     continue
00523             self.collect_info(ignore_uninstalled=True)
00524             info_collected = True
00525 
00526         if not info_collected:
00527             self.ui_info_message(_('Updating problem report'),
00528                                  _('No additional information collected.'))
00529             return False
00530 
00531         self.report.add_user_info()
00532         self.report.add_proc_environ()
00533         self.add_extra_tags()
00534 
00535         # delete the uninteresting keys
00536         del self.report['ProblemType']
00537         del self.report['Date']
00538         try:
00539             del self.report['SourcePackage']
00540         except KeyError:
00541             pass
00542 
00543         if len(self.report) == 0:
00544             self.ui_info_message(_('Updating problem report'),
00545                                  _('No additional information collected.'))
00546             return False
00547 
00548         # show what's being sent
00549         allowed_to_report = apport.fileutils.allowed_to_report()
00550         response = self.ui_present_report_details(allowed_to_report)
00551         if response['report']:
00552             self.crashdb.update(self.options.update_report, self.report,
00553                                 'apport information', change_description=is_reporter,
00554                                 attachment_comment='apport information')
00555             return True
00556 
00557         return False
00558 
00559     def run_symptoms(self):
00560         '''Report a bug from a list of available symptoms.
00561 
00562         Return False if no symptoms are available.
00563         '''
00564         scripts = glob.glob(os.path.join(symptom_script_dir, '*.py'))
00565 
00566         symptom_names = []
00567         symptom_descriptions = []
00568         for script in scripts:
00569             # scripts with an underscore can be used for private libraries
00570             if os.path.basename(script).startswith('_'):
00571                 continue
00572             symb = {}
00573             try:
00574                 with open(script) as f:
00575                     exec(compile(f.read(), script, 'exec'), symb)
00576             except:
00577                 apport.error('symptom script %s is invalid', script)
00578                 traceback.print_exc()
00579                 continue
00580             if 'run' not in symb:
00581                 apport.error('symptom script %s does not define run() function', script)
00582                 continue
00583             symptom_names.append(os.path.splitext(os.path.basename(script))[0])
00584             symptom_descriptions.append(symb.get('description', symptom_names[-1]))
00585 
00586         if not symptom_names:
00587             return False
00588 
00589         symptom_descriptions, symptom_names = \
00590             zip(*sorted(zip(symptom_descriptions, symptom_names)))
00591         symptom_descriptions = list(symptom_descriptions)
00592         symptom_names = list(symptom_names)
00593         symptom_names.append(None)
00594         symptom_descriptions.append('Other problem')
00595 
00596         ch = self.ui_question_choice(_('What kind of problem do you want to report?'),
00597                                      symptom_descriptions, False)
00598 
00599         if ch is not None:
00600             symptom = symptom_names[ch[0]]
00601             if symptom:
00602                 self.run_report_bug(os.path.join(symptom_script_dir, symptom + '.py'))
00603             else:
00604                 return False
00605 
00606         return True
00607 
00608     def run_symptom(self):
00609         '''Report a bug with a symptom script.'''
00610 
00611         script = os.path.join(symptom_script_dir, self.options.symptom + '.py')
00612         if not os.path.exists(script):
00613             self.ui_error_message(_('Unknown symptom'),
00614                                   _('The symptom "%s" is not known.') % self.options.symptom)
00615             return
00616 
00617         self.run_report_bug(script)
00618 
00619     def run_argv(self):
00620         '''Call appopriate run_* method according to command line arguments.
00621 
00622         Return True if at least one report has been processed, and False
00623         otherwise.
00624         '''
00625         if self.options.symptom:
00626             self.run_symptom()
00627             return True
00628         elif hasattr(self.options, 'pid') and self.options.hanging:
00629             self.run_hang(self.options.pid)
00630             return True
00631         elif self.options.filebug:
00632             return self.run_report_bug()
00633         elif self.options.update_report is not None:
00634             return self.run_update_report()
00635         elif self.options.version:
00636             print(__version__)
00637             return True
00638         elif self.options.crash_file:
00639             try:
00640                 self.run_crash(self.options.crash_file, False)
00641             except OSError as e:
00642                 self.ui_error_message(_('Invalid problem report'), excstr(e))
00643             return True
00644         elif self.options.window:
00645                 self.ui_info_message('', _('After closing this message '
00646                                            'please click on an application window to report a problem about it.'))
00647                 xprop = subprocess.Popen(['xprop', '_NET_WM_PID'],
00648                                          stdout=subprocess.PIPE, stderr=subprocess.PIPE)
00649                 (out, err) = xprop.communicate()
00650                 if xprop.returncode == 0:
00651                     try:
00652                         self.options.pid = int(out.split()[-1])
00653                     except ValueError:
00654                         self.ui_error_message(_('Cannot create report'),
00655                                               _('xprop failed to determine process ID of the window'))
00656                         return True
00657                     return self.run_report_bug()
00658                 else:
00659                     self.ui_error_message(_('Cannot create report'),
00660                                           _('xprop failed to determine process ID of the window') + '\n\n' + err)
00661                     return True
00662         else:
00663             return self.run_crashes()
00664 
00665     #
00666     # methods that implement workflow bits
00667     #
00668 
00669     def parse_argv_update(self):
00670         '''Parse command line options when being invoked in update mode.
00671 
00672         Return (options, args).
00673         '''
00674         optparser = optparse.OptionParser(_('%prog <report number>'))
00675         optparser.add_option('-p', '--package',
00676                              help=_('Specify package name.'))
00677         optparser.add_option('--tag', action='append', default=[],
00678                              help=_('Add an extra tag to the report. Can be specified multiple times.'))
00679         (self.options, self.args) = optparser.parse_args()
00680 
00681         if len(self.args) != 1 or not self.args[0].isdigit():
00682             optparser.error('You need to specify a report number to update')
00683             sys.exit(1)
00684 
00685         self.options.update_report = int(self.args[0])
00686         self.options.symptom = None
00687         self.options.filebug = False
00688         self.options.crash_file = None
00689         self.options.version = None
00690         self.args = []
00691 
00692     def parse_argv(self):
00693         '''Parse command line options.
00694 
00695         If a single argument is given without any options, this tries to "do
00696         what I mean".
00697         '''
00698         # invoked in update mode?
00699         if len(sys.argv) > 0:
00700             if 'APPORT_INVOKED_AS' in os.environ:
00701                 sys.argv[0] = os.path.join(os.path.dirname(sys.argv[0]),
00702                                            os.path.basename(os.environ['APPORT_INVOKED_AS']))
00703             cmd = sys.argv[0]
00704             if cmd.endswith('-update-bug') or cmd.endswith('-collect'):
00705                 self.parse_argv_update()
00706                 return
00707 
00708         optparser = optparse.OptionParser(_('%prog [options] [symptom|pid|package|program path|.apport/.crash file]'))
00709         optparser.add_option('-f', '--file-bug', action='store_true',
00710                              dest='filebug', default=False,
00711                              help=_('Start in bug filing mode. Requires --package and an optional --pid, or just a --pid. If neither is given, display a list of known symptoms. (Implied if a single argument is given.)'))
00712         optparser.add_option('-w', '--window', action='store_true', default=False,
00713                              help=_('Click a window as a target for filing a problem report.'))
00714         optparser.add_option('-u', '--update-bug', type='int', dest='update_report',
00715                              help=_('Start in bug updating mode. Can take an optional --package.'))
00716         optparser.add_option('-s', '--symptom', metavar='SYMPTOM',
00717                              help=_('File a bug report about a symptom. (Implied if symptom name is given as only argument.)'))
00718         optparser.add_option('-p', '--package',
00719                              help=_('Specify package name in --file-bug mode. This is optional if a --pid is specified. (Implied if package name is given as only argument.)'))
00720         optparser.add_option('-P', '--pid', type='int',
00721                              help=_('Specify a running program in --file-bug mode. If this is specified, the bug report will contain more information.  (Implied if pid is given as only argument.)'))
00722         optparser.add_option('--hanging', action='store_true', default=False,
00723                              help=_('The provided pid is a hanging application.'))
00724         optparser.add_option('-c', '--crash-file', metavar='PATH',
00725                              help=_('Report the crash from given .apport or .crash file instead of the pending ones in %s. (Implied if file is given as only argument.)') % apport.fileutils.report_dir)
00726         optparser.add_option('--save', metavar='PATH',
00727                              help=_('In bug filing mode, save the collected information into a file instead of reporting it. This file can then be reported later on from a different machine.'))
00728         optparser.add_option('--tag', action='append', default=[],
00729                              help=_('Add an extra tag to the report. Can be specified multiple times.'))
00730         optparser.add_option('-v', '--version', action='store_true',
00731                              help=_('Print the Apport version number.'))
00732 
00733         if len(sys.argv) > 0 and cmd.endswith('-bug'):
00734             for o in ('-f', '-u', '-s', '-p', '-P', '-c'):
00735                 optparser.get_option(o).help = optparse.SUPPRESS_HELP
00736 
00737         (self.options, self.args) = optparser.parse_args()
00738 
00739         # "do what I mean" for zero or one arguments
00740         if len(sys.argv) == 0:
00741             return
00742 
00743         # no argument: default to "show pending crashes" except when called in
00744         # bug mode
00745         # NOTE: uses sys.argv, since self.args if empty for all the options,
00746         # e.g. "-v" or "-u $BUG"
00747         if len(sys.argv) == 1 and cmd.endswith('-bug'):
00748             self.options.filebug = True
00749             return
00750 
00751         # one argument: guess "file bug" mode by argument type
00752         if len(self.args) != 1:
00753             return
00754 
00755         # symptom?
00756         if os.path.exists(os.path.join(symptom_script_dir, self.args[0] + '.py')):
00757             self.options.filebug = True
00758             self.options.symptom = self.args[0]
00759             self.args = []
00760 
00761         # .crash/.apport file?
00762         elif self.args[0].endswith('.crash') or self.args[0].endswith('.apport'):
00763             self.options.crash_file = self.args[0]
00764             self.args = []
00765 
00766         # PID?
00767         elif self.args[0].isdigit():
00768             self.options.filebug = True
00769             self.options.pid = self.args[0]
00770             self.args = []
00771 
00772         # executable?
00773         elif '/' in self.args[0]:
00774             pkg = apport.packaging.get_file_package(self.args[0])
00775             if not pkg:
00776                 optparser.error('%s does not belong to a package.' % self.args[0])
00777                 sys.exit(1)
00778             self.args = []
00779             self.options.filebug = True
00780             self.options.package = pkg
00781 
00782         # otherwise: package name
00783         else:
00784             self.options.filebug = True
00785             self.options.package = self.args[0]
00786             self.args = []
00787 
00788     def format_filesize(self, size):
00789         '''Format the given integer as humanly readable and i18n'ed file size.'''
00790 
00791         if size < 1000000:
00792             return locale.format('%.1f', size / 1000.) + ' KB'
00793         if size < 1000000000:
00794             return locale.format('%.1f', size / 1000000.) + ' MB'
00795         return locale.format('%.1f', size / float(1000000000)) + ' GB'
00796 
00797     def get_complete_size(self):
00798         '''Return the size of the complete report.'''
00799 
00800         # report wasn't loaded, so count manually
00801         size = 0
00802         for k in self.report:
00803             if self.report[k]:
00804                 try:
00805                     # if we have a compressed value, take its size, but take
00806                     # base64 overhead into account
00807                     size += len(self.report[k].gzipvalue) * 8 / 6
00808                 except AttributeError:
00809                     size += len(self.report[k])
00810         return size
00811 
00812     def get_reduced_size(self):
00813         '''Return the size of the reduced report.'''
00814 
00815         size = 0
00816         for k in self.report:
00817             if k != 'CoreDump':
00818                 if self.report[k]:
00819                     try:
00820                         # if we have a compressed value, take its size, but take
00821                         # base64 overhead into account
00822                         size += len(self.report[k].gzipvalue) * 8 / 6
00823                     except AttributeError:
00824                         size += len(self.report[k])
00825 
00826         return size
00827 
00828     def can_examine_locally(self):
00829         '''Check whether to offer the "Examine locally" button.
00830 
00831         This will be true if the report has a core dump, apport-retrace is
00832         installed and a terminal is available (see ui_run_terminal()).
00833         '''
00834         if not self.report or 'CoreDump' not in self.report:
00835             return False
00836 
00837         try:
00838             p = subprocess.Popen(['apport-retrace', '--help'],
00839                                  stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
00840             p.communicate()
00841             if p.returncode != 0:
00842                 return False
00843         except OSError:
00844             return False
00845 
00846         try:
00847             return self.ui_run_terminal(None)
00848         except NotImplementedError:
00849             return False
00850 
00851     def restart(self):
00852         '''Reopen the crashed application.'''
00853 
00854         assert 'ProcCmdline' in self.report
00855 
00856         if os.fork() == 0:
00857             os.setsid()
00858             os.execlp('sh', 'sh', '-c', self.report.get('RespawnCommand', self.report['ProcCmdline']))
00859             sys.exit(1)
00860 
00861     def examine(self):
00862         '''Locally examine crash report.'''
00863 
00864         response = self.ui_question_choice(
00865             _('This will launch apport-retrace in a terminal window to examine the crash.'),
00866             [_('Run gdb session'),
00867              _('Run gdb session without downloading debug symbols'),
00868              #TRANSLATORS: %s contains the crash report file name
00869              _('Update %s with fully symbolic stack trace') % self.report_file,
00870             ],
00871             False)
00872 
00873         if response is None:
00874             return
00875 
00876         retrace_with_download = 'apport-retrace -S system -C %s -v ' % os.path.expanduser(
00877             '~/.cache/apport/retrace')
00878         retrace_no_download = 'apport-retrace '
00879         filearg = "'" + self.report_file.replace("'", "'\\''") + "'"
00880 
00881         cmds = {
00882             0: retrace_with_download + '--gdb ' + filearg,
00883             1: retrace_no_download + '--gdb ' + filearg,
00884             2: retrace_with_download + '--output ' + filearg + ' ' + filearg,
00885         }
00886 
00887         self.ui_run_terminal(cmds[response[0]])
00888 
00889     def collect_info(self, symptom_script=None, ignore_uninstalled=False,
00890                      on_finished=None):
00891         '''Collect additional information.
00892 
00893         Call all the add_*_info() methods and display a progress dialog during
00894         this.
00895 
00896         In particular, this adds OS, package and gdb information and checks bug
00897         patterns.
00898 
00899         If a symptom script is given, this will be run first (used by
00900         run_symptom()).
00901         '''
00902         # check if binary changed since the crash happened
00903         if 'ExecutablePath' in self.report and 'ExecutableTimestamp' in self.report:
00904             orig_time = int(self.report['ExecutableTimestamp'])
00905             del self.report['ExecutableTimestamp']
00906             cur_time = int(os.stat(self.report['ExecutablePath']).st_mtime)
00907 
00908             if orig_time != cur_time:
00909                 self.report['UnreportableReason'] = (
00910                     _('The problem happened with the program %s which changed '
00911                       'since the crash occurred.') % self.report['ExecutablePath'])
00912                 return
00913 
00914         if not self.cur_package and 'ExecutablePath' not in self.report \
00915                 and not symptom_script:
00916             # this happens if we file a bug without specifying a PID or a
00917             # package
00918             self.report.add_os_info()
00919         else:
00920             # check if we already ran, skip if so
00921             if (self.report.get('ProblemType') == 'Crash' and 'Stacktrace' in self.report) or (self.report.get('ProblemType') != 'Crash' and 'Dependencies' in self.report):
00922                 if on_finished:
00923                     on_finished()
00924                 return
00925 
00926             # since this might take a while, create separate threads and
00927             # display a progress dialog.
00928             self.ui_start_info_collection_progress()
00929 
00930             hookui = HookUI(self)
00931 
00932             if 'Stacktrace' not in self.report:
00933                 # save original environment, in case hooks change it
00934                 orig_env = os.environ.copy()
00935                 icthread = apport.REThread.REThread(target=thread_collect_info,
00936                                                     name='thread_collect_info',
00937                                                     args=(self.report, self.report_file, self.cur_package,
00938                                                           hookui, symptom_script, ignore_uninstalled))
00939                 icthread.start()
00940                 while icthread.isAlive():
00941                     self.ui_pulse_info_collection_progress()
00942                     try:
00943                         hookui.process_event()
00944                     except KeyboardInterrupt:
00945                         sys.exit(1)
00946 
00947                 icthread.join()
00948 
00949                 # restore original environment
00950                 os.environ.clear()
00951                 os.environ.update(orig_env)
00952 
00953                 icthread.exc_raise()
00954 
00955             if 'CrashDB' in self.report:
00956                 self.crashdb = get_crashdb(None, self.report['CrashDB'])
00957 
00958             # check bug patterns
00959             if self.report['ProblemType'] == 'KernelCrash' or self.report['ProblemType'] == 'KernelOops' or 'Package' in self.report:
00960                 bpthread = apport.REThread.REThread(target=self.report.search_bug_patterns,
00961                                                     args=(self.crashdb.get_bugpattern_baseurl(),))
00962                 bpthread.start()
00963                 while bpthread.isAlive():
00964                     self.ui_pulse_info_collection_progress()
00965                     try:
00966                         bpthread.join(0.1)
00967                     except KeyboardInterrupt:
00968                         sys.exit(1)
00969                 bpthread.exc_raise()
00970                 if bpthread.return_value():
00971                     self.report['KnownReport'] = bpthread.return_value()
00972 
00973             # check crash database if problem is known
00974             if self.report['ProblemType'] != 'Bug':
00975                 known_thread = apport.REThread.REThread(target=self.crashdb.known,
00976                                                         args=(self.report,))
00977                 known_thread.start()
00978                 while known_thread.isAlive():
00979                     self.ui_pulse_info_collection_progress()
00980                     try:
00981                         known_thread.join(0.1)
00982                     except KeyboardInterrupt:
00983                         sys.exit(1)
00984                 known_thread.exc_raise()
00985                 val = known_thread.return_value()
00986                 if val is not None:
00987                     if val is True:
00988                         self.report['KnownReport'] = '1'
00989                     else:
00990                         self.report['KnownReport'] = val
00991 
00992             # anonymize; needs to happen after duplicate checking, otherwise we
00993             # might damage the stack trace
00994             anonymize_thread = apport.REThread.REThread(target=self.report.anonymize)
00995             anonymize_thread.start()
00996             while anonymize_thread.isAlive():
00997                 self.ui_pulse_info_collection_progress()
00998                 try:
00999                     anonymize_thread.join(0.1)
01000                 except KeyboardInterrupt:
01001                     sys.exit(1)
01002             anonymize_thread.exc_raise()
01003 
01004             self.ui_stop_info_collection_progress()
01005 
01006             # check that we were able to determine package names
01007             if ('SourcePackage' not in self.report or
01008                 (not self.report['ProblemType'].startswith('Kernel')
01009                  and 'Package' not in self.report)):
01010                 self.ui_error_message(_('Invalid problem report'),
01011                                       _('Could not determine the package or source package name.'))
01012                 # TODO This is not called consistently, is it really needed?
01013                 self.ui_shutdown()
01014                 sys.exit(1)
01015 
01016         if on_finished:
01017             on_finished()
01018 
01019     def open_url(self, url):
01020         '''Open the given URL in a new browser window.
01021 
01022         Display an error dialog if everything fails.
01023         '''
01024         (r, w) = os.pipe()
01025         if os.fork() > 0:
01026             os.close(w)
01027             (pid, status) = os.wait()
01028             if status:
01029                 title = _('Unable to start web browser')
01030                 error = _('Unable to start web browser to open %s.' % url)
01031                 message = os.fdopen(r).readline()
01032                 if message:
01033                     error += '\n' + message
01034                 self.ui_error_message(title, error)
01035             try:
01036                 os.close(r)
01037             except OSError:
01038                 pass
01039             return
01040 
01041         os.setsid()
01042         os.close(r)
01043 
01044         # If we are called through sudo, determine the real user id and run the
01045         # browser with it to get the user's web browser settings.
01046         try:
01047             uid = int(os.getenv('SUDO_UID'))
01048             sudo_prefix = ['sudo', '-H', '-u', '#' + str(uid)]
01049         except TypeError:
01050             sudo_prefix = []
01051 
01052         try:
01053             try:
01054                 subprocess.call(sudo_prefix + ['xdg-open', url])
01055             except OSError as e:
01056                 # fall back to webbrowser
01057                 webbrowser.open(url, new=True, autoraise=True)
01058                 sys.exit(0)
01059         except Exception as e:
01060             os.write(w, str(e))
01061             sys.exit(1)
01062 
01063     def file_report(self):
01064         '''Upload the current report and guide the user to the reporting web page.'''
01065         # FIXME: This behaviour is not really correct, but necessary as
01066         # long as we only support a single crashdb and have whoopsie
01067         # hardcoded. Once we have multiple crash dbs, we need to check
01068         # accepts() earlier, and not even present the data if none of
01069         # the DBs wants the report. See LP#957177 for details.
01070         if not self.crashdb.accepts(self.report):
01071             return
01072         # drop PackageArchitecture if equal to Architecture
01073         if self.report.get('PackageArchitecture') == self.report.get('Architecture'):
01074             try:
01075                 del self.report['PackageArchitecture']
01076             except KeyError:
01077                 pass
01078 
01079         # StacktraceAddressSignature is redundant and does not need to clutter
01080         # the database
01081         try:
01082             del self.report['StacktraceAddressSignature']
01083         except KeyError:
01084             pass
01085 
01086         global __upload_progress
01087         __upload_progress = None
01088 
01089         def progress_callback(sent, total):
01090             global __upload_progress
01091             __upload_progress = float(sent) / total
01092 
01093         self.ui_start_upload_progress()
01094         upthread = apport.REThread.REThread(target=self.crashdb.upload,
01095                                             args=(self.report, progress_callback))
01096         upthread.start()
01097         while upthread.isAlive():
01098             self.ui_set_upload_progress(__upload_progress)
01099             try:
01100                 upthread.join(0.1)
01101                 upthread.exc_raise()
01102             except KeyboardInterrupt:
01103                 sys.exit(1)
01104             except NeedsCredentials as e:
01105                 message = _('Please enter your account information for the '
01106                             '%s bug tracking system')
01107                 data = self.ui_question_userpass(message % excstr(e))
01108                 if data is not None:
01109                     user, password = data
01110                     self.crashdb.set_credentials(user, password)
01111                     upthread = apport.REThread.REThread(target=self.crashdb.upload,
01112                                                         args=(self.report, progress_callback))
01113                     upthread.start()
01114             except (TypeError, SyntaxError, ValueError):
01115                 raise
01116             except Exception as e:
01117                 self.ui_error_message(_('Network problem'),
01118                                       '%s\n\n%s' % (
01119                                           _('Cannot connect to crash database, please check your Internet connection.'),
01120                                           excstr(e)))
01121                 return
01122 
01123         upthread.exc_raise()
01124         ticket = upthread.return_value()
01125         self.ui_stop_upload_progress()
01126 
01127         url = self.crashdb.get_comment_url(self.report, ticket)
01128         if url:
01129             self.open_url(url)
01130 
01131     def load_report(self, path):
01132         '''Load report from given path and do some consistency checks.
01133 
01134         This might issue an error message and return False if the report cannot
01135         be processed, otherwise self.report is initialized and True is
01136         returned.
01137         '''
01138         try:
01139             self.report = apport.Report()
01140             with open(path, 'rb') as f:
01141                 self.report.load(f, binary='compressed')
01142             if 'ProblemType' not in self.report:
01143                 raise ValueError('Report does not contain "ProblemType" field')
01144         except MemoryError:
01145             self.report = None
01146             self.ui_error_message(_('Memory exhaustion'),
01147                                   _('Your system does not have enough memory to process this crash report.'))
01148             return False
01149         except IOError as e:
01150             self.report = None
01151             self.ui_error_message(_('Invalid problem report'), e.strerror)
01152             return False
01153         except (TypeError, ValueError, AssertionError, zlib.error) as e:
01154             self.report = None
01155             self.ui_error_message(_('Invalid problem report'),
01156                                   '%s\n\n%s' % (
01157                                       _('This problem report is damaged and cannot be processed.'),
01158                                       repr(e)))
01159             return False
01160 
01161         if 'Package' in self.report:
01162             self.cur_package = self.report['Package'].split()[0]
01163         else:
01164             self.cur_package = apport.fileutils.find_file_package(self.report.get('ExecutablePath', ''))
01165 
01166         # ensure that the crashed program is still installed:
01167         if self.report['ProblemType'] == 'Crash':
01168             exe_path = self.report.get('ExecutablePath', '')
01169             if not os.path.exists(exe_path):
01170                 msg = _('This problem report applies to a program which is not installed any more.')
01171                 if exe_path:
01172                     msg = '%s (%s)' % (msg, self.report['ExecutablePath'])
01173                 self.report = None
01174                 self.ui_info_message(_('Invalid problem report'), msg)
01175                 return False
01176 
01177             if 'InterpreterPath' in self.report:
01178                 if not os.path.exists(self.report['InterpreterPath']):
01179                     msg = _('This problem report applies to a program which is not installed any more.')
01180                     self.ui_info_message(_('Invalid problem report'), '%s (%s)'
01181                                          % (msg, self.report['InterpreterPath']))
01182                     return False
01183 
01184         return True
01185 
01186     def check_unreportable(self):
01187         '''Check if the current report is unreportable.
01188 
01189         If so, display an info message and return True.
01190         '''
01191         if not self.crashdb.accepts(self.report):
01192             return False
01193         if 'UnreportableReason' in self.report:
01194             if type(self.report['UnreportableReason']) == bytes:
01195                 self.report['UnreportableReason'] = self.report['UnreportableReason'].decode('UTF-8')
01196             if 'Package' in self.report:
01197                 title = _('Problem in %s') % self.report['Package'].split()[0]
01198             else:
01199                 title = ''
01200             self.ui_info_message(title, _('The problem cannot be reported:\n\n%s') %
01201                                  self.report['UnreportableReason'])
01202             return True
01203         return False
01204 
01205     def get_desktop_entry(self):
01206         '''Return a .desktop info dictionary for the current report.
01207 
01208         Return None if report cannot be associated to a .desktop file.
01209         '''
01210         if 'DesktopFile' in self.report and os.path.exists(self.report['DesktopFile']):
01211             desktop_file = self.report['DesktopFile']
01212         else:
01213             try:
01214                 desktop_file = apport.fileutils.find_package_desktopfile(self.cur_package)
01215             except ValueError:
01216                 return None
01217 
01218         if not desktop_file:
01219             return None
01220 
01221         cp = ConfigParser(interpolation=None)
01222         cp.read(desktop_file, encoding='UTF-8')
01223         if not cp.has_section('Desktop Entry'):
01224             return None
01225         result = dict(cp.items('Desktop Entry'))
01226         if 'name' not in result:
01227             return None
01228         return result
01229 
01230     def handle_duplicate(self):
01231         '''Check if current report matches a bug pattern.
01232 
01233         If so, tell the user about it, open the existing bug in a browser, and
01234         return True.
01235         '''
01236         if not self.crashdb.accepts(self.report):
01237             return False
01238         if 'KnownReport' not in self.report:
01239             return False
01240 
01241         # if we have an URL, open it; otherwise this is just a marker that we
01242         # know about it
01243         if self.report['KnownReport'].startswith('http'):
01244             self.ui_info_message(_('Problem already known'),
01245                                  _('This problem was already reported in the bug report displayed \
01246 in the web browser. Please check if you can add any further information that \
01247 might be helpful for the developers.'))
01248 
01249             self.open_url(self.report['KnownReport'])
01250         else:
01251             self.ui_info_message(_('Problem already known'),
01252                                  _('This problem was already reported to developers. Thank you!'))
01253 
01254         return True
01255 
01256     def add_extra_tags(self):
01257         '''Add extra tags to report specified with --tags on CLI.'''
01258 
01259         assert self.report
01260         if self.options.tag:
01261             tags = self.report.get('Tags', '')
01262             if tags:
01263                 tags += ' '
01264             self.report['Tags'] = tags + ' '.join(self.options.tag)
01265 
01266     #
01267     # abstract UI methods that must be implemented in derived classes
01268     #
01269 
01270     def ui_present_report_details(self, allowed_to_report=True):
01271         '''Show details of the bug report.
01272 
01273         Return the action and options as a dictionary:
01274 
01275         - Valid keys are: report the crash ('report'), restart
01276           the crashed application ('restart'), or blacklist further crashes
01277           ('blacklist').
01278         '''
01279         raise NotImplementedError('this function must be overridden by subclasses')
01280 
01281     def ui_info_message(self, title, text):
01282         '''Show an information message box with given title and text.'''
01283 
01284         raise NotImplementedError('this function must be overridden by subclasses')
01285 
01286     def ui_error_message(self, title, text):
01287         '''Show an error message box with given title and text.'''
01288 
01289         raise NotImplementedError('this function must be overridden by subclasses')
01290 
01291     def ui_start_info_collection_progress(self):
01292         '''Open a indefinite progress bar for data collection.
01293 
01294         This tells the user to wait while debug information is being
01295         collected.
01296         '''
01297         raise NotImplementedError('this function must be overridden by subclasses')
01298 
01299     def ui_pulse_info_collection_progress(self):
01300         '''Advance the data collection progress bar.
01301 
01302         This function is called every 100 ms.
01303         '''
01304         raise NotImplementedError('this function must be overridden by subclasses')
01305 
01306     def ui_stop_info_collection_progress(self):
01307         '''Close debug data collection progress window.'''
01308 
01309         raise NotImplementedError('this function must be overridden by subclasses')
01310 
01311     def ui_start_upload_progress(self):
01312         '''Open progress bar for data upload.
01313 
01314         This tells the user to wait while debug information is being uploaded.
01315         '''
01316         raise NotImplementedError('this function must be overridden by subclasses')
01317 
01318     def ui_set_upload_progress(self, progress):
01319         '''Update data upload progress bar.
01320 
01321         Set the progress bar in the debug data upload progress window to the
01322         given ratio (between 0 and 1, or None for indefinite progress).
01323 
01324         This function is called every 100 ms.
01325         '''
01326         raise NotImplementedError('this function must be overridden by subclasses')
01327 
01328     def ui_stop_upload_progress(self):
01329         '''Close debug data upload progress window.'''
01330 
01331         raise NotImplementedError('this function must be overridden by subclasses')
01332 
01333     def ui_shutdown(self):
01334         '''Called right before terminating the program.
01335 
01336         This can be used for for cleaning up.
01337         '''
01338         pass
01339 
01340     def ui_run_terminal(self, command):
01341         '''Run command in, or check for a terminal window.
01342 
01343         If command is given, run command in a terminal window; raise an exception
01344         if terminal cannot be opened.
01345 
01346         If command is None, merely check if a terminal application is available
01347         and can be launched.
01348         '''
01349         raise NotImplementedError('this function must be overridden by subclasses')
01350 
01351     #
01352     # Additional UI dialogs; these are not required by Apport itself, but can
01353     # be used by interactive package hooks
01354     #
01355 
01356     def ui_question_yesno(self, text):
01357         '''Show a yes/no question.
01358 
01359         Return True if the user selected "Yes", False if selected "No" or
01360         "None" on cancel/dialog closing.
01361         '''
01362         raise NotImplementedError('this function must be overridden by subclasses')
01363 
01364     def ui_question_choice(self, text, options, multiple):
01365         '''Show an question with predefined choices.
01366 
01367         options is a list of strings to present. If multiple is True, they
01368         should be check boxes, if multiple is False they should be radio
01369         buttons.
01370 
01371         Return list of selected option indexes, or None if the user cancelled.
01372         If multiple == False, the list will always have one element.
01373         '''
01374         raise NotImplementedError('this function must be overridden by subclasses')
01375 
01376     def ui_question_file(self, text):
01377         '''Show a file selector dialog.
01378 
01379         Return path if the user selected a file, or None if cancelled.
01380         '''
01381         raise NotImplementedError('this function must be overridden by subclasses')
01382 
01383     def ui_question_userpass(self, message):
01384         '''Request username and password from user.
01385 
01386         message is the text to be presented to the user when requesting for
01387         username and password information.
01388 
01389         Return a tuple (username, password), or None if cancelled.
01390         '''
01391         raise NotImplementedError('this function must be overridden by subclasses')
01392 
01393 
01394 class HookUI:
01395     '''Interactive functions which can be used in package hooks.
01396 
01397     This provides an interface for package hooks which need to ask interactive
01398     questions. Directly passing the UserInterface instance to the hooks needs
01399     to be avoided, since we need to call the UI methods in a different thread,
01400     and also don't want hooks to be able to poke in the UI.
01401     '''
01402     def __init__(self, ui):
01403         '''Create a HookUI object.
01404 
01405         ui is the UserInterface instance to wrap.
01406         '''
01407         self.ui = ui
01408 
01409         # variables for communicating with the UI thread
01410         self._request_event = threading.Event()
01411         self._response_event = threading.Event()
01412         self._request_fn = None
01413         self._request_args = None
01414         self._response = None
01415 
01416     #
01417     # API for hooks
01418     #
01419 
01420     def information(self, text):
01421         '''Show an information with OK/Cancel buttons.
01422 
01423         This can be used for asking the user to perform a particular action,
01424         such as plugging in a device which does not work.
01425         '''
01426         return self._trigger_ui_request('ui_info_message', '', text)
01427 
01428     def yesno(self, text):
01429         '''Show a yes/no question.
01430 
01431         Return True if the user selected "Yes", False if selected "No" or
01432         "None" on cancel/dialog closing.
01433         '''
01434         return self._trigger_ui_request('ui_question_yesno', text)
01435 
01436     def choice(self, text, options, multiple=False):
01437         '''Show an question with predefined choices.
01438 
01439         options is a list of strings to present. If multiple is True, they
01440         should be check boxes, if multiple is False they should be radio
01441         buttons.
01442 
01443         Return list of selected option indexes, or None if the user cancelled.
01444         If multiple == False, the list will always have one element.
01445         '''
01446         return self._trigger_ui_request('ui_question_choice', text, options, multiple)
01447 
01448     def file(self, text):
01449         '''Show a file selector dialog.
01450 
01451         Return path if the user selected a file, or None if cancelled.
01452         '''
01453         return self._trigger_ui_request('ui_question_file', text)
01454 
01455     #
01456     # internal API for inter-thread communication
01457     #
01458 
01459     def _trigger_ui_request(self, fn, *args):
01460         '''Called by HookUi functions in info collection thread.'''
01461 
01462         # only one at a time
01463         assert not self._request_event.is_set()
01464         assert not self._response_event.is_set()
01465         assert self._request_fn is None
01466 
01467         self._response = None
01468         self._request_fn = fn
01469         self._request_args = args
01470         self._request_event.set()
01471         self._response_event.wait()
01472 
01473         self._request_fn = None
01474         self._response_event.clear()
01475 
01476         return self._response
01477 
01478     def process_event(self):
01479         '''Called by GUI thread to check and process hook UI requests.'''
01480 
01481         # sleep for 0.1 seconds to wait for events
01482         self._request_event.wait(0.1)
01483         if not self._request_event.is_set():
01484             return
01485 
01486         assert not self._response_event.is_set()
01487         self._request_event.clear()
01488         self._response = getattr(self.ui, self._request_fn)(*self._request_args)
01489         self._response_event.set()