Skip to content

Instantly share code, notes, and snippets.

@rezazoom
Last active June 23, 2026 08:52
Show Gist options
  • Select an option

  • Save rezazoom/88e6ed49641ccc5b8969aabb04a8ff0f to your computer and use it in GitHub Desktop.

Select an option

Save rezazoom/88e6ed49641ccc5b8969aabb04a8ff0f to your computer and use it in GitHub Desktop.
#!/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 "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment