#!/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:
# 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
debuginfodirs="${3//:/ }"
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 $#
}

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
	for d in $debuginfodirs; do
	    test -f "$d/$file" && continue 2
	done
	echo -n "$build_id "
    done
}

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

    # If cachedir is specified, tempdir is just a staging area. Delete it
    if test x"$cachedir" != x""; then
        $debug && echo "Removing $tempdir"
        $debug || 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"
}

# $1: iteration (1,2...)
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" >&2
    fi
    local cmd="yum $yumopts --enablerepo='*debuginfo*' --quiet provides $missing_debuginfo_files"
    echo "$cmd" >"yum_provides.$1.OUT"
    local yum_provides_OUT=`$cmd 2>&1`
    local err=$?
    printf "%s\nexitcode:%s\n" "$yum_provides_OUT" $err >>"yum_provides.$1.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).
    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 || 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 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" || exit 2
	echo "Unpacking: $file"
	echo "Processing: $file" >>unpack.OUT
	rpm2cpio <"$file" 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"

	    # 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
		cp --remove-destination "$file" "$cachedir/$file"
		continue
	    fi
	done
    fi
}


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

# 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`
    $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`
    $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
exit 0
