Android日志能力之持久化转储及回传的闭环方案(下)

1,103 阅读8分钟

上帝反馈后

在碰到一些奇奇怪怪的系统问题时,如视频无法播放、网络异常、触摸无响应等等疑难杂症单单靠应用级别的异常信息怕是不能正常分析的,如果没有相应的日志获取能力,那面对用户的应该就是一块板砖,而面对我们的可能就是来自上帝的批判,因此如何在上帝反馈问题后第一时间能够获取到有用的信息并给予用户正向的反馈是及其重要的。

因此,在系统层面设计了一种持久化转储日志信息的三把刀方案:logdump、kdump、xdump。

logdump

logdump最初的设计初衷是为了持久化存储Android log,能够在用户机器出现问题时提供给研发端更多的信息便于分析定位。起初在设计时V1.0版本时只能保存最大两次开机后的日志信息,并且没有增加时间戳区别,回传的日志文件比较不清晰,在进行二次重构时,充分考虑了时间因素,增加了时间戳,能更明显的辨别日志记录的起始位置,由于持久化记录日志会对emmc有损耗,因此,结合公司的物联服务做了下发开关的操作,使得能够进行按需操作:

#!/system/bin/sh

LOG_PATH="/data/logdump"
LOG_DIR_MAX_COUNT=5

if [ ! -d "$LOG_PATH" ]; then
    mkdir -p $LOG_PATH
fi

# remove files not in correct directory.
REMOVE_FILES=`ls -F $LOG_PATH | grep -v /$`
for r_file in $REMOVE_FILES
do
    rm -f $LOG_PATH/$r_file
done

CUR_LOG_DIR_COUNT=`ls -F $LOG_PATH | grep /$ | wc -l`

if [ "$CUR_LOG_DIR_COUNT" -ge "$LOG_DIR_MAX_COUNT" ]; then
    REMOVE_DIR_COUNT=`expr $CUR_LOG_DIR_COUNT - $LOG_DIR_MAX_COUNT + 1`
    REMOVE_DIRS=`ls -F $LOG_PATH | grep /$ | head -$REMOVE_DIR_COUNT`
    for r_dir in $REMOVE_DIRS
    do
        rm -rf $LOG_PATH/$r_dir
    done
fi

if [ "$CUR_LOG_DIR_COUNT" -gt "0" ]; then
    COPY_DIR=`ls -F $LOG_PATH | grep /$ | tail -1`
    cp -rf /sys/fs/pstore/ $LOG_PATH/$COPY_DIR
fi

CUR_LOG_DIR_WITH_TIMESTAMP=`date +%Y-%m-%d_%H%M%S`
mkdir -p $LOG_PATH/$CUR_LOG_DIR_WITH_TIMESTAMP

logcat -G 16m
logcat -v threadtime -r8192 -n6 -f $LOG_PATH/$CUR_LOG_DIR_WITH_TIMESTAMP/log.txt &

在boot阶段logcat的服务已经启动并开始记录日志,因此在init.rc配置中增加了如下属性检测,当检测到persist.sys.logdump=true时则启动logdump的service。

service logdump /system/bin/logdump
    class main
    user root
    group root lodump system
    seclabel u:r:logdump:s0
    oneshot

on boot && property:persist.sys.logdump=true
    start logdump

而persist.sys.logdump的开关控制则由物联平台进行管理下发,实现了远程按需配置的功能。

kdump

通常在userdebug中获取kernel的日志时都是通过dmesg获取,但是dmesg仅仅只能将目前内核日志缓冲区打印出来,而内核日志缓冲区大小是有限的,往往我们在发现问题时再去获取内核日志时打印的dmesg已经不是现场了,对与内核相关问题的调试成为业务很大痛点。并且userdebug我们还能使用adb进行打印,但是出货的user版本我们根本无法获取现场的内核信息,若出现内核相关问题,只能换机调回来调式,因此非常有必要集成一个持续获取内核日志的工具服务。

采用从init起一个服务的方法,专门用于往/data/kdump/中记录kernel log,最大保存5次开机以来的kernel log,每次存储的日志元数据单文件最大支持20M,采用gzip流压缩方式,将占用空间大小尽可能缩减,避免更多的EMMC占用,采用shell编写了一个覆盖算法将新日志保存,实现旧日志清除。

#!/system/bin/sh

LOG_ROOT_DIR="/data/kdump"
KMSG_DIR_MAX_COUNT=5
KMSG_FILE_MAX_COUNT=10

# Create kmsg root directory if not exist.
if [ ! -f "$LOG_ROOT_DIR" ]; then
    mkdir -p $LOG_ROOT_DIR
fi

# Remove files which're not in correct directory.
REMOVE_FILES=`ls -F $LOG_ROOT_DIR | grep -v /$`
for r_file in $REMOVE_FILES
do
    rm -f $LOG_ROOT_DIR/$r_file
done

# Remove more than $KMSG_DIR_MAX_COUNT kmsg storage directories.
CUR_LOG_DIR_COUNT=`ls -F $LOG_ROOT_DIR | grep /$ | wc -l`

if [ "$CUR_LOG_DIR_COUNT" -ge "$KMSG_DIR_MAX_COUNT" ]; then
    REMOVE_DIR_COUNT=`expr $CUR_LOG_DIR_COUNT - $KMSG_DIR_MAX_COUNT + 1`
    REMOVE_DIRS=`ls -F $LOG_ROOT_DIR | grep /$ | head -$REMOVE_DIR_COUNT`
    for r_dir in $REMOVE_DIRS
    do
        rm -rf $LOG_ROOT_DIR/$r_dir
    done
