342 lines
8.0 KiB
Bash
Executable File
342 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# Advanced ADB Logcat Monitor for Android apps
|
||
#
|
||
# Author: Reza Esmaeili
|
||
# Description:
|
||
# Tail logcat for a specific Android app process (by package) with:
|
||
# - Device selection (auto or explicit)
|
||
# - Configurable filters / logcat args
|
||
# - Optional app restart
|
||
# - Safe cleanup on exit
|
||
# - Log files with timestamps in a dedicated directory
|
||
#
|
||
# Usage:
|
||
# ./adb-log-monitor.sh [options] [LOGCAT_FILTER...]
|
||
#
|
||
# Examples:
|
||
# ./adb-log-monitor.sh
|
||
# ./adb-log-monitor.sh -p ir.avaair.avaair -a avaair -f '*:E'
|
||
# ./adb-log-monitor.sh -d emulator-5554 --no-restart
|
||
#
|
||
# Options:
|
||
# -p PACKAGE Android package name (default: ir.avaair.avaair)
|
||
# -a APP_NAME Logical app name used for messages & filenames (default: derived from package)
|
||
# -d DEVICE Device ID (adb serial). If omitted, will prompt if multiple devices.
|
||
# -o DIR Logs output directory (default: logs)
|
||
# -f FILTER Logcat filter spec (default: *:V) – can also pass as positional args
|
||
# --no-restart Do NOT auto-restart the app when process dies/startup fails
|
||
# --help Show this help and exit
|
||
#
|
||
# Exit codes:
|
||
# 0 Success / user interrupt
|
||
# 1 Usage / config error
|
||
# 2 adb / device error
|
||
# 3 (unused now, kept for compatibility if you want to reintroduce timeouts)
|
||
#
|
||
|
||
set -Euo pipefail
|
||
|
||
########################
|
||
# Default configuration
|
||
########################
|
||
|
||
DEFAULT_PACKAGE_NAME="ir.avaair.avaair"
|
||
DEFAULT_LOG_DIR="logs"
|
||
DEFAULT_FILTER="*:V"
|
||
|
||
PACKAGE_NAME="${PACKAGE_NAME:-$DEFAULT_PACKAGE_NAME}"
|
||
APP_NAME="${APP_NAME:-}"
|
||
DEVICE="${DEVICE:-}"
|
||
LOG_DIR="${LOG_DIR:-$DEFAULT_LOG_DIR}"
|
||
FILTER="${DEFAULT_FILTER}"
|
||
AUTO_RESTART=true
|
||
|
||
########################
|
||
# Pretty output helpers
|
||
########################
|
||
|
||
RED="\033[0;31m"
|
||
GREEN="\033[0;32m"
|
||
YELLOW="\033[1;33m"
|
||
BLUE="\033[0;34m"
|
||
NC="\033[0m" # No Color
|
||
|
||
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||
log_ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
|
||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
log_error() { echo -e "${RED}[ERR ]${NC} $*" >&2; }
|
||
|
||
########################
|
||
# Usage
|
||
########################
|
||
|
||
usage() {
|
||
sed -n '1,80p' "$0" | sed 's/^# \{0,1\}//'
|
||
}
|
||
|
||
########################
|
||
# Parse CLI args
|
||
########################
|
||
|
||
ARGS=()
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--no-restart)
|
||
AUTO_RESTART=false
|
||
shift
|
||
;;
|
||
--help|-h)
|
||
usage
|
||
exit 0
|
||
;;
|
||
-p)
|
||
[[ $# -lt 2 ]] && { log_error "-p requires PACKAGE"; exit 1; }
|
||
PACKAGE_NAME="$2"
|
||
shift 2
|
||
;;
|
||
-a)
|
||
[[ $# -lt 2 ]] && { log_error "-a requires APP_NAME"; exit 1; }
|
||
APP_NAME="$2"
|
||
shift 2
|
||
;;
|
||
-d)
|
||
[[ $# -lt 2 ]] && { log_error "-d requires DEVICE serial"; exit 1; }
|
||
DEVICE="$2"
|
||
shift 2
|
||
;;
|
||
-o)
|
||
[[ $# -lt 2 ]] && { log_error "-o requires DIR"; exit 1; }
|
||
LOG_DIR="$2"
|
||
shift 2
|
||
;;
|
||
-f)
|
||
[[ $# -lt 2 ]] && { log_error "-f requires FILTER"; exit 1; }
|
||
FILTER="$2"
|
||
shift 2
|
||
;;
|
||
--)
|
||
shift
|
||
break
|
||
;;
|
||
-*)
|
||
log_error "Unknown option: $1"
|
||
usage
|
||
exit 1
|
||
;;
|
||
*)
|
||
ARGS+=("$1")
|
||
shift
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# If user passed extra args as filters, override FILTER
|
||
if [[ ${#ARGS[@]} -gt 0 ]]; then
|
||
FILTER="${ARGS[*]}"
|
||
fi
|
||
|
||
# Derive APP_NAME from PACKAGE_NAME if not set
|
||
if [[ -z "${APP_NAME}" ]]; then
|
||
APP_NAME="${PACKAGE_NAME##*.}"
|
||
fi
|
||
|
||
mkdir -p "${LOG_DIR}"
|
||
|
||
########################
|
||
# Cleanup
|
||
########################
|
||
|
||
LOGCAT_PID=""
|
||
|
||
cleanup() {
|
||
log_warn "Cleaning up..."
|
||
if [[ -n "${LOGCAT_PID}" ]]; then
|
||
kill "${LOGCAT_PID}" 2>/dev/null || true
|
||
fi
|
||
log_ok "Done."
|
||
}
|
||
|
||
trap cleanup EXIT
|
||
trap 'exit 0' SIGINT SIGTERM
|
||
|
||
########################
|
||
# Preconditions
|
||
########################
|
||
|
||
if ! command -v adb >/dev/null 2>&1; then
|
||
log_error "'adb' command not found. Install Android platform-tools and ensure it's in PATH."
|
||
exit 2
|
||
fi
|
||
|
||
########################
|
||
# Device selection
|
||
########################
|
||
|
||
select_device() {
|
||
local devices
|
||
mapfile -t devices < <(adb devices | awk 'NR>1 && $2=="device" {print $1}')
|
||
|
||
if adb devices | grep -q "unauthorized"; then
|
||
log_error "Device unauthorized. Accept the RSA prompt on your device."
|
||
exit 2
|
||
fi
|
||
|
||
if adb devices | grep -q "offline"; then
|
||
log_error "Device offline. Reconnect or restart adb server."
|
||
exit 2
|
||
fi
|
||
|
||
if [[ ${#devices[@]} -eq 0 ]]; then
|
||
log_error "No devices found. Connect a device and ensure it's authorized."
|
||
exit 2
|
||
fi
|
||
|
||
if [[ -n "${DEVICE}" ]]; then
|
||
if printf '%s\n' "${devices[@]}" | grep -qx "${DEVICE}"; then
|
||
log_ok "Using specified device: ${DEVICE}"
|
||
return
|
||
else
|
||
log_error "Specified device '${DEVICE}' is not connected."
|
||
exit 2
|
||
fi
|
||
fi
|
||
|
||
if [[ ${#devices[@]} -eq 1 ]]; then
|
||
DEVICE="${devices[0]}"
|
||
log_ok "Using single connected device: ${DEVICE}"
|
||
return
|
||
fi
|
||
|
||
log_warn "Multiple devices found:"
|
||
local i=1
|
||
for d in "${devices[@]}"; do
|
||
echo " [$i] $d"
|
||
((i++))
|
||
done
|
||
|
||
local choice
|
||
read -r -p "Select device number: " choice
|
||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#devices[@]} )); then
|
||
log_error "Invalid selection."
|
||
exit 1
|
||
fi
|
||
|
||
DEVICE="${devices[$((choice-1))]}"
|
||
log_ok "Selected device: ${DEVICE}"
|
||
}
|
||
|
||
########################
|
||
# PID helpers
|
||
########################
|
||
|
||
get_pid() {
|
||
local pid=""
|
||
|
||
# Try pidof first
|
||
if adb -s "${DEVICE}" shell "command -v pidof >/dev/null 2>&1" >/dev/null 2>&1; then
|
||
pid=$(adb -s "${DEVICE}" shell "pidof -s '${PACKAGE_NAME}' 2>/dev/null || true" | tr -d '\r' || true)
|
||
fi
|
||
|
||
# Fallback to ps if pidof gave nothing
|
||
if [[ -z "${pid}" ]]; then
|
||
if adb -s "${DEVICE}" shell "ps -A >/dev/null 2>&1" >/dev/null 2>&1; then
|
||
pid=$(adb -s "${DEVICE}" shell "ps -A | grep '${PACKAGE_NAME}' | awk '{print \$2}' | head -n 1" 2>/dev/null | tr -d '\r' || true)
|
||
else
|
||
pid=$(adb -s "${DEVICE}" shell "ps | grep '${PACKAGE_NAME}' | awk '{print \$2}' | head -n 1" 2>/dev/null | tr -d '\r' || true)
|
||
fi
|
||
fi
|
||
|
||
echo "${pid}"
|
||
return 0
|
||
}
|
||
|
||
########################
|
||
# App restart helper
|
||
########################
|
||
|
||
restart_app() {
|
||
if [[ "${AUTO_RESTART}" != true ]]; then
|
||
return
|
||
fi
|
||
log_warn "Restarting app ${PACKAGE_NAME}..."
|
||
adb -s "${DEVICE}" shell am force-stop "${PACKAGE_NAME}" >/dev/null 2>&1 || true
|
||
adb -s "${DEVICE}" shell monkey -p "${PACKAGE_NAME}" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1 || true
|
||
}
|
||
|
||
########################
|
||
# Start logcat
|
||
########################
|
||
|
||
start_logcat() {
|
||
local pid="$1"
|
||
local log_file="${LOG_DIR}/${APP_NAME}_$(date +'%Y-%m-%d_%H-%M-%S')_pid_${pid}.log"
|
||
|
||
log_ok "Logging PID ${pid} (${PACKAGE_NAME}) -> ${log_file}"
|
||
log_info "Device: ${DEVICE}"
|
||
log_info "Filter: ${FILTER}"
|
||
echo "------------------------------------------------------------"
|
||
|
||
(
|
||
adb -s "${DEVICE}" logcat --pid="${pid}" ${FILTER} | tee -a "${log_file}"
|
||
) &
|
||
LOGCAT_PID=$!
|
||
}
|
||
|
||
########################
|
||
# Main loop
|
||
########################
|
||
|
||
main() {
|
||
select_device
|
||
|
||
log_info "Monitoring app logs for package: ${PACKAGE_NAME} (name: ${APP_NAME})"
|
||
log_info "Log directory: ${LOG_DIR}"
|
||
log_info "Auto-restart: ${AUTO_RESTART}"
|
||
|
||
local last_pid=""
|
||
local consecutive_misses=0
|
||
|
||
while true; do
|
||
local pid
|
||
pid="$(get_pid)"
|
||
|
||
if [[ -z "${pid}" ]]; then
|
||
((consecutive_misses++))
|
||
|
||
if (( consecutive_misses == 1 )); then
|
||
log_warn "Process '${PACKAGE_NAME}' not running. Waiting for it to start..."
|
||
restart_app
|
||
else
|
||
log_info "Still waiting for process '${PACKAGE_NAME}' to start..."
|
||
fi
|
||
|
||
sleep 3
|
||
continue
|
||
fi
|
||
|
||
if (( consecutive_misses > 0 )); then
|
||
log_ok "Process '${PACKAGE_NAME}' started with PID ${pid}."
|
||
fi
|
||
consecutive_misses=0
|
||
|
||
if [[ "${pid}" != "${last_pid}" ]]; then
|
||
if [[ -n "${last_pid}" ]]; then
|
||
log_info "Process restarted (old PID: ${last_pid} -> new PID: ${pid})."
|
||
fi
|
||
|
||
if [[ -n "${LOGCAT_PID}" ]]; then
|
||
kill "${LOGCAT_PID}" 2>/dev/null || true
|
||
LOGCAT_PID=""
|
||
fi
|
||
|
||
last_pid="${pid}"
|
||
start_logcat "${pid}"
|
||
fi
|
||
|
||
sleep 5
|
||
done
|
||
}
|
||
|
||
main "$@"
|