JasonWang's Blog

一个ADB无法识别设备的问题

字数统计: 3k阅读时长: 13 min
2021/02/20

这两天隔壁部门的同事反馈说新项目上, 车机(Android系统)上挂载的USB外设(一个可以上网的TBOX设备)无法通过ADB(Android Debug Bridge)进行连接. 心里有点纳闷, USB不都识别到了吗, 上次也把ADB相关的客户端都移植过去了, 为啥还会识别不到设备了? 只得从头开始理下ADB相关的代码与逻辑.先来看看ADB的基本原理.

ADB(全称Android Debug Bridge)是Android上用途十分广泛的调试工具, 可用于与开发设备进行连接;ADB命令既可以用来主机与设备之间传输文件, 也可以通过SHELL命令对设备进行操作. 如下所示, 是ADB的一个工作原理图:

How ADB works

可以看到, ADB主要有3个部分组成:

  • ADB clients: 客户端程序, 运行在开发设备上, 用于发送指令, 用户可以通过adb shell/devices来激活客户端
  • ADBD: 守护进程, 作为后台进程运行在调试设备上, 一般在系统启动的时候加载
  • ADB server: 服务端进程, 运行在开发设备上, 负责ADBD与客户端进程的通讯, 由ADB客户端负责加载

对于模拟设备, ADB server与ADB一般是通过TCP/IP协议建立的通讯链接; 对于外接的USB调试设备, 则可以选择通过USB/TCP两种方式进行连接. 一般来说, 一个ADB指令的执行需要经过如下几个步骤:

  • 用户输入adb shell/devices时, 会判断是否有ADB server进程存在,如果没有就启动一个进程
  • ADB server通过TCP 5037端口监听来自客户进程请求, 在初始化的时候, 如果使用的是TCP连接模式, ADB Server会通过扫面一个固定的TCP端口范围5555 ~ 5585中的奇数号来尝试连接到目标调试设备;如果是USB连接模式, 则会通过扫描当前已连接的USB设备, 从中找出用于ADB通讯的接口用于通讯. 不指定连接模式, 则会扫描所有的设备
  • ADB server找到设备并建立连接后, 就可以把来自客户端的请求发送给调试设备的ADBD进程了, 由ADBD来执行用户的请求并返回结果

这里就从源码的角度具体分析下ADB的具体执行流程(主要以USB连接模式为例). 在分析具体的流程之前, 不妨来简单看看Android ADB的代码结构. 以Android Pie代码为例:

  • ADB的源码位于/system/core/adb, 客户端代码位于client目录, 而ADBD的代码则位于daemon目录
  • 根目录下的代码包含了客户端/服务端以及守护进程的共用的逻辑; 查看共用代码部分时, 定义了ADB_HOST宏的表示是需要在HOST端执行的代码逻辑(包括客户端以及服务端), 其他则是调试设备上ADBD的逻辑.

ADB客户端/服务进程的启动

查看client下面的代码, 有个main函数, 就是输入adb shell//devices执行的入口:

1
2
3
4
5
6

int main(int argc, char** argv) {
adb_trace_init(argv);
return adb_commandline(argc - 1, const_cast<const char**>(argv + 1));
}

adb_commandline最终会调用函数adb_connect尝试连接服务进程, 如果当前没有服务进程,则启动一个新的进程, 然后读取服务端返回的结果, 直接看下adb_connect 的逻辑:

  • 首先尝试连接服务端进程, 没有, 则通过launch_server启动一个新的进程
  • 启动完成后, 通过_adb_connect连接到服务进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

int adb_connect(const std::string& service, std::string* error) {
// first query the adb server's version
int fd = _adb_connect("host:version", error);

D("adb_connect: service %s", service.c_str());
if (fd == -2 && !is_local_socket_spec(__adb_server_socket_spec)) {
fprintf(stderr, "* cannot start server on remote host\n");
// error is the original network connection error
return fd;
} else if (fd == -2) {
fprintf(stderr, "* daemon not running; starting now at %s\n", __adb_server_socket_spec);
start_server:
if (launch_server(__adb_server_socket_spec)) {
fprintf(stderr, "* failed to start daemon\n");
// launch_server() has already printed detailed error info, so just
// return a generic error string about the overall adb_connect()
// that the caller requested.
*error = "cannot connect to daemon";
return -1;
} else {
fprintf(stderr, "* daemon started successfully\n");
}
// The server will wait until it detects all of its connected devices before acking.
// Fall through to _adb_connect.
} else {
// If a server is already running, check its version matches.
int version = ADB_SERVER_VERSION - 1;

// If we have a file descriptor, then parse version result.
if (fd >= 0) {
std::string version_string;
if (!ReadProtocolString(fd, &version_string, error)) {
adb_close(fd);
return -1;
}

ReadOrderlyShutdown(fd);
adb_close(fd);

if (sscanf(&version_string[0], "%04x", &version) != 1) {
*error = android::base::StringPrintf("cannot parse version string: %s",
version_string.c_str());
return -1;
}
} else {
// If fd is -1 check for "unknown host service" which would
// indicate a version of adb that does not support the
// version command, in which case we should fall-through to kill it.
if (*error != "unknown host service") {
return fd;
}
}

if (version != ADB_SERVER_VERSION) {
fprintf(stderr, "adb server version (%d) doesn't match this client (%d); killing...\n",
version, ADB_SERVER_VERSION);
adb_kill_server();
goto start_server;
}
}

// if the command is start-server, we are done.
if (service == "host:start-server") {
return 0;
}

fd = _adb_connect(service, error);
if (fd == -1) {
D("_adb_connect error: %s", error->c_str());
} else if(fd == -2) {
fprintf(stderr, "* daemon still not running\n");
}
D("adb_connect: return fd %d", fd);

return fd;
}

