#!/bin/bash
#
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#

# A generic script used to attach to a running Chromium process and
# debug it. Most users should not use this directly, but one of the
# wrapper scripts like adb_gdb_content_shell
#
# Use --help to print full usage instructions.
#

PROGNAME=$(basename "$0")
PROGDIR=$(dirname "$0")

# Location of Chromium-top-level sources.
CHROMIUM_SRC=$(cd "$PROGDIR"/../.. >/dev/null && pwd 2>/dev/null)

# Location of Chromium out/ directory.
if [ -z "$CHROMIUM_OUT_DIR" ]; then
  CHROMIUM_OUT_DIR=out
fi

TMPDIR=
GDBSERVER_PIDFILE=
TARGET_GDBSERVER=
COMMAND_PREFIX=

clean_exit () {
  if [ "$TMPDIR" ]; then
    GDBSERVER_PID=$(cat $GDBSERVER_PIDFILE 2>/dev/null)
    if [ "$GDBSERVER_PID" ]; then
      log "Killing background gdbserver process: $GDBSERVER_PID"
      kill -9 $GDBSERVER_PID >/dev/null 2>&1
    fi
    if [ "$TARGET_GDBSERVER" ]; then
      log "Removing target gdbserver binary: $TARGET_GDBSERVER."
      "$ADB" shell "$COMMAND_PREFIX" rm "$TARGET_GDBSERVER" >/dev/null 2>&1
    fi
    log "Cleaning up: $TMPDIR"
    rm -rf "$TMPDIR"
  fi
  trap "" EXIT
  exit $1
}

# Ensure clean exit on Ctrl-C or normal exit.
trap "clean_exit 1" INT HUP QUIT TERM
trap "clean_exit \$?" EXIT

panic () {
  echo "ERROR: $@" >&2
  exit 1
}

fail_panic () {
  if [ $? != 0 ]; then panic "$@"; fi
}

log () {
  if [ "$VERBOSE" -gt 0 ]; then
    echo "$@"
  fi
}

DEFAULT_PULL_LIBS_DIR=/tmp/$USER-adb-gdb-libs

# NOTE: Allow wrapper scripts to set various default through ADB_GDB_XXX
# environment variables. This is only for cosmetic reasons, i.e. to
# display proper

# Allow wrapper scripts to set the default activity through
# the ADB_GDB_ACTIVITY variable. Users are still able to change the
# final activity name through --activity=<name> option.
#
# This is only for cosmetic reasons, i.e. to display the proper default
# in the --help output.
#
DEFAULT_ACTIVITY=${ADB_GDB_ACTIVITY:-".Main"}

# Allow wrapper scripts to set the program name through ADB_GDB_PROGNAME
PROGNAME=${ADB_GDB_PROGNAME:-$(basename "$0")}

ACTIVITY=$DEFAULT_ACTIVITY
ADB=
ANNOTATE=
# Note: Ignore BUILDTYPE variable, because the Ninja build doesn't use it.
BUILDTYPE=
FORCE=
GDBEXEPOSTFIX=gdb
GDBINIT=
GDBSERVER=
HELP=
NDK_DIR=
NO_PULL_LIBS=
PACKAGE_NAME=
PID=
PORT=
PRIVILEGED=
PRIVILEGED_INDEX=
PROGRAM_NAME="activity"
PULL_LIBS=
PULL_LIBS_DIR=
SANDBOXED=
SANDBOXED_INDEX=
START=
SU_PREFIX=
SYMBOL_DIR=
TARGET_ARCH=
TOOLCHAIN=
VERBOSE=0

for opt; do
  optarg=$(expr "x$opt" : 'x[^=]*=\(.*\)')
  case $opt in
    --adb=*)
      ADB=$optarg
      ;;
    --activity=*)
      ACTIVITY=$optarg
      ;;
    --annotate=3)
      ANNOTATE=$optarg
      ;;
    --force)
      FORCE=true
      ;;
    --gdbserver=*)
      GDBSERVER=$optarg
      ;;
    --gdb=*)
      GDB=$optarg
      ;;
    --help|-h|-?)
      HELP=true
      ;;
    --ndk-dir=*)
      NDK_DIR=$optarg
      ;;
    --no-pull-libs)
      NO_PULL_LIBS=true
      ;;
    --package-name=*)
      PACKAGE_NAME=$optarg
      ;;
    --pid=*)
      PID=$optarg
      ;;
    --port=*)
      PORT=$optarg
      ;;
    --privileged)
      PRIVILEGED=true
      ;;
    --privileged=*)
      PRIVILEGED=true
      PRIVILEGED_INDEX=$optarg
      ;;
    --program-name=*)
      PROGRAM_NAME=$optarg
      ;;
    --pull-libs)
      PULL_LIBS=true
      ;;
    --pull-libs-dir=*)
      PULL_LIBS_DIR=$optarg
      ;;
    --sandboxed)
      SANDBOXED=true
      ;;
    --sandboxed=*)
      SANDBOXED=true
      SANDBOXED_INDEX=$optarg
      ;;
    --script=*)
      GDBINIT=$optarg
      ;;
    --start)
      START=true
      ;;
    --su-prefix=*)
      SU_PREFIX=$optarg
      ;;
    --symbol-dir=*)
      SYMBOL_DIR=$optarg
      ;;
    --out-dir=*)
      CHROMIUM_OUT_DIR=$optarg
      ;;
    --target-arch=*)
      TARGET_ARCH=$optarg
      ;;
    --toolchain=*)
      TOOLCHAIN=$optarg
      ;;
    --ui)
      GDBEXEPOSTFIX=gdbtui
      ;;
    --verbose)
      VERBOSE=$(( $VERBOSE + 1 ))
      ;;
    --debug)
      BUILDTYPE=Debug
      ;;
    --release)
      BUILDTYPE=Release
      ;;
    -*)
      panic "Unknown option $OPT, see --help." >&2
      ;;
    *)
      if [ "$PACKAGE_NAME" ]; then
        panic "You can only provide a single package name as argument!\
 See --help."
      fi
      PACKAGE_NAME=$opt
      ;;
  esac
