返回

adbd挂了怎么办?嵌入式Linux本地检测与自动重启

Android

本地与 adbd 掰手腕:如何检测嵌入式设备上的 adbd 是否正常工作

搞嵌入式 Linux 开发的时候,adbd 是个挺方便的工具,能让我们通过 adb shell 轻松连上设备。但有时候,它也会闹点小脾气。比如,你可能会遇到明明设备跑得好好的,adb shell 却突然告诉你 error: no devices/emulators found,非得跑到串口控制台手动重启 adbd 才行。这可太折腾人了!

咋办呢?要是能在设备上自己检查 adbd 是不是挂了,然后自动重启它,不就省事多了?这篇博客,咱们就来聊聊怎么在设备本地跟 adbd "交流",判断它的状态。

先看看现场情况:在一个跑着 kernel-5.10.188 和 busybox 的嵌入式 Linux 系统里,adbd 确实在运行。用 lsofnetstat 瞅瞅:

# lsof | grep adbd
201     /usr/bin/adbd   ... (省略部分输出) ...
201     /usr/bin/adbd   12      socket:[3943]  # Unix domain socket?
201     /usr/bin/adbd   13      socket:[3944]  # Unix domain socket?
... (省略部分输出) ...

# netstat -antp | grep 5037  # 过滤 TCP 监听端口
tcp        0      0 127.0.0.1:5037          0.0.0.0:*               LISTEN      -
# netstat -anp # 看下完整的,包含 unix domain sockets
Active Internet connections (servers and established)
...
tcp        0      0 localhost:5037          0.0.0.0:*               LISTEN
...
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node Path
...
unix  2      [ ACC ]     STREAM     LISTENING       3344 @jdwp-control # 这个看起来和 adbd 有关
...
unix  3      [ ]         STREAM     CONNECTED       3944             # adbd 的 lsof 输出里有
unix  3      [ ]         STREAM     CONNECTED       3346             # adbd 的 lsof 输出里有
unix  3      [ ]         STREAM     CONNECTED       3943             # adbd 的 lsof 输出里有
...

netstat 输出看,adbd (或者某个相关进程) 确实在 127.0.0.15037 端口上监听 TCP 连接。

直觉上,既然它在监听 5037 端口,那我们直接用代码连上去,发个命令试试水不就行了?于是,有人尝试了下面的 C 代码,想通过 127.0.0.1:5037 发送 host:versionhost:devices 命令:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define ADB_HOST "127.0.0.1"
#define ADB_PORT 5037

void send_adb_command(const char *command) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket creation failed");
        return;
    }

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(ADB_PORT),
        .sin_addr.s_addr = inet_addr(ADB_HOST)
    };

    // 设置一个短暂的连接和接收超时,避免卡死
    struct timeval timeout = {2, 0}; // 2 秒超时
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
    setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));


    if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("connect failed");
        close(sock);
        return;
    }

    // ADB 协议:先发4字节的命令长度(十六进制字符串),再发命令本身
    char header[5];
    snprintf(header, sizeof(header), "%04X", (unsigned int)strlen(command));

    if (send(sock, header, 4, 0) < 0) {
        perror("send header failed");
        close(sock);
        return;
    }
    if (send(sock, command, strlen(command), 0) < 0) {
        perror("send command failed");
        close(sock);
        return;
    }

    char response[1024] = {0}; // 初始化,避免打印垃圾数据
    ssize_t bytes_received = recv(sock, response, sizeof(response)-1, 0);

    if (bytes_received < 0) {
        perror("recv failed");
    } else if (bytes_received == 0) {
        printf("Connection closed by peer.\n");
    } else {
        // response[bytes_received] = '\0'; // 确保字符串结尾
        printf("Raw Response (length %zd): %s\n", bytes_received, response);
        // 这里可以加更复杂的解析,检查是否符合预期
    }

    close(sock);
}

int main() {
    printf("Attempting command: host:version\n");
    send_adb_command("host:version");

    printf("\nAttempting command: host:devices\n");
    send_adb_command("host:devices");

    return 0;
}

运行这段代码,结果可能并不如意。往往是连接成功,但收不到预期的响应,甚至直接报错。

为啥 host: 命令不好使?

问题的关键在于 adb 的架构和 adbd 的角色。

通常我们用 adb 的时候,是这样的:

  1. adb 客户端 (PC 上) :你敲 adb shell 的那个程序。
  2. adb 服务器 (PC 上) :一个后台进程,通常叫 adb server。它负责管理所有连接的设备,监听 TCP 端口 5037 (默认在本机 127.0.0.1)。客户端发出的命令,比如 adb devices,实际上是发给这个服务器的。
  3. adbd 守护进程 (设备上) :跑在你的嵌入式设备或手机上的后台进程。它负责执行具体的操作,比如启动一个 shell。adbd 会通过 USB 或者 TCP/IP 跟 PC 上的 adb server 连接。

现在我们分析一下 host: 前缀的命令,比如 host:versionhost:deviceshost:transport:serial-number。这些命令其实是 adb 客户端发给 PC 上的 adb server 的,用来查询服务器信息或请求连接到特定设备。

而设备上的 adbd 呢?它的主要任务是 响应来自 adb server 的针对 本设备 的请求 ,比如执行 shell 命令、传输文件 (push/pull) 等。它通常 处理 host: 这种管理性质的命令。

用户提到 adbd 是用 ADB_HOST = 0 编译的。这通常意味着 adbd 在编译时被配置为 扮演 "host" (即 adb server) 的角色。当你在设备本地尝试连接 127.0.0.1:5037 并发送 host: 命令时,adbd 很可能直接拒绝或者不理解这种命令,因为它觉得自己只是一个 "device daemon",不是 "host server"。即使它监听了 5037 端口(通常是为了支持 TCP/IP 模式的 adb 连接),它的内部逻辑也决定了它只处理设备相关的指令流。

所以,想用 host:version 这类命令来探测本地 adbd 的状态,路子可能就走歪了。

那该咋探测本地 adbd

既然 host: 命令不行,我们需要找到 adbd 能听懂 的命令。adbd 真正关心的,是那些没有 host: 前缀的、具体到设备操作的命令。那么,哪些方法能用来判断本地 adbd 还活着并且能响应呢?

方法一:检查进程是否存在

最简单粗暴的办法:看看 adbd 进程还在不在。

# 使用 pgrep (如果 busybox 支持)
pgrep adbd
# 或者使用 ps 配合 grep
ps w | grep -v grep | grep adbd
  • 原理: 直接检查操作系统进程列表。
  • 优点: 非常简单、快速。
  • 缺点: 只能确认进程存在,不能保证它没卡死或者功能正常。有时候进程活着,但已经无法响应连接了。

方法二:检查端口是否在监听

既然我们看到 adbd127.0.0.1:5037 监听,那就检查这个端口是不是真的还在监听状态。

# 使用 netstat (比较通用)
netstat -ltnp | grep ':5037' | grep 'LISTEN'

# 或者使用 ss (更新一些的系统可能更推荐)
ss -ltnp | grep ':5037' | grep 'LISTEN'
  • 原理: 查看系统的网络连接状态表,确认是否有进程在指定地址和端口上等待连接。
  • 优点: 能确认网络服务的基础设施还在。
  • 缺点: 同方法一,监听不代表服务一定健康。可能 adbd 监听着端口,但内部处理逻辑已经卡住了。

方法三:模拟客户端,发送有效服务请求

这是最靠谱的方法,因为它直接模拟了正常客户端(比如 adb shell 命令背后发生的事)与 adbd 的交互过程。我们需要连接到 adbd 监听的端口(这里是 127.0.0.1:5037),并发送一个 adbd 能理解的、代表具体设备操作的命令。

啥命令能用呢?

adbd 支持多种服务 (service)。比如,启动一个 shell 会用到 shell: 服务。我们可以尝试发送一个非常简单的 shell 命令,比如 echo 一个特定字符串。

一个完整的 ADB 交互流程(简化版)是:

  1. 客户端连接到 adbd
  2. 客户端发送 "transport" 请求(如果需要指定设备,但我们是本地直连,或许可以省略,或者用一个通用方式指定本地)。更正: 当直接连接 adbd (特别是 TCP 模式),通常是先建立连接,然后直接发送服务请求。
  3. 客户端发送服务请求,格式为 NNNN<service-string>NNNN 是服务请求字符串长度的4位十六进制表示。例如,请求执行 echo test,服务字符串可能是 shell:echo test
  4. adbd 响应 OKAYFAIL,后面跟上数据(比如 shell 的输出)或错误信息。

