#!/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


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

    # 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...)
# 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
    # when we look for debuginfo we need only -debuginfo* repos, so we can disable the rest and thus make it faster
    # also we want only fedora repositories, because abrt won't work for other packages anyway
    # --showduplicates: do not just show the latest package
    local cmd="yum $yumopts '--disablerepo=*' '--enablerepo=fedora-debuginfo*' '--enablerepo=updates-debuginfo*' --showduplicates --quiet provides $missing_debuginfo_files"
    echo "$cmd" >"yum_provides.$1.OUT"
    # eval is needed to strip away ''s; cant remove them above and just use
    # $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
	# We can't handle packages from non Fedora repos, so we look and download only
	# from Fedora repos which makes it faster
	yumdownloader --disablerepo="*" --enablerepo="fedora-debuginfo*" --enablerepo="updates-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; }
	# 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"
	    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'"

mkdir -- "$tempdir" || exit 2
cd "$tempdir" || exit 2
$debug && echo "Installing 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"

# 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
