Last active
June 23, 2026 08:52
-
-
Save rezazoom/88e6ed49641ccc5b8969aabb04a8ff0f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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