done

print_help_options () {
  cat <<EOF
EOF
}

if [ "$HELP" ]; then
  if [ "$ADB_GDB_PROGNAME" ]; then
    # Assume wrapper scripts all provide a default package name.
    cat <<EOF
Usage: $PROGNAME [options]

Attach gdb to a running Android $PROGRAM_NAME process.
EOF
  else
    # Assume this is a direct call to adb_gdb
  cat <<EOF
Usage: $PROGNAME [options] [<package-name>]

Attach gdb to a running Android $PROGRAM_NAME process.

If provided, <package-name> must be the name of the Android application's
package name to be debugged. You can also use --package-name=<name> to
specify it.
EOF
  fi

  cat <<EOF

This script is used to debug a running $PROGRAM_NAME process.
This can be a regular Android application process, sandboxed (if you use the
--sandboxed or --sandboxed=<num> option) or a privileged (--privileged or
--privileged=<num>) service.

This script needs several things to work properly. It will try to pick
them up automatically for you though:

   - target gdbserver binary
   - host gdb client (e.g. arm-linux-androideabi-gdb)
   - directory with symbolic version of $PROGRAM_NAME's shared libraries.

You can also use --ndk-dir=<path> to specify an alternative NDK installation
directory.

The script tries to find the most recent version of the debug version of
shared libraries under one of the following directories:

  \$CHROMIUM_SRC/<out>/Release/lib/           (used by Ninja builds)
  \$CHROMIUM_SRC/<out>/Debug/lib/             (used by Ninja builds)
  \$CHROMIUM_SRC/<out>/Release/lib.target/    (used by Make builds)
  \$CHROMIUM_SRC/<out>/Debug/lib.target/      (used by Make builds)

Where <out> is 'out' by default, unless the --out=<name> option is used or
the CHROMIUM_OUT_DIR environment variable is defined.

You can restrict this search by using --release or --debug to specify the
build type, or simply use --symbol-dir=<path> to specify the file manually.

The script tries to extract the target architecture from your target device,
but if this fails, will default to 'arm'. Use --target-arch=<name> to force
its value.

Otherwise, the script will complain, but you can use the --gdbserver,
--gdb and --symbol-lib options to specify everything manually.

An alternative to --gdb=<file> is to use --toollchain=<path> to specify
the path to the host target-specific cross-toolchain.

You will also need the 'adb' tool in your path. Otherwise, use the --adb
option. The script will complain if there is more than one device connected
and ANDROID_SERIAL is not defined.

The first time you use it on a device, the script will pull many system
libraries required by the process into a temporary directory. This
is done to strongly improve the debugging experience, like allowing
readable thread stacks and more. The libraries are copied to the following
directory by default:

  $DEFAULT_PULL_LIBS_DIR/

But you can use the --pull-libs-dir=<path> option to specify an
alternative. The script can detect when you change the connected device,
and will re-pull the libraries only in this case. You can however force it
with the --pull-libs option.

Any local .gdbinit script will be ignored, but it is possible to pass a
gdb command script with the --script=<file> option. Note that its commands
will be passed to gdb after the remote connection and library symbol
loading have completed.

Valid options:
  --help|-h|-?          Print this message.
  --verbose             Increase verbosity.

  --sandboxed           Debug first sandboxed process we find.
  --sandboxed=<num>     Debug specific sandboxed process.
  --symbol-dir=<path>   Specify directory with symbol shared libraries.
  --out-dir=<path>      Specify the out directory.
  --package-name=<name> Specify package name (alternative to 1st argument).
  --privileged          Debug first privileged process we find.
  --privileged=<num>    Debug specific privileged process.
  --program-name=<name> Specify program name (cosmetic only).
  --pid=<pid>           Specify application process pid.
  --force               Kill any previous debugging session, if any.
  --start               Start package's activity on device.
  --ui                  Use gdbtui instead of gdb
  --activity=<name>     Activity name for --start [$DEFAULT_ACTIVITY].
  --annotate=<num>      Enable gdb annotation.
  --script=<file>       Specify extra GDB init script.

  --gdbserver=<file>    Specify target gdbserver binary.
  --gdb=<file>          Specify host gdb client binary.
  --target-arch=<name>  Specify NDK target arch.
  --adb=<file>          Specify host ADB binary.
  --port=<port>         Specify the tcp port to use.

  --su-prefix=<prefix>  Prepend <prefix> to 'adb shell' commands that are
                        run by this script. This can be useful to use
                        the 'su' program on rooted production devices.
                        e.g. --su-prefix="su -c"

  --pull-libs           Force system libraries extraction.
  --no-pull-libs        Do not extract any system library.
  --libs-dir=<path>     Specify system libraries extraction directory.

  --debug               Use libraries under out/Debug.
  --release             Use libraries under out/Release.