如何实施?

方案 A: 使用修改后的 C 代码

我们可以修改之前的 C 代码,把 host:version 换成一个有效的服务请求,比如 shell:echo adb_ping

这个命令字符串 "shell:echo adb_ping" 的长度是 19。转换成 4 位十六进制是 0013。 (注意: 实际实现中,最好动态计算长度)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h> // 引入 errno

#define ADB_HOST "127.0.0.1"
#define ADB_PORT 5037
#define TIMEOUT_SECONDS 2

// 返回值:0 表示成功(收到 OKAY),-1 表示失败
int check_adbd_responsive() {
    int sock = -1;
    char response_prefix[5] = {0}; // 用于接收 OKAY 或 FAIL

    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket creation failed");
        return -1;
    }

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(ADB_PORT),
        .sin_addr.s_addr = inet_addr(ADB_HOST)
    };

    struct timeval timeout = {TIMEOUT_SECONDS, 0};
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
    setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));

    if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        // 这里要区分是连接被拒绝(端口没监听)还是超时
        // 如果 connect 返回 ECONNREFUSED,说明端口没监听或adbd挂了
        // 如果返回 ETIMEDOUT,可能是网络问题或 adbd 卡死无响应
        perror("connect failed");
        close(sock);
        return -1;
    }

    const char *command = "shell:echo adb_ping"; // 一个简单的 shell 命令
    int cmd_len = strlen(command);
    char header[5];
    snprintf(header, sizeof(header), "%04X", cmd_len);

    printf("Sending header: %s\n", header);
    if (send(sock, header, 4, 0) != 4) {
        perror("send header failed");
        close(sock);
        return -1;
    }

    printf("Sending command: %s\n", command);
    if (send(sock, command, cmd_len, 0) != cmd_len) {
        perror("send command failed");
        close(sock);
        return -1;
    }

    // adbd 应该先回复一个 4 字节的 "OKAY" 或 "FAIL"
    ssize_t bytes_received = recv(sock, response_prefix, 4, 0);

    if (bytes_received < 0) {
        perror("recv response status failed");
        close(sock);
        return -1;
    } else if (bytes_received == 0) {
        printf("Connection closed by peer unexpectedly after sending command.\n");
        close(sock);
        return -1;
    } else if (bytes_received == 4) {
        response_prefix[4] = '\0'; // 确保 null 结尾
        printf("Received status: %s\n", response_prefix);
        if (strcmp(response_prefix, "OKAY") == 0) {
            printf("adbd responded OKAY. Seems healthy.\n");
            // 这里可以继续接收后续数据(echo 的输出),或者直接认为 OKAY 就行
            // 为了简单起见,我们收到 OKAY 就认为成功
            close(sock);
            return 0; // 成功
        } else {
            printf("adbd responded with non-OKAY status: %s\n", response_prefix);
            // 收到 FAIL 或其他东西,也算 adbd 有反应,但可能服务不可用?
            // 根据需求,这里可以返回 0 (有反应) 或 -1 (非预期反应)
            // 我们严格点,非 OKAY 算失败
            close(sock);
            return -1; // 失败
        }
    } else {
         printf("Received unexpected number of bytes for status: %zd\n", bytes_received);
         close(sock);
         return -1; // 失败
    }

    // 代码实际上到不了这里
    close(sock);
    return -1;
}

int main() {
    printf("Checking local adbd on %s:%d...\n", ADB_HOST, ADB_PORT);
    if (check_adbd_responsive() == 0) {
        printf("Result: adbd seems responsive.\n");
    } else {
        printf("Result: adbd might be down or unresponsive.\n");
    }
    return 0;
}
  • 原理: 遵循 ADB 协议,连接到 adbd 并发送一个 shell: 服务请求。adbd 如果正常工作,应该会响应一个 OKAY
  • 优点: 直接测试了 adbd 的核心服务处理能力,比只检查进程或端口更准确。
  • 缺点: 需要编写和编译代码,稍微复杂点。需要处理好网络编程的各种细节(超时、错误处理)。
  • 安全建议: 确保 C 代码健壮,正确处理各种错误情况和边界条件,避免自身成为问题源头。

方案 B: 使用 netcat (nc) 工具

如果你的 busybox 包含了 nc (netcat) 工具,或者你可以安装一个,那事情就简单多了,可以用脚本来模拟这个过程。

