#!/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-debuginfo-install 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"). core="$1" tempdir="$2" debuginfodirs="${3//:/ }" cachedir="${3%%:*}" debug=false # Useful if you need to see saved rpms, command outputs etc keep_tmp=false # 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 } 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 err=$? echo "exitcode:$err" >>yumdownloader.OUT echo >>yumdownloader.OUT test $err = 0 || { echo "Download of $pkg failed!"; sleep 1; } # 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" cpio -id <"unpacked.cpio" >>unpack.OUT 2>&1 || error_msg_and_die "Can't unpack '$file' cpio archive" rm "unpacked.cpio" # 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 $debug && echo "Downloading rpms to $tempdir" # 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 echo "Getting list of build IDs" # Observed errors: # eu-unstrip: /var/cache/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`" # 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. # (Without -C, yum for some reason wants to talk to repos! If one is down, it becomes S..L..O..W) yum_repo_opts="'--disablerepo=*'" 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 # 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