#!/bin/bash
### Run Emacs under GDB or JDB on Android.

## Copyright (C) 2023 Free Software Foundation, Inc.

## This file is part of GNU Emacs.

## GNU Emacs is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.

## GNU Emacs is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.

## You should have received a copy of the GNU General Public License
## along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

set -m
oldpwd=`pwd`
cd `dirname $0`

devices=`adb devices | grep device | awk -- '/device\y/ { print $1 }' -`
device=
progname=$0
package=org.gnu.emacs
activity=org.gnu.emacs.EmacsActivity
gdb_port=5039
jdb_port=64013
jdb=no
attach_existing=no
gdbserver=
gdb=gdb

while [ $# -gt 0 ]; do
    case "$1" in
	## This option specifies the serial number of a device to use.
	"--device" )
	    device="$2"
	    if [ -z device ]; then
		echo "You must specify an argument to --device"
		exit 1
	    fi
	    shift
	    ;;
	"--help" )
	    echo "Usage: $progname [options] -- [gdb options]"
	    echo ""
	    echo "  --device DEVICE	run Emacs on the specified device"
	    echo "  --port PORT		run the GDB server on a specific port"
	    echo "  --jdb-port PORT	run the JDB server on a specific port"
	    echo "  --jdb		run JDB instead of GDB"
	    echo "  --gdb		use specified GDB binary"
	    echo "  --attach-existing	attach to an existing process"
	    echo "  --gdbserver BINARY	upload and use the specified gdbserver binary"
	    echo "  --help		print this message"
	    echo ""
	    echo "Available devices:"
	    for device in $devices; do
		echo "  " $device
	    done
	    echo ""
	    exit 0
	    ;;
	"--jdb" )
	    jdb=yes
	    ;;
	"--gdb" )
	    shift
	    gdb=$1
	    ;;
	"--gdbserver" )
	    shift
	    gdbserver=$1
	    ;;
	"--port" )
	    shift
	    gdb_port=$1
	    ;;
	"--jdb-port" )
	    shift
	    jdb_port=$1
	    ;;
	"--attach-existing" )
	    attach_existing=yes
	    ;;
	"--" )
	    shift
	    gdbargs=$@
	    break;
	    ;;
	* )
	    echo "$progname: Unrecognized argument $1"
	    exit 1
	    ;;
    esac
    shift
done

if [ -z "$devices" ]; then
    echo "No devices are available."
    exit 1
fi

if [ -z $device ]; then
    device=$devices
fi

if [ `wc -w <<< "$devices"` -gt 1 ] && [ -z device ]; then
    echo "Multiple devices are available.  Please pick one using"
    echo "--device and try again."
fi

echo "Looking for $package on device $device"

# Find the application data directory
app_data_dir=`adb -s $device shell run-as $package sh -c 'pwd 2> /dev/null'`

if [ -z $app_data_dir ]; then
   echo "The data directory for the package $package was not found."
   echo "Is it installed?"
fi

echo "Found application data directory at" "$app_data_dir"

# Generate an awk script to extract PIDs from Android ps output.  It
# is enough to run `ps' as the package user on newer versions of
# Android, but that doesn't work on Android 2.3.
cat << EOF > tmp.awk
BEGIN {
  pid = 0;
  pid_column = 2;
}

{
  # Remove any trailing carriage return from the input line.
  gsub ("\r", "", \$NF)

  # If this is line 1, figure out which column contains the PID.
  if (NR == 1)
    {
      for (n = 1; n <= NF; ++n)
	{
	  if (\$n == "PID")
	    pid_column=n;
	}
    }
  else if (\$NF == "$package")
   print \$pid_column
}
EOF

# Make sure that file disappears once this script exits.
trap "rm -f $(pwd)/tmp.awk" 0

# First, run ps to fetch the list of process IDs.
package_pids=`adb -s $device shell ps`

# Next, extract the list of PIDs currently running.
package_pids=`awk -f tmp.awk <<< $package_pids`

if [ "$attach_existing" != "yes" ]; then
    # Finally, kill each existing process.
    for pid in $package_pids; do
	echo "Killing existing process $pid..."
	adb -s $device shell run-as $package kill -9 $pid &> /dev/null
    done

    # Now run the main activity.  This must be done as the adb user and
    # not as the package user.
    echo "Starting activity $activity and attaching debugger"

    # Exit if the activity could not be started.
    adb -s $device shell am start -D -n "$package/$activity"
    if [ ! $? ]; then
	exit 1;
    fi

    # Sleep for a bit.  Otherwise, the process may not have started
    # yet.
    sleep 1

    # Now look for processes matching the package again.
    package_pids=`adb -s $device shell ps`

    # Next, remove lines matching "ps" itself.
    package_pids=`awk -f tmp.awk <<< $package_pids`
fi

pid=$package_pids
num_pids=`wc -w <<< "$package_pids"`

if [ $num_pids -gt 1 ]; then
    echo "More than one process was started:"
    echo ""
    adb -s $device shell run-as $package ps | awk -- "{
      if (!match (\$0, /ps/) && match (\$0, /$package/))
        print \$0
    }"
    echo ""
    printf "Which one do you want to attach to? "
    read pid