# 命令: shell:echo adb_ping (长度 19 => 0x13)
COMMAND="shell:echo adb_ping"
CMD_LEN=$(printf "%04X" $(echo -n "$COMMAND" | wc -c))
REQUEST="${CMD_LEN}${COMMAND}"

# 发送请求并等待响应(设置超时 -w 2 秒)
# 注意:不同版本的 nc 参数可能略有不同, -w 或 -W
RESPONSE=$(echo -n "$REQUEST" | nc -w 2 127.0.0.1 5037)

# 检查响应是否以 "OKAY" 开头
# adbd 对 shell:echo 会先回 OKAY,然后马上跟一个长度头+数据
# 所以简单的响应可能看起来像 "OKAY000aadb_ping\n" (长度是adb_ping\n = 9+1 = 10 => 0x000a)
# 我们只关心开头的 OKAY
if [[ "$RESPONSE" == OKAY* ]]; then
  echo "adbd seems responsive (via nc)."
  exit 0
else
  echo "adbd did not respond OKAY (via nc). Response: [$RESPONSE]"
  # 这里可能需要更仔细地分析 $RESPONSE
  # 比如完全没响应,或者响应了 FAIL 等
  exit 1
fi
  • 原理: 和 C 代码类似,但用现成的命令行工具发送数据和接收响应。
  • 优点: 无需编译,方便集成到 shell 脚本中。
  • 缺点: 依赖 nc 工具。对响应的解析可能不如 C 代码那么灵活。nc 的超时行为和错误处理可能需要适配具体版本。
  • 安全建议: 无特殊安全建议,但脚本本身的健壮性(如错误检查)仍然重要。

方法四:尝试使用本地 adb 客户端连接本地 adbd (如果可行)

有时候,设备上可能也包含了 adb 客户端工具。可以试试强制它连接本地 127.0.0.1:5037

# 尝试通过指定 Host(-H) 连接本地 adbd
adb -H 127.0.0.1 shell echo adb_ping
  • 原理: 让设备上的 adb 客户端直接与同一设备上的 adbd 通信(如果 adbd 允许这种连接并且监听了 TCP 端口)。
  • 优点: 使用标准工具,简单直接。
  • 缺点:
    • 不一定所有嵌入式系统都自带 adb 客户端。
    • 如前所述,adbd (特别是 ADB_HOST=0 编译的) 可能不接受来自本地 TCP 端口的连接,或者即使连接上了也不响应被它识别为 "host" 的某些命令流。需要实际测试验证这种方法在你的特定系统上是否有效。 它很可能会失败。

方法五:检查 adbd 相关日志

如果 adbd 出了问题,它可能会在系统日志里留下线索。

# 检查 dmesg 内核日志
dmesg | grep -i adb

# 检查系统日志文件 (具体路径取决于你的系统配置, 可能是 /var/log/messages 等)
# logread # 如果使用 openwrt 或类似系统
cat /var/log/messages | grep -i adb # 示例路径

# 如果是 Android 系统 (虽然这里是通用 Linux, 但提一下)
logcat -d | grep -i adb
  • 原理: 通过分析日志判断 adbd 是否崩溃、报错或有异常行为。
  • 优点: 能提供更详细的故障信息。
  • 缺点: 需要知道日志在哪里,adbd 不一定总会留下清晰的日志,且日志检查是被动的。

整合:写一个简单的监控脚本

结合上面几种方法,我们可以写一个 shell 脚本来监控 adbd 状态,并在发现问题时尝试重启它。优先使用最可靠的方法(比如方法三)。

#!/bin/sh

ADB_HOST="127.0.0.1"
ADB_PORT="5037"
ADBD_COMMAND="/usr/bin/adbd" # adbd 实际路径
RESTART_DELAY=5 # 重启后的等待时间 (秒)
MAX_RETRY=3 # 最多尝试重启次数
RETRY_COUNT=0

