#!/usr/bin/env bash
# on-panic.sh — run a command under gdb, break on Rust panic, dump backtrace +
# locals + the droidsaw pretty-printed IR in scope, then continue.
#
# Usage:
#   tools/gdb/on-panic.sh [--interactive] [--printers PATH] -- CMD [ARGS...]
#
# Environment:
#   RUSTFLAGS       — if unset, defaults to "-C force-frame-pointers=yes -C debuginfo=2"
#   RUST_BACKTRACE  — if unset, defaults to "full"
#
# Exit code: 0 if CMD exits 0 and no panic caught; CMD's exit code otherwise.
#            134 if gdb itself could not be exec'd.

set -u

SCRIPT_DIR="$(cd -- "$(dirname -- "$(readlink -f "$0")")" && pwd)"
PRINTERS="${SCRIPT_DIR}/droidsaw.py"
INTERACTIVE=0

usage() {
    sed -n '3,12p' "$0" >&2
    exit 2
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --interactive) INTERACTIVE=1; shift ;;
        --printers)    PRINTERS="${2:?--printers needs a path}"; shift 2 ;;
        --help|-h)     usage ;;
        --)            shift; break ;;
        *)             echo "unknown arg: $1" >&2; usage ;;
    esac
done

if [[ $# -lt 1 ]]; then
    echo "error: no command given after --" >&2
    usage
fi

if [[ ! -r "$PRINTERS" ]]; then
    echo "error: printers file not readable: $PRINTERS" >&2
    exit 2
fi

if ! command -v gdb >/dev/null 2>&1; then
    echo "error: gdb not on PATH" >&2
    exit 134
fi

export RUSTFLAGS="${RUSTFLAGS:--C force-frame-pointers=yes -C debuginfo=2}"
export RUST_BACKTRACE="${RUST_BACKTRACE:-full}"

# Materialize a gdb script that installs breakpoints with command blocks.
# Multi-line `commands` can't be passed via `-ex` (each -ex is a single command).
SCRIPT=$(mktemp -t droidsaw-on-panic.XXXXXX.gdb)
cleanup() { rm -f "$SCRIPT"; }
trap cleanup EXIT

cat > "$SCRIPT" <<GDB
set pagination off
set print pretty on
set print frame-arguments all
set print entry-values no
set debuginfod enabled off
set auto-load safe-path /
source ${PRINTERS}
set breakpoint pending on
# Catch the panic as early as gdb can resolve it. Try three symbol names —
# rustc has moved the canonical entry across releases. Whichever resolves first
# will fire; the others stay pending and do nothing.
break std::panicking::panic_with_hook
commands
silent
printf "\n========== droidsaw: rust panic caught (panic_with_hook) ==========\n"
bt full
printf "\n-- locals (innermost Rust frame) --\n"
info locals
printf "\n========== (end panic report) ==========\n\n"
continue
end
break std::panicking::rust_panic
commands
silent
printf "\n========== droidsaw: rust panic caught (rust_panic) ==========\n"
bt full
printf "\n-- locals (innermost Rust frame) --\n"
info locals
printf "\n========== (end panic report) ==========\n\n"
continue
end
break rust_panic
commands
silent
printf "\n========== droidsaw: rust panic caught (rust_panic unqualified) ==========\n"
bt full
printf "\n========== (end panic report) ==========\n\n"
continue
end
GDB

GDB_PRE_ARGS=(-q -iex 'set debuginfod enabled off' -iex 'set auto-load safe-path /')

if [[ $INTERACTIVE -eq 1 ]]; then
    exec gdb "${GDB_PRE_ARGS[@]}" -x "$SCRIPT" -ex run --args "$@"
fi

gdb -batch "${GDB_PRE_ARGS[@]}" -x "$SCRIPT" -ex run -ex 'quit $_exitcode' --args "$@"
exit $?
