mirror of https://gitee.com/openkylin/apport.git
348 lines
10 KiB
Python
Executable File
348 lines
10 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
# Copyright (C) 2007 - 2011 Canonical Ltd.
|
|
# Author: Martin Pitt <martin.pitt@ubuntu.com>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by the
|
|
# Free Software Foundation; either version 2 of the License, or (at your
|
|
# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
|
|
# the full text of the license.
|
|
|
|
"""Do duplicate check and retrace crashes in the crash database."""
|
|
|
|
# pylint: disable=invalid-name
|
|
# pylint: enable=invalid-name
|
|
|
|
import argparse
|
|
import errno
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import zlib
|
|
|
|
import apport
|
|
from apport.crashdb import get_crashdb
|
|
|
|
|
|
# pylint: disable-next=missing-class-docstring
|
|
class CrashDigger: # pylint: disable=too-many-instance-attributes
|
|
def __init__(
|
|
self,
|
|
config_dir,
|
|
auth_file,
|
|
cache_dir,
|
|
sandbox_dir,
|
|
apport_retrace,
|
|
verbose=False,
|
|
dup_db=None,
|
|
dupcheck_mode=False,
|
|
publish_dir=None,
|
|
crash_db=None,
|
|
gdb_sandbox=False,
|
|
): # pylint: disable=too-many-arguments
|
|
"""Initialize pools."""
|
|
|
|
self.retrace_pool = set()
|
|
self.dupcheck_pool = set()
|
|
self.config_dir = config_dir
|
|
self.cache_dir = cache_dir
|
|
self.sandbox_dir = sandbox_dir
|
|
self.verbose = verbose
|
|
self.auth_file = auth_file
|
|
self.dup_db = dup_db
|
|
self.dupcheck_mode = dupcheck_mode
|
|
self.gdb_sandbox = gdb_sandbox
|
|
try:
|
|
self.crashdb = get_crashdb(auth_file, name=crash_db)
|
|
except KeyError:
|
|
apport.error("Crash database %s does not exist", crash_db)
|
|
sys.exit(1)
|
|
self.lp = False
|
|
try:
|
|
if self.crashdb.launchpad:
|
|
self.lp = True
|
|
except AttributeError:
|
|
pass
|
|
self.apport_retrace = apport_retrace
|
|
self.publish_dir = publish_dir
|
|
if config_dir:
|
|
self.releases = os.listdir(config_dir)
|
|
self.releases.sort()
|
|
apport.log(f"Available releases: {str(self.releases)}", True)
|
|
else:
|
|
self.releases = None
|
|
|
|
if self.dup_db:
|
|
self.crashdb.init_duplicate_db(self.dup_db)
|
|
# this verified DB integrity; make a backup now
|
|
shutil.copy2(self.dup_db, f"{self.dup_db}.backup")
|
|
|
|
def fill_pool(self):
|
|
"""Query crash db for new IDs to process."""
|
|
|
|
if self.dupcheck_mode:
|
|
self.dupcheck_pool.update(self.crashdb.get_dup_unchecked())
|
|
apport.log(
|
|
f"fill_pool: dup check pool now: {str(self.dupcheck_pool)}", True
|
|
)
|
|
else:
|
|
self.retrace_pool.update(self.crashdb.get_unretraced())
|
|
apport.log(f"fill_pool: retrace pool now: {str(self.retrace_pool)}", True)
|
|
|
|
def retrace_next(self):
|
|
"""Grab an ID from the retrace pool and retrace it."""
|
|
|
|
crash_id = self.retrace_pool.pop()
|
|
apport.log(
|
|
f"retracing {'LP: ' if self.lp else ''}#{crash_id}"
|
|
f" (left in pool: {len(self.retrace_pool)})",
|
|
True,
|
|
)
|
|
|
|
try:
|
|
rel = self.crashdb.get_distro_release(crash_id)
|
|
except ValueError:
|
|
apport.log("could not determine release -- no DistroRelease field?", True)
|
|
self.crashdb.mark_retraced(crash_id)
|
|
return
|
|
if rel not in self.releases:
|
|
apport.log(
|
|
f"crash is release {rel} which does not have a config"
|
|
f" available, skipping",
|
|
True,
|
|
)
|
|
return
|
|
|
|
argv = [
|
|
self.apport_retrace,
|
|
"-S",
|
|
self.config_dir,
|
|
"--auth",
|
|
self.auth_file,
|
|
"--timestamps",
|
|
]
|
|
if self.cache_dir:
|
|
argv += ["--cache", self.cache_dir]
|
|
if self.sandbox_dir:
|
|
argv += ["--sandbox-dir", self.sandbox_dir]
|
|
if self.dup_db:
|
|
argv += ["--duplicate-db", self.dup_db]
|
|
if self.gdb_sandbox:
|
|
argv += ["--gdb-sandbox"]
|
|
if self.verbose:
|
|
argv.append("-v")
|
|
argv.append(str(crash_id))
|
|
|
|
result = subprocess.call(argv, stdout=sys.stdout, stderr=subprocess.STDOUT)
|
|
if result != 0:
|
|
apport.log(
|
|
f"retracing {'LP: ' if self.lp else ''}#{crash_id} failed"
|
|
f" with status: {result}",
|
|
True,
|
|
)
|
|
if result == 99:
|
|
self.retrace_pool = set()
|
|
apport.log("transient error reported; halting", True)
|
|
return
|
|
|
|
self.crashdb.mark_retraced(crash_id)
|
|
|
|
def dupcheck_next(self):
|
|
"""Grab an ID from the dupcheck pool and process it."""
|
|
|
|
crash_id = self.dupcheck_pool.pop()
|
|
apport.log(
|
|
f"checking {'LP: ' if self.lp else ''}#{crash_id} for duplicate"
|
|
f" (left in pool: {len(self.dupcheck_pool)})",
|
|
True,
|
|
)
|
|
|
|
try:
|
|
report = self.crashdb.download(crash_id)
|
|
except (
|
|
MemoryError,
|
|
TypeError,
|
|
ValueError,
|
|
OSError,
|
|
AssertionError,
|
|
zlib.error,
|
|
) as error:
|
|
if str(error) == "bug description must contain standard apport format data":
|
|
apport.log(f"Cannot download report: {str(error)}", True)
|
|
apport.error("Cannot download report %s: %s", crash_id, str(error))
|
|
return
|
|
apport.log(f"Cannot download report: {str(error)}", True)
|
|
apport.error("Cannot download report %i: %s", crash_id, str(error))
|
|
return
|
|
|
|
res = self.crashdb.check_duplicate(crash_id, report)
|
|
if res:
|
|
if res[1] is None:
|
|
version = "not fixed yet"
|
|
elif res[1] == "":
|
|
version = "fixed in latest version"
|
|
else:
|
|
version = f"fixed in version {res[1]}"
|
|
apport.log(f"Report is a duplicate of #{res[0]} ({version})", True)
|
|
else:
|
|
apport.log("Duplicate check negative", True)
|
|
|
|
def run(self):
|
|
"""Process the work pools until they are empty."""
|
|
|
|
self.fill_pool()
|
|
while self.dupcheck_pool:
|
|
self.dupcheck_next()
|
|
while self.retrace_pool:
|
|
self.retrace_next()
|
|
|
|
if self.publish_dir:
|
|
self.crashdb.duplicate_db_publish(self.publish_dir)
|
|
|
|
|
|
def parse_options():
|
|
"""Parse command line options and return arguments."""
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"-c",
|
|
"--config-dir",
|
|
metavar="DIR",
|
|
help="Packaging system configuration base directory.",
|
|
)
|
|
parser.add_argument(
|
|
"--sandbox-dir",
|
|
metavar="DIR",
|
|
help="Directory for unpacked packages. Future runs will assume that"
|
|
" any already downloaded package is also extracted to this sandbox.",
|
|
)
|
|
parser.add_argument(
|
|
"--gdb-sandbox",
|
|
dest="gdb_sandbox",
|
|
action="store_true",
|
|
help="Use a temporary sandbox for installing the version of GDB (and"
|
|
" its dependencies) from the same release as the report.",
|
|
)
|
|
parser.add_argument(
|
|
"-C",
|
|
"--cache",
|
|
metavar="DIR",
|
|
help="Cache directory for packages downloaded in the sandbox",
|
|
)
|
|
parser.add_argument(
|
|
"-a",
|
|
"--auth",
|
|
dest="auth_file",
|
|
help="Path to a file with the crash database authentication information.",
|
|
)
|
|
parser.add_argument(
|
|
"-l",
|
|
"--lock",
|
|
dest="lockfile",
|
|
help="Lock file; will be created and removed on successful exit, and "
|
|
"program immediately aborts if it already exists",
|
|
)
|
|
parser.add_argument(
|
|
"-d",
|
|
"--duplicate-db",
|
|
dest="dup_db",
|
|
metavar="PATH",
|
|
help="Path to the duplicate sqlite database (default: disabled)",
|
|
)
|
|
parser.add_argument(
|
|
"--crash-db",
|
|
metavar="NAME",
|
|
help='Use a different crash database than the "default"'
|
|
" in /etc/apport/crashdb.conf",
|
|
)
|
|
parser.add_argument(
|
|
"-D",
|
|
"--dupcheck",
|
|
dest="dupcheck_mode",
|
|
action="store_true",
|
|
help="Only check duplicates for architecture independent crashes"
|
|
" (like Python exceptions)",
|
|
)
|
|
parser.add_argument(
|
|
"-v",
|
|
"--verbose",
|
|
action="store_true",
|
|
help="Verbose operation (also passed to apport-retrace)",
|
|
)
|
|
parser.add_argument(
|
|
"--apport-retrace",
|
|
metavar="PATH",
|
|
help="Path to apport-retrace script"
|
|
" (default: directory of crash-digger or $PATH)",
|
|
)
|
|
parser.add_argument(
|
|
"--publish-db",
|
|
metavar="DIR",
|
|
help="After processing all reports, publish duplicate database"
|
|
" to given directory",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.config_dir and not args.dupcheck_mode:
|
|
apport.fatal("Error: --config-dir or --dupcheck needs to be given")
|
|
if not args.auth_file:
|
|
apport.fatal("Error: -a/--auth needs to be given")
|
|
|
|
return args
|
|
|
|
|
|
# pylint: disable-next=missing-function-docstring
|
|
def main():
|
|
opts = parse_options()
|
|
|
|
# support running from tree, then fall back to $PATH
|
|
if not opts.apport_retrace:
|
|
opts.apport_retrace = os.path.join(
|
|
os.path.dirname(sys.argv[0]), "apport-retrace"
|
|
)
|
|
if not os.access(opts.apport_retrace, os.X_OK):
|
|
opts.apport_retrace = "apport-retrace"
|
|
|
|
if opts.lockfile:
|
|
try:
|
|
lock_file = os.open(
|
|
opts.lockfile, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666
|
|
)
|
|
os.write(lock_file, (f"{os.getpid()}\n").encode())
|
|
os.close(lock_file)
|
|
except OSError as error:
|
|
if error.errno == errno.EEXIST:
|
|
sys.exit(0)
|
|
else:
|
|
raise
|
|
|
|
try:
|
|
CrashDigger(
|
|
opts.config_dir,
|
|
opts.auth_file,
|
|
opts.cache,
|
|
opts.sandbox_dir,
|
|
opts.apport_retrace,
|
|
opts.verbose,
|
|
opts.dup_db,
|
|
opts.dupcheck_mode,
|
|
opts.publish_db,
|
|
opts.crash_db,
|
|
opts.gdb_sandbox,
|
|
).run()
|
|
except SystemExit as error:
|
|
if error.code == 99:
|
|
pass # fall through lock cleanup
|
|
else:
|
|
raise
|
|
|
|
if opts.lockfile:
|
|
os.unlink(opts.lockfile)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|