check_adbd() {
    # 优先使用 netcat 方式检查响应性
    COMMAND="shell:echo adb_ping_$(date +%s)" # 加时间戳避免缓存?(可能不需要)
    CMD_LEN=$(printf "%04X" $(echo -n "$COMMAND" | wc -c))
    REQUEST="${CMD_LEN}${COMMAND}"

    # 使用 timeout 命令控制 nc 的整体运行时间,更保险
    RESPONSE=$(echo -n "$REQUEST" | timeout 3 nc -w 2 $ADB_HOST $ADB_PORT)
    NC_EXIT_CODE=$?

    if [ $NC_EXIT_CODE -eq 0 ] && [[ "$RESPONSE" == OKAY* ]]; then
        echo "$(date): OK - adbd responded OKAY via nc."
        return 0 # 健康
    elif [ $NC_EXIT_CODE -eq 124 ]; then # timeout 命令超时
         echo "$(date): FAIL - nc command timed out."
         return 1 # 不健康
    else
         echo "$(date): FAIL - nc exited with code $NC_EXIT_CODE or response was not OKAY. Response: [$RESPONSE]"
         # 进一步检查进程是否存在,区分是完全没跑还是卡死了
         if ! pgrep adbd > /dev/null; then
             echo "$(date): FAIL - adbd process not found."
         else
             echo "$(date): FAIL - adbd process exists but unresponsive/failed check."
         fi
         return 1 # 不健康
    fi
}

restart_adbd() {
    echo "$(date): Attempting to restart adbd..."
    # 先尝试杀掉旧进程,避免多个实例
    killall adbd > /dev/null 2>&1
    sleep 1
    # 如果 killall 不行,可以尝试 pkill 或 kill $(pgrep adbd)
    pkill -9 adbd > /dev/null 2>&1 # 强制杀死
    sleep 1

    # 启动 adbd (根据你的系统启动方式修改)
    echo "$(date): Starting adbd process: $ADBD_COMMAND"
    $ADBD_COMMAND & # 在后台启动
    sleep $RESTART_DELAY # 等待 adbd 初始化
}

# 主循环
while true; do
    if ! check_adbd; then
        RETRY_COUNT=$((RETRY_COUNT + 1))
        if [ $RETRY_COUNT -le $MAX_RETRY ]; then
            echo "$(date): adbd health check failed ($RETRY_COUNT/$MAX_RETRY). Restarting..."
            restart_adbd
        else
            echo "$(date): adbd failed health check after $MAX_RETRY retries. Giving up for a while."
            # 这里可以加入更复杂的逻辑,比如发送报警、等待更长时间再试等
            RETRY_COUNT=0 # 重置计数器,过段时间再重试
            sleep 300 # 等待 5 分钟
        fi
    else
        # 如果健康,重置重试计数器
        RETRY_COUNT=0
    fi
    # 每隔一段时间检查一次,例如 60 秒
    sleep 60
done

  • 说明:
    • 这个脚本会定期(默认 60 秒)调用 check_adbd 函数。
    • check_adbd 使用 nc 方法发送 shell:echo 命令来探测。增加了 timeout 命令防止 nc 卡死。
    • 如果检查失败,它会调用 restart_adbd 来杀掉旧的 adbd 进程并重新启动。
    • 加入了简单的重试逻辑,避免因为短暂的网络波动或其他瞬时问题导致频繁重启。
    • 你需要根据自己系统上 adbd 的实际路径和启动方式修改脚本。
    • 需要确保 nctimeout (通常在 coreutils 包里) 可用。
  • 进阶使用技巧:
    • 可以将日志输出到文件而不是控制台。
    • 集成到系统的服务管理框架(如 systemd, sysvinit)中,使其更稳定可靠。
    • 根据 nc 返回的具体错误码和响应内容做更细致的判断。
    • 如果 TCP 端口监听在 0.0.0.0:5037 而非 127.0.0.1:5037,务必考虑安全影响,可能的话配置防火墙规则限制访问。如果只在本地使用,监听 127.0.0.1 更安全。

总结一下

要从设备本地检测 adbd 是否正常工作,直接照搬 PC 上 adb 客户端发给 adb serverhost: 命令通常行不通。更靠谱的方法是模拟一个合法的客户端请求,比如通过 socket 连接到 adbd 监听的端口(可能是 TCP 5037 或某个 Unix domain socket),发送一个 adbd 能理解的服务命令(如 shell:echo test),并检查是否收到预期的 OKAY 响应。你可以用 C 代码实现精确控制,或者利用 nc 等工具快速集成到监控脚本里。别忘了结合进程检查和端口监听检查,多管齐下更放心。有了这样的本地监控和自动恢复机制,下次再遇到 adb shell 连接不上的问题,说不定它自己就悄悄解决了。