#!/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").


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-debuginfo-install [-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