查看launch_server linux部分的实现, 其实际的入口与客户端的函数入口是一样的, 只是参数不一样而已: 服务端启动完成之后, 发送OK的状态给客户端, 告知其启动完成.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

int fd[2];
if (pipe(fd)) {
fprintf(stderr, "pipe failed in launch_server, errno: %d\n", errno);
return -1;
}

std::string path = android::base::GetExecutablePath();

pid_t pid = fork();
if (pid < 0) return -1;

if (pid == 0) {
// child side of the fork

adb_close(fd[0]);

char reply_fd[30];
snprintf(reply_fd, sizeof(reply_fd), "%d", fd[1]);
// child process
int result = execl(path.c_str(), "adb", "-L", socket_spec.c_str(), "fork-server", "server",
"--reply-fd", reply_fd, NULL);
// this should not return
fprintf(stderr, "adb: execl returned %d: %s\n", result, strerror(errno));
} else {
// parent side of the fork
char temp[3] = {};
// wait for the "OK\n" message
adb_close(fd[1]);
int ret = adb_read(fd[0], temp, 3);
int saved_errno = errno;
adb_close(fd[0]);
if (ret < 0) {
fprintf(stderr, "could not read ok from ADB Server, errno = %d\n", saved_errno);
return -1;
}
if (ret != 3 || temp[0] != 'O' || temp[1] != 'K' || temp[2] != '\n') {
ReportServerStartupFailure(pid);
return -1;
}
}


设备的识别与扫描

我们重点来看下ADB SERVER是如何扫描设备的. 执行服务端进程后, 调用adb_server_main函数来执行服务端的逻辑:

  • usb_init扫描USB设备
  • adb_wait_for_device_initialization等待扫描完成后, 发送OK给客户端进程
  • fdevent_loop 进入循环, 等待来自客户端的请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

int adb_server_main(int is_daemon, const std::string& socket_spec, int ack_reply_fd) {
signal(SIGINT, [](int) {
fdevent_run_on_main_thread([]() { exit(0); });
});

char* leak = getenv("ADB_LEAK");
if (leak && strcmp(leak, "1") == 0) {
intentionally_leak();
}

init_transport_registration();
init_mdns_transport_discovery();

usb_init();
local_init(DEFAULT_ADB_LOCAL_TRANSPORT_PORT);

std::string error;

auto start = std::chrono::steady_clock::now();

// If we told a previous adb server to quit because of version mismatch, we can get to this
// point before it's finished exiting. Retry for a while to give it some time.
while (install_listener(socket_spec, "*smartsocket*", nullptr, 0, nullptr, &error) !=
INSTALL_STATUS_OK) {
if (std::chrono::steady_clock::now() - start > 0.5s) {
fatal("could not install *smartsocket* listener: %s", error.c_str());
}

std::this_thread::sleep_for(100ms);
}

adb_auth_init();

if (is_daemon) {
// Start a new session for the daemon. Do this here instead of after the fork so
// that a ctrl-c between the "starting server" and "done starting server" messages
// gets a chance to terminate the server.
// setsid will fail with EPERM if it's already been a lead process of new session.
// Ignore such error.
if (setsid() == -1 && errno != EPERM) {
fatal("setsid() failed: %s", strerror(errno));
}

// Wait for the USB scan to complete before notifying the parent that we're up.
// We need to perform this in a thread, because we would otherwise block the event loop.
std::thread notify_thread([ack_reply_fd]() {
adb_wait_for_device_initialization();

// Any error output written to stderr now goes to adb.log. We could
// keep around a copy of the stderr fd and use that to write any errors
// encountered by the following code, but that is probably overkill.


// TODO(danalbert): Can't use SendOkay because we're sending "OK\n", not
// "OKAY".
if (!android::base::WriteStringToFd("OK\n", ack_reply_fd)) {
fatal_errno("error writing ACK to fd %d", ack_reply_fd);
}
unix_close(ack_reply_fd);

});
notify_thread.detach();
}

D("Event loop starting");
fdevent_loop();

return 0;
}


这里着重看下USB设备的扫描usb_init<usb_dispatch.cpp>, 对ADB客户端/服务端都不是使用libusb来识别USB设备, 直接看linux下的实现usb_linux.cpp:

1
2
3
4
5
6
7
8
9
10
11
12

// usb_dispatch.cpp
void usb_init() {
if (should_use_libusb()) {
LOG(DEBUG) << "using libusb backend";
libusb::usb_init();
} else {
LOG(DEBUG) << "using native backend";
native::usb_init();
}
}

