diff options
Diffstat (limited to 'src/plugins/abrt-action-install-debuginfo')
| -rwxr-xr-x | src/plugins/abrt-action-install-debuginfo | 418 |
1 files changed, 418 insertions, 0 deletions
diff --git a/src/plugins/abrt-action-install-debuginfo b/src/plugins/abrt-action-install-debuginfo new file mode 100755 index 00000000..c1b8fdb9 --- /dev/null +++ b/src/plugins/abrt-action-install-debuginfo @@ -0,0 +1,418 @@ +#!/bin/sh +# Called by abrtd before producing a backtrace. +# The task of this script is to install debuginfos. +# +# Just using [pk-]debuginfo-install does not work well. +# - they can't install more than one version of debuginfo +# for a package +# - their output is unsuitable for scripting +# - debuginfo-install aborts if yum lock is busy +# - pk-debuginfo-install was observed to hang +# +# Usage: abrt-action-install-debuginfo CORE TEMPDIR [CACHEDIR[:DEBUGINFODIR1:DEBUGINFODIR2...]] +# If CACHEDIR is specified, debuginfos should be installed there. +# If not, debuginfos should be installed into TEMPDIR. +# +# Currently, we are called with CACHEDIR set to "/var/cache/abrt-di", +# but in the future it may be omitted or set to something else. +# Script must be ready for those cases too. Consider, for example, +# corner cases of "" and "/". +# +# Output goes to GUI as debuginfo install log. The script should be careful +# to give useful, but not overly cluttered info to stdout. +# Additionally, abrt daemon handles "MISSING:xxxx" messages specially: +# it is used to inform about missing debuginfos. +# +# Exitcodes: +# 0 - all debuginfos are installed +# 1 - not all debuginfos are installed +# 2+ - serious problem +# +# Algorithm: +# - Create TEMPDIR +# - Extract build-ids from coredump +# - For every build-id, check /usr/lib/debug/.build-id/XX/XXXX.debug +# and CACHEDIR/usr/lib/debug/.build-id/XX/XXXX.debug +# - If they all exist, exit 0 +# - Using "yum provides /usr/lib/debug/.build-id/XX/XXXX.debug", +# figure out which debuginfo packages are needed +# - Download them using "yumdownloader PACKAGE..." +# - Unpack them with rpm2cpio | cpio to TEMPDIR +# - If CACHEDIR is specified, copy usr/lib/debug/.build-id/XX/XXXX.debug +# to CACHEDIR/usr/lib/debug/.build-id/XX/XXXX.debug and delete TEMPDIR +# - Report which XX/XXXX.debug are still missing. +# +# For better debuggability, eu_unstrip.OUT, yum_provides.OUT etc files +# are saved in TEMPDIR, and TEMPDIR is not deleted if we exit with exitcode 2 +# ("serious problem"). + + +debug=false +# Useful if you need to see saved rpms, command outputs etc +keep_tmp=false + + +# Handle options +if test x"$1" = x"--"; then + shift +else + if test x"$1" = x"-v"; then + debug=true + shift + fi + if test $# -lt 2 || test x"$1" = x"--help"; then + echo "Usage:" + echo + echo "abrt-action-install-debuginfo [-v] CORE TEMPDIR [CACHEDIR[:DEBUGINFODIR...]]" + echo + echo "TEMPDIR must be a name of a new temporary directory. It must not exist." + echo "If CACHEDIR is specified, debuginfos are installed in CACHEDIR," + echo "and TEMPDIR is deleted on exit." + echo "Otherwise, debuginfos are installed into TEMPDIR, which is not deleted." + echo + echo "Options:" + echo " -v Verbose (for debugging)" + echo + exit + fi +fi + + +# Parse params +core="$1" +tempdir="$2" +debuginfodirs="${3//:/ }" +cachedir="${3%%:*}" + + +# stderr may be used for status messages too +exec 2>&1 + + +error_msg_and_die() { + echo "$*" + exit 2 +} + +count_words() { + echo $# +} + +print_missing_build_ids() { + local build_id + local build_id1 + local build_id2 + local file + local d + for build_id in $build_ids; do + build_id1=${build_id:0:2} + build_id2=${build_id:2} + file="usr/lib/debug/.build-id/$build_id1/$build_id2.debug" + test -f "/$file" && continue + # On 2nd pass, we may already have some debuginfos in tempdir + test -f "$tempdir/$file" && continue + # Check cachedir if we have one + for d in $debuginfodirs; do + test -f "$d/$file" && continue 2 + done + echo -n "$build_id " + done +} + +# Note: it is run in `backticks`, use >&2 for error messages +print_missing_debuginfos() { + local build_id + local build_id1 + local build_id2 + local file + local d + for build_id in $build_ids; do + build_id1=${build_id:0:2} + build_id2=${build_id:2} + file="usr/lib/debug/.build-id/$build_id1/$build_id2.debug" + test -f "/$file" && continue + # On 2nd pass, we may already have some debuginfos in tempdir + test -f "$tempdir/$file" && continue + # Check cachedir if we have one + if test x"$cachedir" != x""; then + for d in $debuginfodirs; do + test -f "$d/$file" && continue 2 + done + fi + echo -n "/$file " + done +} + +cleanup_and_report_missing() { +# Which debuginfo files are still missing, including those we just unpacked? + missing_build_ids=`print_missing_build_ids` + $debug && echo "missing_build_ids:$missing_build_ids" >&2 + + # If cachedir is specified, tempdir is just a staging area. Delete it + if test x"$cachedir" != x""; then + $keep_tmp && echo "NOT removing $tempdir (keep_tmp debugging is on)" >&2 + $keep_tmp || { $debug && echo "Removing $tempdir" >&2; rm -rf "$tempdir"; } + fi + + for missing in $missing_build_ids; do + echo "MISSING:$missing" >&2 + done + + test x"$missing_build_ids" != x"" && echo "`count_words $missing_build_ids` debuginfos can't be found" >&2 +} + +# $1: iteration (1,2...) +# Note: it is run in `backticks`, use >&2 for error messages +print_package_names() { + # We'll run something like: + # yum --enablerepo=*debuginfo* --quiet provides \ + # /usr/lib/debug/.build-id/bb/11528d59940983f495e9cb099cafb0cb206051.debug \ + # /usr/lib/debug/.build-id/c5/b84c0ad3676509dc30bfa7d42191574dac5b06.debug ... + local yumopts="" + if test x"$1" = x"1"; then + yumopts="-C" + echo "`count_words $missing_debuginfo_files` missing debuginfos, getting package list from cache" >&2 + else + echo "`count_words $missing_debuginfo_files` missing debuginfos, getting package list from repositories" >&2 + fi + # --showduplicates: do not just show the latest package + # (tried to use -R2 to abort on stuck yum lock but -R is not about that) + local cmd="yum $yumopts $yum_repo_opts --showduplicates --quiet provides $missing_debuginfo_files" + echo "$cmd" >"yum_provides.$1.OUT" + $debug && echo "Running: $cmd" >&2 + # eval is needed to strip away ''s in $yum_repo_opts; cant remove them and just use + # unquoted $cmd, that would perform globbing on '*' + local yum_provides_OUT="`eval $cmd 2>&1`" + local err=$? + printf "%s\nyum exitcode:%s\n" "$yum_provides_OUT" $err >>"yum_provides.$1.OUT" + test $err = 0 || error_msg_and_die "yum provides... exited with $err: +`head yum_provides.$1.OUT`" >&2 + + # The output is pretty machine-unfriendly: + # glibc-debuginfo-2.10.90-24.x86_64 : Debug information for package glibc + # Repo : rawhide-debuginfo + # Matched from: + # Filename : /usr/lib/debug/.build-id/5b/c784c8d63f87dbdeb747a773940956a18ecd2f.debug + # + # 1:dbus-debuginfo-1.2.12-2.fc11.x86_64 : Debug information for package dbus + # Repo : updates-debuginfo + # Matched from: + # Filename : /usr/lib/debug/.build-id/bc/da7d09eb6c9ee380dae0ed3d591d4311decc31.debug + # Need to massage it a lot. + # There can be duplicates (one package may provide many debuginfos). + printf "%s\n" "$yum_provides_OUT" \ + | grep -- -debuginfo- \ + | sed 's/^[0-9]*://' \ + | sed -e 's/ .*//' -e 's/:.*//' \ + | sort | uniq | xargs +} + +abort_if_low_on_disk_space() { + local mb + # free_blocks * block_size / (1024*1024), careful to not overflow: + mb=$((`stat -f -c "%a / 8192 * %S / 128" "$tempdir"`)) + if test $mb -lt $1; then + $debug && echo "Removing $tempdir" >&2 + rm -rf "$tempdir" + error_msg_and_die "Less than $1 Mb of free space in $tempdir: $mb Mb" + fi + if test x"$cachedir" != x"" && test -d "$cachedir"; then + mb=$((`stat -f -c "%a / 8192 * %S / 128" "$cachedir"`)) + if test $mb -lt $1; then + $debug && echo "Removing $tempdir" >&2 + rm -rf "$tempdir" + error_msg_and_die "Less than $1 Mb of free space in $cachedir: $mb Mb" + fi + fi +} + +download_packages() { + local pkg + local err + local file + local build_id + local build_id1 + local build_id2 + local d + + ## Download with one command (too silent): + ## Redirecting, since progress bar stuff only messes up our output + ##yumdownloader --enablerepo=*debuginfo* --quiet $packages >yumdownloader.OUT 2>&1 + ##err=$? + ##echo "exitcode:$err" >>yumdownloader.OUT + ##test $err = 0 || error_msg_and_die ... + >yumdownloader.OUT + i=1 + for pkg in $packages; do + echo "Download $i/$num_packages: $pkg" + echo "Download $i/$num_packages: $pkg" >>yumdownloader.OUT + cmd="yumdownloader $yum_repo_opts --quiet $pkg" + $debug && echo "Running: $cmd" >&2 + # eval is needed to strip away ''s in $yum_repo_opts + eval $cmd >>yumdownloader.OUT 2>&1 & + # using EXIT handler and this, make sure we kill yumdownloader if we exit: + CHILD_PID=$! + wait + err=$? + CHILD_PID="" + echo "exitcode:$err" >>yumdownloader.OUT + echo >>yumdownloader.OUT + test $err = 0 || echo "Download of $pkg failed!" + abort_if_low_on_disk_space 256 + + # Process and delete the *.rpm file just downloaded + # We do it right after download: some users have smallish disks... + for file in *.rpm; do + # Happens if no .rpm's were downloaded (yumdownloader problem) + # In this case, $f is the literal "*.rpm" string + test -f "$file" || { echo "No rpm file downloaded"; continue; } + echo "Unpacking: $file" + echo "Processing: $file" >>unpack.OUT + rpm2cpio <"$file" >"unpacked.cpio" 2>>unpack.OUT || error_msg_and_die "Can't convert '$file' to cpio" + $keep_tmp || rm "$file" + abort_if_low_on_disk_space 256 + cpio -id <"unpacked.cpio" >>unpack.OUT 2>&1 || error_msg_and_die "Can't unpack '$file' cpio archive" + rm "unpacked.cpio" + abort_if_low_on_disk_space 256 + # Copy debuginfo files to cachedir + if test x"$cachedir" != x"" && test -d "$cachedir"; then + # For every needed debuginfo, check whether we have it + for build_id in $build_ids; do + build_id1=${build_id:0:2} + build_id2=${build_id:2} + file="usr/lib/debug/.build-id/$build_id1/$build_id2.debug" + # Do not copy it if it can be found in any of $debuginfodirs + test -f "/$file" && continue + if test x"$cachedir" != x""; then + for d in $debuginfodirs; do + test -f "$d/$file" && continue 2 + done + fi + if test -f "$file"; then + # File is one of those we just installed, cache it + mkdir -p "$cachedir/usr/lib/debug/.build-id/$build_id1" + # Note: this does not preserve symlinks. This is intentional + $debug && echo Copying "$file" to "$cachedir/$file" >&2 + echo "Caching debuginfo: $file" + cp --remove-destination "$file" "$cachedir/$file" || error_msg_and_die "Can't copy $file (disk full?)" + continue + fi + done + fi + # Delete remaining files unpacked from .cpio + # which we didn't need after all + rm -r etc bin sbin usr var opt 2>/dev/null + done + : $((i++)) + done +} + + +# Sanity checking +test -f "$core" || error_msg_and_die "not a file: '$core'" +# cachedir is optional +test x"$cachedir" = x"" || test -d "$cachedir" || error_msg_and_die "bad cachedir '$cachedir'" +# tempdir must not exist +test -e "$tempdir" && error_msg_and_die "tempdir exists: '$tempdir'" + +# Intentionally not using -p: we want to abort if tempdir exists +mkdir -- "$tempdir" || exit 2 +cd "$tempdir" || exit 2 + + +abort_if_low_on_disk_space 1024 + + +# A hook to stop yumdownloader, in case we are terminated by kill -TERM etc. +CHILD_PID="" +trap 'test x"$CHILD_PID" != x"" && kill -- "$CHILD_PID"' EXIT + + +$debug && echo "Downloading rpms to $tempdir" + + +echo "Getting list of build IDs" +# Observed errors: +# eu-unstrip: /var/spool/abrt/ccpp-1256301004-2754/coredump: Callback returned failure +eu_unstrip_OUT=`eu-unstrip "--core=$core" -n 2>eu_unstrip.ERR` +err=$? +printf "%s\neu-unstrip exitcode:%s\n" "$eu_unstrip_OUT" $err >eu_unstrip.OUT +test $err = 0 || error_msg_and_die "eu-unstrip exited with $err: +`cat eu_unstrip.ERR` +`head eu_unstrip.OUT`" + +# eu-unstrip output example: +# 0x400000+0x209000 23c77451cf6adff77fc1f5ee2a01d75de6511dda@0x40024c - - [exe] +# or +# 0x400000+0x20d000 233aa1a57e9ffda65f53efdaf5e5058657a39993@0x40024c /usr/libexec/im-settings-daemon /usr/lib/debug/usr/libexec/im-settings-daemon.debug [exe] +# 0x7fff5cdff000+0x1000 0d3eb4326fd7489fcf9b598269f1edc420e2c560@0x7fff5cdff2f8 . - linux-vdso.so.1 +# 0x3d15600000+0x208000 20196628d1bc062279622615cc9955554e5bb227@0x3d156001a0 /usr/lib64/libnotify.so.1.1.3 /usr/lib/debug/usr/lib64/libnotify.so.1.1.3.debug libnotify.so.1 +# 0x7fd8ae931000+0x62d000 dd49f44f958b5a11a1635523b2f09cb2e45c1734@0x7fd8ae9311a0 /usr/lib64/libgtk-x11-2.0.so.0.1600.6 /usr/lib/debug/usr/lib64/libgtk-x11-2.0.so.0.1600.6.debug +# +# Get space-separated list of all build-ids +# There can be duplicates (observed in real world) +build_ids=`printf "%s\n" "$eu_unstrip_OUT" \ +| while read junk1 build_id binary_file di_file lib_name junk2; do + build_id=${build_id%%@*} + + # This filters out linux-vdso.so, among others + test x"$lib_name" != x"[exe]" && test x"${binary_file:0:1}" != x"/" && continue + # Sanitize build_id: must be longer than 2 chars + test ${#build_id} -le 2 && continue + # Sanitize build_id: must have only hex digits + test x"${build_id//[0-9a-f]/}" != x"" && continue + + echo "$build_id" +done | sort | uniq | xargs` +$debug && echo "build_ids:$build_ids" + + +# Prepare list of repos to use. +# When we look for debuginfo we need only -debuginfo* repos, we can disable the rest +# and thus make it faster. +yum_repo_opts="'--disablerepo=*'" +#// Disabled. Too often, debuginfo repos have names which do not conform to "foo-debuginfo" scheme, +#// and users get bad backtraces. +#// # (Without -C, yum for some reason wants to talk to repos! If one is down, it becomes S..L..O..W) +#// for enabled_repo in `LANG=C yum -C repolist all | grep 'enabled:' | cut -f1 -d' ' | grep -v -- '-debuginfo'`; do +#// yum_repo_opts="$yum_repo_opts '--enablerepo=${enabled_repo}-debuginfo*'" +#// done +yum_repo_opts="$yum_repo_opts '--enablerepo=*-debug*'" + + +# We try to not run yum without -C unless absolutely necessary. +# Therefore we loop. yum is run by print_package_names function, +# on first iteration it is run with -C, on second - without, +# which usually causes yum to download updated filelists, +# which in turn takes several minutes and annoys users. +iter=0 +while test $((++iter)) -le 2; do + # Analyze $build_ids and check which debuginfos are present + missing_debuginfo_files=`print_missing_debuginfos` + # Did print_missing_debuginfos fail? + test $? = 0 || exit 2 + $debug && echo "missing_debuginfo_files:$missing_debuginfo_files" + + test x"$missing_debuginfo_files" = x"" && break + + # Map $missing_debuginfo_files to package names. + # yum is run here. + packages=`print_package_names $iter` + # Did print_package_names fail? + test $? = 0 || exit 2 + $debug && echo "packages ($iter):$packages" + + # yum may return "" here if it found no packages (say, if coredump + # is from a new, unreleased package fresh from koji). + test x"$packages" = x"" && continue + + num_packages=`count_words $packages` + echo "Downloading $num_packages packages" + download_packages +done + +cleanup_and_report_missing + +test x"$missing_build_ids" != x"" && exit 1 +echo "All needed debuginfos are present" +exit 0 |
