修改注释及代码重构
This commit is contained in:
34
.gitignore
vendored
34
.gitignore
vendored
@@ -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/*
|
||||
.vs/
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.log
|
||||
14
README.md
14
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
|
||||
)
|
||||
|
||||
104
adb/connect.go
104
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
|
||||
}
|
||||
|
||||
137
adb/device.go
137
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 <state> key:val key:val ...
|
||||
// adb -l 格式:serial <state> 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:<cmd>"
|
||||
// 执行 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) }
|
||||
|
||||
21
adb/doc.go
Normal file
21
adb/doc.go
Normal file
@@ -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
|
||||
59
adb/sync.go
59
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
|
||||
}
|
||||
}
|
||||
|
||||
68
cmd/main.go
68
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
4
go.mod
4
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
16
libs/doc.go
Normal file
16
libs/doc.go
Normal file
@@ -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
|
||||
@@ -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
|
||||
112
libs/selector.go
112
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])
|
||||
}
|
||||
|
||||
5
services/doc.go
Normal file
5
services/doc.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// Package services 提供了 UIAutomator2 服务的安装和部署功能。
|
||||
//
|
||||
// 本包负责将 UIAutomator2 相关的资源文件(u2.jar、APK)
|
||||
// 推送到 Android 设备并完成安装。
|
||||
package services
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user