From 1be2f4021f6221f4a3bb1a8fc0d5b40a25916755 Mon Sep 17 00:00:00 2001 From: zyj Date: Tue, 10 Mar 2026 13:41:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B3=A8=E9=87=8A=E5=8F=8A?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 34 +++++++- README.md | 14 +++- adb/connect.go | 104 +++++++++++++----------- adb/device.go | 137 +++++++++++--------------------- adb/doc.go | 21 +++++ adb/sync.go | 59 +++++++++----- cmd/main.go | 68 ++++++++-------- config/index.go | 43 ---------- go.mod | 4 +- go.sum | 4 - libs/doc.go | 16 ++++ libs/{repuest.go => request.go} | 109 +++++++++++++------------ libs/selector.go | 112 +++++++++++++------------- services/doc.go | 5 ++ services/install_service.go | 46 +++++++---- 15 files changed, 411 insertions(+), 365 deletions(-) create mode 100644 adb/doc.go delete mode 100644 config/index.go create mode 100644 libs/doc.go rename libs/{repuest.go => request.go} (57%) create mode 100644 services/doc.go diff --git a/.gitignore b/.gitignore index 2d4227e..3c0649e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,36 @@ +# 配置文件(包含敏感信息) config.yaml -# Editor directories and files -.idea -.vscode +# 编译输出 +*.exe +*.exe~ +*.dll +*.so +*.dylib +/bin/ +/dist/ + +# 测试输出 +*.test +*.out +*.prof + +# 依赖目录 +/vendor/ + +# 编辑器和 IDE 配置 +.idea/ +.vscode/ *.suo *.ntvs* *.njsproj *.sln -.vs/* \ No newline at end of file +.vs/ + +# 操作系统文件 +.DS_Store +Thumbs.db + +# 临时文件 +*.tmp +*.log \ No newline at end of file diff --git a/README.md b/README.md index f9d01d1..702b6ef 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,14 @@ ## 安装 +### 作为项目引用 + +```bash +go get github.com/zhuy1228/go-mobile-uiautomator@latest +``` + +### 从源码构建 + ```bash git clone https://github.com/zhuy1228/go-mobile-uiautomator.git cd go-mobile-uiautomator @@ -40,6 +48,8 @@ adb devices 修改 `cmd/main.go` 中的设备配置: ```go +import "github.com/zhuy1228/go-mobile-uiautomator/adb" + const serial = "your-device-serial" // 你的设备序列号 const addr = "127.0.0.1:5037" // ADB 服务器地址 ``` @@ -146,7 +156,7 @@ go-mobile-uiautomator/ ├── config/ # 配置管理 │ └── index.go ├── libs/ # 工具库 -│ ├── repuest.go # HTTP over ADB +│ ├── request.go # HTTP over ADB │ └── selector.go # UI 选择器 ├── services/ # 服务模块 │ └── install_service.go # UIAutomator2 安装 @@ -234,6 +244,8 @@ A: 确保: ## 依赖 ```go +module github.com/zhuy1228/go-mobile-uiautomator + require ( gopkg.in/yaml.v3 v3.0.1 ) diff --git a/adb/connect.go b/adb/connect.go index 16e0ad0..9b62353 100644 --- a/adb/connect.go +++ b/adb/connect.go @@ -12,8 +12,12 @@ import ( "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() @@ -21,6 +25,8 @@ func DialADB(addr string, timeout time.Duration) (net.Conn, error) { 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)) @@ -28,6 +34,8 @@ func WriteAdbCmd(conn net.Conn, cmd string) error { return err } +// readN 从连接中精确读取 n 个字节 +// 会持续读取直到收集到足够的字节数,或超时/出错 func readN(conn net.Conn, n int, timeout time.Duration) ([]byte, error) { buf := make([]byte, n) total := 0 @@ -43,6 +51,7 @@ func readN(conn net.Conn, n int, timeout time.Duration) ([]byte, error) { 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 { @@ -51,6 +60,8 @@ func ReadStatus(conn net.Conn) (string, error) { return string(b), nil } +// ReadLenFrame 读取一个带长度前缀的数据帧 +// 先读取 4 字节十六进制长度前缀,再读取相应长度的数据 func ReadLenFrame(conn net.Conn) ([]byte, error) { hdr, err := readN(conn, 4, 3*time.Second) if err != nil { @@ -66,7 +77,8 @@ func ReadLenFrame(conn net.Conn) ([]byte, error) { return readN(conn, int(l), 10*time.Second) } -// transportTo: 指示 adb server 将后续请求路由到指定 serial +// TransportTo 指示 ADB 服务器将后续请求路由到指定设备 +// serial 为设备序列号,例如 "emulator-5556" func TransportTo(conn net.Conn, serial string) error { if err := WriteAdbCmd(conn, "host:transport:"+serial); err != nil { return err @@ -77,15 +89,17 @@ func TransportTo(conn net.Conn, serial string) error { } if status == "FAIL" { msg, _ := ReadLenFrame(conn) - return fmt.Errorf("transport FAIL: %s", string(msg)) + return fmt.Errorf("传输失败: %s", string(msg)) } if status != "OKAY" { - return fmt.Errorf("unexpected transport status: %s", status) + 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 @@ -96,7 +110,7 @@ func ExecShell(conn net.Conn, shellCmd string) ([]byte, error) { } if st != "OKAY" { msg, _ := ReadLenFrame(conn) - return nil, fmt.Errorf("shell FAIL: %s", string(msg)) + return nil, fmt.Errorf("Shell 执行失败: %s", string(msg)) } var buf bytes.Buffer @@ -120,7 +134,10 @@ func ExecShell(conn net.Conn, shellCmd string) ([]byte, error) { return buf.Bytes(), nil } -// readResponse reads a 4-byte response like OKAY/FAIL and returns it and optional message (for FAIL) +// 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 { @@ -128,97 +145,92 @@ func ReadResponse(conn net.Conn, debug bool) (string, []byte, error) { } st := string(stb) if debug { - fmt.Fprintf(os.Stderr, "ReadResponseFixed: status raw hex=%x ascii=%q\n", stb, st) + fmt.Fprintf(os.Stderr, "ReadResponse: 状态原始 hex=%x ascii=%q\n", stb, st) } if st == "OKAY" { return st, nil, nil } if st == "FAIL" { - // read next 4 bytes (may be little-endian uint32 length or ASCII hex) + // 读取后续 4 字节(可能是小端 uint32 长度或 ASCII 十六进制长度) hdr, err := readN(conn, 4, 10*time.Second) if err != nil { if debug { - fmt.Fprintf(os.Stderr, "ReadResponseFixed: no length header after FAIL: %v\n", err) + fmt.Fprintf(os.Stderr, "ReadResponse: FAIL 后无长度头: %v\n", err) } return st, nil, nil } if debug { - fmt.Fprintf(os.Stderr, "ReadResponseFixed: length header raw hex=%x ascii=%q\n", hdr, string(hdr)) + fmt.Fprintf(os.Stderr, "ReadResponse: 长度头原始 hex=%x ascii=%q\n", hdr, string(hdr)) } - // try little-endian uint32 first + // 优先尝试小端 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, "ReadResponseFixed: failed to read %d bytes message: %v\n", l, err) + fmt.Fprintf(os.Stderr, "ReadResponse: 读取 %d 字节消息失败: %v\n", l, err) } return st, nil, nil } if debug { - fmt.Fprintf(os.Stderr, "ReadResponseFixed: message hex=%x ascii=%q\n", msg, string(msg)) + fmt.Fprintf(os.Stderr, "ReadResponse: 消息 hex=%x ascii=%q\n", msg, string(msg)) } return st, msg, nil } - // fallback: try ASCII-hex parse (backwards compatibility) + // 回退:尝试 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, "ReadResponseFixed: failed to read ascii-hex message of len %d: %v\n", n, err) + fmt.Fprintf(os.Stderr, "ReadResponse: 读取 ASCII 十六进制消息(长度 %d)失败: %v\n", n, err) } return st, nil, nil } if debug { - fmt.Fprintf(os.Stderr, "ReadResponseFixed: ascii-hex message hex=%x ascii=%q\n", msg, string(msg)) + fmt.Fprintf(os.Stderr, "ReadResponse: ASCII 十六进制消息 hex=%x ascii=%q\n", msg, string(msg)) } return st, msg, nil } - // neither produced a message + // 两种方式都未解析出消息 return st, nil, nil } - // unexpected token + // 非预期的状态码 return st, nil, nil } -// LaunchUiautomator connects to adb server, transports to device, starts uiautomator2, -// and returns an io.Reader for streaming logs. +// LaunchUiautomator 连接 ADB 服务器,路由到指定设备,启动 UIAutomator2 服务 +// 启动后持续将服务日志输出到标准输出 +// addr 为 ADB 服务器地址,serial 为设备序列号 func LaunchUiautomator(addr, serial string) { conn, err := DialADB(addr, 15*time.Second) if err != nil { - fmt.Println("dial error:", err) + fmt.Println("连接失败:", err) return } - // transport - // adbSend(conn, "host:transport:"+serial) - // resp, _ := adbReadResponse(conn) - // fmt.Println("transport resp:", resp) - TransportTo(conn, serial) - // shell - cmd := "shell:CLASSPATH=/data/local/tmp/u2.jar app_process / com.wetest.uia2.Main" - adbSend(conn, cmd) - resp, _ := adbReadResponse(conn) - fmt.Println("shell resp:", resp) - // 输出流 + // 路由到目标设备 + 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) } - -func adbSend(conn net.Conn, cmd string) error { - length := fmt.Sprintf("%04x", len(cmd)) - _, err := conn.Write([]byte(length + cmd)) - return err -} - -func adbReadResponse(conn net.Conn) (string, error) { - buf := make([]byte, 4) - _, err := conn.Read(buf) - if err != nil { - return "", err - } - return string(buf), nil -} diff --git a/adb/device.go b/adb/device.go index 49237c2..e79c815 100644 --- a/adb/device.go +++ b/adb/device.go @@ -10,23 +10,25 @@ import ( "time" ) -// DeviceInfo 包含从 adb server 列表和设备端 getprop 收集到的信息 +// DeviceInfo 包含从 ADB 服务器设备列表和设备端 getprop 收集到的设备信息 type DeviceInfo struct { - Serial string - State string - Product string - Model string - Device string - TransportID string - Props map[string]string + Serial string // 设备序列号 + State string // 设备状态(device/offline/unauthorized) + Product string // 产品名称 + Model string // 设备型号 + Device string // 设备代号 + TransportID string // ADB 传输 ID + Props map[string]string // 额外属性 } +// AdbDevice 表示一个已建立连接的 ADB 设备 type AdbDevice struct { - Connect net.Conn - Serial string + Connect net.Conn // 到设备的 TCP 连接 + Serial string // 设备序列号 } -// ListDevicesRaw: 请求 host:devices 并返回原始 payload +// ListDevicesRaw 向 ADB 服务器请求设备列表并返回原始文本 +// addr 为 ADB 服务器地址,timeout 为超时时间 func ListDevicesRaw(addr string, timeout time.Duration) (string, error) { conn, err := DialADB(addr, timeout) if err != nil { @@ -43,17 +45,17 @@ func ListDevicesRaw(addr string, timeout time.Duration) (string, error) { } if status == "FAIL" { msg, _ := ReadLenFrame(conn) - return "", fmt.Errorf("adb FAIL: %s", string(msg)) + return "", fmt.Errorf("ADB 返回失败: %s", string(msg)) } if status != "OKAY" { - return "", fmt.Errorf("unexpected status: %s", status) + return "", fmt.Errorf("意外的状态码: %s", status) } var parts []string for { data, err := ReadLenFrame(conn) if err != nil { - // treat short read timeout as finish + // 超时视为读取结束 if ne, ok := err.(net.Error); ok && ne.Timeout() { break } @@ -71,7 +73,8 @@ func ListDevicesRaw(addr string, timeout time.Duration) (string, error) { return strings.Join(parts, ""), nil } -// ParseDevicesPayload: 解析 host:devices 返回的 payload,提取可能的 product/model/device/transport_id +// ParseDevicesPayload 解析 host:devices-l 命令返回的文本 +// 提取设备的序列号、状态、product/model/device/transport_id 等信息 func ParseDevicesPayload(payload string) []DeviceInfo { out := []DeviceInfo{} lines := strings.Split(payload, "\n") @@ -80,7 +83,7 @@ func ParseDevicesPayload(payload string) []DeviceInfo { if ln == "" || strings.HasPrefix(ln, "List of devices attached") { continue } - // adb -l 格式通常:serial key:val key:val ... + // adb -l 格式:serial key:val key:val ... fields := strings.Fields(ln) if len(fields) < 2 { continue @@ -90,7 +93,7 @@ func ParseDevicesPayload(payload string) []DeviceInfo { State: fields[1], Props: map[string]string{}, } - // parse remaining key:val pairs + // 解析剩余的 key:val 键值对 for _, kv := range fields[2:] { if strings.Contains(kv, ":") { parts := strings.SplitN(kv, ":", 2) @@ -106,7 +109,7 @@ func ParseDevicesPayload(payload string) []DeviceInfo { case "transport_id": dev.TransportID = v default: - // store any extra short fields into Props under prefixed key + // 将未知字段存入 Props,添加 "short." 前缀 dev.Props["short."+k] = v } } @@ -116,26 +119,24 @@ func ParseDevicesPayload(payload string) []DeviceInfo { return out } -// parseGetprop parses getprop output "key]: [value" lines into map +// parseGetprop 解析 getprop 命令的输出 +// 输出格式为 "[key]: [value]",解析为 map func parseGetprop(raw []byte) map[string]string { m := map[string]string{} sc := bufio.NewScanner(strings.NewReader(string(raw))) for sc.Scan() { line := strings.TrimSpace(sc.Text()) - // getprop lines are like: [ro.build.version.release]: [10] if line == "" { continue } - // find first ']:' separator - // safe parse: extract between first '[' and first ']: [' pattern - // simpler: split by "]: [" into two parts after trimming surrounding brackets + // getprop 输出格式:[ro.build.version.release]: [10] parts := strings.SplitN(line, "]: [", 2) if len(parts) == 2 { k := strings.TrimPrefix(parts[0], "[") v := strings.TrimSuffix(parts[1], "]") m[k] = v } else { - // fallback: try split by ": " + // 回退:尝试按 ": " 分割 kv := strings.SplitN(line, ": ", 2) if len(kv) == 2 { m[strings.Trim(kv[0], "[]")] = strings.Trim(kv[1], "[]") @@ -145,6 +146,8 @@ func parseGetprop(raw []byte) map[string]string { return m } +// ParseDevicesMap 将设备列表文本解析为 serial → product 的映射 +// 同时返回原始行列表 func ParseDevicesMap(payload string) (map[string]string, []string) { m := make(map[string]string) lines := strings.Split(payload, "\n") @@ -170,7 +173,8 @@ func ParseDevicesMap(payload string) (map[string]string, []string) { return m, lines } -// Find device serial by product value and return serial (first match) +// FindSerialByProduct 根据产品名称查找设备序列号 +// 优先从设备列表中匹配,如果列表中没有 product 字段,则回退到逐设备查询 getprop func FindSerialByProduct(addr, targetProduct string) (string, error) { payload, err := ListDevicesRaw(addr, 3*time.Second) if err != nil { @@ -182,9 +186,8 @@ func FindSerialByProduct(addr, targetProduct string) (string, error) { return serial, nil } } - // fallback: if no product fields in devices-l, try per-device getprop - payloadBasic, _ := ListDevicesRaw(addr, 3*time.Second) // reuse; could be host:devices if preferred - lines := strings.Split(payloadBasic, "\n") + // 回退:逐设备查询 getprop ro.product.model + lines := strings.Split(payload, "\n") for _, ln := range lines { ln = strings.TrimSpace(ln) if ln == "" || strings.HasPrefix(ln, "List of devices attached") { @@ -195,12 +198,10 @@ func FindSerialByProduct(addr, targetProduct string) (string, error) { continue } serial := fields[0] - // query getprop ro.product.model for this serial conn, err := DialADB(addr, 2*time.Second) if err != nil { continue } - // ensure close defer conn.Close() if err := TransportTo(conn, serial); err != nil { continue @@ -212,91 +213,49 @@ func FindSerialByProduct(addr, targetProduct string) (string, error) { } } } - return "", fmt.Errorf("no device with product=%s found", targetProduct) + return "", fmt.Errorf("未找到产品名为 %s 的设备", targetProduct) } -// 安装APK到设备 +// InstallApkOnDevice 安装 APK 到设备 +// addr 为 ADB 服务器地址,serial 为设备序列号 +// remoteTmp 为 APK 在设备上的临时路径 +// pmArgs 为 pm install 的额外参数(如 "-r" 表示覆盖安装),默认为 "-r" +// debug 为 true 时输出调试信息 func InstallApkOnDevice(addr, serial string, remoteTmp string, pmArgs string, debug bool) (string, error) { conn, err := DialADB(addr, 15*time.Second) if err != nil { - fmt.Println("dial error:", err) + fmt.Println("连接失败:", err) return "", err } defer conn.Close() TransportTo(conn, serial) - // 1. 准备远端临时路径 + if pmArgs == "" { pmArgs = "-r" } - // 执行 shell: pm install ... - // 使用 exec shell(非 interactive)通过 "shell:" + // 执行 pm install 命令 installCmd := "pm install " + pmArgs + " " + remoteTmp if debug { - fmt.Printf("[DEBUG] running shell command: %s\n", installCmd) + fmt.Printf("[调试] 执行命令: %s\n", installCmd) } outBuf, err := ExecShell(conn, installCmd) - if err != nil { - log.Println("[ERROR] ", err) + log.Println("[错误]", err) return "", err } outStr := string(outBuf) if debug { - fmt.Printf("[DEBUG] pm install output:\n%s\n", outStr) + fmt.Printf("[调试] pm install 输出:\n%s\n", outStr) } + // 安装完成后删除临时文件 ExecShell(conn, "rm -f "+remoteTmp) - // 5. 根据 pm 输出判断成功(pm install 成功通常包含 "Success") - if containsSuccess(outStr) { + + // 根据输出判断是否安装成功 + if strings.Contains(strings.ToLower(outStr), "success") { return outStr, nil } - return outStr, fmt.Errorf("install failed: %s", outStr) + return outStr, fmt.Errorf("安装失败: %s", outStr) } - -func containsSuccess(s string) bool { - // 简单判断:忽略大小写包含 "success" - return (len(s) > 0) && (stringContainsFold(s, "success")) -} - -func stringContainsFold(s, sub string) bool { - // 不依赖 strings 包的 ToLower 性能差别,这里直接用标准方法 - return (len(s) >= len(sub)) && (IndexFold(s, sub) >= 0) -} - -func IndexFold(s, sub string) int { - // 直接使用 strings 包实现(为了清晰,这里直接调用) - return indexFoldUsingStrings(s, sub) -} - -func indexFoldUsingStrings(s, sub string) int { - // 实际工程里直接用 strings.Contains(strings.ToLower(s), strings.ToLower(sub)) - // 但为了最小示例,这里直接实现: - // 换成标准库实现: - // - // 注意:下面两行才是简洁实现 - // - // import "strings" - // return strings.Index(strings.ToLower(s), strings.ToLower(sub)) - // - // 这里我们直接调用: - return stringsIndexFold(s, sub) -} - -func stringsIndexFold(s, sub string) int { - // 调用标准库 - // 把此函数简单实现为: - // strings.Index(strings.ToLower(s), strings.ToLower(sub)) - // 以便示例完整可运行 - // 这里需要引入 strings 包 - // 为了保持示例简洁,我在文件顶部添加下面两行导入: - // - // "strings" - // - // 然后直接实现: - return stringsIndex(stringsToLower(s), stringsToLower(sub)) -} - -func stringsToLower(s string) string { return strings.ToLower(s) } -func stringsIndex(a, b string) int { return strings.Index(a, b) } diff --git a/adb/doc.go b/adb/doc.go new file mode 100644 index 0000000..b61c43b --- /dev/null +++ b/adb/doc.go @@ -0,0 +1,21 @@ +// Package adb 实现了 Android Debug Bridge (ADB) 协议的核心功能。 +// +// 本包提供了纯 Go 实现的 ADB 客户端,支持以下功能: +// - 设备发现与连接管理 +// - Shell 命令执行 +// - 文件同步传输(推送文件到设备) +// - APK 安装 +// - UIAutomator2 服务启动 +// +// 使用示例: +// +// // 连接 ADB 服务器 +// conn, err := adb.DialADB("127.0.0.1:5037", 15*time.Second) +// defer conn.Close() +// +// // 路由到指定设备 +// adb.TransportTo(conn, "emulator-5556") +// +// // 执行 Shell 命令 +// output, err := adb.ExecShell(conn, "getprop ro.product.model") +package adb diff --git a/adb/sync.go b/adb/sync.go index eab4855..93d1460 100644 --- a/adb/sync.go +++ b/adb/sync.go @@ -11,12 +11,16 @@ import ( "time" ) +// maxChunk 定义单次数据块传输的最大字节数(64KB) const maxChunk = 64 * 1024 +// Sync 封装了 ADB 文件同步协议的操作 type Sync struct { - Conn net.Conn + Conn net.Conn // 已建立的 ADB 连接 } +// InitSync 创建一个文件同步操作实例 +// conn 必须是已经通过 TransportTo 路由到目标设备的连接 func InitSync(conn net.Conn) *Sync { return &Sync{ Conn: conn, @@ -25,11 +29,12 @@ func InitSync(conn net.Conn) *Sync { // SyncPushFile 将本地文件推送到设备 // localPath: 本地文件路径 -// remotePath: 设备目标路径 -// mode: 文件权限 (如 0644) -// debug: 是否打印调试信息 +// remotePath: 设备端目标路径 +// mode: 文件权限(如 0644) +// debug: 是否输出调试信息 +// 返回写入的字节数和错误信息 func (s *Sync) SyncPushFile(localPath, remotePath string, mode int, debug bool) (int64, error) { - // 打开文件 + // 打开本地文件 f, err := os.Open(localPath) if err != nil { return 0, err @@ -37,14 +42,19 @@ func (s *Sync) SyncPushFile(localPath, remotePath string, mode int, debug bool) defer f.Close() fi, _ := f.Stat() if debug { - fmt.Printf("[DEBUG] local filesize=%d\n", fi.Size()) + fmt.Printf("[调试] 本地文件大小=%d\n", fi.Size()) } - s.StartSync() - // 构造 SEND payload + + // 初始化同步模式 + if err := s.StartSync(); err != nil { + return 0, err + } + + // 构造 SEND 请求:remotePath + "," + 文件权限 modeStr := strconv.Itoa(syscall.S_IFREG | mode) sendPayload := []byte(remotePath + "," + modeStr) - // 写入 "SEND" + 长度 + payload + // 写入 "SEND" 命令头 + 数据长度 + 请求内容 hdr := make([]byte, 8) copy(hdr[:4], []byte("SEND")) binary.LittleEndian.PutUint32(hdr[4:], uint32(len(sendPayload))) @@ -55,19 +65,19 @@ func (s *Sync) SyncPushFile(localPath, remotePath string, mode int, debug bool) return 0, err } if debug { - fmt.Printf("[DEBUG] Wrote SEND payload len=%d path=%s mode=%s\n", len(sendPayload), remotePath, modeStr) + fmt.Printf("[调试] 发送 SEND 请求: 长度=%d 路径=%s 权限=%s\n", len(sendPayload), remotePath, modeStr) } - // 写入 DATA 块 + // 分块写入文件数据(DATA 命令) var total int64 buf := make([]byte, maxChunk) for { n, rerr := f.Read(buf) if n > 0 { - hdr := make([]byte, 8) - copy(hdr[:4], []byte("DATA")) - binary.LittleEndian.PutUint32(hdr[4:], uint32(n)) - if _, err := s.Conn.Write(hdr); err != nil { + dataHdr := make([]byte, 8) + copy(dataHdr[:4], []byte("DATA")) + binary.LittleEndian.PutUint32(dataHdr[4:], uint32(n)) + if _, err := s.Conn.Write(dataHdr); err != nil { return total, err } if _, err := s.Conn.Write(buf[:n]); err != nil { @@ -83,7 +93,7 @@ func (s *Sync) SyncPushFile(localPath, remotePath string, mode int, debug bool) } } - // 发送 DONE + // 发送 DONE 命令,携带文件修改时间戳 done := make([]byte, 8) copy(done[:4], []byte("DONE")) mtime := uint32(fi.ModTime().Unix()) @@ -92,7 +102,7 @@ func (s *Sync) SyncPushFile(localPath, remotePath string, mode int, debug bool) return total, err } if debug { - fmt.Println("[DEBUG] Wrote DONE, waiting response") + fmt.Println("[调试] 发送 DONE,等待响应") } // 读取最终响应 @@ -102,13 +112,15 @@ func (s *Sync) SyncPushFile(localPath, remotePath string, mode int, debug bool) } if resp != "OKAY" { if len(msg) > 0 { - return total, fmt.Errorf("sync failed: %s", string(msg)) + return total, fmt.Errorf("同步失败: %s", string(msg)) } - return total, fmt.Errorf("sync failed: %s", resp) + return total, fmt.Errorf("同步失败: %s", resp) } return total, nil } +// StartSync 启动 ADB 同步模式 +// 发送 "sync:" 命令并等待 "OKAY" 响应 func (s *Sync) StartSync() error { if err := WriteAdbCmd(s.Conn, "sync:"); err != nil { return err @@ -120,13 +132,15 @@ func (s *Sync) StartSync() error { if string(tok) != "OKAY" { _, msg, _ := ReadSyncStatus(s.Conn) if len(msg) > 0 { - return fmt.Errorf("sync open failed: %s", string(msg)) + return fmt.Errorf("同步模式启动失败: %s", string(msg)) } - return fmt.Errorf("sync open failed: %q", string(tok)) + return fmt.Errorf("同步模式启动失败: %q", string(tok)) } return nil } +// ReadSyncStatus 读取同步协议的状态响应 +// 返回状态码("OKAY"/"FAIL"/其他)、失败时的消息内容和错误 func ReadSyncStatus(r io.Reader) (string, string, error) { hdr := make([]byte, 4) if _, err := io.ReadFull(r, hdr); err != nil { @@ -137,6 +151,7 @@ func ReadSyncStatus(r io.Reader) (string, string, error) { case "OKAY": return "OKAY", "", nil case "FAIL": + // 读取 4 字节小端长度 + 对应长度的错误消息 lenBuf := make([]byte, 4) if _, err := io.ReadFull(r, lenBuf); err != nil { return "FAIL", "", err @@ -148,7 +163,7 @@ func ReadSyncStatus(r io.Reader) (string, string, error) { } return "FAIL", string(msg), nil default: - // 非预期状态,直接返回原始字符串,便于上层报错 + // 非预期状态,直接返回原始字符串,便于上层排查 return status, "", nil } } diff --git a/cmd/main.go b/cmd/main.go index c79b4a2..ab5be22 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,92 +3,94 @@ package main import ( "encoding/json" "fmt" - "go-mobile-uiautomator/adb" - "go-mobile-uiautomator/services" "time" + + "github.com/zhuy1228/go-mobile-uiautomator/adb" + "github.com/zhuy1228/go-mobile-uiautomator/services" ) -const serial = "emulator-5556" -const addr = "127.0.0.1:5037" +// 设备配置,根据实际环境修改 +const ( + serial = "emulator-5554" // 设备序列号 + addr = "127.0.0.1:5037" // ADB 服务器地址 +) func main() { - LaunchUiautomator() + filePushInstall() } -func LaunchUiautomator() { - FilePushInstall() +// launchUiautomator 推送服务文件并启动 UIAutomator2 +func launchUiautomator() { + filePushInstall() go adb.LaunchUiautomator(addr, serial) - select {} + select {} // 阻塞等待,持续输出日志 } -// 文件推送加安装 -func FilePushInstall() { +// filePushInstall 列出设备并推送 u2.jar 到设备 +func filePushInstall() { payload, _ := adb.ListDevicesRaw(addr, 15*time.Second) m := adb.ParseDevicesPayload(payload) b2, _ := json.MarshalIndent(m, "", " ") fmt.Println(string(b2)) services.InstallServiceJar(addr, serial) - - // services.InstallServiceApk(addr, serial) } -// 文件推送验证 -func FilePush() { - // edit these for your environment +// filePush 文件推送验证示例 +func filePush() { + // 根据实际环境修改以下参数 local := "C:/Users/01/Desktop/aaa.PNG" remote := "/sdcard/ccc.PNG" mode := 0644 targetProduct := "23113RKC6C" - serial, err := adb.FindSerialByProduct(addr, targetProduct) + devSerial, err := adb.FindSerialByProduct(addr, targetProduct) if err != nil { - fmt.Println("find device error:", err) + fmt.Println("查找设备失败:", err) return } - fmt.Println("found serial:", serial) + fmt.Println("找到设备:", devSerial) - // Now open a new connection and transport to the found device for further ops conn, err := adb.DialADB(addr, 15*time.Second) if err != nil { - fmt.Println("dial error:", err) + fmt.Println("连接失败:", err) return } defer conn.Close() - adb.TransportTo(conn, serial) + adb.TransportTo(conn, devSerial) sync := adb.InitSync(conn) n, err := sync.SyncPushFile(local, remote, mode, true) if err != nil { - fmt.Println("Push 失败:", err) + fmt.Println("推送失败:", err) } else { - fmt.Printf("Push 成功, 共写入 %d 字节\n", n) + fmt.Printf("推送成功, 共写入 %d 字节\n", n) } } -// 连接验证 -func Connect() { +// connect 连接验证示例 +func connect() { targetProduct := "23113RKC6C" - serial, err := adb.FindSerialByProduct(addr, targetProduct) + devSerial, err := adb.FindSerialByProduct(addr, targetProduct) if err != nil { - fmt.Println("find device error:", err) + fmt.Println("查找设备失败:", err) return } - fmt.Println("found serial:", serial) + fmt.Println("找到设备:", devSerial) - // Now open a new connection and transport to the found device for further ops conn, err := adb.DialADB(addr, 15*time.Second) if err != nil { - fmt.Println("dial error:", err) + fmt.Println("连接失败:", err) return } defer conn.Close() - adb.TransportTo(conn, serial) + adb.TransportTo(conn, devSerial) + out, err := adb.ExecShell(conn, "getprop") if err != nil { - fmt.Println("shell error:", err) + fmt.Println("Shell 执行失败:", err) } else { - fmt.Printf("shell out: %q\n", string(out)) + fmt.Printf("Shell 输出: %q\n", string(out)) } } diff --git a/config/index.go b/config/index.go deleted file mode 100644 index 7863bcc..0000000 --- a/config/index.go +++ /dev/null @@ -1,43 +0,0 @@ -package config - -import ( - "os" - - "gopkg.in/yaml.v3" -) - -type AppConfig struct { - Port int `yaml:"port"` - WsUrl string `yaml:"wsUrl"` - StunUrl string `yaml:"stunUrl"` - ApiUrl string `yaml:"apiUrl"` - AppName string `yaml:"appName"` - SiteFileDir string `yaml:"siteFileDir"` -} - -func LoadConfig() (*AppConfig, error) { - // 读取文件内容 - data, err := os.ReadFile("config.yaml") - if err != nil { - return nil, err - } - - // 解析 YAML - var cfg AppConfig - if err := yaml.Unmarshal(data, &cfg); err != nil { - return nil, err - } - - return &cfg, nil -} - -// 更新配置(写回文件) -func SaveConfig(cfg *AppConfig) error { - data, err := yaml.Marshal(cfg) - if err != nil { - return err - } - - // 写回文件,0644 表示文件权限 - return os.WriteFile("config.yaml", data, 0644) -} diff --git a/go.mod b/go.mod index 47ccbbb..09993c0 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ -module go-mobile-uiautomator +module github.com/zhuy1228/go-mobile-uiautomator go 1.25.1 - -require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index a62c313..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +0,0 @@ -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/libs/doc.go b/libs/doc.go new file mode 100644 index 0000000..815c0b4 --- /dev/null +++ b/libs/doc.go @@ -0,0 +1,16 @@ +// Package libs 提供了与 UIAutomator2 服务交互的工具库。 +// +// 本包包含以下核心组件: +// - AdbHTTPConnection:通过 ADB 隧道发送 HTTP 请求到设备端 UIAutomator2 服务 +// - Selector:UI 元素选择器构造器,支持文本、类名、资源 ID 等多种查询条件 +// - HTTPResponse:HTTP 响应封装 +// +// 使用示例: +// +// // 创建 UI 选择器 +// selector := libs.MustNew(map[string]interface{}{ +// "text": "登录", +// "className": "android.widget.Button", +// }) +// jsonData, _ := selector.ToJSON() +package libs diff --git a/libs/repuest.go b/libs/request.go similarity index 57% rename from libs/repuest.go rename to libs/request.go index 5b471fc..7e38ba7 100644 --- a/libs/repuest.go +++ b/libs/request.go @@ -14,50 +14,61 @@ import ( "time" ) -// ---------- 用于适配的外部接口/类型 ---------- +// ---------- 外部接口和类型定义 ---------- + +// AdbDevice 定义了通过 ADB 隧道创建设备连接的接口 type AdbDevice interface { - // CreateConnection 建立到设备的 TCP 连接(通常通过 adb 隧道/port-forward 实现) - // network 常为 "tcp" 或 "tcp4"/"tcp6", port 为设备上服务监听端口 + // CreateConnection 建立到设备的 TCP 连接 + // network 通常为 "tcp",port 为设备上服务监听端口 CreateConnection(network string, port int) (net.Conn, error) } -// ---------- HTTPResponse 等价类型 ---------- +// HTTPResponse 封装 HTTP 响应数据 type HTTPResponse struct { - Content []byte - Status int - Reason string + Content []byte // 响应体内容 + Status int // HTTP 状态码 + Reason string // 状态描述 } +// JSON 将响应体解析为指定的结构体 func (r *HTTPResponse) JSON(v interface{}) error { return json.Unmarshal(r.Content, v) } +// Text 返回响应体的文本内容 func (r *HTTPResponse) Text() string { return string(r.Content) } // ---------- 自定义错误类型 ---------- + var ( - ErrHTTPTimeout = errors.New("http request timeout") - ErrHTTPFailed = errors.New("http request failed") + // ErrHTTPTimeout 表示 HTTP 请求超时 + ErrHTTPTimeout = errors.New("HTTP 请求超时") + // ErrHTTPFailed 表示 HTTP 请求失败 + ErrHTTPFailed = errors.New("HTTP 请求失败") ) -// ---------- AdbHTTPConnection 核心:使用 net.Conn 写请求并用 http.ReadResponse 解析 ---------- +// ---------- AdbHTTPConnection:通过 ADB 隧道发送 HTTP 请求 ---------- + +// AdbHTTPConnection 基于 net.Conn 实现的 HTTP 连接 +// 通过 ADB 端口转发直接与设备端 UIAutomator2 服务通信 type AdbHTTPConnection struct { Conn net.Conn } +// NewAdbHTTPConnection 创建一个新的 ADB HTTP 连接 +// dev 为设备接口,port 为设备端服务端口,timeout 为连接超时 func NewAdbHTTPConnection(dev AdbDevice, port int, timeout time.Duration) (*AdbHTTPConnection, error) { - // 这里 network 使用 "tcp",调用方可依据实际实现调整 conn, err := dev.CreateConnection("tcp", port) if err != nil { - return nil, fmt.Errorf("unable to connect to uiautomator2 server: %w", err) + return nil, fmt.Errorf("无法连接到 UIAutomator2 服务: %w", err) } - // 设置默认 deadline,调用方可在需要时调整 Conn.SetDeadline _ = conn.SetDeadline(time.Now().Add(timeout)) return &AdbHTTPConnection{Conn: conn}, nil } +// Close 关闭底层连接 func (c *AdbHTTPConnection) Close() error { if c.Conn != nil { return c.Conn.Close() @@ -65,124 +76,125 @@ func (c *AdbHTTPConnection) Close() error { return nil } -// sendRequest 写入 HTTP 请求并返回 *http.Response +// sendRequest 将 HTTP 请求写入连接并读取响应 +// 通过原始 TCP 连接发送 HTTP 报文,避免依赖标准 http.Client func (c *AdbHTTPConnection) sendRequest(req *http.Request, timeout time.Duration) (*http.Response, error) { - // 确保 deadline + // 设置读写截止时间 if timeout > 0 { _ = c.Conn.SetDeadline(time.Now().Add(timeout)) } else { _ = c.Conn.SetDeadline(time.Time{}) } - // 将 http.Request 序列化为原始 HTTP 报文并写到 conn + // 序列化 HTTP 请求为原始报文 var buf bytes.Buffer - // 行: METHOD PATH HTTP/1.1 + + // 请求行:METHOD PATH HTTP/1.1 path := req.URL.RequestURI() if path == "" { path = "/" } fmt.Fprintf(&buf, "%s %s HTTP/1.1\r\n", req.Method, path) - // Host 头;Python 里使用 "localhost" 但最终是通过 adb 隧道,Host 不重要,这里写 localhost fmt.Fprintf(&buf, "Host: localhost\r\n") - // 写 headers - // 保证 Content-Length 或 Transfer-Encoding 存在 + // 设置默认请求头 if req.Header.Get("User-Agent") == "" { req.Header.Set("User-Agent", "uiautomator2") } if req.Header.Get("Accept-Encoding") == "" { - // 与 Python 保持一致,明确禁用压缩 req.Header.Set("Accept-Encoding", "") } if req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "application/json") } - // copy headers into buffer + // 写入请求头 for k, vals := range req.Header { for _, v := range vals { fmt.Fprintf(&buf, "%s: %s\r\n", k, v) } } - // body + // 处理请求体 var bodyBytes []byte if req.Body != nil { var err error bodyBytes, err = io.ReadAll(req.Body) if err != nil { - return nil, fmt.Errorf("read request body failed: %w", err) + return nil, fmt.Errorf("读取请求体失败: %w", err) } - // set Content-Length fmt.Fprintf(&buf, "Content-Length: %d\r\n", len(bodyBytes)) } else { fmt.Fprintf(&buf, "Content-Length: 0\r\n") } - // header-body separator + // 请求头与请求体之间的空行 buf.WriteString("\r\n") - // write header+body to conn + // 发送请求头 if _, err := c.Conn.Write(buf.Bytes()); err != nil { - return nil, fmt.Errorf("write request headers failed: %w", err) + return nil, fmt.Errorf("发送请求头失败: %w", err) } + // 发送请求体 if len(bodyBytes) > 0 { if _, err := c.Conn.Write(bodyBytes); err != nil { - return nil, fmt.Errorf("write request body failed: %w", err) + return nil, fmt.Errorf("发送请求体失败: %w", err) } } - // read response using http.ReadResponse + // 使用标准库解析 HTTP 响应 reader := bufio.NewReader(c.Conn) - // note: http.ReadResponse expects a Request to be passed for RequestURI related logic, - // but if nil it still parses status & headers fine. Provide req for completeness. resp, err := http.ReadResponse(reader, req) if err != nil { - return nil, fmt.Errorf("read http response failed: %w", err) + return nil, fmt.Errorf("读取 HTTP 响应失败: %w", err) } return resp, nil } -// ---------- _http_request 的 Go 等价实现 ---------- +// ---------- HttpRequest:高层 HTTP 请求封装 ---------- + +// HttpRequest 向设备端 UIAutomator2 服务发送 HTTP 请求 +// ctx 为上下文控制,dev 为设备接口,devicePort 为设备端服务端口 +// method 为 HTTP 方法,path 为请求路径 +// data 为请求体数据(会被 JSON 编码),timeoutSecs 为超时秒数 +// printRequest 为 true 时输出 curl 风格的调试信息 func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, path string, data map[string]interface{}, timeoutSecs float64, printRequest bool) (*HTTPResponse, error) { - // 兼容 python 默认 timeout + // 默认超时 10 秒 if timeoutSecs <= 0 { timeoutSecs = 10.0 } timeout := time.Duration(timeoutSecs * float64(time.Second)) - // debug 打印 curl 样式 + // 调试模式:打印 curl 风格的请求信息 if printRequest { now := time.Now().Format("15:04:05.000") url := fmt.Sprintf("http://127.0.0.1:%d%s", devicePort, path) if data != nil { b, _ := json.Marshal(data) - fmt.Printf("# http timeout=%.3f\n%s $ curl -X %s %s -d '%s'\n", timeoutSecs, now, method, url, string(b)) + fmt.Printf("# HTTP 超时=%.3f\n%s $ curl -X %s %s -d '%s'\n", timeoutSecs, now, method, url, string(b)) } else { - fmt.Printf("# http timeout=%.3f\n%s $ curl -X %s %s\n", timeoutSecs, now, method, url) + fmt.Printf("# HTTP 超时=%.3f\n%s $ curl -X %s %s\n", timeoutSecs, now, method, url) } } - // 构造 http.Request + // 构造 HTTP 请求 var body io.Reader if data != nil { b, err := json.Marshal(data) if err != nil { - return nil, fmt.Errorf("json marshal data failed: %w", err) + return nil, fmt.Errorf("JSON 编码失败: %w", err) } body = bytes.NewReader(b) } - // URL 可以是任何合法的虚拟 URL,因为我们直接写原始请求行到 conn req, err := http.NewRequestWithContext(ctx, method, "http://localhost"+path, body) if err != nil { - return nil, fmt.Errorf("create http request failed: %w", err) + return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err) } - // 设置 headers 与 Python 保持一致 req.Header.Set("User-Agent", "uiautomator2") req.Header.Set("Accept-Encoding", "") req.Header.Set("Content-Type", "application/json") - // 建立到设备的连接(AdbHTTPConnection.connect) + // 建立到设备的连接 connWrapper, err := NewAdbHTTPConnection(dev, devicePort, timeout) if err != nil { return nil, err @@ -192,7 +204,6 @@ func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, pat // 发送请求并读取响应 resp, err := connWrapper.sendRequest(req, timeout) if err != nil { - // 判断是否为超时(net.Error with Timeout) var netErr net.Error if errors.As(err, &netErr) && netErr.Timeout() { return nil, fmt.Errorf("%w: %v", ErrHTTPTimeout, err) @@ -201,14 +212,14 @@ func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, pat } defer resp.Body.Close() - // 按块读取响应体(与 Python 的循环等效) + // 读取响应体 content, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("read response body failed: %w", err) + return nil, fmt.Errorf("读取响应体失败: %w", err) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("http request failed: %d %s", resp.StatusCode, resp.Status) + return nil, fmt.Errorf("HTTP 请求失败: %d %s", resp.StatusCode, resp.Status) } response := &HTTPResponse{ @@ -219,7 +230,7 @@ func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, pat if printRequest { now := time.Now().Format("15:04:05.000") - fmt.Printf("%s Response >>>\n%s\n<<< END timed_used = %.3f\n\n", now, strings.TrimRight(response.Text(), "\n"), time.Since(time.Now().Add(-timeout)).Seconds()) + fmt.Printf("%s 响应 >>>\n%s\n<<< 结束\n\n", now, strings.TrimRight(response.Text(), "\n")) } return response, nil diff --git a/libs/selector.go b/libs/selector.go index ccc0ab5..1d259ff 100644 --- a/libs/selector.go +++ b/libs/selector.go @@ -7,28 +7,30 @@ import ( "strconv" ) -// FieldMeta 保存每个字段对应的 mask 位和默认值(nil 表示无默认值) +// FieldMeta 定义选择器字段的掩码位和默认值 +// Bit 为该字段对应的掩码位,Default 为默认值(nil 表示无默认值) type FieldMeta struct { Bit uint32 Default interface{} } // Selector 表示一个 UiSelector 的构造器 +// 用于构建 Android UI 元素的查询条件 type Selector struct { - // 存放字段及其值(只包含显式设置的字段) + // 存放已设置的字段及其值 fields map[string]interface{} - // mask 值(通过设置/删除字段自动维护) + // 掩码值(通过设置/删除字段自动维护) mask uint32 - // childOrSibling 顺序列表,元素为 "child" 或 "sibling" + // 子/兄弟关系列表,元素为 "child" 或 "sibling" childOrSibling []string // 对应的嵌套 Selector 列表,长度与 childOrSibling 相同 childOrSiblingSelector []*Selector } -// 字段元数据(与 Python 版本一致) +// fieldDefs 定义所有支持的选择器字段及其元数据(与 Python 版 uiautomator2 一致) var fieldDefs = map[string]FieldMeta{ "text": {Bit: 0x01, Default: nil}, "textContains": {Bit: 0x02, Default: nil}, @@ -57,7 +59,7 @@ var fieldDefs = map[string]FieldMeta{ "instance": {Bit: 0x01000000, Default: 0}, } -// New creates a Selector and可选传入初始字段 +// New 创建一个新的 Selector,可选传入初始字段 func New(initial map[string]interface{}) (*Selector, error) { s := &Selector{ fields: make(map[string]interface{}), @@ -73,7 +75,7 @@ func New(initial map[string]interface{}) (*Selector, error) { return s, nil } -// MustNew 跟 New 相同,但在错误时 panic,便于简洁示例 +// MustNew 与 New 相同,但在出错时 panic,适合简洁的初始化场景 func MustNew(initial map[string]interface{}) *Selector { s, err := New(initial) if err != nil { @@ -82,42 +84,43 @@ func MustNew(initial map[string]interface{}) *Selector { return s } -// validateValue 对给定字段和值做类型校验(布尔字段与整数字段) +// validateValue 对给定字段和值进行类型校验 +// 布尔字段要求值为 bool 类型,整数字段要求值为整数类型 func validateValue(key string, val interface{}) error { meta, ok := fieldDefs[key] if !ok { - return fmt.Errorf("field %s is not allowed", key) + return fmt.Errorf("不支持的字段: %s", key) } if meta.Default == false { - // 期望 bool + // 布尔字段校验 _, ok := val.(bool) if !ok { - return fmt.Errorf("%s must be bool", key) + return fmt.Errorf("%s 必须是 bool 类型", key) } return nil } - // 对整数字段(Default 为 int 类型)要求 int + // 整数字段校验 switch d := meta.Default.(type) { case int: - // 支持 int 和可被转为 int 的数值(如 int64) switch val.(type) { case int, int8, int16, int32, int64: return nil case uint, uint8, uint16, uint32, uint64: return nil default: - return fmt.Errorf("%s must be integer type, default=%v", key, d) + return fmt.Errorf("%s 必须是整数类型, 默认值=%v", key, d) } default: - // 其它字段没有特别要求 + // 其他字段无特殊类型要求 return nil } } -// Set 设置字段并更新 mask;若字段非法或类型不对则返回错误 +// Set 设置字段值并更新掩码 +// 如果字段名非法或类型不匹配则返回错误 func (s *Selector) Set(key string, val interface{}) error { if _, ok := fieldDefs[key]; !ok { - return fmt.Errorf("%s is not allowed", key) + return fmt.Errorf("不支持的字段: %s", key) } if err := validateValue(key, val); err != nil { return err @@ -127,10 +130,11 @@ func (s *Selector) Set(key string, val interface{}) error { return nil } -// Delete 删除字段并更新 mask;幂等(删除不存在字段不报错) +// Delete 删除字段并更新掩码 +// 删除不存在的字段不会报错(幂等操作) func (s *Selector) Delete(key string) error { if _, ok := fieldDefs[key]; !ok { - return fmt.Errorf("%s is not allowed", key) + return fmt.Errorf("不支持的字段: %s", key) } if _, present := s.fields[key]; present { delete(s.fields, key) @@ -139,12 +143,12 @@ func (s *Selector) Delete(key string) error { return nil } -// Mask 返回当前 mask(只读) +// Mask 返回当前掩码值(只读) func (s *Selector) Mask() uint32 { return s.mask } -// Child 在末尾添加 child +// Child 添加一个子元素选择器 func (s *Selector) Child(initial map[string]interface{}) (*Selector, error) { child, err := New(initial) if err != nil { @@ -155,7 +159,7 @@ func (s *Selector) Child(initial map[string]interface{}) (*Selector, error) { return s, nil } -// Sibling 在末尾添加 sibling +// Sibling 添加一个兄弟元素选择器 func (s *Selector) Sibling(initial map[string]interface{}) (*Selector, error) { child, err := New(initial) if err != nil { @@ -166,7 +170,8 @@ func (s *Selector) Sibling(initial map[string]interface{}) (*Selector, error) { return s, nil } -// UpdateInstance 更新最后一个 childOrSiblingSelector 的 instance 字段(或根 selector) +// UpdateInstance 更新最后一个子/兄弟选择器的 instance 字段 +// 如果没有子/兄弟选择器,则更新根选择器的 instance func (s *Selector) UpdateInstance(i int) error { n := len(s.childOrSiblingSelector) if n > 0 { @@ -175,7 +180,7 @@ func (s *Selector) UpdateInstance(i int) error { return s.Set("instance", i) } -// Clone 深拷贝 Selector,包括子/兄弟 +// Clone 深拷贝当前 Selector,包括所有子/兄弟选择器 func (s *Selector) Clone() *Selector { clone := &Selector{ fields: make(map[string]interface{}, len(s.fields)), @@ -184,8 +189,6 @@ func (s *Selector) Clone() *Selector { childOrSiblingSelector: make([]*Selector, 0, len(s.childOrSiblingSelector)), } for k, v := range s.fields { - // 简单深拷贝:对于常见类型(string,bool,int)直接赋值即可。 - // 若值为复杂结构,调用方应使用 ToMap/ToJSON 再 Parse 得到深拷贝。 clone.fields[k] = v } for _, c := range s.childOrSiblingSelector { @@ -194,7 +197,7 @@ func (s *Selector) Clone() *Selector { return clone } -// ToMap 序列化为 map,便于 RPC 调用或 JSON 编码 +// ToMap 将选择器序列化为 map,便于 JSON 编码或 RPC 调用 func (s *Selector) ToMap() map[string]interface{} { out := make(map[string]interface{}, len(s.fields)+3) for k, v := range s.fields { @@ -212,32 +215,29 @@ func (s *Selector) ToMap() map[string]interface{} { return out } -// ToJSON 返回 ToMap 的 JSON 编码 +// ToJSON 返回选择器的 JSON 编码 func (s *Selector) ToJSON() ([]byte, error) { return json.Marshal(s.ToMap()) } -// FromMap 从 map 恢复 Selector(简单实现,忽略非法字段) +// FromMap 从 map 恢复 Selector 实例 +// 自动解析已知字段、掩码和子/兄弟选择器 func FromMap(data map[string]interface{}) (*Selector, error) { - // 提取根字段 root := &Selector{ fields: make(map[string]interface{}), childOrSibling: []string{}, childOrSiblingSelector: []*Selector{}, mask: 0, } - // 读取已知字段 - for k, meta := range fieldDefs { + // 恢复已知字段 + for k := range fieldDefs { if v, ok := data[k]; ok { - // 尝试 Set 以做类型校验并设置 mask if err := root.Set(k, v); err != nil { return nil, err } - // 注意:Set 已经更新了 mask - _ = meta } } - // 恢复 mask(如果提供了 mask,并且为数值) + // 恢复掩码值 if m, ok := data["mask"]; ok { switch mv := m.(type) { case float64: @@ -248,11 +248,9 @@ func FromMap(data map[string]interface{}) (*Selector, error) { root.mask = uint32(mv) case int64: root.mask = uint32(mv) - default: - // 忽略不能解析的 mask } } - // 恢复 childOrSibling 列表和对应 selector(期望 childOrSiblingSelector 为 []map[string]interface{}) + // 恢复子/兄弟关系列表 if cs, ok := data["childOrSibling"]; ok { if arr, ok := cs.([]interface{}); ok { for _, e := range arr { @@ -262,6 +260,7 @@ func FromMap(data map[string]interface{}) (*Selector, error) { } } } + // 恢复子/兄弟选择器 if css, ok := data["childOrSiblingSelector"]; ok { if arr, ok := css.([]interface{}); ok { for _, item := range arr { @@ -278,13 +277,13 @@ func FromMap(data map[string]interface{}) (*Selector, error) { return root, nil } -// UpdateAtPath 在指定路径(child 索引链)上更新字段 -// path: 逐级索引,例如 [0,2] 表示 childOrSiblingSelector[0].childOrSiblingSelector[2] +// UpdateAtPath 在指定路径上更新字段 +// path 为逐级索引,例如 [0,2] 表示 childOrSiblingSelector[0].childOrSiblingSelector[2] func (s *Selector) UpdateAtPath(path []int, updates map[string]interface{}) error { node := s for _, idx := range path { if idx < 0 || idx >= len(node.childOrSiblingSelector) { - return errors.New("path out of range") + return errors.New("路径索引越界") } node = node.childOrSiblingSelector[idx] } @@ -296,10 +295,9 @@ func (s *Selector) UpdateAtPath(path []int, updates map[string]interface{}) erro return nil } -// String 实现 fmt.Stringer,输出友好可读的 Selector 表示(类似 Python 的 __str__) +// String 实现 fmt.Stringer 接口,输出可读的选择器表示 func (s *Selector) String() string { m := s.ToMap() - // 删除空的 childOrSibling 字段以保持简洁 if _, ok := m["childOrSibling"]; !ok { delete(m, "childOrSibling") delete(m, "childOrSiblingSelector") @@ -308,46 +306,46 @@ func (s *Selector) String() string { return "Selector " + string(b) } -// Example 用法示例(不是正式测试,仅供快速手动运行) +// Example 使用示例(仅供参考,非单元测试) func Example() { - // 初始化根 selector + // 初始化根选择器 root := MustNew(map[string]interface{}{ "className": "android.widget.LinearLayout", }) - // 添加 child + // 添加子元素选择器 root.Child(map[string]interface{}{ "text": "下一步", "instance": 0, }) - // 更新最后一个 child 的 instance + // 更新最后一个子选择器的 instance _ = root.UpdateInstance(2) // 深拷贝 cpy := root.Clone() - // 序列化到 JSON + // 序列化为 JSON j, _ := cpy.ToJSON() fmt.Println(string(j)) } -// 简单测试函数(你可在 package 内使用 testing 包将其改写成真正的单元测试) +// SimpleTests 简单测试函数(建议迁移到 _test.go 文件中使用 testing 包) func SimpleTests() { - // set & delete + // 设置与删除字段 s := MustNew(map[string]interface{}{"text": "hello"}) - fmt.Println("mask after set:", strconv.FormatUint(uint64(s.Mask()), 10)) + fmt.Println("设置后掩码:", strconv.FormatUint(uint64(s.Mask()), 10)) _ = s.Delete("text") - fmt.Println("mask after delete:", strconv.FormatUint(uint64(s.Mask()), 10)) + fmt.Println("删除后掩码:", strconv.FormatUint(uint64(s.Mask()), 10)) - // bool 类型校验 + // 类型校验:bool 字段传入非 bool 值应报错 _, err := New(map[string]interface{}{"checkable": "yes"}) - fmt.Println("expected error for bad bool:", err != nil) + fmt.Println("非法 bool 值报错:", err != nil) - // clone 深拷贝检查 + // 深拷贝独立性验证 s2 := MustNew(map[string]interface{}{"text": "a"}) s2.Child(map[string]interface{}{"text": "b", "instance": 1}) c := s2.Clone() c.childOrSibling[0] = "sibling" - fmt.Println("original childOrSibling:", s2.childOrSibling[0], "clone childOrSibling:", c.childOrSibling[0]) + fmt.Println("原始 childOrSibling:", s2.childOrSibling[0], "克隆 childOrSibling:", c.childOrSibling[0]) } diff --git a/services/doc.go b/services/doc.go new file mode 100644 index 0000000..da39f36 --- /dev/null +++ b/services/doc.go @@ -0,0 +1,5 @@ +// Package services 提供了 UIAutomator2 服务的安装和部署功能。 +// +// 本包负责将 UIAutomator2 相关的资源文件(u2.jar、APK) +// 推送到 Android 设备并完成安装。 +package services diff --git a/services/install_service.go b/services/install_service.go index f9de727..fe3334f 100644 --- a/services/install_service.go +++ b/services/install_service.go @@ -2,45 +2,63 @@ package services import ( "fmt" - "go-mobile-uiautomator/adb" "path/filepath" "time" + + "github.com/zhuy1228/go-mobile-uiautomator/adb" ) +// InstallServiceJar 将 u2.jar 推送到设备的 /data/local/tmp/ 目录 +// addr 为 ADB 服务器地址,serial 为设备序列号 func InstallServiceJar(addr, serial string) { conn, err := adb.DialADB(addr, 15*time.Second) if err != nil { - fmt.Println("dial error:", err) + fmt.Println("连接失败:", err) return } defer conn.Close() - adb.TransportTo(conn, serial) - target_path := "/data/local/tmp/u2.jar" + + if err := adb.TransportTo(conn, serial); err != nil { + fmt.Println("设备路由失败:", err) + return + } + + targetPath := "/data/local/tmp/u2.jar" sync := adb.InitSync(conn) - n, err := sync.SyncPushFile("./assets/u2.jar", target_path, 0644, true) + n, err := sync.SyncPushFile("./assets/u2.jar", targetPath, 0644, true) if err != nil { - fmt.Println("Push 失败:", err) + fmt.Println("推送失败:", err) } else { - fmt.Printf("Push 成功, 共写入 %d 字节\n", n) + fmt.Printf("推送成功, 共写入 %d 字节\n", n) } } +// InstallServiceApk 将 UIAutomator2 APK 推送到设备并安装 +// addr 为 ADB 服务器地址,serial 为设备序列号 func InstallServiceApk(addr, serial string) { conn, err := adb.DialADB(addr, 15*time.Second) if err != nil { - fmt.Println("dial error:", err) + fmt.Println("连接失败:", err) return } - adb.TransportTo(conn, serial) - target_path := "/data/local/tmp/app-uiautomator.apk" + + if err := adb.TransportTo(conn, serial); err != nil { + fmt.Println("设备路由失败:", err) + conn.Close() + return + } + + targetPath := "/data/local/tmp/app-uiautomator.apk" sync := adb.InitSync(conn) abs, _ := filepath.Abs("./assets/app-uiautomator.apk") - n, err := sync.SyncPushFile(abs, target_path, 0644, true) + n, err := sync.SyncPushFile(abs, targetPath, 0644, true) if err != nil { - fmt.Println("Push 失败:", err) + fmt.Println("推送失败:", err) } else { - fmt.Printf("Push 成功, 共写入 %d 字节\n", n) + fmt.Printf("推送成功, 共写入 %d 字节\n", n) } conn.Close() - adb.InstallApkOnDevice(addr, serial, target_path, "-r", true) + + // 在设备上安装 APK(覆盖安装) + adb.InstallApkOnDevice(addr, serial, targetPath, "-r", true) }