elif [ -z $package_pids ]; then
    echo "No processes were found to attach to."
    exit 1
fi

# If either --jdb was specified or debug.sh is not connecting to an
# existing process, then store a suitable JDB invocation in
# jdb_command.  GDB will then run JDB to unblock the application from
# the wait dialog after startup.

if [ "$jdb" = "yes" ] || [ "$attach_existing" != yes ]; then
    adb -s $device forward --remove-all
    adb -s $device forward "tcp:$jdb_port" "jdwp:$pid"

    if [ ! $? ]; then
	echo "Failed to forward jdwp:$pid to $jdb_port!"
	echo "Perhaps you need to specify a different port with --port?"
	exit 1;
    fi

    jdb_command="jdb -connect \
		 com.sun.jdi.SocketAttach:hostname=localhost,port=$jdb_port"

    if [ $jdb = "yes" ]; then
	# Just start JDB and then exit
	$jdb_command
	exit 1
    fi
fi

if [ -n "$jdb_command" ]; then
    echo "Starting JDB to unblock application."

    # Start JDB to unblock the application.
    coproc JDB { $jdb_command; }

    # Tell JDB to first suspend all threads.
    echo "suspend" >&${JDB[1]}

    # Tell JDB to print a magic string once the program is
    # initialized.
    echo "print \"__verify_jdb_has_started__\"" >&${JDB[1]}

    # Now wait for JDB to give the string back.
    line=
    while :; do
	read -u ${JDB[0]} line
	if [ ! $? ]; then
	    echo "Failed to read JDB output"
	    exit 1
	fi

	case "$line" in
	    *__verify_jdb_has_started__*)
		# Android only polls for a Java debugger every 200ms, so
		# the debugger must be connected for at least that long.
		echo "Pausing 1 second for the program to continue."
		sleep 1
		break
		;;
	esac
    done

    # Note that JDB does not exit until GDB is fully attached!
fi

# See if gdbserver has to be uploaded
gdbserver_cmd=
is_root=
if [ -z "$gdbserver" ]; then
    gdbserver_bin=/system/bin/gdbserver64
else
    gdbserver_bin=/data/local/tmp/gdbserver
    gdbserver_cat="cat $gdbserver_bin | run-as $package sh -c \
		   \"tee gdbserver > /dev/null\""

    # Upload the specified gdbserver binary to the device.
    adb -s $device push "$gdbserver" "$gdbserver_bin"

    if (adb -s $device shell ls /system/bin | grep -G tee); then
	# Copy it to the user directory.
	adb -s $device shell "$gdbserver_cat"
	adb -s $device shell "run-as $package chmod 777 gdbserver"
	gdbserver_cmd="./gdbserver"
    else
	# Hopefully this is an old version of Android which allows
	# execution from /data/local/tmp.  Its `chmod' doesn't support
	# `+x' either.
	adb -s $device shell "chmod 777 $gdbserver_bin"
	gdbserver_cmd="$gdbserver_bin"

	# If the user is root, then there is no need to open any kind
	# of TCP socket.
	if (adb -s $device shell id | grep -G root); then
	    gdbserver=
	    is_root=yes
	fi
    fi
fi

# Now start gdbserver on the device asynchronously.

echo "Attaching gdbserver to $pid on $device..."
exec 5<> /tmp/file-descriptor-stamp
rm -f /tmp/file-descriptor-stamp

if [ -z "$gdbserver" ]; then
    if [ "$is_root" = "yes" ]; then
	adb -s $device shell $gdbserver_bin --multi \
	    "0.0.0.0:7564" --attach $pid >&5 &
	gdb_socket="tcp:7564"
    else
	adb -s $device shell $gdbserver_bin --multi \
	    "0.0.0.0:7564" --attach $pid >&5 &
	gdb_socket="tcp:7564"
    fi
else
    # Normally the program cannot access $gdbserver_bin when it is
    # placed in /data/local/tmp.
    adb -s $device shell run-as $package $gdbserver_cmd --multi \
	"+debug.$package.socket" --attach $pid >&5 &
    gdb_socket="localfilesystem:$app_data_dir/debug.$package.socket"
fi

# In order to allow adb to forward to the gdbserver socket, make the
# app data directory a+x.
adb -s $device shell run-as $package chmod a+x $app_data_dir

# Wait until gdbserver successfully runs.
line=
while read -u 5 line; do
    case "$line" in
	*Attached* )
	    break;
	    ;;
	*error* | *Error* | failed )
	    echo "GDB error:" $line
	    exit 1
	    ;;
	* )
	    ;;
    esac
done

# Now that GDB is attached, tell the Java debugger to resume execution
# and then exit.

if [ -n "$jdb_command" ]; then
    echo "resume" >&${JDB[1]}
    echo "exit" >&${JDB[1]}
fi

# Forward the gdb server port here.
adb -s $device forward "tcp:$gdb_port" $gdb_socket
if [ ! $? ]; then
    echo "Failed to forward $app_data_dir/debug.$package.socket"
    echo "to $gdb_port!  Perhaps you need to specify a different port"
    echo "with --port?"
    exit 1;
fi

# Finally, start gdb with any extra arguments needed.
cd "$oldpwd"
$gdb --eval-command "target remote localhost:$gdb_port" $gdbargs