usb_init主要是启动一个设备查找的线程, 用于不断识别接入的USB设备而已. 到这里, 我们大概知道了如何查找到USB设备并且读取设备的配置: 通过遍历/dev/bus/usb下面的节点, 即对应USB设备的字符设备, USB设备的信息就可以通过这些字符设备读取到.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

static void device_poll_thread() {
adb_thread_setname("device poll");
D("Created device thread");
while (true) {
// TODO: Use inotify.
find_usb_device("/dev/bus/usb", register_device);
kick_disconnected_devices();
std::this_thread::sleep_for(1s);
}
}

// usb_linux.cpp
void usb_init() {
struct sigaction actions;
memset(&actions, 0, sizeof(actions));
sigemptyset(&actions.sa_mask);
actions.sa_flags = 0;
actions.sa_handler = [](int) {};
sigaction(SIGALRM, &actions, nullptr);

std::thread(device_poll_thread).detach();
}

对于ADB设备节点来说, 其有特殊的类别以及协议, 因此需要通过is_adb_interface来过滤掉非ADB的设备.感兴趣的可以仔细看下find_usb_device这个函数看下具体的设备查找流程.

1
2
3
4
5
6
7
8
9
10

#define ADB_CLASS 0xff
#define ADB_SUBCLASS 0x42
#define ADB_PROTOCOL 0x1

int is_adb_interface(int usb_class, int usb_subclass, int usb_protocol) {
return (usb_class == ADB_CLASS && usb_subclass == ADB_SUBCLASS && usb_protocol == ADB_PROTOCOL);
}


无法识别ADB设备的问题

梳理了ADB的一些代码, 我们大致知道对于客户端来说,在USB模式下, 只是通过一个特定的USB设备节点来与调试设备通讯, 那么, 客户端找不到设备的原因就只有两个(如前所述, HOST端已经识别到了调试设备):

  • 客户端的配置有问题
  • 调试设备的ADB配置有问题

可问题在于, 同样的设备在PC上是可以正常识别到并使用ADB的, 那么可以基本可以确信调试设备上的ADB配置是没有问题的;关于如何在调试设备上适配ADB, 大家可以参考之前的一篇文章Recovery下如何配置ADB. 那么, 问题就可以聚焦在客户端的配置上了.

对比下正常的配置与异常的配置, 发现异常的配置tty设备枚举到了5个(从ttyUSB0ttyUSB4), 而正常只有4个(ttyUSB0ttyUSB3), 进入/sys/bus/usb/devices找到对应的ttyUSB4 设备, 看了下相应的配置,
bInterfaceClass/bInterfaceProtocol/bInterfaceSubClass与上述ADB的配置是完全一致的, interface的结果为ADB interface. 现在应该可以肯定, 这个多出来的ttyUSB4 串口设备应该就是用于ADB通讯的设备, 但是不知道为何被枚举成了串口. 看看串口驱动源码(drivers/usb/serial/option.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

static int option_probe(struct usb_serial *serial,
const struct usb_device_id *id)
{
struct usb_interface_descriptor *iface_desc =
&serial->interface->cur_altsetting->desc;
struct usb_device_descriptor *dev_desc = &serial->dev->descriptor;
unsigned long device_flags = id->driver_info;

/* Never bind to the CD-Rom emulation interface */
if (iface_desc->bInterfaceClass == 0x08)
return -ENODEV;

/*
* Don't bind reserved interfaces (like network ones) which often have
* the same class/subclass/protocol as the serial interfaces. Look at
* the Windows driver .INF files for reserved interface numbers.
*/
if (device_flags & RSVD(iface_desc->bInterfaceNumber))
return -ENODEV;
/*
* Don't bind network interface on Samsung GT-B3730, it is handled by
* a separate module.
*/
if (dev_desc->idVendor == cpu_to_le16(SAMSUNG_VENDOR_ID) &&
dev_desc->idProduct == cpu_to_le16(SAMSUNG_PRODUCT_GT_B3730) &&
iface_desc->bInterfaceClass != USB_CLASS_CDC_DATA)
return -ENODEV;

...
}


这里明确指出了, 有些保留(RSVD)的端口不需要枚举, 从这里我们大致可以推测问题的原因: ADB对应的设备节点被枚举成了串口, 导致ADB服务端无法与调试设备进行连接通讯. 就是说, 只要把对应端口号的设备保留下来即可. 找到调试设备VID/PID所在的地方, 加上RSVD标志即可:

1
2
3
4

{ USB_DEVICE(QUECTEL_VENDOR_ID, QUECTEL_PRODUCT_AG35),
.driver_info = RSVD(4)}, // 4 对应设备的节点数字, bInterfaceNumber

修改后, 就可以看到adb devices有设备列出了.

参考资料

原文作者:Jason Wang

更新日期:2021-08-11, 14:52:25

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. ADB客户端/服务进程的启动
  2. 2. 设备的识别与扫描
  3. 3. 无法识别ADB设备的问题
  4. 4. 参考资料