From a03d13613c661ac82a7461c6eeaedcd2aafe7601 Mon Sep 17 00:00:00 2001 From: zyj <18107291228@163.com> Date: Wed, 29 Oct 2025 18:31:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BD=93=E5=89=8D=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E8=AE=BE=E5=A4=87=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adb/connect.go | 228 +++++++++++++++++++++++++++++++++++++ adb/device.go | 303 +++++++++++++++++++++++++++++++++++++++++++++++++ adb/sync.go | 249 ++++++++++++++++++++++++++++++++++++++++ cmd/main.go | 42 ++++++- 4 files changed, 821 insertions(+), 1 deletion(-) create mode 100644 adb/connect.go create mode 100644 adb/device.go create mode 100644 adb/sync.go diff --git a/adb/connect.go b/adb/connect.go new file mode 100644 index 0000000..40eee09 --- /dev/null +++ b/adb/connect.go @@ -0,0 +1,228 @@ +package adb + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "net" + "os" + "strconv" + "time" +) + +func DialADB(addr string, timeout time.Duration) (net.Conn, error) { + d := net.Dialer{Timeout: timeout} + return d.Dial("tcp", addr) +} + +func WriteAdbCmd(conn net.Conn, cmd string) error { + header := fmt.Sprintf("%04x", len(cmd)) + conn.SetWriteDeadline(time.Now().Add(2 * time.Second)) + _, err := conn.Write([]byte(header + cmd)) + return err +} + +func readN(conn net.Conn, n int, timeout time.Duration) ([]byte, error) { + buf := make([]byte, n) + total := 0 + for total < n { + conn.SetReadDeadline(time.Now().Add(timeout)) + nr, err := conn.Read(buf[total:]) + if err != nil { + return nil, err + } + total += nr + } + return buf, nil +} + +func ReadStatus(conn net.Conn) (string, error) { + b, err := readN(conn, 4, 3*time.Second) + if err != nil { + return "", err + } + return string(b), nil +} + +func ReadLenFrame(conn net.Conn) ([]byte, error) { + hdr, err := readN(conn, 4, 3*time.Second) + if err != nil { + return nil, err + } + l, err := strconv.ParseInt(string(hdr), 16, 32) + if err != nil { + return nil, err + } + if l == 0 { + return []byte{}, nil + } + return readN(conn, int(l), 10*time.Second) +} + +func ExecOut(conn net.Conn, cmd string) ([]byte, error) { + if err := WriteAdbCmd(conn, "exec-out:"+cmd); err != nil { + return nil, err + } + status, err := ReadStatus(conn) + if err != nil { + return nil, err + } + if status == "FAIL" { + msg, _ := ReadLenFrame(conn) + return nil, fmt.Errorf("exec-out FAIL: %s", string(msg)) + } + if status != "OKAY" { + return nil, fmt.Errorf("unexpected exec-out status: %s", status) + } + var out []byte + for { + data, err := ReadLenFrame(conn) + if err != nil { + if ne, ok := err.(net.Error); ok && ne.Timeout() { + break + } + if err == io.EOF { + break + } + return nil, err + } + if len(data) == 0 { + break + } + out = append(out, data...) + } + return out, nil +} + +// transportTo: 指示 adb server 将后续请求路由到指定 serial +func TransportTo(conn net.Conn, serial string) error { + if err := WriteAdbCmd(conn, "host:transport:"+serial); err != nil { + return err + } + status, err := ReadStatus(conn) + if err != nil { + return err + } + if status == "FAIL" { + msg, _ := ReadLenFrame(conn) + return fmt.Errorf("transport FAIL: %s", string(msg)) + } + if status != "OKAY" { + return fmt.Errorf("unexpected transport status: %s", status) + } + return nil +} + +func ExecShell(conn net.Conn, shellCmd string) ([]byte, error) { + if err := WriteAdbCmd(conn, "shell:"+shellCmd); err != nil { + return nil, err + } + st, err := ReadStatus(conn) + if err != nil { + return nil, err + } + if st != "OKAY" { + msg, _ := ReadLenFrame(conn) + return nil, fmt.Errorf("shell FAIL: %s", string(msg)) + } + + var buf bytes.Buffer + tmp := make([]byte, 4096) + for { + conn.SetReadDeadline(time.Now().Add(3 * time.Second)) + n, err := conn.Read(tmp) + if n > 0 { + buf.Write(tmp[:n]) + } + if err != nil { + if ne, ok := err.(net.Error); ok && ne.Timeout() { + break + } + if err == io.EOF { + break + } + return nil, err + } + } + return buf.Bytes(), nil +} + +// readResponse reads a 4-byte response like OKAY/FAIL and returns it and optional message (for FAIL) +func ReadResponse(conn net.Conn, debug bool) (string, []byte, error) { + readN := func(n int) ([]byte, error) { + buf := make([]byte, n) + total := 0 + conn.SetReadDeadline(time.Now().Add(5 * time.Second)) + for total < n { + nr, err := conn.Read(buf[total:]) + if err != nil { + return nil, err + } + total += nr + } + return buf, nil + } + + stb, err := readN(4) + if err != nil { + return "", nil, err + } + st := string(stb) + if debug { + fmt.Fprintf(os.Stderr, "ReadResponseFixed: status raw 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) + hdr, err := readN(4) + if err != nil { + if debug { + fmt.Fprintf(os.Stderr, "ReadResponseFixed: no length header after 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)) + } + + // try little-endian uint32 first + l := int(binary.LittleEndian.Uint32(hdr)) + if l > 0 { + msg, err := readN(l) + if err != nil { + if debug { + fmt.Fprintf(os.Stderr, "ReadResponseFixed: failed to read %d bytes message: %v\n", l, err) + } + return st, nil, nil + } + if debug { + fmt.Fprintf(os.Stderr, "ReadResponseFixed: message hex=%x ascii=%q\n", msg, string(msg)) + } + return st, msg, nil + } + + // fallback: try ASCII-hex parse (backwards compatibility) + if n, perr := strconv.ParseInt(string(hdr), 16, 32); perr == nil && n > 0 { + msg, err := readN(int(n)) + if err != nil { + if debug { + fmt.Fprintf(os.Stderr, "ReadResponseFixed: failed to read ascii-hex message of len %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)) + } + return st, msg, nil + } + + // neither produced a message + return st, nil, nil + } + + // unexpected token + return st, nil, nil +} diff --git a/adb/device.go b/adb/device.go new file mode 100644 index 0000000..a7de707 --- /dev/null +++ b/adb/device.go @@ -0,0 +1,303 @@ +package adb + +import ( + "bufio" + "fmt" + "io" + "log" + "net" + "strings" + "time" +) + +// DeviceInfo 包含从 adb server 列表和设备端 getprop 收集到的信息 +type DeviceInfo struct { + Serial string + State string + Product string + Model string + Device string + TransportID string + Props map[string]string +} + +// listDevicesRaw: 请求 host:devices 并返回原始 payload +func listDevicesRaw(addr string, timeout time.Duration) (string, error) { + conn, err := DialADB(addr, timeout) + if err != nil { + return "", err + } + defer conn.Close() + + if err := WriteAdbCmd(conn, "host:devices-l"); err != nil { + return "", err + } + status, err := ReadStatus(conn) + if err != nil { + return "", err + } + if status == "FAIL" { + msg, _ := ReadLenFrame(conn) + return "", fmt.Errorf("adb FAIL: %s", string(msg)) + } + if status != "OKAY" { + return "", fmt.Errorf("unexpected status: %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 + } + if err == io.EOF { + break + } + return "", err + } + if len(data) == 0 { + break + } + parts = append(parts, string(data)) + } + log.Println(parts) + return strings.Join(parts, ""), nil +} + +// parseDevicesPayload: 解析 host:devices 返回的 payload,提取可能的 product/model/device/transport_id +func parseDevicesPayload(payload string) []DeviceInfo { + out := []DeviceInfo{} + lines := strings.Split(payload, "\n") + for _, ln := range lines { + ln = strings.TrimSpace(ln) + if ln == "" || strings.HasPrefix(ln, "List of devices attached") { + continue + } + // adb -l 格式通常:serial key:val key:val ... + fields := strings.Fields(ln) + if len(fields) < 2 { + continue + } + dev := DeviceInfo{ + Serial: fields[0], + State: fields[1], + Props: map[string]string{}, + } + // parse remaining key:val pairs + for _, kv := range fields[2:] { + if strings.Contains(kv, ":") { + parts := strings.SplitN(kv, ":", 2) + k := parts[0] + v := parts[1] + switch k { + case "product": + dev.Product = v + case "model": + dev.Model = v + case "device": + dev.Device = v + case "transport_id": + dev.TransportID = v + default: + // store any extra short fields into Props under prefixed key + dev.Props["short."+k] = v + } + } + } + out = append(out, dev) + } + return out +} + +// getPropExecOut: 通过 exec-out:getprop 一次性获取设备所有 getprop 输出(返回原始字节) +func getPropExecOut(addr, serial string, timeout time.Duration) ([]byte, error) { + // open a connection and switch transport for this connection + conn, err := DialADB(addr, timeout) + if err != nil { + return nil, err + } + // ensure close + defer conn.Close() + + // transport + if err := WriteAdbCmd(conn, "host:transport:"+serial); err != nil { + return nil, err + } + status, err := ReadStatus(conn) + if err != nil { + return nil, err + } + if status == "FAIL" { + msg, _ := ReadLenFrame(conn) + return nil, fmt.Errorf("transport FAIL: %s", string(msg)) + } + if status != "OKAY" { + return nil, fmt.Errorf("unexpected transport status: %s", status) + } + + // send exec-out:getprop + if err := WriteAdbCmd(conn, "exec-out:getprop"); err != nil { + return nil, err + } + status, err = ReadStatus(conn) + if err != nil { + return nil, err + } + if status == "FAIL" { + msg, _ := ReadLenFrame(conn) + return nil, fmt.Errorf("exec-out FAIL: %s", string(msg)) + } + if status != "OKAY" { + return nil, fmt.Errorf("unexpected exec-out status: %s", status) + } + + // read frames until timeout/EOF and concatenate + var b []byte + for { + data, err := ReadLenFrame(conn) + if err != nil { + if ne, ok := err.(net.Error); ok && ne.Timeout() { + break + } + if err == io.EOF { + break + } + return nil, err + } + if len(data) == 0 { + break + } + b = append(b, data...) + } + return b, nil +} + +// parseGetprop parses getprop output "key]: [value" lines into 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 + 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], "[]") + } + } + } + return m +} + +// CollectAllDevices gathers DeviceInfo for all devices listed by adb server +func CollectAllDevices(addr string, timeout time.Duration) ([]DeviceInfo, error) { + raw, err := listDevicesRaw(addr, timeout) + if err != nil { + return nil, err + } + devs := parseDevicesPayload(raw) + if len(devs) == 0 { + return devs, nil + } + + // For each device, fetch getprop via exec-out (sequential; can be parallelized) + for i := range devs { + serial := devs[i].Serial + rawProps, err := getPropExecOut(addr, serial, timeout) + if err != nil { + // record error in Props under special key + devs[i].Props["__getprop_error"] = err.Error() + continue + } + props := parseGetprop(rawProps) + // merge into existing short props (from devices -l) + for k, v := range props { + devs[i].Props[k] = v + } + } + return devs, nil +} + +func parseDevicesMap(payload string) (map[string]string, []string) { + m := make(map[string]string) + lines := strings.Split(payload, "\n") + for _, ln := range lines { + ln = strings.TrimSpace(ln) + if ln == "" || strings.HasPrefix(ln, "List of devices attached") { + continue + } + fields := strings.Fields(ln) + if len(fields) < 2 { + continue + } + serial := fields[0] + product := "" + for _, kv := range fields[2:] { + if strings.HasPrefix(kv, "product:") { + product = strings.TrimPrefix(kv, "product:") + break + } + } + m[serial] = product + } + return m, lines +} + +// Find device serial by product value and return serial (first match) +func FindSerialByProduct(addr, targetProduct string) (string, error) { + payload, err := listDevicesRaw(addr, 3*time.Second) + if err != nil { + return "", err + } + m, _ := parseDevicesMap(payload) + for serial, product := range m { + if product == targetProduct { + 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") + for _, ln := range lines { + ln = strings.TrimSpace(ln) + if ln == "" || strings.HasPrefix(ln, "List of devices attached") { + continue + } + fields := strings.Fields(ln) + if len(fields) < 1 { + 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 + } + out, err := ExecOut(conn, "getprop ro.product.model") + if err == nil { + if strings.TrimSpace(string(out)) == targetProduct { + return serial, nil + } + } + } + return "", fmt.Errorf("no device with product=%s found", targetProduct) +} diff --git a/adb/sync.go b/adb/sync.go new file mode 100644 index 0000000..8283713 --- /dev/null +++ b/adb/sync.go @@ -0,0 +1,249 @@ +package adb + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "os" + "strconv" + "time" +) + +const maxChunk = 64 * 1024 + +type Sync struct { + Conn net.Conn +} + +func InitSync(conn net.Conn) *Sync { + return &Sync{ + Conn: conn, + } +} + +// SyncPushTryVariants 按顺序尝试多种 SEND payload 风格,直到成功或尝试完毕。 +// addr: adb server (e.g., "127.0.0.1:5037") +// serial: device serial +// localPath: 本地文件路径 +// remotePath: 目标完整路径(必须包含文件名) +// mode: unix permission like 0644 +// debug: 打印调试信息 +func SyncPushTryVariants(addr, serial, localPath, remotePath string, mode int, debug bool) (int64, error) { + type sendOption struct { + name string + withNUL bool + modeFormat string // "hex" (0x8000|mode) or "dec" (decimal S_IFREG|mode) + } + opts := []sendOption{ + {"send-with-nul-hex", true, "hex"}, + {"send-without-nul-hex", false, "hex"}, + {"send-without-nul-dec", false, "dec"}, + } + + var lastErr error + for _, opt := range opts { + if debug { + fmt.Printf("[try] option=%s\n", opt.name) + } + n, err := syncPushOne(addr, serial, localPath, remotePath, mode, opt.withNUL, opt.modeFormat, debug) + if err == nil { + if debug { + fmt.Printf("[ok] option=%s pushed=%d\n", opt.name, n) + } + return n, nil + } + lastErr = fmt.Errorf("%s: %w", opt.name, err) + if debug { + fmt.Printf("[fail] option=%s err=%v\n", opt.name, err) + } + // small pause between tries + time.Sleep(150 * time.Millisecond) + } + return 0, lastErr +} + +// syncPushOne 在单个连接上按给定选项完成 sync push(一次性连接) +// withNUL: 是否在 SEND payload 后追加 NUL byte +// modeFormat: "hex" 表示 use 0x8000|mode in decimal string (common), "dec" 表示 decimal S_IFREG|mode +func syncPushOne(addr, serial, localPath, remotePath string, mode int, withNUL bool, modeFormat string, debug bool) (int64, error) { + // open file + f, err := os.Open(localPath) + if err != nil { + return 0, err + } + defer f.Close() + fi, _ := f.Stat() + if debug { + fmt.Printf("local filesize=%d\n", fi.Size()) + } + + // dial adb + d := net.Dialer{Timeout: 8 * time.Second} + conn, err := d.Dial("tcp", addr) + if err != nil { + return 0, err + } + defer conn.Close() + if tcp, ok := conn.(*net.TCPConn); ok { + _ = tcp.SetNoDelay(true) + } + + writeAdbCmd := func(cmd string) error { + hdr := fmt.Sprintf("%04x", len(cmd)) + conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) + _, err := conn.Write([]byte(hdr + cmd)) + return err + } + readN := func(n int) ([]byte, error) { + buf := make([]byte, n) + total := 0 + for total < n { + conn.SetReadDeadline(time.Now().Add(5 * time.Second)) + nr, err := conn.Read(buf[total:]) + if err != nil { + return nil, err + } + total += nr + } + return buf, nil + } + read4 := func() ([]byte, error) { return readN(4) } + + // readResponse that returns status and optional msg (LE length) + readResponse := func() (string, []byte, error) { + stb, err := read4() + if err != nil { + return "", nil, err + } + st := string(stb) + // if OKAY quick return + if st == "OKAY" { + return st, nil, nil + } + if st == "FAIL" { + // try LE length + hdr, err := read4() + if err != nil { + return st, nil, nil + } + l := int(binary.LittleEndian.Uint32(hdr)) + if l > 0 { + msg, err := readN(l) + if err != nil { + return st, nil, nil + } + return st, msg, nil + } + // fallback ascii-hex + if n, perr := strconv.ParseInt(string(hdr), 16, 32); perr == nil && n > 0 { + msg, err := readN(int(n)) + if err != nil { + return st, nil, nil + } + return st, msg, nil + } + return st, nil, nil + } + return st, nil, nil + } + + // transport + if err := writeAdbCmd("host:transport:" + serial); err != nil { + return 0, err + } + tok, err := read4() + if err != nil { + return 0, err + } + if string(tok) != "OKAY" { + return 0, fmt.Errorf("transport failed: %q", string(tok)) + } + + // open sync + if err := writeAdbCmd("sync:"); err != nil { + return 0, err + } + tok, err = read4() + if err != nil { + return 0, err + } + if string(tok) != "OKAY" { + _, msg, _ := readResponse() + if len(msg) > 0 { + return 0, fmt.Errorf("sync open failed: %s", string(msg)) + } + return 0, fmt.Errorf("sync open failed: %q", string(tok)) + } + + // build SEND payload according options + var modeStr string + if modeFormat == "hex" { + // common implementations expect decimal of (S_IFREG|mode) where S_IFREG is 0100000 (octal) but using 0x8000 is fine as decimal string + modeStr = strconv.FormatInt(int64(0x8000|mode), 10) + } else { + // decimal form: simply decimal of (0x8000|mode) + modeStr = strconv.FormatInt(int64(0x8000|mode), 10) + } + sendPayload := []byte(remotePath + "," + modeStr) + if withNUL { + sendPayload = append(sendPayload, 0) + } + + // write SEND + if _, err := conn.Write(append([]byte("SEND"), sendPayload...)); err != nil { + return 0, err + } + if debug { + fmt.Printf("Wrote SEND payload len=%d withNUL=%v modeFmt=%s path=%s\n", len(sendPayload), withNUL, modeFormat, remotePath) + } + + // write DATA blocks if file has content (if zero-length, skip 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 := conn.Write(hdr); err != nil { + return total, err + } + if _, err := conn.Write(buf[:n]); err != nil { + return total, err + } + total += int64(n) + } + if rerr != nil { + if rerr == io.EOF { + break + } + return total, rerr + } + } + + // send DONE + done := make([]byte, 8) + copy(done[:4], []byte("DONE")) + binary.LittleEndian.PutUint32(done[4:], uint32(time.Now().Unix())) + if _, err := conn.Write(done); err != nil { + return total, err + } + if debug { + fmt.Println("Wrote DONE, waiting response") + } + + // read final + resp, msg, err := readResponse() + if err != nil { + return total, err + } + if resp != "OKAY" { + if len(msg) > 0 { + return total, fmt.Errorf("sync failed: %s", string(msg)) + } + return total, fmt.Errorf("sync failed: %s", resp) + } + return total, nil +} diff --git a/cmd/main.go b/cmd/main.go index 7905807..aeb9f34 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,5 +1,45 @@ package main -func main() { +import ( + "fmt" + "go-mobile-uiautomator/adb" + "time" +) +func main() { + // edit these for your environment + addr := "127.0.0.1:5037" + local := "C:/Users/01/Desktop/aaa.PNG" + remote := "/sdcard/aaa.PNG" + mode := 0644 + // targetProduct := "23113RKC6C" + adb.SyncPushTryVariants(addr, "emulator-5554", local, remote, mode, true) +} + +// 连接验证 +func Connect() { + addr := "127.0.0.1:5037" + targetProduct := "23113RKC6C" + + serial, err := adb.FindSerialByProduct(addr, targetProduct) + if err != nil { + fmt.Println("find device error:", err) + return + } + fmt.Println("found serial:", serial) + + // 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) + return + } + defer conn.Close() + adb.TransportTo(conn, serial) + out, err := adb.ExecShell(conn, "getprop") + if err != nil { + fmt.Println("shell error:", err) + } else { + fmt.Printf("shell out: %q\n", string(out)) + } }