fi

# Create new kmsg storage directory with named timestamp.
CUR_LOG_DIR_WITH_TIMESTAMP=`date +%Y-%m-%d_%H%M%S`
mkdir -p $LOG_ROOT_DIR/$CUR_LOG_DIR_WITH_TIMESTAMP
echo "Create $LOG_ROOT_DIR/$CUR_LOG_DIR_WITH_TIMESTAMP"

do_kmsg_dump(){
    while :
    do
        for i in $(seq 1 $KMSG_FILE_MAX_COUNT)
        do
            f_index=$(expr $KMSG_FILE_MAX_COUNT - $i)
            # Move new kmsg to overlay old.
            if [ -f "$LOG_ROOT_DIR/$CUR_LOG_DIR_WITH_TIMESTAMP/kernel_msg_$(expr $f_index - 1)".gz ]; then
                mv $LOG_ROOT_DIR/$CUR_LOG_DIR_WITH_TIMESTAMP/kernel_msg_$(expr $f_index - 1).gz \
                    $LOG_ROOT_DIR/$CUR_LOG_DIR_WITH_TIMESTAMP/kernel_msg_$f_index.gz -f
            fi
        done
        # Save up to 40MB kmsg metadata in every dump file, use gzip to compress it.
        dd if=/proc/kmsg bs=1k count=40960 | gzip > $LOG_ROOT_DIR/$CUR_LOG_DIR_WITH_TIMESTAMP/kernel_msg_0.gz
    done
}

do_kmsg_dump

在设置单文件元数据大小时发现dd的流采集大小设置为20M时实际最多采集8MB大小原始kmsg,因此调整数据流大小最大40MB,压缩后的gzip包最大不超过2MB,日志目录保存最大10个文件,单目录最大20MBEMMC损耗,全系统最大20*5=100MBEMMC空间损耗,对用户来说整体空间损耗可忽略不计。

xdump

前两个dump功能是日志转储的话,xdump则是进行日志收集功能,将所有的有用信息进行可选择性的收集及上报操作。

在设计时采用了getopt进行参数解析,使得容错性更强,采用主从分离架构,使得单个脚本能够单独使用,由xdump作为总执行脚本放在/system/xbin/xdump下,其余脚本放在/system/etc/xdumps/script下,分别有:android_base_dump、data_data_dump、kernel_base_dump、android_apk_dump:

image.png

xdump v2.00 (2021-08-31) log catcher tool.

usage: xdump [COMMAND] [ARGS]

     -a: dump all logs
     -b [args]: dump android base logs
          all : dump all android base log
          system : dump system core info message.
          media : dump media [video|aduio] info.
          net : dump network info.
          graphic : dump graphic [activity|screen|window] info.
          log : dump system log [anr|dropbox|logdump] info.
     -d [args|<package name>]: dump /data/data packages files.
          all : dump all packages data files
          thirdpart : dump all thirdpart packages data files
          company : dump all company packages data files
          <package name> : dump specific packages data files,
                           each package split with ':'.
     -k: dump kernel base logs
     -s [all|<package name>]: dump company apk xlog files.
          all : dump all packages data files
          <package name> : dump specific packages data files,
                           each package split with ':'.
     -P <path>: dump files path.
     -z : pack xdump files with gzip.
     -h: show this message.
#!/bin/sh

############################################
# @Description: xdump log tool
# @Version: V2.0 2021-08-31
############################################

DEFAULT_XDUMP_ROOT_PATH=/sdcard
xdump_log_root_path=$DEFAULT_XDUMP_ROOT_PATH

XDUMP_SCRIPT_PATH=/system/etc/xdumps/script
XDUMP_LOG_ROOT_NAME=$(getprop ro.serialno)
XDUMP_PACK_FILE_NAME=`date +%Y-%m-%d_%H%M%S`

......

#Help
do_help()
{
    ......
}

do_start()
{
    echo "------------------start xdump------------------"
    start_time=$(date +%s)
}

do_finish()
{
    end_time=$(date +%s)
    delta_time=$(expr ${end_time} - ${start_time})
    echo "-----finish xdump. spend: ${delta_time} s------"
    if [ $XDUMP_PACK_FLAG -eq 1 ]; then
        echo "Log file path: $xdump_file_path/${XDUMP_PACK_FILE_NAME}.tgz"
    else
        echo "Log file path: $xdump_file_path/${XDUMP_LOG_ROOT_NAME}"
    fi
}

create_xdump_log_dir()
{
    xdump_file_path=$xdump_log_root_path/xdumps
    echo "create xdump log dir: $xdump_file_path"
    create_dir_success=`mkdir -p $xdump_file_path 2>&1 > /dev/null ; echo $?`
    if [ $create_dir_success -ne 0 ]; then
        echo "Failed to create $xdump_file_path, please check path."
        exit 1
    fi
}

parse_android_base_params()
{
......
}

parse_data_data_params()
{
......
}

parse_android_apk_params()
{
......
}

