Files
go-mobile-uiautomator/adb/connect.go
2026-03-10 13:41:18 +08:00

237 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package adb
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
"os"
"strconv"
"sync"
"time"
)
// mu 用于保护 ADB 连接的互斥锁,避免并发连接冲突
var mu sync.Mutex
// DialADB 建立到 ADB 服务器的 TCP 连接
// addr 格式为 "host:port",例如 "127.0.0.1:5037"
// timeout 为连接超时时间
func DialADB(addr string, timeout time.Duration) (net.Conn, error) {
mu.Lock()
defer mu.Unlock()
d := net.Dialer{Timeout: timeout}
return d.Dial("tcp", addr)
}
// WriteAdbCmd 向 ADB 连接发送命令
// 协议格式4 位十六进制长度前缀 + 命令内容
func WriteAdbCmd(conn net.Conn, cmd string) error {
header := fmt.Sprintf("%04x", len(cmd))
conn.SetWriteDeadline(time.Now().Add(2 * time.Second))
_, err := conn.Write([]byte(header + cmd))
return err
}
// readN 从连接中精确读取 n 个字节
// 会持续读取直到收集到足够的字节数,或超时/出错
func readN(conn net.Conn, n int, timeout time.Duration) ([]byte, error) {
buf := make([]byte, n)
total := 0
for total < n {
conn.SetReadDeadline(time.Now().Add(timeout))
nr, err := conn.Read(buf[total:])
if err != nil {
return nil, err
}
total += nr
}
conn.SetReadDeadline(time.Time{})
return buf, nil
}
// ReadStatus 读取 ADB 协议的 4 字节状态码(如 "OKAY" 或 "FAIL"
func ReadStatus(conn net.Conn) (string, error) {
b, err := readN(conn, 4, 3*time.Second)
if err != nil {
return "", err
}
return string(b), nil
}
// ReadLenFrame 读取一个带长度前缀的数据帧
// 先读取 4 字节十六进制长度前缀,再读取相应长度的数据
func ReadLenFrame(conn net.Conn) ([]byte, error) {
hdr, err := readN(conn, 4, 3*time.Second)
if err != nil {
return nil, err
}
l, err := strconv.ParseInt(string(hdr), 16, 32)
if err != nil {
return nil, err
}
if l == 0 {
return []byte{}, nil
}
return readN(conn, int(l), 10*time.Second)
}
// TransportTo 指示 ADB 服务器将后续请求路由到指定设备
// serial 为设备序列号,例如 "emulator-5556"
func TransportTo(conn net.Conn, serial string) error {
if err := WriteAdbCmd(conn, "host:transport:"+serial); err != nil {
return err
}
status, err := ReadStatus(conn)
if err != nil {
return err
}
if status == "FAIL" {
msg, _ := ReadLenFrame(conn)
return fmt.Errorf("传输失败: %s", string(msg))
}
if status != "OKAY" {
return fmt.Errorf("意外的传输状态: %s", status)
}
conn.SetWriteDeadline(time.Time{})
return nil
}
// ExecShell 在设备上执行 Shell 命令并返回输出结果
// shellCmd 为要执行的 Shell 命令字符串
func ExecShell(conn net.Conn, shellCmd string) ([]byte, error) {
if err := WriteAdbCmd(conn, "shell:"+shellCmd); err != nil {
return nil, err
}
st, err := ReadStatus(conn)
if err != nil {
return nil, err
}
if st != "OKAY" {
msg, _ := ReadLenFrame(conn)
return nil, fmt.Errorf("Shell 执行失败: %s", string(msg))
}
var buf bytes.Buffer
tmp := make([]byte, 4096)
for {
conn.SetReadDeadline(time.Now().Add(3 * time.Second))
n, err := conn.Read(tmp)
if n > 0 {
buf.Write(tmp[:n])
}
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
break
}
if err == io.EOF {
break
}
return nil, err
}
}
return buf.Bytes(), nil
}
// ReadResponse 读取 ADB 协议响应
// 返回状态码("OKAY"/"FAIL"/其他)、消息内容和错误
// 支持小端 uint32 和 ASCII 十六进制两种长度编码格式
// debug 为 true 时会将调试信息输出到 stderr
func ReadResponse(conn net.Conn, debug bool) (string, []byte, error) {
stb, err := readN(conn, 4, 10*time.Second)
if err != nil {
return "", nil, err
}
st := string(stb)
if debug {
fmt.Fprintf(os.Stderr, "ReadResponse: 状态原始 hex=%x ascii=%q\n", stb, st)
}
if st == "OKAY" {
return st, nil, nil
}
if st == "FAIL" {
// 读取后续 4 字节(可能是小端 uint32 长度或 ASCII 十六进制长度)
hdr, err := readN(conn, 4, 10*time.Second)
if err != nil {
if debug {
fmt.Fprintf(os.Stderr, "ReadResponse: FAIL 后无长度头: %v\n", err)
}
return st, nil, nil
}
if debug {
fmt.Fprintf(os.Stderr, "ReadResponse: 长度头原始 hex=%x ascii=%q\n", hdr, string(hdr))
}
// 优先尝试小端 uint32 解析
l := int(binary.LittleEndian.Uint32(hdr))
if l > 0 {
msg, err := readN(conn, l, 10*time.Second)
if err != nil {
if debug {
fmt.Fprintf(os.Stderr, "ReadResponse: 读取 %d 字节消息失败: %v\n", l, err)
}
return st, nil, nil
}
if debug {
fmt.Fprintf(os.Stderr, "ReadResponse: 消息 hex=%x ascii=%q\n", msg, string(msg))
}
return st, msg, nil
}
// 回退:尝试 ASCII 十六进制解析(向后兼容)
if n, perr := strconv.ParseInt(string(hdr), 16, 32); perr == nil && n > 0 {
msg, err := readN(conn, int(n), 10*time.Second)
if err != nil {
if debug {
fmt.Fprintf(os.Stderr, "ReadResponse: 读取 ASCII 十六进制消息(长度 %d失败: %v\n", n, err)
}
return st, nil, nil
}
if debug {
fmt.Fprintf(os.Stderr, "ReadResponse: ASCII 十六进制消息 hex=%x ascii=%q\n", msg, string(msg))
}
return st, msg, nil
}
// 两种方式都未解析出消息
return st, nil, nil
}
// 非预期的状态码
return st, nil, nil
}
// LaunchUiautomator 连接 ADB 服务器,路由到指定设备,启动 UIAutomator2 服务
// 启动后持续将服务日志输出到标准输出
// addr 为 ADB 服务器地址serial 为设备序列号
func LaunchUiautomator(addr, serial string) {
conn, err := DialADB(addr, 15*time.Second)
if err != nil {
fmt.Println("连接失败:", err)
return
}
// 路由到目标设备
if err := TransportTo(conn, serial); err != nil {
fmt.Println("设备路由失败:", err)
return
}
// 启动 UIAutomator2 服务
cmd := "shell:CLASSPATH=/data/local/tmp/u2.jar app_process / com.wetest.uia2.Main"
if err := WriteAdbCmd(conn, cmd); err != nil {
fmt.Println("发送命令失败:", err)
return
}
status, err := ReadStatus(conn)
if err != nil {
fmt.Println("读取状态失败:", err)
return
}
fmt.Println("UIAutomator2 启动状态:", status)
// 持续输出 UIAutomator2 服务日志
io.Copy(os.Stdout, conn)
}