EOF
  exit 0
fi

if [ -z "$PACKAGE_NAME" ]; then
  panic "Please specify a package name on the command line. See --help."
fi

if [ -z "$NDK_DIR" ]; then
  ANDROID_NDK_ROOT=$(PYTHONPATH=$CHROMIUM_SRC/build/android python -c \
'from pylib.constants import ANDROID_NDK_ROOT; print ANDROID_NDK_ROOT,')
else
  if [ ! -d "$NDK_DIR" ]; then
    panic "Invalid directory: $NDK_DIR"
  fi
  if [ ! -f "$NDK_DIR/ndk-build" ]; then
    panic "Not a valid NDK directory: $NDK_DIR"
  fi
  ANDROID_NDK_ROOT=$NDK_DIR
fi

if [ "$GDBINIT" -a ! -f "$GDBINIT" ]; then
  panic "Unknown --script file: $GDBINIT"
fi

# Check that ADB is in our path
if [ -z "$ADB" ]; then
  ADB=$(which adb 2>/dev/null)
  if [ -z "$ADB" ]; then
    panic "Can't find 'adb' tool in your path. Install it or use \
--adb=<file>"
  fi
  log "Auto-config: --adb=$ADB"
fi

# Check that it works minimally
ADB_VERSION=$($ADB version 2>/dev/null)
echo "$ADB_VERSION" | fgrep -q -e "Android Debug Bridge"
if [ $? != 0 ]; then
  panic "Your 'adb' tool seems invalid, use --adb=<file> to specify a \
different one: $ADB"
fi

# If there are more than one device connected, and ANDROID_SERIAL is not
# defined, print an error message.
NUM_DEVICES_PLUS2=$($ADB devices 2>/dev/null | wc -l)
if [ "$NUM_DEVICES_PLUS2" -lt 3 -a -z "$ANDROID_SERIAL" ]; then
  echo "ERROR: There is more than one Android device connected to ADB."
  echo "Please define ANDROID_SERIAL to specify which one to use."
  exit 1
fi

# Run a command through adb shell, strip the extra \r from the output
# and return the correct status code to detect failures. This assumes
# that the adb shell command prints a final \n to stdout.
# $1+: command to run
# Out: command's stdout
# Return: command's status
# Note: the command's stderr is lost
adb_shell () {
  local TMPOUT="$(mktemp)"
  local LASTLINE RET
  local ADB=${ADB:-adb}

  # The weird sed rule is to strip the final \r on each output line
  # Since 'adb shell' never returns the command's proper exit/status code,
  # we force it to print it as '%%<status>' in the temporary output file,
  # which we will later strip from it.
  $ADB shell $@ ";" echo "%%\$?" 2>/dev/null | \
      sed -e 's![[:cntrl:]]!!g' > $TMPOUT
  # Get last line in log, which contains the exit code from the command
  LASTLINE=$(sed -e '$!d' $TMPOUT)
  # Extract the status code from the end of the line, which must
  # be '%%<code>'.
  RET=$(echo "$LASTLINE" | \
    awk '{ if (match($0, "%%[0-9]+$")) { print substr($0,RSTART+2); } }')
  # Remove the status code from the last line. Note that this may result
  # in an empty line.
  LASTLINE=$(echo "$LASTLINE" | \
    awk '{ if (match($0, "%%[0-9]+$")) { print substr($0,1,RSTART-1); } }')
  # The output itself: all lines except the status code.
  sed -e '$d' $TMPOUT && printf "%s" "$LASTLINE"
  # Remove temp file.
  rm -f $TMPOUT
  # Exit with the appropriate status.
  return $RET
}

# Find the target architecture from the target device.
# This returns an NDK-compatible architecture name.
# out: NDK Architecture name, or empty string.
get_gyp_target_arch () {
  local ARCH=$(adb_shell getprop ro.product.cpu.abi)
  case $ARCH in
    mips|x86|x86_64) echo "$ARCH";;
    arm64*) echo "arm64";;
    arm*) echo "arm";;
    *) echo "";
  esac
}

if [ -z "$TARGET_ARCH" ]; then
  TARGET_ARCH=$(get_gyp_target_arch)
  if [ -z "$TARGET_ARCH" ]; then
    TARGET_ARCH=arm
  fi
else
  # Nit: accept Chromium's 'ia32' as a valid target architecture. This
  # script prefers the NDK 'x86' name instead because it uses it to find
  # NDK-specific files (host gdb) with it.
  if [ "$TARGET_ARCH" = "ia32" ]; then
    TARGET_ARCH=x86
    log "Auto-config: --arch=$TARGET_ARCH  (equivalent to ia32)"
  fi
fi