parse_arguments()
{
    XDUMP_PACK_FLAG=0
    ALL_DUMP_FLAG=0
    ANDROID_BASE_DUMP_FLAG=0
    KERNEL_DUMP_FLAG=0
    DATA_DUMP_FLAG=0
    ANDROID_APK_DUMP_FLAG=0

    if [ $# -le 0 ]; then
        ALL_DUMP_FLAG=1
        XDUMP_PACK_FLAG=1
        return
    fi

    while getopts ":hab:d:ks:P:z" opt
    do
        case $opt in
            h)
                do_help
                exit 0
                ;;
            a)
                ALL_DUMP_FLAG=1
                ;;
            b)
                ANDROID_BASE_DUMP_FLAG=1
                xdump_android_base_args=$OPTARG
                parse_android_base_params
                ;;
            d)
                DATA_DUMP_FLAG=1
                xdump_data_data_args=$OPTARG
                parse_data_data_params
                ;;
            k)
                KERNEL_DUMP_FLAG=1
                ;;
            s)
                ANDROID_APK_DUMP_FLAG=1
                xdump_android_apk_args=$OPTARG
                parse_android_apk_params
                ;;
            P)
                xdump_log_root_path=$OPTARG
                ;;
            z)
                XDUMP_PACK_FLAG=1
                ;;
            ?)
                echo "Unrecognized parameter."
                do_help
                exit 1
                ;;
        esac
    done

    shift $(($OPTIND - 1))

    if [ $# -gt 0 ]; then
        echo "Invalid parameter, command has non-parameters."
        do_help
        exit 1
    fi
}

dump_android_base()
{
    if [ $XDUMP_PACK_FLAG -eq 1 ]; then
        android_base_args="-z $android_base_args"
    fi
    sh $XDUMP_SCRIPT_PATH/android_base_dump \
        -P $xdump_file_path/${XDUMP_LOG_ROOT_NAME} \
        $android_base_args
}

dump_data_data()
{
    if [ $XDUMP_PACK_FLAG -eq 1 ]; then
        data_data_args="-z $data_data_args"
    fi
    sh $XDUMP_SCRIPT_PATH/data_data_dump \
        -P $xdump_file_path/${XDUMP_LOG_ROOT_NAME} \
        $data_data_args
}

dump_kernel_base()
{
    if [ $XDUMP_PACK_FLAG -eq 1 ]; then
        kernel_base_args="-z $kernel_base_args"
    fi
    sh $XDUMP_SCRIPT_PATH/kernel_base_dump \
        -P $xdump_file_path/${XDUMP_LOG_ROOT_NAME} \
        $kernel_base_args
}

dump_android_apk()
{
    if [ $XDUMP_PACK_FLAG -eq 1 ]; then
        android_apk_args="-z $android_apk_args"
    fi
    sh $XDUMP_SCRIPT_PATH/android_apk_dump \
        -P $xdump_file_path/${XDUMP_LOG_ROOT_NAME} \
        $android_apk_args
}

dump_all()
{
    android_base_args="-a"
    data_data_args="-a"
    android_apk_args="-a"

    dump_android_base &
    dump_data_data &
    dump_kernel_base &
    dump_android_apk &
    wait
}

execute_scripts()
{
    if [ $ALL_DUMP_FLAG -eq 1 ]; then
        dump_all
    else
        if [ $ANDROID_BASE_DUMP_FLAG -eq 1 ]; then
            dump_android_base &
        fi

        if [ $KERNEL_DUMP_FLAG -eq 1 ]; then
            dump_kernel_base &
        fi

        if [ $DATA_DUMP_FLAG -eq 1 ]; then
            dump_data_data &
        fi

        if [ $ANDROID_APK_DUMP_FLAG -eq 1 ]; then
            dump_android_apk &
        fi
        wait
    fi
}

pack_xdump_files()
{
    if [ -d $xdump_file_path/${XDUMP_LOG_ROOT_NAME} ]; then
        cd $xdump_file_path
        tar zcf ${XDUMP_PACK_FILE_NAME}.tgz ./${XDUMP_LOG_ROOT_NAME}
        rm -rf $xdump_file_path/${XDUMP_LOG_ROOT_NAME}
    fi
}


do_xdump()
{
    do_start
    parse_arguments $*
    create_xdump_log_dir
    execute_scripts
    if [ $XDUMP_PACK_FLAG -eq 1 ]; then
        pack_xdump_files
    fi
    do_finish
}

do_xdump $*

Android基础信息可选化导出功能

由于公司物联平台的上行带宽并不宽裕,且并非每次回传都需要获取所有完整日志,因此,在设计时加入了针对system core、network、media等等的分类可选参数,使得我们能够自己选择回传哪些日志信息:

usage: android_base_dump [ARGS]

     -a: dump all android base logs.
     -s: dump system core info message.
     -m: dump media [video|aduio] info.
     -n: dump network info.
     -g: dump graphic [activity|screen|window] info.
     -l: dump system log [anr|dropbox|logdump] info.
     -P <path>: dump files path.
     -z : pack files with gzip.
     -h: show this message.
#!/bin/sh

DEFAULT_ANDROID_BASE_ROOT_PATH=/sdcard/xdumps
dump_android_base_root_path=$DEFAULT_ANDROID_BASE_ROOT_PATH

#Help
do_help()
{
......
}

create_log_dir()
{
    android_base_log_path=$dump_android_base_root_path/android_base
    # echo "create android base log dir: $android_base_log_path"
    create_dir_success=`mkdir -p $android_base_log_path 2>&1 > /dev/null ; echo $?`
    if [ $create_dir_success -ne 0 ]; then
        echo "Failed to create $android_base_log_path, please check path."
        exit 1
    fi
}

clear_default_history()
{
    if [ -e $DEFAULT_ANDROID_BASE_ROOT_PATH/android_base ]; then
        echo "clear history default dump path."
        rm -rf $DEFAULT_ANDROID_BASE_ROOT_PATH/android_base
    fi
}

