Back to index

apport  2.4
fileutils.py
Go to the documentation of this file.
00001 '''Functions to manage apport problem report files.'''
00002 
00003 # Copyright (C) 2006 - 2009 Canonical Ltd.
00004 # Author: Martin Pitt <martin.pitt@ubuntu.com>
00005 #
00006 # This program is free software; you can redistribute it and/or modify it
00007 # under the terms of the GNU General Public License as published by the
00008 # Free Software Foundation; either version 2 of the License, or (at your
00009 # option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
00010 # the full text of the license.
00011 
00012 import os, glob, subprocess, os.path, time
00013 
00014 try:
00015     from configparser import ConfigParser, NoOptionError, NoSectionError
00016     (ConfigParser, NoOptionError, NoSectionError)  # pyflakes
00017 except ImportError:
00018     # Python 2
00019     from ConfigParser import ConfigParser, NoOptionError, NoSectionError
00020 
00021 from problem_report import ProblemReport
00022 
00023 from apport.packaging_impl import impl as packaging
00024 
00025 report_dir = os.environ.get('APPORT_REPORT_DIR', '/var/crash')
00026 
00027 _config_file = '~/.config/apport/settings'
00028 _whoopsie_config_file = '/etc/default/whoopsie'
00029 
00030 
00031 def allowed_to_report():
00032     '''Check whether crash reporting is enabled.'''
00033 
00034     return get_config('General', 'report_crashes', default=True,
00035                       path=_whoopsie_config_file, bool=True)
00036 
00037 
00038 def find_package_desktopfile(package):
00039     '''Return a package's .desktop file.
00040 
00041     If given package is installed and has a single .desktop file, return the
00042     path to it, otherwise return None.
00043     '''
00044     if package is None:
00045         return None
00046 
00047     desktopfile = None
00048 
00049     for line in packaging.get_files(package):
00050         if line.endswith('.desktop'):
00051             if desktopfile:
00052                 return None  # more than one
00053             else:
00054                 desktopfile = line
00055 
00056     return desktopfile
00057 
00058 
00059 def likely_packaged(file):
00060     '''Check whether the given file is likely to belong to a package.
00061 
00062     This is semi-decidable: A return value of False is definitive, a True value
00063     is only a guess which needs to be checked with find_file_package().
00064     However, this function is very fast and does not access the package
00065     database.
00066     '''
00067     pkg_whitelist = ['/bin/', '/boot', '/etc/', '/initrd', '/lib', '/sbin/',
00068                      '/usr/', '/var']  # packages only ship executables in these directories
00069 
00070     whitelist_match = False
00071     for i in pkg_whitelist:
00072         if file.startswith(i):
00073             whitelist_match = True
00074             break
00075     return whitelist_match and not file.startswith('/usr/local/') and not \
00076         file.startswith('/var/lib/')
00077 
00078 
00079 def find_file_package(file):
00080     '''Return the package that ships the given file.
00081 
00082     Return None if no package ships it.
00083     '''
00084     # resolve symlinks in directories
00085     (dir, name) = os.path.split(file)
00086     resolved_dir = os.path.realpath(dir)
00087     if os.path.isdir(resolved_dir):
00088         file = os.path.join(resolved_dir, name)
00089 
00090     if not likely_packaged(file):
00091         return None
00092 
00093     return packaging.get_file_package(file)
00094 
00095 
00096 def seen_report(report):
00097     '''Check whether the report file has already been processed earlier.'''
00098 
00099     st = os.stat(report)
00100     return (st.st_atime > st.st_mtime) or (st.st_size == 0)
00101 
00102 
00103 def mark_report_upload(report):
00104     report = '%s.upload' % report.rsplit('.', 1)[0]
00105     with open(report, 'a'):
00106         pass
00107 
00108 
00109 def mark_hanging_process(report, pid):
00110     if 'ExecutablePath' in report:
00111         subject = report['ExecutablePath'].replace('/', '_')
00112     else:
00113         raise ValueError('report does not have the ExecutablePath attribute')
00114 
00115     uid = os.getuid()
00116     base = '%s.%s.%s.hanging' % (subject, str(uid), pid)
00117     path = os.path.join(report_dir, base)
00118     with open(path, 'a'):
00119         pass
00120 
00121 
00122 def mark_report_seen(report):
00123     '''Mark given report file as seen.'''
00124 
00125     st = os.stat(report)
00126     try:
00127         os.utime(report, (st.st_mtime, st.st_mtime - 1))
00128     except OSError:
00129         # file is probably not our's, so do it the slow and boring way
00130         # change the file's access time until it stat's different than the mtime.
00131         # This might take a while if we only have 1-second resolution. Time out
00132         # after 1.2 seconds.
00133         timeout = 12
00134         while timeout > 0:
00135             f = open(report)
00136             f.read(1)
00137             f.close()
00138             try:
00139                 st = os.stat(report)
00140             except OSError:
00141                 return
00142 
00143             if st.st_atime > st.st_mtime:
00144                 break
00145             time.sleep(0.1)
00146             timeout -= 1
00147 
00148         if timeout == 0:
00149             # happens on noatime mounted partitions; just give up and delete
00150             delete_report(report)
00151 
00152 
00153 def get_all_reports():
00154     '''Return a list with all report files accessible to the calling user.'''
00155 
00156     reports = []
00157     for r in glob.glob(os.path.join(report_dir, '*.crash')):
00158         try:
00159             if os.path.getsize(r) > 0 and os.access(r, os.R_OK):
00160                 reports.append(r)
00161         except OSError:
00162             # race condition, can happen if report disappears between glob and
00163             # stat
00164             pass
00165     return reports
00166 
00167 
00168 def get_new_reports():
00169     '''Get new reports for calling user.
00170 
00171     Return a list with all report files which have not yet been processed
00172     and are accessible to the calling user.
00173     '''
00174     reports = []
00175     for r in get_all_reports():
00176         try:
00177             if not seen_report(r):
00178                 reports.append(r)
00179         except OSError:
00180             # race condition, can happen if report disappears between glob and
00181             # stat
00182             pass
00183     return reports
00184 
00185 
00186 def get_all_system_reports():
00187     '''Get all system reports.
00188 
00189     Return a list with all report files which belong to a system user (i. e.
00190     uid < 500 according to LSB).
00191     '''
00192     reports = []
00193     for r in glob.glob(os.path.join(report_dir, '*.crash')):
00194         try:
00195             if os.path.getsize(r) > 0 and os.stat(r).st_uid < 500:
00196                 reports.append(r)
00197         except OSError:
00198             # race condition, can happen if report disappears between glob and
00199             # stat
00200             pass
00201     return reports
00202 
00203 
00204 def get_new_system_reports():
00205     '''Get new system reports.
00206 
00207     Return a list with all report files which have not yet been processed
00208     and belong to a system user (i. e. uid < 500 according to LSB).
00209     '''
00210     return [r for r in get_all_system_reports() if not seen_report(r)]
00211 
00212 
00213 def delete_report(report):
00214     '''Delete the given report file.
00215 
00216     If unlinking the file fails due to a permission error (if report_dir is not
00217     writable to normal users), the file will be truncated to 0 bytes instead.
00218     '''
00219     try:
00220         os.unlink(report)
00221     except OSError:
00222         with open(report, 'w') as f:
00223             f.truncate(0)
00224 
00225 
00226 def get_recent_crashes(report):
00227     '''Return the number of recent crashes for the given report file.
00228 
00229     Return the number of recent crashes (currently, crashes which happened more
00230     than 24 hours ago are discarded).
00231     '''
00232     pr = ProblemReport()
00233     pr.load(report, False)
00234     try:
00235         count = int(pr['CrashCounter'])
00236         report_time = time.mktime(time.strptime(pr['Date']))
00237         cur_time = time.mktime(time.localtime())
00238         # discard reports which are older than 24 hours
00239         if cur_time - report_time > 24 * 3600:
00240             return 0
00241         return count
00242     except (ValueError, KeyError):
00243         return 0
00244 
00245 
00246 def make_report_path(report, uid=None):
00247     '''Construct a canonical pathname for the given report.
00248 
00249     If uid is not given, it defaults to the uid of the current process.
00250     '''
00251     if 'ExecutablePath' in report:
00252         subject = report['ExecutablePath'].replace('/', '_')
00253     elif 'Package' in report:
00254         subject = report['Package'].split(None, 1)[0]
00255     else:
00256         raise ValueError('report has neither ExecutablePath nor Package attribute')
00257 
00258     if not uid:
00259         uid = os.getuid()
00260 
00261     return os.path.join(report_dir, '%s.%s.crash' % (subject, str(uid)))
00262 
00263 
00264 def check_files_md5(sumfile):
00265     '''Check file integrity against md5 sum file.
00266 
00267     sumfile must be md5sum(1) format (relative to /).
00268 
00269     Return a list of files that don't match.
00270     '''
00271     assert os.path.exists(sumfile)
00272     m = subprocess.Popen(['/usr/bin/md5sum', '-c', sumfile],
00273                          stdout=subprocess.PIPE, stderr=subprocess.PIPE,
00274                          cwd='/', env={})
00275     out = m.communicate()[0].decode()
00276 
00277     # if md5sum succeeded, don't bother parsing the output
00278     if m.returncode == 0:
00279         return []
00280 
00281     mismatches = []
00282     for l in out.splitlines():
00283         if l.endswith('FAILED'):
00284             mismatches.append(l.rsplit(':', 1)[0])
00285 
00286     return mismatches
00287 
00288 
00289 def get_config(section, setting, default=None, path=None, bool=False):
00290     '''Return a setting from user configuration.
00291 
00292     This is read from ~/.config/apport/settings or path. If bool is True, the
00293     value is interpreted as a boolean.
00294     '''
00295     if not get_config.config:
00296         get_config.config = ConfigParser()
00297         if path:
00298             get_config.config.read(path)
00299         else:
00300             get_config.config.read(os.path.expanduser(_config_file))
00301 
00302     try:
00303         if bool:
00304             return get_config.config.getboolean(section, setting)
00305         else:
00306             return get_config.config.get(section, setting)
00307     except (NoOptionError, NoSectionError):
00308         return default
00309 
00310 get_config.config = None