#!/bin/sh
#
#   Bacula(R) - The Network Backup Solution
#
#   Copyright (C) 2000-2022 Kern Sibbald
#   Copyright (C) 2000-2014 Free Software Foundation Europe e.V.
#
#   The original author of Bacula is Kern Sibbald, with contributions
#   from many others, a complete list can be found in the file AUTHORS.
#
#   You may use this file and others of this release according to the
#   license defined in the LICENSE file, which includes the Affero General
#   Public License, v3.0 ("AGPLv3") and some additional permissions and
#   terms pursuant to its AGPLv3 Section 7.
#
#   This notice must be preserved when any source code is 
#   conveyed and/or propagated.
#
#   Bacula(R) is a registered trademark of Kern Sibbald.
#
# Script to do a stackdump of a Bacula daemon/program.
#
# We attempt to attach to running program
#
# Arguments to this script are
#  $1 = path to executable
#  $2 = main pid of running program to be traced back.
#  $3 = working directory
#

ret=1

# The gdb_* functions and variables are used to improve gdb output.
# gdb_init() display some bacula's variables and initiate the command
# "thread apply all bt"
# the output is parsed by gdb_interact() that will identify the
# faulty thread if any, jump in it and initiate a "bt full"
# A "tee" duplicate the gdb output up to gdb_interact() and up to the
# traceback file
# gdb and gdb_interact() are connected via a named pipe "$mypipe" (see
# below)
# gdb is now run in "interactive" mode, not in "batch" mode anymore

# default value if the thread that with the signal_handler don't exist
gdb_th=999999
gdb_thread=999999
gdb_ret_file=$3/bacula.$2.ret

gdb_init()
{
cat <<EOF
printf "fail_time=\"%s\"\n", fail_time
printf "myname=\"%s\"\n", my_name
printf "exename=\"%s\"\n", exename
printf "exepath=\"%s\"\n", exepath
printf "assert_msg=\"%s\"\n", assert_msg
printf "db_engine_name=\"%s\"\n", db_engine_name
printf "version=\"%s\"\n", version
printf "host_os=\"%s\"\n", host_os
printf "distname=\"%s\"\n", distname
printf "distver=\"%s\"\n", distver
printf "host_name=\"%s\"\n", host_name
printf "dist_name=\"%s\"\n", dist_name
show env TestName
thread apply all bt
printf "waiting for more commands\n"
EOF
}

gdb_interact()
{
gdb_init
# read gdb output line by line until the EOF
# works with bash & /bin/sh to detect empty line
while IFS='' read -r line || [ -n "$LINE" ]; do
  case $line in
  Thread\ [1-9]*)
    gdb_th=`echo $line | cut -d " " -f 2`
#    echo "THREAD $gdb_th" >&2
    ;;
  *signal\ handler\ called*)
    gdb_thread=$gdb_th
#    echo "echo FOUND signal in thread $gdb_thread $2" >&2
    ;;
  *waiting\ for\ more\ commands*)
#    echo "REACHED waiting for more commands thread=$gdb_thread" >&2
    # use shell printf that is more portable instead of echo to handle \n
    if [ "$gdb_thread" != "999999" ] ; then
       printf "thread $gdb_thread\n"
       printf "printf \"==================  Faulty thread and variables  ==================\\\\n\"\n"
       printf "bt full\n"
       printf "printf \"=========================  Faulty thread  =========================\\\\n\"\n"
       printf "bt\n"
    fi
    printf "printf \"this is the last line to parse\\\\n\"\n"
    printf "detach\n"
    printf "set confirm off\n"
    printf "quit\n"
    echo 2 > $gdb_ret_file # nearly a SUCCESS
    ;;
*this\ is\ the\ last\ line\ to\ parse*)
#    echo "FINISH" >&2
    echo 0 > $gdb_ret_file # a TRUE SUCCESS
    # don't break here, read the useless remaining line
    ;;
  *)
#    echo $line >&2
    true
    ;;
  esac
done
}

PNAME=`basename $1`
WD="$3"
# When parsing the configuration, the working directory is not
# yet initialized
if [ "$WD" = "" ]; then
    WD=/tmp
fi

# Start constructing message. Point to log files for better user
# experience, append backtrace or message about missing debugger later
echo "Check the log files for more information." > ${WD}/bacula.$2.traceback
echo "" >> ${WD}/bacula.$2.traceback

case `uname -s` in
SunOS)
   #
   # See what debuggers are available on this platform.
   # We need to to some tricks to find out as a which on
   # a non existing binary gives:
   #
   # no <debugger> in <PATH>
   #
   # So we use the return code which is 0 when it finds
   # somethings and 1 if not.
   #
   which gdb > /dev/null 2>&1 && GDB=`which gdb` || GDB=''
   which dbx > /dev/null 2>&1 && DBX=`which dbx` || DBX=''
   which mdb > /dev/null 2>&1 && MDB=`which mdb` || MDB=''
   gcore -o ${WD}/${PNAME} $2
   if [ ! -z "${DBX}" ]; then
      ${DBX} $1 $2 < /opt/bacula/scripts/btraceback.dbx >> ${WD}/bacula.$2.traceback 2>&1
      ret=$?
   elif [ ! -z "${GDB}" ]; then
      ${GDB} -quiet -batch -x /opt/bacula/scripts/btraceback.gdb $1 $2 >> ${WD}/bacula.$2.traceback 2>&1
      ret=$?
   elif [ ! -z "${MDB}" ]; then
      ${MDB} -u -p $2 < /opt/bacula/scripts/btraceback.mdb >> ${WD}/bacula.$2.traceback 2>&1
      ret=$?
   fi
   ;;
*)
   if false; then
      # the old school
      gdb -quiet -batch -x /opt/bacula/scripts/btraceback.gdb $1 $2 >> ${WD}/bacula.$2.traceback 2>&1
      ret=$?
   else
     # using a gdb improved output, see above
     myfifo=${WD}/bacula.$2.fifo
     rm -f $myfifo
     mkfifo $myfifo
     gdb_interact < $myfifo | gdb -quiet -ex "set prompt" $1 $2 | tee -a ${WD}/bacula.$2.traceback > $myfifo 2>&1
     rm -f $myfifo
     if [ -e $gdb_ret_file ] ; then
       # collect exit status from gdb_interact
       ret=`cat $gdb_ret_file`
       rm -f $gdb_ret_file
     fi
   fi
   PNAME="${PNAME} on `hostname`"
   cat ${WD}/bacula.$2.traceback \
    | /opt/bacula/bin/bsmtp -h localhost -f root -s "Bacula GDB traceback of ${PNAME}" root
   ;;
esac

exit $ret