parse_arguments()
{
    ALL_FLAG=0
    PACK_FLAG=0
    SYSTEM_DUMP_FLAG=0
    MEDIA_DUMP_FLAG=0
    NETWORK_DUMP_FLAG=0
    GRAPHIC_DUMP_FLAG=0
    LOG_DUMP_FLAG=0

    if [ $# -le 0 ]; then
        echo "Invalid call, an arguments excpet -P must be specified."
        do_help
        exit 1
    fi

    while getopts ":hasmnglP:z" opt
    do
        case $opt in
            h)
                do_help
                exit 0
                ;;
            a)
                ALL_FLAG=1
                ;;
            s)
                SYSTEM_DUMP_FLAG=1
                ;;
            m)
                MEDIA_DUMP_FLAG=1
                ;;
            n)
                NETWORK_DUMP_FLAG=1
                ;;
            g)
                GRAPHIC_DUMP_FLAG=1
                ;;
            l)
                LOG_DUMP_FLAG=1
                ;;
            P)
                dump_android_base_root_path=$OPTARG
                ;;
            z)
                PACK_FLAG=1
                ;;
            ?)
                echo "Invalid parameters."
                do_help
                exit 1
                ;;
        esac
    done

    shift $(($OPTIND - 1))

    if [ $dump_android_base_root_path != $DEFAULT_ANDROID_BASE_ROOT_PATH ]; then
        if [ $ALL_FLAG -eq 0 -a $SYSTEM_DUMP_FLAG -eq 0 -a $MEDIA_DUMP_FLAG -eq 0 -a \
            $NETWORK_DUMP_FLAG -eq 0 -a $GRAPHIC_DUMP_FLAG -eq 0 -a $LOG_DUMP_FLAG -eq 0 ]; then
            echo "Invalid call, an arguments excpet -P must be specified."
            do_help
            exit 1
        fi
    fi

    if [ $# -gt 0 ]; then
        echo "Invalid parameter, command has non-parameters."
        do_help
        exit 1
    fi

    if [ $ALL_FLAG -eq 1 ]; then
        SYSTEM_DUMP_FLAG=1
        MEDIA_DUMP_FLAG=1
        NETWORK_DUMP_FLAG=1
        GRAPHIC_DUMP_FLAG=1
        LOG_DUMP_FLAG=1
    fi
}

# system dump
system_dump()
{
    SYSTEM_LOGPATH=${android_base_log_path}/system
    mkdir -p ${SYSTEM_LOGPATH}
    chmod 755 ${SYSTEM_LOGPATH}
    dumpsys meminfo > ${SYSTEM_LOGPATH}/dumpsys_meminfo.txt
    dumpsys cpuinfo > ${SYSTEM_LOGPATH}/dumpsys_cpuinfo.txt
    dumpsys mount > ${SYSTEM_LOGPATH}/dumpsys_mount.txt
    dumpsys input > ${SYSTEM_LOGPATH}/dumpsys_input.txt
    pm list packages -f > ${SYSTEM_LOGPATH}/package_list.txt
    df -h > ${SYSTEM_LOGPATH}/disk_info.txt
    free -h > ${SYSTEM_LOGPATH}/free_info.txt
    ps -efZ > ${SYSTEM_LOGPATH}/ps.txt
    lsmod > ${SYSTEM_LOGPATH}/lsmod.txt
    lsusb > ${SYSTEM_LOGPATH}/lsusb.txt
    service list > ${SYSTEM_LOGPATH}/service_list.txt
    cat /proc/meminfo > ${SYSTEM_LOGPATH}/meminfo.txt
    cat /proc/cpuinfo > ${SYSTEM_LOGPATH}/cpuinfo.txt
}

# Media dump
media_dump()
{
    MEDIA_LOGPATH=${android_base_log_path}/media
    mkdir -p ${MEDIA_LOGPATH}
    chmod 755 ${MEDIA_LOGPATH}
    dumpsys media.audio_policy > ${MEDIA_LOGPATH}/dumpsys_audio_policy.txt
    dumpsys media.audio_flinger > ${MEDIA_LOGPATH}/dumpsys_audio_flinger.txt
    dumpsys audio > ${MEDIA_LOGPATH}/dumpsys_audio.txt
    dumpsys media.camera > ${MEDIA_LOGPATH}/dumpsys_camera.txt
}


#network
network_dump()
{
    NETWORK_LOGPATH=${android_base_log_path}/network
    mkdir -p ${NETWORK_LOGPATH}
    chmod 755 ${NETWORK_LOGPATH}
    ifconfig -a > ${NETWORK_LOGPATH}/ifconfig.txt
    ip rule > ${NETWORK_LOGPATH}/ip_rule.txt
    ip route show table main > ${NETWORK_LOGPATH}/ip_route.txt
    dumpsys wifi > ${NETWORK_LOGPATH}/dumpsys_wifi.txt
    dumpsys ethernet > ${NETWORK_LOGPATH}/dumpsys_ethernet.txt
    iptables -t filter -nvL > ${NETWORK_LOGPATH}/iptables_filter.txt
    iptables -t nat -nvL > ${NETWORK_LOGPATH}/iptables_nat.txt
    iptables -t mangle -nvL > ${NETWORK_LOGPATH}/iptables_mangle.txt
}