# Detect the NDK system name, i.e. the name used to identify the host.
# out: NDK system name (e.g. 'linux' or 'darwin')
get_ndk_host_system () {
  local HOST_OS
  if [ -z "$NDK_HOST_SYSTEM" ]; then
    HOST_OS=$(uname -s)
    case $HOST_OS in
      Linux) NDK_HOST_SYSTEM=linux;;
      Darwin) NDK_HOST_SYSTEM=darwin;;
      *) panic "You can't run this script on this system: $HOST_OS";;
    esac
  fi
  echo "$NDK_HOST_SYSTEM"
}

# Detect the NDK host architecture name.
# out: NDK arch name (e.g. 'x86' or 'x86_64')
get_ndk_host_arch () {
  local HOST_ARCH HOST_OS
  if [ -z "$NDK_HOST_ARCH" ]; then
    HOST_OS=$(get_ndk_host_system)
    HOST_ARCH=$(uname -p)
    case $HOST_ARCH in
      i?86) NDK_HOST_ARCH=x86;;
      x86_64|amd64) NDK_HOST_ARCH=x86_64;;
      *) panic "You can't run this script on this host architecture: $HOST_ARCH";;
    esac
    # Darwin trick: "uname -p" always returns i386 on 64-bit installations.
    if [ "$HOST_OS" = darwin -a "$NDK_HOST_ARCH" = "x86" ]; then
      # Use '/usr/bin/file', not just 'file' to avoid buggy MacPorts
      # implementations of the tool. See http://b.android.com/53769
      HOST_64BITS=$(/usr/bin/file -L "$SHELL" | grep -e "x86[_-]64")
      if [ "$HOST_64BITS" ]; then
        NDK_HOST_ARCH=x86_64
      fi
    fi
  fi
  echo "$NDK_HOST_ARCH"
}

# Convert an NDK architecture name into a GNU configure triplet.
# $1: NDK architecture name (e.g. 'arm')
# Out: Android GNU configure triplet (e.g. 'arm-linux-androideabi')
get_arch_gnu_config () {
  case $1 in
    arm)
      echo "arm-linux-androideabi"
      ;;
    arm64)
      echo "aarch64-linux-android"
      ;;
    x86)
      echo "i686-linux-android"
      ;;
    x86_64)
      echo "x86_64-linux-android"
      ;;
    mips)
      echo "mipsel-linux-android"
      ;;
    *)
      echo "$ARCH-linux-android"
      ;;
  esac
}

# Convert an NDK architecture name into a toolchain name prefix
# $1: NDK architecture name (e.g. 'arm')
# Out: NDK toolchain name prefix (e.g. 'arm-linux-androideabi')
get_arch_toolchain_prefix () {
  # Return the configure triplet, except for x86!
  if [ "$1" = "x86" ]; then
    echo "$1"
  else
    get_arch_gnu_config $1
  fi
}

# Find a NDK toolchain prebuilt file or sub-directory.
# This will probe the various arch-specific toolchain directories
# in the NDK for the needed file.
# $1: NDK install path
# $2: NDK architecture name
# $3: prebuilt sub-path to look for.
# Out: file path, or empty if none is found.
get_ndk_toolchain_prebuilt () {
  local NDK_DIR="${1%/}"
  local ARCH="$2"
  local SUBPATH="$3"
  local NAME="$(get_arch_toolchain_prefix $ARCH)"
  local FILE TARGET
  FILE=$NDK_DIR/toolchains/$NAME-4.9/prebuilt/$SUBPATH
  if [ ! -f "$FILE" ]; then
    FILE=$NDK_DIR/toolchains/$NAME-4.8/prebuilt/$SUBPATH
    if [ ! -f "$FILE" ]; then
      FILE=
    fi
  fi
  echo "$FILE"
}

# Find the path to an NDK's toolchain full prefix for a given architecture
# $1: NDK install path
# $2: NDK target architecture name
# Out: install path + binary prefix (e.g.
#      ".../path/to/bin/arm-linux-androideabi-")
get_ndk_toolchain_fullprefix () {
  local NDK_DIR="$1"
  local ARCH="$2"
  local TARGET NAME HOST_OS HOST_ARCH GCC CONFIG

  # NOTE: This will need to be updated if the NDK changes the names or moves
  #        the location of its prebuilt toolchains.
  #
  GCC=
  HOST_OS=$(get_ndk_host_system)
  HOST_ARCH=$(get_ndk_host_arch)
  CONFIG=$(get_arch_gnu_config $ARCH)
  GCC=$(get_ndk_toolchain_prebuilt \
        "$NDK_DIR" "$ARCH" "$HOST_OS-$HOST_ARCH/bin/$CONFIG-gcc")
  if [ -z "$GCC" -a "$HOST_ARCH" = "x86_64" ]; then
    GCC=$(get_ndk_toolchain_prebuilt \
          "$NDK_DIR" "$ARCH" "$HOST_OS-x86/bin/$CONFIG-gcc")
  fi
  if [ ! -f "$GCC" -a "$ARCH" = "x86" ]; then
    # Special case, the x86 toolchain used to be incorrectly
    # named i686-android-linux-gcc!
    GCC=$(get_ndk_toolchain_prebuilt \
          "$NDK_DIR" "$ARCH" "$HOST_OS-x86/bin/i686-android-linux-gcc")
  fi
  if [ -z "$GCC" ]; then
    panic "Cannot find Android NDK toolchain for '$ARCH' architecture. \
Please verify your NDK installation!"
  fi
  echo "${GCC%%gcc}"
}

