#!/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]
# 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:
# xxxx will be prepended to backtrace. This 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").
#
# In the future, we may want to use a separate CACHEDIR (say, /var/cache/abrt-di)
# and use it with this gdb command:
#   set debug-file-directory /usr/lib/debug/.build-id:CACHEDIR/usr/lib/debug/.build-id
# but current gdb can't handle DIR1:DIR2.
# So, currently we are called with CACHEDIR set to "/", and don't pass
# "set debug-file-directory" to gdb.
# This is ugly, since it messes up /usr/lib/debug/.build-id over time
# by piling up debuginfos there without any means to control their amount,
# but it's the only way to make it work with current gdb.


core=$1
tempdir=$2
cachedir=$3
debug=false

exec 2>&1

test -f "$core" || exit 2
# cachedir is optional
test x"$cachedir" = x"" || test -d "$cachedir" || exit 2
# tempdir must not exist
test -e "$tempdir" && exit 2

mkdir -- "$tempdir" || exit 2
cd "$tempdir" || exit 2
$debug && echo "Installing rpms to $tempdir"


count_words() {
    echo $#
}

cleanup_and_report_missing() {
# Which debuginfo files are still missing, including those we just unpacked?
    missing_build_ids=`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
	test -f "$cachedir/$file" && continue
	echo -n "$build_id "
    done`
    $debug && echo "missing_build_ids:$missing_build_ids"

    # If cachedir is specified, tempdir is just a staging area. Delete it
    if test x"$cachedir" != x""; then
        $debug && echo "Removing $tempdir"
        rm -rf "$tempdir"
    fi

    for missing in $missing_build_ids; do
        echo "MISSING:$missing"
    done

    test x"$missing_build_ids" != x"" && echo "`count_words $missing_build_ids` debuginfos can't be found"
}


# 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\nexitcode:%s\n" "$eu_unstrip_OUT" $err >eu_unstrip.OUT
test $err = 0 || exit 2

# 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"

# Which debuginfo files are missing?
missing_debuginfo_files=`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"
    if test x"$cachedir" != x"" && test x"$cachedir" != x"/" ; then
	test -f "$cachedir/$file" && continue
    fi
    test -f "/$file" && continue
    echo -n "/$file "
done`
$debug && echo "missing_debuginfo_files:$missing_debuginfo_files"

if test x"$missing_debuginfo_files" = x""; then
    cleanup_and_report_missing
    exit 0
fi

# 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 ...
echo "Determining list of packages for `count_words $missing_debuginfo_files` missing debuginfos"
yum_provides_OUT=`yum --enablerepo='*debuginfo*' --quiet provides $missing_debuginfo_files 2>&1`
err=$?
printf "%s\nexitcode:%s\n" "$yum_provides_OUT" $err >yum_provides.OUT
test $err = 0 || exit 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).
packages=`printf "%s\n" "$yum_provides_OUT" \
| grep -- -debuginfo- \
| sed 's/^[0-9]*://' \
| sed -e 's/ .*//' -e 's/:.*//' \
| sort | uniq | xargs`
$debug && echo "packages:$packages"

# yum may return "" here if it found no packages (say, if coredump is from a new,
# unreleased package fresh from koji).
if test x"$packages" = x""; then
    cleanup_and_report_missing
    exit 1
fi

num_packages=`count_words $packages`
echo "Downloading $num_packages packages"
## 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 || exit 2
>yumdownloader.OUT
i=1
for pkg in $packages; do
    echo "Download $i/$num_packages: $pkg"
    echo "Download $i/$num_packages: $pkg" >>yumdownloader.OUT
    yumdownloader --enablerepo='*debuginfo*' --quiet $pkg >>yumdownloader.OUT 2>&1
    err=$?
    echo "exitcode:$err" >>yumdownloader.OUT
    echo >>yumdownloader.OUT
    test $err = 0 || { echo "Download of $pkg failed!"; sleep 1; }
    : $((i++))
done

for f in *.rpm; do
    # Happens if no .rpm's were downloaded (yumdownloader problem)
    # In this case, $f is the literal "*.rpm" string
    test -f "$f" || exit 2
    echo "Unpacking: $f"
    echo "Processing: $f" >>unpack.OUT
    rpm2cpio <"$f" 2>>unpack.OUT | cpio -id >>unpack.OUT 2>&1
done

# Copy debuginfo files to cachedir
if test x"$cachedir" != x"" && test -d "$cachedir"; then
    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
	test x"$cachedir" != x"/" && test -f "$cachedir/$file" && continue

	if test -f "$file"; then
	    # file is one of those we just installed.
	    # Cache it if cachedir is specified.
	    mkdir -p "$cachedir/usr/lib/debug/.build-id/$build_id1"
	    # Note: this does not preserve symlinks. This is intentional
	    $debug && echo Copying2 "$file" to "$cachedir/$file" >&2
	    cp --remove-destination "$file" "$cachedir/$file"
	    continue
	fi
    done
fi
$debug && echo "missing_build_ids:$missing_build_ids"

cleanup_and_report_missing

test x"$missing_build_ids" != x"" && exit 1
exit 0