#capture
graphic_dump()
{
    GRAPHIC_LOGPATH=${android_base_log_path}/graphic
    mkdir -p ${GRAPHIC_LOGPATH}
    chmod 755 ${GRAPHIC_LOGPATH}
    dumpsys activity > ${GRAPHIC_LOGPATH}/dumpsys_activity.txt
    dumpsys window > ${GRAPHIC_LOGPATH}/dumpsys_window.txt
    screencap -p ${GRAPHIC_LOGPATH}/`date +%Y%m%d%H%M%S`.png
    #echo capture > /proc/msp/hifb0
}

#copy
log_dump()
{
    if [ -e /data/anr ]; then
        cp -R /data/anr ${android_base_log_path}/
    fi

    if [ -e /data/tombstones ]; then
        cp -R /data/tombstones ${android_base_log_path}/
    fi

    if [ -e /data/system/dropbox ]; then
        cp -R /data/system/dropbox ${android_base_log_path}/
    fi

    if [ -e /data/logdump ]; then
        cp -R /data/logdump ${android_base_log_path}/
    fi
}

pack_log_files()
{
    if [ -d $android_base_log_path ]; then
        cd $android_base_log_path/..
        tar zcf $(basename $android_base_log_path).tgz ./$(basename $android_base_log_path)
        rm -rf $android_base_log_path
    fi
}

do_dump_android_base()
{
    parse_arguments $*
    clear_default_history
    create_log_dir
    # echo "Dumping android_base logs ..."
    if [ $SYSTEM_DUMP_FLAG -eq 1 ]; then
        system_dump &
    fi
    if [ $MEDIA_DUMP_FLAG -eq 1 ]; then
        media_dump &
    fi
    if [ $NETWORK_DUMP_FLAG -eq 1 ]; then
        network_dump &
    fi
    if [ $GRAPHIC_DUMP_FLAG -eq 1 ]; then
        graphic_dump &
    fi
    if [ $LOG_DUMP_FLAG -eq 1 ]; then
        log_dump &
    fi
    wait

    if [ $PACK_FLAG -eq 1 ]; then
        pack_log_files
    fi
}

do_dump_android_base $*

指定包名获取data/data/文件功能

在各种售后问题中,需要获取三方应用/data分区数据来分析问题的情况不在少数,作为之前一直遗留的业务痛点,此次在设计中增加了针对应用包名的/data/data/包名的文件导出功能,能够在三方应用出现问题时有能力获取其私有目录,提供给三方应用开发方一同分析解决。

#!/bin/sh

DEFAULT_THIRDPART_ROOT_PATH=/sdcard/xdumps
dump_data_root_path=$DEFAULT_THIRDPART_ROOT_PATH

#Help
do_help()
{
......
}

clear_history()
{
......
}

create_data_data_log_dir()
{
......
}

create_third_part_log_dir()
{
    thirdpart_log_path=$data_data_log_path/thirdpart
    # echo "create data thirdpart log dir: $thirdpart_log_path"
    create_thirdpart_dir_success=`mkdir -p $thirdpart_log_path 2>&1 > /dev/null ; echo $?`
    if [ $create_thirdpart_dir_success -ne 0 ]; then
        echo "Failed to create $thirdpart_log_path, please check path."
        exit 1
    fi
}

parse_arguments()
{
    ALL_FLAG=0
    PACK_FLAG=0
    THIRTPART_ALL_FLAG=0
    ANDROID_ALL_FLAG=0
    while getopts ":hatsP:z" opt
    do
        case $opt in
            h)
                do_help
                exit 0
                ;;
            a)
                ALL_FLAG=1
                ;;
            t)
                THIRTPART_ALL_FLAG=1
                ;;
            P)
                dump_data_root_path=$OPTARG
                ;;
            z)
                PACK_FLAG=1
                ;;
            ?)
                echo "Invalid parameters."
                do_help
                exit 1
                ;;
        esac
    done
    shift $(($OPTIND - 1))
    if [ $# -le 0 ]; then
        if [ $ALL_FLAG -eq 0 -a $ANDROID_ALL_FLAG -eq 0 -a $THIRTPART_ALL_FLAG -eq 0 ]; then
            echo "Invalid parameter, application package name was not specified."
            do_help
            exit 1
        fi
    fi
    specific_packages=$*
}

# Only dump non-system_app in /data/data/
dump_thirdpart_all()
{
    create_third_part_log_dir
    thirdpart_all_packages_in_data=`ls /data/data |\
        grep -v -E "^com.android|^com.mediatek|^android"`
    for thirdpart_package in $thirdpart_all_packages_in_data
    do
        is_system_app=`pm list packages -f | grep $thirdpart_package |\
            grep -E "\/system\/app\/|\/system\/priv-app\/" 2>&1 > /dev/null ; \
            echo $?`
        if [ $is_system_app -eq 0 ]; then
            continue
        fi
        # echo "Dumping package $thirdpart_package ..."
        cp -R /data/data/$thirdpart_package $thirdpart_log_path/ 2>&1 >> /dev/null
    done
}

dump_all()
{
    dump_thirdpart_all &
    wait
}

dump_specific_packages()
{
    create_third_part_log_dir
    for thirdpart_package in $specific_packages
    do
        if [ -e /data/data/$thirdpart_package ]; then
            # echo "Dumping $thirdpart_package ..."
            cp -R /data/data/$thirdpart_package $thirdpart_log_path/ 2>&1 >> /dev/null
        else
            echo "Specific package $thirdpart_package not exist in /data/data, skip it."
        fi
    done
}

pack_log_files()
{
    if [ -d $data_data_log_path ]; then
        cd $data_data_log_path/..
        tar zcf $(basename $data_data_log_path).tgz ./$(basename $data_data_log_path)
        rm -rf $data_data_log_path
    fi
}