# $1: NDK install path
# $2: target architecture.
get_ndk_gdbserver () {
  local NDK_DIR="$1"
  local ARCH=$2
  local BINARY

  # The location has moved after NDK r8
  BINARY=$NDK_DIR/prebuilt/android-$ARCH/gdbserver/gdbserver
  if [ ! -f "$BINARY" ]; then
    BINARY=$(get_ndk_toolchain_prebuilt "$NDK_DIR" "$ARCH" gdbserver)
  fi
  echo "$BINARY"
}

# Check/probe the path to the Android toolchain installation. Always
# use the NDK versions of gdb and gdbserver. They must match to avoid
# issues when both binaries do not speak the same wire protocol.
#
if [ -z "$TOOLCHAIN" ]; then
  ANDROID_TOOLCHAIN=$(get_ndk_toolchain_fullprefix \
                      "$ANDROID_NDK_ROOT" "$TARGET_ARCH")
  ANDROID_TOOLCHAIN=$(dirname "$ANDROID_TOOLCHAIN")
  log "Auto-config: --toolchain=$ANDROID_TOOLCHAIN"
else
  # Be flexible, allow one to specify either the install path or the bin
  # sub-directory in --toolchain:
  #
  if [ -d "$TOOLCHAIN/bin" ]; then
    TOOLCHAIN=$TOOLCHAIN/bin
  fi
  ANDROID_TOOLCHAIN=$TOOLCHAIN
fi

# Cosmetic: Remove trailing directory separator.
ANDROID_TOOLCHAIN=${ANDROID_TOOLCHAIN%/}

