1158 lines
34 KiB
Bash
Executable File
1158 lines
34 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Turn on extended globbing
|
|
shopt -s extglob
|
|
shopt -s nullglob
|
|
# Bail on any error
|
|
set -e
|
|
|
|
prog=$(basename $0)
|
|
|
|
# NOTE: <(cmd) is a bash construct that returns a temporary file name
|
|
# from which the command output can be read. In this case, we're
|
|
# extracting the block of text delimited by '#@@@FUNCSSTART@@@'
|
|
# and '#@@@FUNCSEND@@@' from this file and 'source'ing it to
|
|
# get some functions.
|
|
source <(sed -n -r -e "/^#@@@FUNCSSTART@@@/,\${p;/^#@@@FUNCSEND@@@/q}" $0 | sed '1d;$d')
|
|
|
|
# The "!(*.txt)" is a bash construct that excludes files ending with .txt
|
|
# from the glob match.
|
|
declare -a COREDUMPS=( /tmp/core!(*.txt) )
|
|
|
|
# A line starting with ': ' is a POSIX construct that makes the shell
|
|
# perform the operation but ignore the result. This is an alternative to
|
|
# having to do RUNNING=${RUNNING:=false} to set defaults.
|
|
|
|
: ${ASTERISK_BIN:=$(which asterisk)}
|
|
: ${DATEOPTS='-u +%FT%H-%M-%SZ'}
|
|
: ${DELETE_COREDUMPS_AFTER:=false}
|
|
: ${DELETE_RESULTS_AFTER:=false}
|
|
: ${DRY_RUN:=false}
|
|
: ${GDB:=$(which gdb)}
|
|
: ${HELP:=false}
|
|
: ${LATEST:=false}
|
|
: ${OUTPUTDIR:=/tmp}
|
|
: ${PROMPT:=true}
|
|
: ${RUNNING:=false}
|
|
: ${RENAME:=true}
|
|
: ${TARBALL_CONFIG:=false}
|
|
: ${TARBALL_COREDUMPS:=false}
|
|
: ${TARBALL_RESULTS:=false}
|
|
|
|
COMMANDLINE_COREDUMPS=false
|
|
|
|
# Read config files from most important to least important.
|
|
# Variables set on the command line or environment always take precedence.
|
|
[ -f ./ast_debug_tools.conf ] && source ./ast_debug_tools.conf
|
|
[ -f ~/ast_debug_tools.conf ] && source ~/ast_debug_tools.conf
|
|
[ -f /etc/asterisk/ast_debug_tools.conf ] && source /etc/asterisk/ast_debug_tools.conf
|
|
|
|
if [ -n "${DATEFORMAT}" ] ; then
|
|
err <<-EOF
|
|
The DATEFORMAT variable in your ast_debug_tools.conf file has been
|
|
replaced with DATEOPTS which has a different format. See the latest
|
|
ast_debug_tools.conf sample file for more information.
|
|
EOF
|
|
fi
|
|
|
|
|
|
for a in "$@" ; do
|
|
if [[ $a == "--RUNNING" ]] ; then
|
|
RUNNING=true
|
|
PROMPT=false
|
|
elif [[ $a =~ --no-([^=]+)$ ]] ; then
|
|
var=${BASH_REMATCH[1]//-/_}
|
|
eval ${var^^}="false"
|
|
elif [[ $a =~ --([^=]+)$ ]] ; then
|
|
var=${BASH_REMATCH[1]//-/_}
|
|
eval ${var^^}="true"
|
|
elif [[ $a =~ --([^=]+)=(.+)$ ]] ; then
|
|
var=${BASH_REMATCH[1]//-/_}
|
|
eval ${var^^}=${BASH_REMATCH[2]}
|
|
else
|
|
if ! $COMMANDLINE_COREDUMPS ; then
|
|
COMMANDLINE_COREDUMPS=true
|
|
COREDUMPS=()
|
|
fi
|
|
COREDUMPS+=( "$a" )
|
|
fi
|
|
done
|
|
|
|
if $HELP ; then
|
|
print_help
|
|
exit 0
|
|
fi
|
|
|
|
check_gdb
|
|
|
|
if [ -z "${ASTERISK_BIN}" -o ! -x "${ASTERISK_BIN}" ] ; then
|
|
die -2 <<-EOF
|
|
The asterisk binary specified (${ASTERISK_BIN})
|
|
was not found or is not executable. Use the '--asterisk-bin'
|
|
option to specify a valid binary.
|
|
EOF
|
|
fi
|
|
|
|
if [ $EUID -ne 0 ] ; then
|
|
die -13 "You must be root to use $prog."
|
|
fi
|
|
|
|
if [ -z "${OUTPUTDIR}" -o ! -d "${OUTPUTDIR}" ] ; then
|
|
die -20 "OUTPUTDIR ${OUTPUTDIR} doesn't exists or is not a directory"
|
|
fi
|
|
|
|
if $RUNNING ; then
|
|
MAIN_PID=$(find_pid)
|
|
# If find_pid returns an error, the shell will automatically exit.
|
|
|
|
# We only want to process the coredump from the running process.
|
|
COREDUMPS=( )
|
|
|
|
msg "Found a single asterisk instance running as process $MAIN_PID"
|
|
|
|
if $PROMPT ; then
|
|
read -p "WARNING: Taking a core dump of the running asterisk instance will suspend call processing while the dump is saved. Do you wish to continue? (y/N) " answer
|
|
else
|
|
answer=Y
|
|
fi
|
|
|
|
if [[ "$answer" =~ ^[Yy] ]] ; then
|
|
df=$(date ${DATEOPTS})
|
|
cf="${OUTPUTDIR}/core-asterisk-running-$df"
|
|
echo "$(S_COR ${DRY_RUN} 'Simulating dumping' 'Dumping') running asterisk process to $cf"
|
|
if ${DRY_RUN} ; then
|
|
echo "Would run: ${GDB} ${ASTERISK_BIN} -p $MAIN_PID -q --batch --ex gcore $cf"
|
|
else
|
|
${GDB} ${ASTERISK_BIN} -p $MAIN_PID -q --batch --ex "gcore $cf" >/dev/null 2>&1
|
|
fi
|
|
echo "$(S_COR ${DRY_RUN} 'Simulated dump' 'Dump') is complete."
|
|
|
|
COREDUMPS=( "$cf" )
|
|
else
|
|
die -125 "Aborting dump of running process"
|
|
fi
|
|
|
|
$DRY_RUN && exit 0
|
|
else
|
|
|
|
# At this point, all glob entries that match files should be expanded.
|
|
# Any entries that don't exist are probably globs that didn't match anything
|
|
# and need to be pruned. Any non coredumps are also pruned.
|
|
|
|
for i in ${!COREDUMPS[@]} ; do
|
|
if [ ! -f "${COREDUMPS[$i]}" ] ; then
|
|
unset "COREDUMPS[$i]"
|
|
continue
|
|
fi
|
|
# Some versions of 'file' don't allow only the first n bytes of the
|
|
# file to be processed so we use dd to grab just the first 32 bytes.
|
|
mimetype=$(dd if="${COREDUMPS[$i]}" bs=32 count=1 2>/dev/null | file -bi -)
|
|
if [[ ! "$mimetype" =~ coredump ]] ; then
|
|
unset "COREDUMPS[$i]"
|
|
continue
|
|
fi
|
|
|
|
# Let's make sure it's an asterisk coredump by dumping the notes
|
|
# section of the file and grepping for "asterisk".
|
|
readelf -n "${COREDUMPS[$i]}" | grep -q "asterisk" || {
|
|
unset "COREDUMPS[$i]"
|
|
continue
|
|
}
|
|
|
|
done
|
|
|
|
if [ ${#COREDUMPS[@]} -eq 0 ] ; then
|
|
die -2 "No coredumps found"
|
|
fi
|
|
|
|
# Sort and weed out any dups
|
|
COREDUMPS=( $(ls -t "${COREDUMPS[@]}" 2>/dev/null | uniq ) )
|
|
|
|
if [ ${#COREDUMPS[@]} -eq 0 ] ; then
|
|
die -2 "No coredumps found"
|
|
fi
|
|
|
|
if $LATEST ; then
|
|
COREDUMPS=( "${COREDUMPS[0]}" )
|
|
fi
|
|
fi
|
|
|
|
|
|
if [ ${#COREDUMPS[@]} -eq 0 ] ; then
|
|
die -2 "No coredumps found"
|
|
fi
|
|
|
|
# Extract the gdb scripts from the end of this script
|
|
# and save them to /tmp/.gdbinit, then add a trap to
|
|
# clean it up.
|
|
gdbinit=${OUTPUTDIR}/.ast_coredumper.gdbinit
|
|
trap "rm $gdbinit" EXIT
|
|
ss=`egrep -n "^#@@@SCRIPTSTART@@@" $0 |cut -f1 -d:`
|
|
tail -n +${ss} $0 >$gdbinit
|
|
|
|
# Now iterate over the coredumps and dump the debugging info
|
|
for i in "${!COREDUMPS[@]}" ; do
|
|
cf=$(realpath -e ${COREDUMPS[$i]} || : )
|
|
if [ -z "$cf" ] ; then
|
|
continue
|
|
fi
|
|
echo "Processing $cf"
|
|
|
|
if ! $RUNNING && ! [[ "$cf" =~ "running" ]] && $RENAME ; then
|
|
df=$(date -r $cf ${DATEOPTS})
|
|
cfdir=$(dirname "$cf")
|
|
newcf="${cfdir}/core-asterisk-${df}"
|
|
if [ "${newcf}" != "${cf}" ] ; then
|
|
echo "Renaming $cf to $cfdir/core-asterisk-${df}"
|
|
mv "$cf" "${cfdir}/core-asterisk-${df}"
|
|
cf="${cfdir}/core-asterisk-${df}"
|
|
fi
|
|
fi
|
|
|
|
cfname=`basename ${cf}`
|
|
|
|
# Produce all the output files
|
|
${GDB} -n --batch -q --ex "source $gdbinit" "${ASTERISK_BIN}" "$cf" 2>/dev/null | (
|
|
of=/dev/null
|
|
while IFS= read line ; do
|
|
if [[ "$line" =~ !@!@!@!\ ([^\ ]+)\ !@!@!@! ]] ; then
|
|
of=${OUTPUTDIR}/${cfname}-${BASH_REMATCH[1]}
|
|
of=${of//:/-}
|
|
rm -f "$of"
|
|
echo "Creating $of"
|
|
fi
|
|
echo -e $"$line" >> "$of"
|
|
done
|
|
)
|
|
|
|
if $TARBALL_COREDUMPS ; then
|
|
# We need to change occurrences of ':' to '-' because
|
|
# Jira won't let you attach a file with colons in the name.
|
|
cfname=${cfname//:/-}
|
|
tf=${OUTPUTDIR}/${cfname}.tar.gz
|
|
echo "Creating ${tf}"
|
|
|
|
dest=${OUTPUTDIR}/${cfname}.output
|
|
rm -rf ${dest} 2>/dev/null || :
|
|
|
|
libdir=""
|
|
|
|
if [ -n "${LIBDIR}" ] ; then
|
|
LIBDIR=$(realpath "${LIBDIR}")
|
|
if [ ! -d "${LIBDIR}/asterisk/modules" ] ; then
|
|
die -2 <<-EOF
|
|
${LIBDIR}/asterisk/modules does not exist.
|
|
The library specified by --libdir or LIBDIR ${LIBDIR})
|
|
either does not exist or does not contain an "asterisk/modules" directory.
|
|
EOF
|
|
fi
|
|
libdir=${LIBDIR}
|
|
else
|
|
abits=$(file -b ${ASTERISK_BIN} | sed -n -r -e "s/.*(32|64)-bit.*/\1/p")
|
|
declare -a searchorder
|
|
if [ $abits -eq 32 ] ; then
|
|
searchorder=( /lib /usr/lib /usr/lib32 /usr/local/lib )
|
|
else
|
|
searchorder=( /usr/lib64 /usr/local/lib64 /usr/lib /usr/local/lib /lib )
|
|
fi
|
|
for d in ${searchorder[@]} ; do
|
|
testmod="${d}/asterisk/modules/bridge_simple.so"
|
|
if [ -e "${testmod}" ] ; then
|
|
lbits=$(file -b ${ASTERISK_BIN} | sed -n -r -e "s/.*(32|64)-bit.*/\1/p")
|
|
if [ $lbits -eq $abits ] ; then
|
|
libdir=$d
|
|
break;
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [ -z "${libdir}" ] ; then
|
|
die -2 <<-EOF
|
|
No standard systemlibrary directory contained asterisk modules.
|
|
Please specify the correct system library directory
|
|
with the --libdir option or the LIBDIR variable.
|
|
${LIBDIR}/asterisk/modules must exist.
|
|
EOF
|
|
fi
|
|
fi
|
|
|
|
mkdir -p ${dest}/tmp ${dest}/${libdir}/asterisk ${dest}/etc ${dest}/usr/sbin
|
|
|
|
ln -s ${cf} ${dest}/tmp/${cfname}
|
|
cp ${OUTPUTDIR}/${cfname}*.txt ${dest}/tmp/
|
|
[ -f /etc/os-release ] && cp /etc/os-release ${dest}/etc/
|
|
if $TARBALL_CONFIG ; then
|
|
cp -a /etc/asterisk ${dest}/etc/
|
|
fi
|
|
cp -a /${libdir}/libasterisk* ${dest}/${libdir}/
|
|
cp -a /${libdir}/asterisk/* ${dest}/${libdir}/asterisk/
|
|
cp -a /usr/sbin/asterisk ${dest}/usr/sbin
|
|
rm -rf ${tf}
|
|
tar -chzf ${tf} --transform="s/^[.]/${cfname}.output/" -C ${dest} .
|
|
sleep 3
|
|
rm -rf ${dest}
|
|
echo "Created $tf"
|
|
elif $TARBALL_RESULTS ; then
|
|
cfname=${cfname//:/-}
|
|
tf=${OUTPUTDIR}/${cfname}.tar.gz
|
|
echo "Creating ${tf}"
|
|
|
|
dest=${OUTPUTDIR}/${cfname}.output
|
|
rm -rf ${dest} 2>/dev/null || :
|
|
mkdir -p ${dest}
|
|
cp ${OUTPUTDIR}/${cfname}*.txt ${dest}/
|
|
if $TARBALL_CONFIG ; then
|
|
mkdir -p ${dest}/etc
|
|
cp -a /etc/asterisk ${dest}/etc/
|
|
fi
|
|
tar -chzf ${tf} --transform="s/^[.]/${cfname}/" -C ${dest} .
|
|
rm -rf ${dest}
|
|
echo "Created $tf"
|
|
fi
|
|
|
|
if $DELETE_COREDUMPS_AFTER ; then
|
|
rm -rf "${cf}"
|
|
fi
|
|
|
|
if $DELETE_RESULTS_AFTER ; then
|
|
to_delete=$cf
|
|
if [ -n "$OUTPUTDIR" ] ; then
|
|
to_delete="$OUTPUTDIR/$cfname"
|
|
fi
|
|
rm -rf "${to_delete//:/-}"-{brief,full,thread1,locks,info}.txt
|
|
fi
|
|
done
|
|
|
|
exit
|
|
# @formatter:off
|
|
|
|
#@@@FUNCSSTART@@@
|
|
print_help() {
|
|
sed -n -r -e "/^#@@@HELPSTART@@@/,\${p;/^#@@@HELPEND@@@/q}" $0 | sed '1d;$d'
|
|
exit 1
|
|
}
|
|
|
|
err() {
|
|
if [ -z "$1" ] ; then
|
|
cat >&2
|
|
else
|
|
echo "$1" >&2
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
msg() {
|
|
if [ -z "$1" ] ; then
|
|
cat
|
|
else
|
|
echo "$1"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
die() {
|
|
if [[ $1 =~ ^-([0-9]+) ]] ; then
|
|
RC=${BASH_REMATCH[1]}
|
|
shift
|
|
fi
|
|
err "$1"
|
|
exit ${RC:-1}
|
|
}
|
|
|
|
S_COR() {
|
|
if $1 ; then
|
|
echo -n "$2"
|
|
else
|
|
echo -n "$3"
|
|
fi
|
|
}
|
|
|
|
check_gdb() {
|
|
if [ -z "${GDB}" -o ! -x "${GDB}" ] ; then
|
|
die -2 <<-EOF
|
|
${GDB} seems to not be installed.
|
|
Please install gdb or use the '--gdb' option to
|
|
point to a valid executable.
|
|
EOF
|
|
fi
|
|
|
|
result=$($GDB --batch --ex "python print('hello')" 2>/dev/null || : )
|
|
if [[ ! "$result" =~ ^hello$ ]] ; then
|
|
die -2 <<-EOF
|
|
$GDB does not support python.
|
|
Use the '--gdb' option to point to one that does.
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
find_pid() {
|
|
if [ -n "$PID" ] ; then
|
|
# Make sure it's at least all numeric
|
|
[[ $PID =~ ^[0-9]+$ ]] || die -22 $"Pid $PID is invalid."
|
|
# Make sure it exists
|
|
cmd=$(ps -p $PID -o comm=) || die -22 "Pid $PID is not a valid process."
|
|
# Make sure the program (without path) is "asterisk"
|
|
[ "$cmd" == "asterisk" ] || die -22 "Pid $PID is '$cmd' not 'asterisk'."
|
|
echo $PID
|
|
return 0
|
|
fi
|
|
|
|
# Some versions of pgrep can't display the program arguments
|
|
# so we'll just get the pids that exactly match a program
|
|
# name of "asterisk".
|
|
pids=$( pgrep -d ',' -x "asterisk")
|
|
if [ -z ${pids} ] ; then
|
|
die -3 <<-EOF
|
|
No running asterisk instances detected.
|
|
If you know the pid of the process you want to dump,
|
|
supply it on the command line with --pid=<pid>.
|
|
EOF
|
|
fi
|
|
|
|
# Now that we have the pids, let's get the command and
|
|
# its args. We'll add them to an array indexed by pid.
|
|
declare -a candidates
|
|
while read LINE ; do
|
|
[[ $LINE =~ ([0-9]+)[\ ]+([^\ ]+)[\ ]+(.*) ]] || continue
|
|
pid=${BASH_REMATCH[1]}
|
|
prog=${BASH_REMATCH[2]}
|
|
args=${BASH_REMATCH[3]}
|
|
# If you run "asterisk -(rRx)", pgrep will find the process (which we
|
|
# really don't want) but thankfully, asterisk.c resets argv[0] to
|
|
# "rasterisk" so the output of ps will show that. This is an easy
|
|
# filter to weed out remote consoles.
|
|
[[ "$prog" == "rasterisk" ]] && continue;
|
|
candidates[$pid]="${prog}^${args}"
|
|
done < <(ps -o pid= -o command= -p $pids)
|
|
|
|
if [ ${#candidates[@]} -eq 0 ] ; then
|
|
die -3 <<-EOF
|
|
No running asterisk instances detected.
|
|
If you know the pid of the process you want to dump,
|
|
supply it on the command line with --pid=<pid>.
|
|
EOF
|
|
fi
|
|
|
|
if [ ${#candidates[@]} -gt 1 ] ; then
|
|
die -22 <<-EOF
|
|
Detected more than one asterisk process running.
|
|
$(printf "%8s %s\n" "PID" "COMMAND")
|
|
$(for p in ${!candidates[@]} ; do printf "%8s %s\n" $p "${candidates[$p]//^/ }" ; done )
|
|
If you know the pid of the process you want to dump,
|
|
supply it on the command line with --pid=<pid>.
|
|
EOF
|
|
fi
|
|
|
|
echo ${!candidates[@]}
|
|
return 0
|
|
}
|
|
#@@@FUNCSEND@@@
|
|
|
|
#@@@HELPSTART@@@
|
|
NAME
|
|
$prog - Dump and/or format asterisk coredump files
|
|
|
|
SYNOPSIS
|
|
$prog [ --help ] [ --running | --RUNNING ] [ --pid="pid" ]
|
|
[ --latest ] [ --OUTPUTDIR="path" ]
|
|
[ --libdir="path" ] [ --asterisk-bin="path" ]
|
|
[ --gdb="path" ] [ --rename ] [ --dateformat="date options" ]
|
|
[ --tarball-coredumps ] [ --delete-coredumps-after ]
|
|
[ --tarball-results ] [ --delete-results-after ]
|
|
[ --tarball-config ]
|
|
[ <coredump> | <pattern> ... ]
|
|
|
|
DESCRIPTION
|
|
|
|
Extracts backtraces and lock tables from Asterisk coredump files.
|
|
For each coredump found, 5 new result files are created:
|
|
- <coredump>-brief.txt: The output of "thread apply all bt".
|
|
|
|
- <coredump>-full.txt: The output of "thread apply all bt full".
|
|
|
|
- <coredump>-info.txt: State info like taskprocessors, channels, etc
|
|
|
|
- <coredump>-locks.txt: If asterisk was compiled with
|
|
"DEBUG_THREADS", this file will contain a dump of the locks
|
|
table similar to doing a "core show locks" from the asterisk
|
|
CLI.
|
|
|
|
- <coredump>-thread1.txt: The output of "thread apply 1 bt full".
|
|
|
|
Options:
|
|
|
|
--help
|
|
Print this help.
|
|
|
|
--running
|
|
Create a coredump from the running asterisk instance and
|
|
process it.
|
|
WARNING: This WILL interrupt call processing. You will be
|
|
asked to confirm.
|
|
|
|
--RUNNING
|
|
Same as --running but without the confirmation prompt.
|
|
DANGEROUS!!
|
|
|
|
--pid=<asterisk main process pid>
|
|
If you are trying to get a dump of the running asterisk
|
|
instance, specifying its pid on the command line will
|
|
bypass the complex logic used to figure it out.
|
|
|
|
--latest
|
|
Process only the latest coredump from those specified (based
|
|
on last-modified time). Only needed when --running was not
|
|
specified and there is more that one coredump matched.
|
|
|
|
--outputdir=<output directory>
|
|
The directory into which output products will be saved.
|
|
Default: same directory as coredump
|
|
|
|
--libdir=<shared libs directory>
|
|
The directory where the libasterisk* shared libraries and
|
|
the asterisk/modules directory are located. The common
|
|
directories like /usr/lib, /usr/lib64, etc are automatically
|
|
searches so only use this option when your asterisk install
|
|
is non-standard.
|
|
|
|
--asterisk-bin=<asterisk binary>
|
|
Path to the asterisk binary.
|
|
Default: look for asterisk in the PATH.
|
|
|
|
--gdb=<path_to_gdb>
|
|
gdb must have python support built-in. Most do.
|
|
Default: /usr/bin/gdb
|
|
|
|
--dateformat=<date options>
|
|
Passed to the 'date' utility to construct dates.
|
|
The default is '-u +%FT%H-%M-%SZ' which results
|
|
in a UTC timestamp.
|
|
|
|
--rename
|
|
Causes the coredump to be renamed using DATEOPTS
|
|
and the output files to be named accordingly.
|
|
This is the default. To disable renaming, specify
|
|
--no-rename
|
|
|
|
--tarball-coredumps
|
|
Creates a gzipped tarball of each coredump processed, their
|
|
results txt files, a copy of /etc/os-release, the
|
|
asterisk binary, and all modules.
|
|
The file will be named like the coredump with '.tar.gz'
|
|
appended.
|
|
WARNING: This file could be quite large!
|
|
Mutually exclusive with --tarball-results
|
|
|
|
--delete-coredumps-after
|
|
Deletes all processed coredumps regardless of whether
|
|
a tarball was created.
|
|
|
|
--tarball-results
|
|
Creates a gzipped tarball of all result files produced.
|
|
The tarball name will be:
|
|
$OUTPUTDIR/asterisk.<timestamp>.results.tar.gz
|
|
Mutually exclusive with --tarball-coredumps
|
|
|
|
--delete-results-after
|
|
Deletes all processed results regardless of whether
|
|
a tarball was created. It probably does not make sense
|
|
to use this option unless you have also specified
|
|
--tarball-results.
|
|
|
|
--tarball-config
|
|
Adds the contents of /etc/asterisk to the tarball created
|
|
with --tarball-coredumps or --tarball-results.
|
|
|
|
<coredump> | <pattern>
|
|
A list of coredumps or coredump search patterns. These
|
|
will override the default and those specified in the config files.
|
|
|
|
The default pattern is "/tmp/core!(*.txt)"
|
|
|
|
The "!(*.txt)" tells bash to ignore any files that match
|
|
the base pattern and end in ".txt". It$'s not strictly
|
|
needed as non asterisk coredumps are always ignored.
|
|
|
|
NOTES
|
|
You must be root to use this program.
|
|
|
|
All options except "running", "RUNNING" and "pid" can be
|
|
specified in the ast_debug_tools.conf file.
|
|
Option names must be translated to upper case and their '-'
|
|
characters replaced by '_'. Boolean options must be set to
|
|
'true' or 'false' (lower case, without the quotes).
|
|
Examples:
|
|
TARBALL_RESULTS=true
|
|
RENAME=false
|
|
ASTERISK_BIN=/usr/sbin/asterisk
|
|
|
|
The script relies on not only bash, but also recent GNU date and
|
|
gdb with python support. *BSD operating systems may require
|
|
installation of the 'coreutils' and 'devel/gdb' packages and minor
|
|
tweaking of the ast_debug_tools.conf file.
|
|
|
|
Any files output will have ':' characters changed to '-'. This is
|
|
to facilitate uploading those files to Jira which doesn't like the
|
|
colons.
|
|
|
|
FILES
|
|
/etc/asterisk/ast_debug_tools.conf
|
|
~/ast_debug_tools.conf
|
|
./ast_debug_tools.conf
|
|
|
|
See the configs/samples/ast_debug_tools.conf file in the asterisk
|
|
source tree for more info.
|
|
|
|
#@@@HELPEND@@@
|
|
|
|
# Be careful editing the inline scripts.
|
|
# They're space-indented.
|
|
|
|
# We need the python bit because lock_infos isn't
|
|
# a valid symbol in asterisk unless DEBUG_THREADS was
|
|
# used during the compile. Also, interrupt and continue
|
|
# are only valid for a running program.
|
|
|
|
#@@@SCRIPTSTART@@@
|
|
python
|
|
|
|
import datetime
|
|
|
|
|
|
def timeval_to_datetime(value):
|
|
"""Convert a timeval struct to a python datetime object
|
|
|
|
Args:
|
|
value: A gdb Value representing a C timeval
|
|
|
|
Return:
|
|
A python datetime object
|
|
"""
|
|
|
|
sec = int(value['tv_sec'])
|
|
usec = int(value['tv_usec'])
|
|
|
|
return datetime.datetime.fromtimestamp(sec + usec / float(1000000))
|
|
|
|
|
|
def s_strip(value):
|
|
"""Convert the given value to a string, and strip any leading/trailing
|
|
spaces and/or quotes.
|
|
|
|
Args:
|
|
name: The gdb Value to convert and strip
|
|
|
|
Return:
|
|
The stripped value as a string
|
|
"""
|
|
|
|
if value == None:
|
|
return "None"
|
|
|
|
try:
|
|
if 'char *' not in str(value.type) and 'char [' not in str(value.type):
|
|
# Use the string method for everything but string pointers (only
|
|
# points to first letter) and non-string values in general
|
|
return value.string().strip('" ') or "<None>"
|
|
except:
|
|
pass
|
|
|
|
return str(value).strip('" ') or "<None>"
|
|
|
|
|
|
def get(name):
|
|
"""Retrieve a named variable's value as a string using GDB.
|
|
|
|
Args:
|
|
name: The name of the variable to look up
|
|
|
|
Return:
|
|
The variable's value as a string
|
|
"""
|
|
|
|
return s_strip(gdb.parse_and_eval(name))
|
|
|
|
|
|
def get_container_hash_objects(name, type, on_object=None):
|
|
"""Retrieve a list of objects from an ao2_container_hash.
|
|
|
|
Expected on_object signature:
|
|
|
|
res, stop = on_object(GDB Value)
|
|
|
|
The given callback, on_object, is called for each object found in the
|
|
container. The callback is passed a dereferenced GDB Value object and
|
|
expects an object to be returned, which is then appended to a list of
|
|
objects to be returned by this function. Iteration can be stopped by
|
|
returning "True" for the second return value.
|
|
|
|
If on_object is not specified then the dereferenced GDB value is instead
|
|
added directly to the returned list.
|
|
|
|
Args:
|
|
name: The name of the ao2_container
|
|
type: The type of objects stored in the container
|
|
on_object: Optional function called on each object found
|
|
|
|
Return:
|
|
A list of container objects
|
|
"""
|
|
|
|
objs = []
|
|
|
|
try:
|
|
|
|
container = gdb.parse_and_eval(name).cast(
|
|
gdb.lookup_type('struct ao2_container_hash').pointer())
|
|
|
|
# Loop over every bucket searching for hash bucket nodes
|
|
for n in range(container['n_buckets']):
|
|
node = container['buckets'][n]['list']['last']
|
|
while node:
|
|
# Each node holds the needed object
|
|
obj = node.dereference()['common']['obj'].cast(
|
|
gdb.lookup_type(type).pointer()).dereference()
|
|
|
|
res, stop = on_object(obj) if on_object else (obj, False)
|
|
|
|
if res:
|
|
objs.append(res)
|
|
|
|
if stop:
|
|
return objs
|
|
|
|
node = node.dereference()['links']['last']
|
|
except Exception as e:
|
|
print("{0} - {1}".format(name, e))
|
|
pass
|
|
|
|
return objs
|
|
|
|
|
|
def get_container_rbtree_objects(name, type, on_object=None):
|
|
"""Retrieve a list of objects from an ao2_container_rbtree.
|
|
|
|
Expected on_object signature:
|
|
|
|
res, stop = on_object(GDB Value)
|
|
|
|
The given callback, on_object, is called for each object found in the
|
|
container. The callback is passed a dereferenced GDB Value object and
|
|
expects an object to be returned, which is then appended to a list of
|
|
objects to be returned by this function. Iteration can be stopped by
|
|
returning "True" for the second return value.
|
|
|
|
If on_object is not specified then the dereferenced GDB value is instead
|
|
added directly to the returned list.
|
|
|
|
Args:
|
|
name: The name of the ao2_container
|
|
type: The type of objects stored in the container
|
|
on_object: Optional function called on each object found
|
|
|
|
Return:
|
|
A list of container objects
|
|
"""
|
|
|
|
objs = []
|
|
|
|
def handle_node(node):
|
|
|
|
if not node:
|
|
return True
|
|
|
|
# Each node holds the needed object
|
|
obj = node.dereference()['common']['obj'].cast(
|
|
gdb.lookup_type(type).pointer()).dereference()
|
|
|
|
res, stop = on_object(obj) if on_object else (obj, False)
|
|
|
|
if res:
|
|
objs.append(res)
|
|
|
|
return not stop and (handle_node(node['left']) and
|
|
handle_node(node['right']))
|
|
|
|
try:
|
|
container = gdb.parse_and_eval(name).cast(
|
|
gdb.lookup_type('struct ao2_container_rbtree').pointer())
|
|
|
|
handle_node(container['root'])
|
|
except Exception as e:
|
|
print("{0} - {1}".format(name, e))
|
|
pass
|
|
|
|
return objs
|
|
|
|
|
|
def build_info():
|
|
|
|
try:
|
|
return ("Asterisk {0} built by {1} @ {2} on a {3} running {4} on {5}"
|
|
.format(get("asterisk_version"),
|
|
get("ast_build_user"),
|
|
get("ast_build_hostname"),
|
|
get("ast_build_machine"),
|
|
get("ast_build_os"),
|
|
get("ast_build_date")))
|
|
except:
|
|
return "Unable to retrieve build info"
|
|
|
|
|
|
def build_opts():
|
|
|
|
try:
|
|
return get("asterisk_build_opts")
|
|
except:
|
|
return "Unable to retrieve build options"
|
|
|
|
|
|
def uptime():
|
|
|
|
try:
|
|
started = timeval_to_datetime(gdb.parse_and_eval("ast_startuptime"))
|
|
loaded = timeval_to_datetime(gdb.parse_and_eval("ast_lastreloadtime"))
|
|
|
|
return ("System started: {0}\n"
|
|
"Last reload: {1}".format(started, loaded))
|
|
except:
|
|
return "Unable to retrieve uptime"
|
|
|
|
|
|
class TaskProcessor(object):
|
|
|
|
template = ("{name:70} {processed:>10} {in_queue:>10} {max_depth:>10} "
|
|
"{low_water:>10} {high_water:>10}")
|
|
|
|
header = {'name': 'Processor', 'processed': 'Processed',
|
|
'in_queue': 'In Queue', 'max_depth': 'Max Depth',
|
|
'low_water': 'Low water', 'high_water': 'High water'}
|
|
|
|
@staticmethod
|
|
def objects():
|
|
|
|
try:
|
|
objs = get_container_hash_objects('tps_singletons',
|
|
'struct ast_taskprocessor', TaskProcessor.from_value)
|
|
|
|
objs.sort(key=lambda x: x.name.lower())
|
|
except Exception as e:
|
|
return []
|
|
|
|
return objs
|
|
|
|
@staticmethod
|
|
def from_value(value):
|
|
|
|
return TaskProcessor(
|
|
value['name'],
|
|
value['stats']['_tasks_processed_count'],
|
|
value['tps_queue_size'],
|
|
value['stats']['max_qsize'],
|
|
value['tps_queue_low'],
|
|
value['tps_queue_high']), False
|
|
|
|
def __init__(self, name, processed, in_queue, max_depth,
|
|
low_water, high_water):
|
|
|
|
self.name = s_strip(name)
|
|
self.processed = int(processed)
|
|
self.in_queue = int(in_queue)
|
|
self.max_depth = int(max_depth)
|
|
self.low_water = int(low_water)
|
|
self.high_water = int(high_water)
|
|
|
|
|
|
class Channel(object):
|
|
|
|
template = ("{name:30} {context:>20} {exten:>20} {priority:>10} {state:>25} "
|
|
"{app:>20} {data:>30} {caller_id:>15} {created:>30} "
|
|
"{account_code:>15} {peer_account:>15} {bridge_id:>38}")
|
|
|
|
header = {'name': 'Channel', 'context': 'Context', 'exten': 'Extension',
|
|
'priority': 'Priority', 'state': "State", 'app': 'Application',
|
|
'data': 'Data', 'caller_id': 'CallerID', 'created': 'Created',
|
|
'account_code': 'Accountcode', 'peer_account': 'PeerAccount',
|
|
'bridge_id': 'BridgeID'}
|
|
|
|
@staticmethod
|
|
def objects():
|
|
|
|
try:
|
|
objs = get_container_hash_objects('channels',
|
|
'struct ast_channel', Channel.from_value)
|
|
|
|
objs.sort(key=lambda x: x.name.lower())
|
|
except:
|
|
return []
|
|
|
|
return objs
|
|
|
|
@staticmethod
|
|
def from_value(value):
|
|
|
|
bridge_id = None
|
|
if value['bridge']:
|
|
bridge_id = value['bridge']['uniqueid']
|
|
|
|
return Channel(
|
|
value['name'],
|
|
value['context'],
|
|
value['exten'],
|
|
value['priority'],
|
|
value['state'],
|
|
value['appl'],
|
|
value['data'],
|
|
value['caller']['id']['number']['str'],
|
|
timeval_to_datetime(value['creationtime']),
|
|
value['accountcode'],
|
|
value['peeraccount'],
|
|
bridge_id), False
|
|
|
|
@staticmethod
|
|
def summary():
|
|
|
|
try:
|
|
return ("{0} active channels\n"
|
|
"{1} active calls\n"
|
|
"{2} calls processed".format(
|
|
int(gdb.parse_and_eval(
|
|
'channels').dereference()['elements']),
|
|
get("countcalls"),
|
|
get("totalcalls")))
|
|
except:
|
|
return "Unable to retrieve channel summary"
|
|
|
|
def __init__(self, name, context=None, exten=None, priority=None,
|
|
state=None, app=None, data=None, caller_id=None,
|
|
created=None, account_code=None, peer_account=None,
|
|
bridge_id=None):
|
|
|
|
self.name = s_strip(name)
|
|
self.context = s_strip(context)
|
|
self.exten = s_strip(exten)
|
|
self.priority = int(priority)
|
|
self.state = s_strip(state)
|
|
self.app = s_strip(app)
|
|
self.data = s_strip(data)
|
|
self.caller_id = s_strip(caller_id)
|
|
self.created = s_strip(created)
|
|
self.account_code = s_strip(account_code)
|
|
self.peer_account = s_strip(peer_account)
|
|
self.bridge_id = s_strip(bridge_id)
|
|
|
|
|
|
class Bridge(object):
|
|
|
|
template = ("{uniqueid:38} {num_channels:>15} {subclass:>10} {tech:>20} "
|
|
"{created:>30}")
|
|
|
|
header = {'uniqueid': 'Bridge-ID', 'num_channels': 'Chans',
|
|
'subclass': 'Type', 'tech': 'Technology', 'created': 'Created'}
|
|
|
|
@staticmethod
|
|
def objects():
|
|
|
|
try:
|
|
objs = get_container_rbtree_objects('bridges',
|
|
'struct ast_bridge', Bridge.from_value)
|
|
|
|
objs.sort(key=lambda x: x.uniqueid.lower())
|
|
except:
|
|
return []
|
|
|
|
return objs
|
|
|
|
@staticmethod
|
|
def from_value(value):
|
|
|
|
return Bridge(
|
|
value['uniqueid'],
|
|
value['num_channels'],
|
|
timeval_to_datetime(value['creationtime']),
|
|
value['v_table']['name'],
|
|
value['technology']['name']), False
|
|
|
|
|
|
def __init__(self, uniqueid, num_channels=None, created=None, subclass=None,
|
|
tech=None):
|
|
|
|
self.uniqueid = s_strip(uniqueid)
|
|
self.num_channels = int(num_channels)
|
|
self.created = s_strip(created)
|
|
self.subclass = s_strip(subclass)
|
|
self.tech = s_strip(tech)
|
|
|
|
|
|
class DumpAsteriskCommand(gdb.Command):
|
|
|
|
def __init__(self):
|
|
super(DumpAsteriskCommand, self).__init__ ("dump-asterisk",
|
|
gdb.COMMAND_OBSCURE, gdb.COMPLETE_COMMAND)
|
|
|
|
def print_table(self, type):
|
|
|
|
plural = "{0}s".format(type.__name__)
|
|
|
|
objs = type.objects()
|
|
|
|
if not len(objs):
|
|
print("{0} not found\n".format(plural))
|
|
return
|
|
|
|
print("{0} ({1}):\n".format(plural, len(objs)))
|
|
|
|
print(type.template.format(**type.header))
|
|
|
|
for obj in objs:
|
|
print(type.template.format(**vars(obj)))
|
|
|
|
print("\n")
|
|
|
|
def invoke(self, arg, from_tty):
|
|
try:
|
|
gdb.execute("interrupt", from_tty)
|
|
except:
|
|
pass
|
|
print("!@!@!@! thread1.txt !@!@!@!\n")
|
|
try:
|
|
gdb.execute("p $_siginfo", from_tty)
|
|
gdb.execute("info signal $_siginfo.si_signo")
|
|
except:
|
|
pass
|
|
try:
|
|
gdb.execute("thread apply 1 bt full", from_tty)
|
|
except:
|
|
pass
|
|
print("!@!@!@! brief.txt !@!@!@!\n")
|
|
try:
|
|
gdb.execute("p $_siginfo", from_tty)
|
|
gdb.execute("info signal $_siginfo.si_signo")
|
|
except:
|
|
pass
|
|
try:
|
|
gdb.execute("thread apply all bt", from_tty)
|
|
except:
|
|
pass
|
|
print("!@!@!@! full.txt !@!@!@!\n")
|
|
try:
|
|
gdb.execute("p $_siginfo", from_tty)
|
|
gdb.execute("info signal $_siginfo.si_signo")
|
|
except:
|
|
pass
|
|
try:
|
|
gdb.execute("thread apply all bt full", from_tty)
|
|
except:
|
|
pass
|
|
print("!@!@!@! locks.txt !@!@!@!\n")
|
|
try:
|
|
gdb.execute("p $_siginfo", from_tty)
|
|
gdb.execute("info signal $_siginfo.si_signo")
|
|
except:
|
|
pass
|
|
try:
|
|
gdb.execute("show_locks", from_tty)
|
|
except:
|
|
pass
|
|
|
|
print("!@!@!@! info.txt !@!@!@!\n")
|
|
|
|
gdb.execute('set print addr off')
|
|
|
|
try:
|
|
print("{0}\n".format(build_info()))
|
|
print("{0}\n".format(uptime()))
|
|
print("Build options = {0}\n".format(build_opts()))
|
|
|
|
self.print_table(TaskProcessor)
|
|
self.print_table(Bridge)
|
|
self.print_table(Channel)
|
|
|
|
print(Channel.summary())
|
|
except:
|
|
pass
|
|
finally:
|
|
gdb.execute('set print addr on')
|
|
|
|
try:
|
|
gdb.execute("continue", from_tty)
|
|
except:
|
|
pass
|
|
|
|
DumpAsteriskCommand ()
|
|
end
|
|
|
|
define show_locks
|
|
set $n = lock_infos.first
|
|
|
|
if $argc == 0
|
|
printf " where_held count-|\n"
|
|
printf " suspended-| |\n"
|
|
printf " type- | times locked-| | |\n"
|
|
printf "thread status file line function lock name | lock addr | | |\n"
|
|
else
|
|
printf "thread,status,file,line,function,lock_name,lock_type,lock_addr,times_locked,suspended,where_held_count,where_held_file,where_held_line,where_held_function,there_held_thread\n"
|
|
end
|
|
|
|
while $n
|
|
if $n->num_locks > 0
|
|
set $i = 0
|
|
while $i < $n->num_locks
|
|
if $n->locks[$i]->suspended == 0
|
|
if ((ast_mutex_t *)$n->locks[$i]->lock_addr)->tracking
|
|
if $n->locks[$i]->type > 0
|
|
set $track = ((ast_rwlock_t *)$n->locks[$i]->lock_addr)->track
|
|
else
|
|
set $track = ((ast_mutex_t *)$n->locks[$i]->lock_addr)->track
|
|
end
|
|
end
|
|
set $reentrancy = $track->reentrancy
|
|
set $pending = $n->locks[$i]->pending
|
|
if $argc > 0
|
|
printf "%p,%d,%s,%d,%s,%s,%d,%p,%d,%d,%d",\
|
|
$n->thread_id, $n->locks[$i]->pending, $n->locks[$i]->file, $n->locks[$i]->line_num, $n->locks[$i]->func,\
|
|
$n->locks[$i]->lock_name, $n->locks[$i]->type, $n->locks[$i]->lock_addr, $n->locks[$i]->times_locked,\
|
|
$n->locks[$i]->suspended, $track->reentrancy
|
|
if $reentrancy
|
|
if $pending
|
|
printf ",%s,%d,%s,%p", $track->file[0], $track->lineno[0], $track->func[0], $track->thread[0]
|
|
end
|
|
end
|
|
else
|
|
if $n->locks[$i]->pending < 0
|
|
printf "%p failed %-20s %6d %-36s %-20s %d %14p %3d %d %d",\
|
|
$n->thread_id,\
|
|
$n->locks[$i]->file, $n->locks[$i]->line_num, $n->locks[$i]->func,\
|
|
$n->locks[$i]->lock_name, $n->locks[$i]->type, $n->locks[$i]->lock_addr, $n->locks[$i]->times_locked,\
|
|
$n->locks[$i]->suspended, $track->reentrancy
|
|
end
|
|
if $n->locks[$i]->pending == 0
|
|
printf "%p holding %-20s %6d %-36s %-20s %d %14p %3d %d %d",\
|
|
$n->thread_id,\
|
|
$n->locks[$i]->file, $n->locks[$i]->line_num, $n->locks[$i]->func,\
|
|
$n->locks[$i]->lock_name, $n->locks[$i]->type, $n->locks[$i]->lock_addr, $n->locks[$i]->times_locked,\
|
|
$n->locks[$i]->suspended, $track->reentrancy
|
|
end
|
|
if $n->locks[$i]->pending > 0
|
|
printf "%p waiting %-20s %6d %-36s %-20s %d %14p %3d %d %d",\
|
|
$n->thread_id,\
|
|
$n->locks[$i]->file, $n->locks[$i]->line_num, $n->locks[$i]->func,\
|
|
$n->locks[$i]->lock_name, $n->locks[$i]->type, $n->locks[$i]->lock_addr, $n->locks[$i]->times_locked,\
|
|
$n->locks[$i]->suspended, $track->reentrancy
|
|
end
|
|
if $reentrancy
|
|
if $pending
|
|
printf "\n held at: %-20s %6d %-36s by 0x%08lx", $track->file[0], $track->lineno[0], $track->func[0], $track->thread_id[0]
|
|
end
|
|
end
|
|
end
|
|
printf "\n"
|
|
end
|
|
set $i = $i + 1
|
|
end
|
|
end
|
|
set $n = $n->entry->next
|
|
end
|
|
end
|
|
|
|
dump-asterisk
|