execute_dump()
{
......
}

execute_dump $*

指定kernel日志导出功能

基于kdump的日志持久化转储功能,在执行xdump时能够选择性将转储的kernel日志进行导出:

#!/bin/sh

DEFAULT_KERNEL_BASE_ROOT_PATH=/sdcard/xdumps
dump_kernel_root_path=$DEFAULT_KERNEL_BASE_ROOT_PATH

#Help
do_help()
{
......
}

create_kernel_log_dir()
{
    kernel_log_path=$dump_kernel_root_path/kernel
    # echo "create kernel base log dir: $kernel_log_path"
    create_dir_success=`mkdir -p $kernel_log_path 2>&1 > /dev/null ; echo $?`
    if [ $create_dir_success -ne 0 ]; then
        echo "Failed to create $kernel_log_path, please check path."
        exit 1
    fi
}

clear_default_history()
{
    if [ -e $DEFAULT_KERNEL_BASE_ROOT_PATH/kernel ]; then
        echo "clear history default dump path."
        rm -rf $DEFAULT_KERNEL_BASE_ROOT_PATH/kernel
    fi
}

parse_arguments()
{
    PACK_FLAG=0
    while getopts ":hP:z" opt
    do
        case $opt in
            h)
                do_help
                exit 0
                ;;
            P)
                dump_kernel_root_path=$OPTARG
                ;;
            z)
                PACK_FLAG=1
                ;;
            ?)
                echo "Invalid parameters."
                do_help
                exit 1
                ;;
        esac
    done

    shift $(($OPTIND - 1))

    if [ $# -gt 0 ]; then
        echo "Invalid parameter, command has non-parameters."
        do_help
        exit 1
    fi
}

pack_log_files()
{
    if [ -d $kernel_log_path ]; then
        cd $kernel_log_path/..
        tar zcf $(basename $kernel_log_path).tgz ./$(basename $kernel_log_path)
        rm -rf $kernel_log_path
    fi
}

kernel_dump()
{
    KDUMP_LOG_PATH=/data/kdump

    if [ -d $KDUMP_LOG_PATH ]; then
        cp -rRf $KDUMP_LOG_PATH/. $kernel_log_path
    else
        dmesg > ${kernel_log_path}/dmesg.txt
    fi

    if [ ${PACK_FLAG} -eq 1 ]; then
        pack_log_files
    fi
}

do_dump_kernel_base()
{
    parse_arguments $*
    clear_default_history
    create_kernel_log_dir
    kernel_dump
}

do_dump_kernel_base $*

指定包名获取xlog功能

由于公司开发的应用均集成了腾讯的xlog,因此做了专项导出的操作,可应用记录的xlog通过包名可选性导出,功能的实现大同小异,此处只展示usage help:

usage: android_apk_dump [ARGS] <package names>\n
     -a: dump all packages sdcard data files
     -P <path>: dump files path.
     -z : pack files with gzip.
     -h: show this message.

日志本地U盘自动导出功能

在设计时增加了U盘自动导出功能,使用时需要在U盘根目录增加校验文件,插入U盘后系统将自动进行文件校验,若校验成功则开始进行日志导出,配合应用端增加导出开始和导出完成的toast提醒。

为了避免系统各个服务间的强耦合阻塞关系,采用了initrc机制进行事件触发的架构。在Android中所有的USB时间由kernel进行监听处理,当检测到U盘插入后进行挂载之后上报uevent事件,之后由Native层Vold执行挂载后的操作,挂载完成后会将事件上报到framework层,ExternalStorageProvider能够获取U盘挂载的绝对路径,因此在ExternalStorageProvider中进行获取挂载路径,并进行文件校验,若检测到U盘中存在上述校验文件后将获取到的U盘挂载路径写入属性sys.local.xdump.root中,并设置标志位属性sys.local.xdump为1,触发系统进行日志捕获,捕获日志完成后与IOT服务联动发出toast提醒:

image.png

    private void checkExecLocalXdumpIfNeeded(File xdumpLicenseKeyRoot) {
        if (xdumpLicenseKeyRoot == null) {
            Log.e(TAG, "Get null pointer mountPath, return.");
            return;
        }
        String xdumpLicenseKeyPath = ".xdump";
        File xdumpLicenseKeyFile = new File(xdumpLicenseKeyRoot, xdumpLicenseKeyPath);
        if (xdumpLicenseKeyFile != null) {
            if (xdumpLicenseKeyFile.exists()) {
                Log.d(TAG, "Xdump license file validation succeeded, do local xdump.");
                String xdumpLicenseKeyRootAbsPath = xdumpLicenseKeyRoot.getAbsolutePath();
                if (xdumpLicenseKeyRootAbsPath != null && xdumpLicenseKeyRootAbsPath.length() != 0) {
                    SystemProperties.set("sys.local.xdump.root", xdumpLicenseKeyRootAbsPath);
                    SystemProperties.set("sys.local.xdump", "1");
                } else {
                    Log.e(TAG, "Get xdumpLicenseKeyRootAbsPath null or empty.");
                    return;
                }
            }
        }
    }

    private void updateVolumesLocked() {
        ......
        if (disk != null && disk.isSd()) {
            root.flags |= Root.FLAG_REMOVABLE_SD;
        } else if (disk != null && disk.isUsb()) {
            root.flags |= Root.FLAG_REMOVABLE_USB;
            if (volume.isMountedWritable()) {
                boolean bootCompleted = "1".equals(SystemProperties.get("sys.boot_completed"));
                if (bootCompleted) {
                    String isXdumpRunning = SystemProperties.get("sys.local.xdump", "");
                    Log.d(TAG, "isXdumpRunning = [" + isXdumpRunning + "]");
                    if (isXdumpRunning != null && !isXdumpRunning.equals("running")) {
                        File mountExternalStroagePath = volume.getPath();
                        if (mountExternalStroagePath != null) {
                            Log.d(TAG, "Found public writable volume: " + volume);
                            checkExecLocalXdumpIfNeeded(mountExternalStroagePath);
                        }
                    }
                }
            }
        }
        ......
    }