# Find host GDB client binary
if [ -z "$GDB" ]; then
  GDB=$(which $ANDROID_TOOLCHAIN/*-$GDBEXEPOSTFIX 2>/dev/null | head -1)
  if [ -z "$GDB" ]; then
    panic "Can't find Android gdb client in your path, check your \
--toolchain or --gdb path."
  fi
  log "Host gdb client: $GDB"
fi

# Find gdbserver binary, we will later push it to /data/local/tmp
# This ensures that both gdbserver and $GDB talk the same binary protocol,
# otherwise weird problems will appear.
#
if [ -z "$GDBSERVER" ]; then
  GDBSERVER=$(get_ndk_gdbserver "$ANDROID_NDK_ROOT" "$TARGET_ARCH")
  if [ -z "$GDBSERVER" ]; then
    panic "Can't find NDK gdbserver binary. use --gdbserver to specify \
valid one!"
  fi
  log "Auto-config: --gdbserver=$GDBSERVER"
fi

# A unique ID for this script's session. This needs to be the same in all
# sub-shell commands we're going to launch, so take the PID of the launcher
# process.
TMP_ID=$$

# Temporary directory, will get cleaned up on exit.
TMPDIR=/tmp/$USER-adb-gdb-tmp-$TMP_ID
mkdir -p "$TMPDIR" && rm -rf "$TMPDIR"/*

GDBSERVER_PIDFILE="$TMPDIR"/gdbserver-$TMP_ID.pid

# If --force is specified, try to kill any gdbserver process started by the
# same user on the device. Normally, these are killed automatically by the
# script on exit, but there are a few corner cases where this would still
# be needed.
if [ "$FORCE" ]; then
  GDBSERVER_PIDS=$(adb_shell ps | awk '$9 ~ /gdbserver/ { print $2; }')
  for GDB_PID in $GDBSERVER_PIDS; do
    log "Killing previous gdbserver (PID=$GDB_PID)"
    adb_shell kill -9 $GDB_PID
  done
fi

if [ "$START" ]; then
  log "Starting $PROGRAM_NAME on device."
  adb_shell am start -n $PACKAGE_NAME/$ACTIVITY 2>/dev/null
  adb_shell ps | grep -q $PACKAGE_NAME
  fail_panic "Could not start $PROGRAM_NAME on device. Are you sure the \
package is installed?"
fi

# Return the timestamp of a given time, as number of seconds since epoch.
# $1: file path
# Out: file timestamp
get_file_timestamp () {
  stat -c %Y "$1" 2>/dev/null
}

# Detect the build type and symbol directory. This is done by finding
# the most recent sub-directory containing debug shared libraries under
# $CHROMIUM_SRC/$CHROMIUM_OUT_DIR/
#
# $1: $BUILDTYPE value, can be empty
# Out: nothing, but this sets SYMBOL_DIR
#
detect_symbol_dir () {
  local SUBDIRS SUBDIR LIST DIR DIR_LIBS TSTAMP
  # Note: Ninja places debug libraries under out/$BUILDTYPE/lib/, while
  # Make places then under out/$BUILDTYPE/lib.target.
  if [ "$1" ]; then
    SUBDIRS="$1/lib $1/lib.target"
  else
    SUBDIRS="Release/lib Debug/lib Release/lib.target Debug/lib.target"
  fi
  LIST=$TMPDIR/scan-subdirs-$$.txt
  printf "" > "$LIST"
  for SUBDIR in $SUBDIRS; do
    DIR=$CHROMIUM_SRC/$CHROMIUM_OUT_DIR/$SUBDIR
    if [ -d "$DIR" ]; then
      # Ignore build directories that don't contain symbol versions
      # of the shared libraries.
      DIR_LIBS=$(ls "$DIR"/lib*.so 2>/dev/null)
      if [ -z "$DIR_LIBS" ]; then
        echo "No shared libs: $DIR"
        continue
      fi
      TSTAMP=$(get_file_timestamp "$DIR")
      printf "%s %s\n" "$TSTAMP" "$SUBDIR" >> "$LIST"
    fi
  done
  SUBDIR=$(cat $LIST | sort -r | head -1 | cut -d" " -f2)
  rm -f "$LIST"

  if [ -z "$SUBDIR" ]; then
    if [ -z "$1" ]; then
      panic "Could not find any build directory under \
$CHROMIUM_SRC/$CHROMIUM_OUT_DIR. Please build the program first!"
    else
      panic "Could not find any $1 directory under \
$CHROMIUM_SRC/$CHROMIUM_OUT_DIR. Check your build type!"
    fi
  fi

  SYMBOL_DIR=$CHROMIUM_SRC/$CHROMIUM_OUT_DIR/$SUBDIR
  log "Auto-config: --symbol-dir=$SYMBOL_DIR"
}

if [ -z "$SYMBOL_DIR" ]; then
  detect_symbol_dir "$BUILDTYPE"
fi

# Allow several concurrent debugging sessions
TARGET_GDBSERVER=/data/data/$PACKAGE_NAME/gdbserver-adb-gdb-$TMP_ID
TMP_TARGET_GDBSERVER=/data/local/tmp/gdbserver-adb-gdb-$TMP_ID

# Return the build fingerprint contained in a build.prop file.
# $1: path to build.prop file
get_build_fingerprint_from () {
  cat "$1" | grep -e '^ro.build.fingerprint=' | cut -d= -f2
}


ORG_PULL_LIBS_DIR=$PULL_LIBS_DIR
PULL_LIBS_DIR=${PULL_LIBS_DIR:-$DEFAULT_PULL_LIBS_DIR}

HOST_FINGERPRINT=
DEVICE_FINGERPRINT=$(adb_shell getprop ro.build.fingerprint)
log "Device build fingerprint: $DEVICE_FINGERPRINT"

# If --pull-libs-dir is not specified, and this is a platform build, look
# if we can use the symbolic libraries under $ANDROID_PRODUCT_OUT/symbols/
# directly, if the build fingerprint matches the device.
if [ -z "$ORG_PULL_LIBS_DIR" -a \
     "$ANDROID_PRODUCT_OUT" -a \
     -f "$ANDROID_PRODUCT_OUT/system/build.prop" ]; then
  ANDROID_FINGERPRINT=$(get_build_fingerprint_from \
                        "$ANDROID_PRODUCT_OUT"/system/build.prop)
  log "Android build fingerprint:  $ANDROID_FINGERPRINT"
  if [ "$ANDROID_FINGERPRINT" = "$DEVICE_FINGERPRINT" ]; then
    log "Perfect match!"
    PULL_LIBS_DIR=$ANDROID_PRODUCT_OUT/symbols
    HOST_FINGERPRINT=$ANDROID_FINGERPRINT
    if [ "$PULL_LIBS" ]; then
      log "Ignoring --pull-libs since the device and platform build \
fingerprints match."
      NO_PULL_LIBS=true
    fi
  fi
fi

# If neither --pull-libs an --no-pull-libs were specified, check the build
# fingerprints of the device, and the cached system libraries on the host.
#
if [ -z "$NO_PULL_LIBS" -a -z "$PULL_LIBS" ]; then
  if [ ! -f "$PULL_LIBS_DIR/build.prop" ]; then
    log "Auto-config: --pull-libs  (no cached libraries)"
    PULL_LIBS=true
  else
    HOST_FINGERPRINT=$(get_build_fingerprint_from "$PULL_LIBS_DIR/build.prop")
    log "Host build fingerprint:   $HOST_FINGERPRINT"
    if [ "$HOST_FINGERPRINT" == "$DEVICE_FINGERPRINT" ]; then
      log "Auto-config: --no-pull-libs (fingerprint match)"
      NO_PULL_LIBS=true
    else
      log "Auto-config: --pull-libs  (fingerprint mismatch)"
      PULL_LIBS=true
    fi
  fi
fi

# Extract the system libraries from the device if necessary.
if [ "$PULL_LIBS" -a -z "$NO_PULL_LIBS" ]; then
  echo "Extracting system libraries into: $PULL_LIBS_DIR"
fi

mkdir -p "$PULL_LIBS_DIR"
fail_panic "Can't create --libs-dir directory: $PULL_LIBS_DIR"

# If requested, work for M-x gdb.  The gdb indirections make it
# difficult to pass --annotate=3 to the gdb binary itself.
GDB_ARGS=
if [ "$ANNOTATE" ]; then
  GDB_ARGS=$GDB_ARGS" --annotate=$ANNOTATE"
fi

# Get the PID from the first argument or else find the PID of the
# browser process.
if [ -z "$PID" ]; then
  PROCESSNAME=$PACKAGE_NAME
  if [ "$SANDBOXED_INDEX" ]; then
    PROCESSNAME=$PROCESSNAME:sandboxed_process$SANDBOXED_INDEX
  elif [ "$SANDBOXED" ]; then
    PROCESSNAME=$PROCESSNAME:sandboxed_process
    PID=$(adb_shell ps | \
          awk '$9 ~ /^'$PROCESSNAME'/ { print $2; }' | head -1)
  elif [ "$PRIVILEGED_INDEX" ]; then
    PROCESSNAME=$PROCESSNAME:privileged_process$PRIVILEGED_INDEX
  elif [ "$PRIVILEGED" ]; then
    PROCESSNAME=$PROCESSNAME:privileged_process
    PID=$(adb_shell ps | \
          awk '$9 ~ /^'$PROCESSNAME'/ { print $2; }' | head -1)
  fi
  if [ -z "$PID" ]; then
    PID=$(adb_shell ps | \
          awk '$9 == "'$PROCESSNAME'" { print $2; }' | head -1)
  fi
  if [ -z "$PID" ]; then
    if [ "$START" ]; then
      panic "Can't find application process PID, did it crash?"
    else
      panic "Can't find application process PID, are you sure it is \
running? Try using --start."
    fi
  fi
  log "Found process PID: $PID"
elif [ "$SANDBOXED" ]; then
  echo "WARNING: --sandboxed option ignored due to use of --pid."
elif [ "$PRIVILEGED" ]; then
  echo "WARNING: --privileged option ignored due to use of --pid."
fi

# Determine if 'adb shell' runs as root or not.
# If so, we can launch gdbserver directly, otherwise, we have to
# use run-as $PACKAGE_NAME ..., which requires the package to be debuggable.
#
if [ "$SU_PREFIX" ]; then
  # Need to check that this works properly.
  SU_PREFIX_TEST_LOG=$TMPDIR/su-prefix.log
  adb_shell $SU_PREFIX \"echo "foo"\" > $SU_PREFIX_TEST_LOG 2>&1
  if [ $? != 0 -o "$(cat $SU_PREFIX_TEST_LOG)" != "foo" ]; then
    echo "ERROR: Cannot use '$SU_PREFIX' as a valid su prefix:"
    echo "$ adb shell $SU_PREFIX \"echo foo\""
    cat $SU_PREFIX_TEST_LOG
    exit 1
  fi
  COMMAND_PREFIX="$SU_PREFIX \""
  COMMAND_SUFFIX="\""
else
  SHELL_UID=$(adb shell cat /proc/self/status | \
              awk '$1 == "Uid:" { print $2; }')
  log "Shell UID: $SHELL_UID"
  if [ "$SHELL_UID" != 0 -o -n "$NO_ROOT" ]; then
    COMMAND_PREFIX="run-as $PACKAGE_NAME"
    COMMAND_SUFFIX=
  else
    COMMAND_PREFIX=
    COMMAND_SUFFIX=
  fi
fi
log "Command prefix: '$COMMAND_PREFIX'"
log "Command suffix: '$COMMAND_SUFFIX'"

# Pull device's system libraries that are mapped by our process.
# Pulling all system libraries is too long, so determine which ones
# we need by looking at /proc/$PID/maps instead
if [ "$PULL_LIBS" -a -z "$NO_PULL_LIBS" ]; then
  echo "Extracting system libraries into: $PULL_LIBS_DIR"
  rm -f $PULL_LIBS_DIR/build.prop
  MAPPINGS=$(adb_shell $COMMAND_PREFIX cat /proc/$PID/maps $COMMAND_SUFFIX)
  if [ $? != 0 ]; then
    echo "ERROR: Could not list process's memory mappings."
    if [ "$SU_PREFIX" ]; then
      panic "Are you sure your --su-prefix is correct?"
    else
      panic "Use --su-prefix if the application is not debuggable."
    fi
  fi
  SYSTEM_LIBS=$(echo "$MAPPINGS" | \
      awk '$6 ~ /\/system\/.*\.so$/ { print $6; }' | sort -u)
  for SYSLIB in /system/bin/linker $SYSTEM_LIBS; do
    echo "Pulling from device: $SYSLIB"
    DST_FILE=$PULL_LIBS_DIR$SYSLIB
    DST_DIR=$(dirname "$DST_FILE")
    mkdir -p "$DST_DIR" && adb pull $SYSLIB "$DST_FILE" 2>/dev/null
    fail_panic "Could not pull $SYSLIB from device !?"
  done
  echo "Pulling device build.prop"
  adb pull /system/build.prop $PULL_LIBS_DIR/build.prop
  fail_panic "Could not pull device build.prop !?"
fi

# Find all the sub-directories of $PULL_LIBS_DIR, up to depth 4
# so we can add them to solib-search-path later.
SOLIB_DIRS=$(find $PULL_LIBS_DIR -mindepth 1 -maxdepth 4 -type d | \
             grep -v "^$" | tr '\n' ':')

# This is a re-implementation of gdbclient, where we use compatible
# versions of gdbserver and $GDBNAME to ensure that everything works
# properly.
#

# Push gdbserver to the device
log "Pushing gdbserver $GDBSERVER to $TARGET_GDBSERVER"
adb push $GDBSERVER $TMP_TARGET_GDBSERVER &>/dev/null
adb shell $COMMAND_PREFIX cp $TMP_TARGET_GDBSERVER $TARGET_GDBSERVER
adb shell rm $TMP_TARGET_GDBSERVER
fail_panic "Could not copy gdbserver to the device!"

if [ -z "$PORT" ]; then
    PORT=5039
fi
HOST_PORT=$PORT
TARGET_PORT=$PORT

# Select correct app_process for architecture.
case $TARGET_ARCH in
      arm|x86|mips) GDBEXEC=app_process;;
      arm64|x86_64) GDBEXEC=app_process64;;
      *) fail_panic "Unknown app_process for architecture!";;
esac

# Detect AddressSanitizer setup on the device. In that case app_process is a
# script, and the real executable is app_process.real.
GDBEXEC_ASAN=app_process.real
adb_shell ls /system/bin/$GDBEXEC_ASAN
if [ $? == 0 ]; then
    GDBEXEC=$GDBEXEC_ASAN
fi

# Pull the app_process binary from the device.
log "Pulling $GDBEXEC from device"
adb pull /system/bin/$GDBEXEC "$TMPDIR"/$GDBEXEC &>/dev/null
fail_panic "Could not retrieve $GDBEXEC from the device!"

# Setup network redirection
log "Setting network redirection (host:$HOST_PORT -> device:$TARGET_PORT)"
adb forward tcp:$HOST_PORT tcp:$TARGET_PORT
fail_panic "Could not setup network redirection from \
host:localhost:$HOST_PORT to device:localhost:$TARGET_PORT!"

# Start gdbserver in the background
# Note that using run-as requires the package to be debuggable.
#
# If not, this will fail horribly. The alternative is to run the
# program as root, which requires of course root privileges.
# Maybe we should add a --root option to enable this?
#
log "Starting gdbserver in the background:"
GDBSERVER_LOG=$TMPDIR/gdbserver-$TMP_ID.log
log "adb shell $COMMAND_PREFIX $TARGET_GDBSERVER :$TARGET_PORT \
--attach $PID $COMMAND_SUFFIX"
("$ADB" shell $COMMAND_PREFIX $TARGET_GDBSERVER :$TARGET_PORT \
 --attach $PID $COMMAND_SUFFIX > $GDBSERVER_LOG 2>&1) &
GDBSERVER_PID=$!
echo "$GDBSERVER_PID" > $GDBSERVER_PIDFILE
log "background job pid: $GDBSERVER_PID"

# Check that it is still running after a few seconds. If not, this means we
# could not properly attach to it
sleep 2
log "Job control: $(jobs -l)"
STATE=$(jobs -l | awk '$2 == "'$GDBSERVER_PID'" { print $3; }')
if [ "$STATE" != "Running" ]; then
  echo "ERROR: GDBServer could not attach to PID $PID!"
  if [ $(adb_shell su -c getenforce) != "Permissive" ];  then
    echo "Device mode is Enforcing. Changing Device mode to Permissive "
    $(adb_shell su -c setenforce 0)
    if [ $(adb_shell su -c getenforce) != "Permissive" ]; then
      echo "ERROR: Failed to Change Device mode to Permissive"
      echo "Failure log (use --verbose for more information):"
      cat $GDBSERVER_LOG
      exit 1
    fi
  else
    echo "Failure log (use --verbose for more information):"
    cat $GDBSERVER_LOG
    exit 1
  fi
fi

# Generate a file containing useful GDB initialization commands
readonly COMMANDS=$TMPDIR/gdb.init
log "Generating GDB initialization commands file: $COMMANDS"
echo -n "" > $COMMANDS
echo "set print pretty 1" >> $COMMANDS
echo "python" >> $COMMANDS
echo "import sys" >> $COMMANDS
echo "sys.path.insert(0, '$CHROMIUM_SRC/tools/gdb/')" >> $COMMANDS
echo "try:" >> $COMMANDS
echo "  import gdb_chrome" >> $COMMANDS
echo "finally:" >> $COMMANDS
echo "  sys.path.pop(0)" >> $COMMANDS
echo "end" >> $COMMANDS
echo "file $TMPDIR/$GDBEXEC" >> $COMMANDS
echo "directory $CHROMIUM_SRC" >> $COMMANDS
echo "set solib-absolute-prefix $PULL_LIBS_DIR" >> $COMMANDS
echo "set solib-search-path $SOLIB_DIRS:$PULL_LIBS_DIR:$SYMBOL_DIR" \
    >> $COMMANDS
echo "echo Attaching and reading symbols, this may take a while.." \
    >> $COMMANDS
echo "target remote :$HOST_PORT" >> $COMMANDS

if [ "$GDBINIT" ]; then
  cat "$GDBINIT" >> $COMMANDS
fi

if [ "$VERBOSE" -gt 0 ]; then
  echo "### START $COMMANDS"
  cat $COMMANDS
  echo "### END $COMMANDS"
fi

log "Launching gdb client: $GDB $GDB_ARGS -x $COMMANDS"
$GDB $GDB_ARGS -x $COMMANDS &&
rm -f "$GDBSERVER_PIDFILE"
