修改当前连接设备方法
This commit is contained in:
228
adb/connect.go
Normal file
228
adb/connect.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
303
adb/device.go
Normal file
303
adb/device.go
Normal file
@@ -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 <state> 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)
|
||||||
|
}
|
||||||
249
adb/sync.go
Normal file
249
adb/sync.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
42
cmd/main.go
42
cmd/main.go
@@ -1,5 +1,45 @@
|
|||||||
package main
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user