init.xdump.rc

service local_xdump /system/bin/local_xdump
    class main
    user root
    group root system
    seclabel u:r:xdump:s0
    oneshot

on property:sys.boot_completed=1 && property:sys.local.xdump=1
    start local_xdump
......
#!/bin/sh

xdump_license_key_file=".xdump"

pre_finish()
{
    # Unlock xdump running.
    setprop sys.local.xdump.root ""
    setprop sys.local.xdump ""
}

check_environment_ready()
{
    # Check boot completed.
    boot_completed=$(getprop sys.boot_completed)
    if [ "$boot_completed" != "1" ]; then
        echo "Not boot completed, exit."
        pre_finish
        exit 1
    fi

    # Check xdump property enable.
    local_xdump_enable=$(getprop sys.local.xdump)
    if [ "$local_xdump_enable" != "1" ]; then
        echo "Local xdump not avalibe, exit."
        pre_finish
        exit 1
    fi

    # Check the mount path and the license key file effective .
    mount_path=$(getprop sys.local.xdump.root)
    if [ ! -f $mount_path/$xdump_license_key_file ]; then
        echo "xdump license file validation failed, exit."
        pre_finish
        exit 1
    fi

    echo "check mount path success."
}

do_local_xdump()
{
    check_environment_ready

    # Lock the xdump running.
    setprop sys.local.xdump "running"
    setenforce 0
    am broadcast -a com.android.action.LOCAL_XDUMP_START
    xdump_success=$(/system/xbin/xdump -a -z 2>&1 >> /dev/null ; echo $?)
    if [ $xdump_success -ne 0 ]; then
        echo "Error, failed to execuate xdump scripts."
        pre_finish
        exit 1
    fi

    # Unlock xdump running.
    setprop sys.local.xdump.root ""
    setprop sys.local.xdump ""

    # Do xdump file copy.
    cp -R /sdcard/xdumps $mount_path
    rm -rf /sdcard/xdumps
    am broadcast -a com.android.action.LOCAL_XDUMP_FINISHED
    setenforce 1
}

do_local_xdump

执行local_xdump前和执行完成后分别发送了两个broadcast:

  • com.android.action.LOCAL_XDUMP_START
  • com.android.action.LOCAL_XDUMP_FINISHED

这两个广播由CIOTExtendService接收并执行弹出toast提醒的操作:

IotService::LocalXDumpReceiver.kt

class LocalXDumpReceiver : BroadcastReceiver() {
    private var isRegisterReceiver = false
    override fun onReceive(context: Context?, intent: Intent?) {
        if (context != null && intent != null) {
            when (intent.action) {
                localXDumpStartIntentAction ->
                Toast.makeText(context, R.string.local_dump_start_text, Toast.LENGTH_LONG).show()
                localXDumpFinishIntentAction ->
                    Toast.makeText(context, R.string.local_dump_finish_text, Toast.LENGTH_LONG).show()
            }
        }
    }

    fun registerLocalXDumpReceiver(mContext: Context) {
        if (!isRegisterReceiver) {
            isRegisterReceiver = true
            val filter = IntentFilter()
            filter.addAction(localXDumpStartIntentAction)
            filter.addAction(localXDumpFinishIntentAction)
            mContext.registerReceiver(this, filter)
        }
    }

    fun unRegisterLocalXDumpReceiver(mContext: Context) {
        if (isRegisterReceiver) {
            isRegisterReceiver = false
            mContext.unregisterReceiver(this)
        }
    }

    companion object {
        private const val TAG = "LocalXDmpReceiver"
        private const val localXDumpStartIntentAction = "com.android.action.LOCAL_XDUMP_START"
        private const val localXDumpFinishIntentAction = "com.android.action.LOCAL_XDUMP_FINISHED"
    }
}

远程下发日志导出机制

在架构设计时考虑到解耦合,同样采用了initrc事件机制进行处理,IotService接收到后台发送的日志请求后进行参数解析,设置Settings数据库Settings.Secure.remote_xdump_parms,保存xdump执行的参数,之后设置sys.remote.xdump属性为1触发远程xdump:

image.png

IotService::LogsReportHandler.kt

class LogsReportHandler : IServiceHandler.Stub(), CoroutineScope {
    private val TAG = "LogsReportHandler"
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Default

    override fun onRequest(topic: String?, method: String?, params: String?, traceId: String?) {
        val remoteXDumpParams = JSONObject(params).get("params") as String?
        setRemoteXDumpParams(remoteXDumpParams)
        setRemoteXDumpEnabled()
    }

    private fun setRemoteXDumpParams(params: String?) {
        ......
    }

    fun setRemoteXDumpEnabled() {
        val remoteXDumpStatus = SystemPropertiesUtils.get(REMOTE_XDUMP_STATUS_PROPERTY, "stopped")
        val remoteXDumpEnabled = SystemPropertiesUtils.get(REMOTE_XDUMP_ENABLE_PROPERTY, "0")
        
        ......
        
        Log.d(TAG, "Execute remote xdump...")
        SystemPropertiesUtils.setInt(REMOTE_XDUMP_ENABLE_PROPERTY, 1)
    }
}

init.xdump.rc

service remote_xdump /system/bin/remote_xdump
    class main
    user root
    group root system
    seclabel u:r:xdump:s0
    oneshot

on property:init.svc.remote_xdump=stopped
    setprop sys.remote.xdump ""

on property:sys.boot_completed=1 && property:sys.remote.xdump=1
    start remote_xdump

remote_xdump

#!/bin/sh

# Settings secure remote_xdump_parms
SETTING_REMOTE_XDUMP_PARMS=remote_xdump_parms
REMOTE_XDUMP_ENABLE=sys.remote.xdump

# Do some clean operation at pre finish.
pre_finish()
{
    settings delete secure $SETTING_REMOTE_XDUMP_PARMS
    setprop $REMOTE_XDUMP_ENABLE ""
}

# Send remote xdump start action to CIOT.
send_start()
{
    am broadcast -a com.android.action.LOCAL_XDUMP_START
}

# Send remote xdump finished action to CIOT.
send_finish()
{
    am broadcast -a com.android.action.LOCAL_XDUMP_FINISHED
}

# Send remote xdump failed action to CIOT.
send_failed()
{
    err_msg=$1
    echo $err_msg
    am broadcast -a com.android.action.REMOTE_XDUMP_FAILED --es msg $err_msg
}

# Send remote xdump success action to CIOT with xdump log path.
send_success()
{
    echo "Remote xdump execute success."
    am broadcast -a com.android.action.REMOTE_XDUMP_SUCCESS --es path $1
}

# Check xdump environment is ready.
check_environment_ready()
{
    # Check boot completed.
    boot_completed=$(getprop sys.boot_completed)
    if [ "$boot_completed" != "1" ]; then
        echo "Not boot completed, exit."
        pre_finish
        exit 1
    fi

    # Check xdump property enable && xdump is not running.
    remote_xdump_enabled=$(getprop $REMOTE_XDUMP_ENABLE)
    if [ "$remote_xdump_enabled" != "1" ]; then
        if [ "$remote_xdump_enabled" == "running" ]; then
            send_failed "Remote xdump is running."
            exit 1
        fi
        send_failed "Remote xdump not avalibe."
        pre_finish
        exit 2
    fi

    echo "check environment success."
}

# Check xdump params is valid.
check_xdump_params_vaild()
{
    # If not set xdump params then set params to default.
    xdump_parms=$(settings get secure $SETTING_REMOTE_XDUMP_PARMS)
    if [ -z "$xdump_parms" -o "$xdump_parms" == "null"]; then
        xdump_parms="-a -z"
    fi
}

# Do remote xdump
do_remote_xdump()
{
    check_environment_ready
    # send_start

    # Lock the xdump running.
    setprop $REMOTE_XDUMP_ENABLE "running"
    setenforce 0
    check_xdump_params_vaild

    xdump_res=$(/system/xbin/xdump $xdump_parms | tail -1 | awk -F':' '{print $2}')
    if [ -z "$xdump_res" -o "$xdump_res" == "" ]; then
        send_failed "Remote xdump execute failed."
    else
        xdump_log_path=$(echo $xdump_res)
        send_success $xdump_log_path
    fi

    # send_finish
    pre_finish
    setenforce 1
}

do_remote_xdump

IotService::RemoteXDumpReceiver.kt

class RemoteXDumpReceiver : BroadcastReceiver() {
    private var isRegisterReceiver = false
    override fun onReceive(context: Context?, intent: Intent?) {
        if (context != null && intent != null) {
            when (intent.action) {
                remoteXDumpSuccessIntentAction -> {
                    val xDumpFilePath = intent.getStringExtra("path")
                    Log.d(TAG, "onReceive: Get remote xdump log path -> [$xDumpFilePath].")
                    if (!xDumpFilePath.isNullOrEmpty()) {
                        val remoteXDumpLogFile = File(xDumpFilePath)
                        if (remoteXDumpLogFile.exists()) {
                            val cstoreListener = CstoreListener()
                            cstoreListener.uploadCStoreFile(xDumpFilePath)
                        } else {
                            Log.e(TAG, "$remoteXDumpLogFile is not exist.")
                        }
                    }
                }
                remoteXDumpFiledIntentAction -> {
                    val errMsg = intent.getStringExtra("msg")
                    if (!errMsg.isNullOrEmpty()) {
                        Log.e(TAG, "onReceive: Get remote xdump execute failed.")
                    }
                }
            }
        }
    }

......

    companion object {
        private const val TAG = "RemoteXDmpReceiver"
        private const val remoteXDumpFiledIntentAction = "com.android.action.REMOTE_XDUMP_FAILED"
        private const val remoteXDumpSuccessIntentAction = "com.android.action.REMOTE_XDUMP_SUCCESS"
        const val REMOTE_XDUMP_STATUS_PROPERTY = "init.svc.remote_xdump"
        const val REMOTE_XDUMP_ENABLE_PROPERTY = "sys.remote.xdump"
        const val REMOTE_XDUMP_PARMS_SETTINGS_KEY = "remote_xdump_parms"
        const val REMOTE_XDUMP_PARMS_DEFAULT = "-b all -k -s all -z"
    }
}

总结

至此,就是日志持久化转储及回传的完整闭环方案了,欢迎各位大佬点评指正。