refactor: move hrp/ to root folder

This commit is contained in:
lilong.129
2025-02-06 10:52:08 +08:00
parent 9376692b71
commit 1f063dd6f7
221 changed files with 206 additions and 211 deletions

11
pkg/gadb/README.md Normal file
View File

@@ -0,0 +1,11 @@
# gadb
This module is initially forked from [electricbubble/gadb@v0.0.7] and optimized by [@appl3s].
- feat: add reverse forward command
- feat: add `RunShellCommandV2` which supports running nohup
- feat: add `InstallAPK` with feature judgment
- feat: add `Uninstall` for specified package name
[electricbubble/gadb@v0.0.7]: https://github.com/electricbubble/gadb/tree/v0.0.7
[@appl3s]: https://github.com/appl3s

240
pkg/gadb/client.go Normal file
View File

@@ -0,0 +1,240 @@
package gadb
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/code"
)
const (
AdbServerPort = 5037
AdbDaemonPort = 5555
)
type Client struct {
host string
port int
}
func NewClient() (Client, error) {
return NewClientWith("localhost")
}
func NewClientWith(host string, port ...int) (adbClient Client, err error) {
if len(port) == 0 {
port = []int{AdbServerPort}
}
adbClient.host = host
adbClient.port = port[0]
var tp transport
if tp, err = adbClient.createTransport(); err != nil {
return Client{}, err
}
defer func() { _ = tp.Close() }()
return
}
func NewClientWithoutTransport(host string, port ...int) (adbClient Client, err error) {
if len(port) == 0 {
port = []int{AdbServerPort}
}
adbClient.host = host
adbClient.port = port[0]
return
}
func (c Client) ServerVersion() (version int, err error) {
var resp string
if resp, err = c.executeCommand("host:version"); err != nil {
return 0, err
}
var v int64
if v, err = strconv.ParseInt(resp, 16, 64); err != nil {
return 0, err
}
version = int(v)
return
}
func (c Client) DeviceSerialList() (serials []string, err error) {
var resp string
if resp, err = c.executeCommand("host:devices"); err != nil {
return
}
lines := strings.Split(resp, "\n")
serials = make([]string, 0, len(lines))
for i := range lines {
fields := strings.Fields(lines[i])
if len(fields) < 2 {
continue
}
serials = append(serials, fields[0])
}
return
}
func (c Client) DeviceList() (devices []*Device, err error) {
defer func() {
if err != nil && errors.Cause(err) == nil {
err = errors.Wrap(code.DeviceConnectionError, err.Error())
}
}()
var resp string
if resp, err = c.executeCommand("host:devices-l"); err != nil {
return
}
lines := strings.Split(resp, "\n")
devices = make([]*Device, 0, len(lines))
for i := range lines {
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 4 || len(fields[0]) == 0 {
log.Error().Str("line", line).Msg("get unexpected line")
continue
}
sliceAttrs := fields[2:]
mapAttrs := map[string]string{}
for _, field := range sliceAttrs {
split := strings.Split(field, ":")
if len(split) == 1 {
continue
}
key, val := split[0], split[1]
mapAttrs[key] = val
}
devices = append(devices, &Device{adbClient: c, serial: fields[0], attrs: mapAttrs})
}
return
}
func (c Client) ForwardList() (deviceForward []DeviceForward, err error) {
var resp string
if resp, err = c.executeCommand("host:list-forward"); err != nil {
return nil, err
}
lines := strings.Split(resp, "\n")
deviceForward = make([]DeviceForward, 0, len(lines))
for i := range lines {
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
fields := strings.Fields(line)
deviceForward = append(deviceForward, DeviceForward{Serial: fields[0], Local: fields[1], Remote: fields[2]})
}
return
}
func (c Client) ForwardKillAll() (err error) {
_, err = c.executeCommand("host:killforward-all", true)
return
}
func (c Client) Connect(ip string, port ...int) (err error) {
if len(port) == 0 {
port = []int{AdbDaemonPort}
}
var resp string
if resp, err = c.executeCommand(fmt.Sprintf("host:connect:%s:%d", ip, port[0])); err != nil {
return err
}
if !strings.HasPrefix(resp, "connected to") && !strings.HasPrefix(resp, "already connected to") {
return fmt.Errorf("adb connect: %s", resp)
}
return
}
func (c Client) Disconnect(ip string, port ...int) (err error) {
cmd := fmt.Sprintf("host:disconnect:%s", ip)
if len(port) != 0 {
cmd = fmt.Sprintf("host:disconnect:%s:%d", ip, port[0])
}
var resp string
if resp, err = c.executeCommand(cmd); err != nil {
return err
}
if !strings.HasPrefix(resp, "disconnected") {
return fmt.Errorf("adb disconnect: %s", resp)
}
return
}
func (c Client) DisconnectAll() (err error) {
var resp string
if resp, err = c.executeCommand("host:disconnect:"); err != nil {
return err
}
if !strings.HasPrefix(resp, "disconnected everything") {
return fmt.Errorf("adb disconnect all: %s", resp)
}
return
}
func (c Client) KillServer() (err error) {
var tp transport
if tp, err = c.createTransport(); err != nil {
return err
}
defer func() { _ = tp.Close() }()
err = tp.Send("host:kill")
return
}
func (c Client) createTransport(readTimeout ...time.Duration) (tp transport, err error) {
return newTransport(fmt.Sprintf("%s:%d", c.host, c.port), readTimeout...)
}
func (c Client) executeCommand(command string, onlyVerifyResponse ...bool) (resp string, err error) {
if len(onlyVerifyResponse) == 0 {
onlyVerifyResponse = []bool{false}
}
var tp transport
if tp, err = c.createTransport(); err != nil {
return "", err
}
defer func() { _ = tp.Close() }()
if err = tp.SendWithCheck(command); err != nil {
return "", err
}
if onlyVerifyResponse[0] {
return
}
if resp, err = tp.UnpackString(); err != nil {
return "", err
}
return
}

121
pkg/gadb/client_test.go Normal file
View File

@@ -0,0 +1,121 @@
//go:build localtest
package gadb
import (
"os"
"testing"
)
var adbClient Client
func setupClient(t *testing.T) {
var err error
adbClient, err = NewClient()
if err != nil {
t.Fatal(err)
}
}
func TestClient_ServerVersion(t *testing.T) {
setupClient(t)
adbServerVersion, err := adbClient.ServerVersion()
if err != nil {
t.Fatal(err)
}
t.Log(adbServerVersion)
}
func TestClient_DeviceSerialList(t *testing.T) {
setupClient(t)
serials, err := adbClient.DeviceSerialList()
if err != nil {
t.Fatal(err)
}
for i := range serials {
t.Log(serials[i])
}
}
func TestClient_DeviceList(t *testing.T) {
setupDevices(t)
for i := range devices {
t.Log(devices[i].serial, devices[i].DeviceInfo())
}
}
func TestClient_ForwardList(t *testing.T) {
setupClient(t)
deviceForwardList, err := adbClient.ForwardList()
if err != nil {
t.Fatal(err)
}
for i := range deviceForwardList {
t.Log(deviceForwardList[i])
}
}
func TestClient_ForwardKillAll(t *testing.T) {
setupClient(t)
err := adbClient.ForwardKillAll()
if err != nil {
t.Fatal(err)
}
}
func TestClient_Connect(t *testing.T) {
setupClient(t)
err := adbClient.Connect("192.168.1.28")
if err != nil {
t.Fatal(err)
}
}
func TestClient_Disconnect(t *testing.T) {
setupClient(t)
err := adbClient.Disconnect("192.168.1.28")
if err != nil {
t.Fatal(err)
}
}
func TestClient_DisconnectAll(t *testing.T) {
setupClient(t)
err := adbClient.DisconnectAll()
if err != nil {
t.Fatal(err)
}
}
func TestClient_KillServer(t *testing.T) {
setupClient(t)
err := adbClient.KillServer()
if err != nil {
t.Fatal(err)
}
}
func TestScreenCap(t *testing.T) {
setupDevices(t)
for _, d := range devices {
res, err := d.ScreenCap()
if err != nil {
t.Error(err)
}
t.Log(len(res))
os.WriteFile("/tmp/1.png", res, 0o644)
}
}

727
pkg/gadb/device.go Normal file
View File

@@ -0,0 +1,727 @@
package gadb
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/builtin"
)
type DeviceFileInfo struct {
Name string
Mode os.FileMode
Size uint32
LastModified time.Time
}
func (info DeviceFileInfo) IsDir() bool {
return (info.Mode & (1 << 14)) == (1 << 14)
}
const DefaultFileMode = os.FileMode(0o664)
type DeviceState string
const (
StateUnknown DeviceState = "UNKNOWN"
StateOnline DeviceState = "online"
StateOffline DeviceState = "offline"
StateDisconnected DeviceState = "disconnected"
StateBootloader DeviceState = "bootloader"
StateRecovery DeviceState = "recovery"
StateUnauthorized DeviceState = "unauthorized"
)
var deviceStateStrings = map[string]DeviceState{
"": StateDisconnected, // no devices/emulators found
"offline": StateOffline,
"bootloader": StateBootloader,
"recovery": StateRecovery,
"unauthorized": StateUnauthorized,
"device": StateOnline,
}
func deviceStateConv(k string) (deviceState DeviceState) {
var ok bool
if deviceState, ok = deviceStateStrings[k]; !ok {
return StateUnknown
}
return
}
type DeviceForward struct {
Serial string
Local string
Remote string
Reverse bool
// LocalProtocol string
// RemoteProtocol string
}
type Device struct {
adbClient Client
serial string
attrs map[string]string
feat Features
}
func (d *Device) HasFeature(name Feature) bool {
feats, err := d.GetFeatures()
if err != nil || len(feats) == 0 {
return false
}
return feats.HasFeature(name)
}
func (d *Device) GetFeatures() (features Features, err error) {
if len(d.feat) > 0 {
return d.feat, nil
}
return d.features()
}
func (d *Device) features() (features Features, err error) {
res, err := d.executeCommand("host:features")
if err != nil {
return nil, err
}
if len(res) > 4 {
// stip hash
res = res[4:]
}
fs := strings.Split(string(res), ",")
features = make(Features, len(fs))
for _, f := range fs {
features[Feature(f)] = struct{}{}
}
d.feat = features
return features, nil
}
func (d *Device) HasAttribute(key string) bool {
_, ok := d.attrs[key]
return ok
}
func (d *Device) Product() (string, error) {
if d.HasAttribute("product") {
return d.attrs["product"], nil
}
return "", errors.New("does not have attribute: product")
}
func (d *Device) Model() (string, error) {
if d.HasAttribute("model") {
return d.attrs["model"], nil
}
return "", errors.New("does not have attribute: model")
}
func (d *Device) Brand() (string, error) {
if d.HasAttribute("brand") {
return d.attrs["brand"], nil
}
brand, err := d.RunShellCommand("getprop", "ro.product.brand")
brand = strings.TrimSpace(brand)
if err != nil {
return "", errors.New("does not have attribute: brand")
}
d.attrs["brand"] = brand
return brand, nil
}
func (d *Device) Usb() (string, error) {
if d.HasAttribute("usb") {
return d.attrs["usb"], nil
}
return "", errors.New("does not have attribute: usb")
}
func (d *Device) transportId() (string, error) {
if d.HasAttribute("transport_id") {
return d.attrs["transport_id"], nil
}
return "", errors.New("does not have attribute: transport_id")
}
func (d *Device) DeviceInfo() map[string]string {
return d.attrs
}
func (d *Device) Serial() string {
// resp, err := d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:get-serialno", d.serial))
return d.serial
}
func (d *Device) IsUsb() (bool, error) {
usb, err := d.Usb()
if err != nil {
return false, err
}
return usb != "", nil
}
func (d *Device) State() (DeviceState, error) {
resp, err := d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:get-state", d.serial))
return deviceStateConv(resp), err
}
func (d *Device) DevicePath() (string, error) {
resp, err := d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:get-devpath", d.serial))
return resp, err
}
func (d *Device) Forward(remoteInterface interface{}, noRebind ...bool) (port int, err error) {
var remote string
switch r := remoteInterface.(type) {
// for unix sockets
case string:
remote = r
case int:
remote = fmt.Sprintf("tcp:%d", r)
}
forwardList, err := d.ForwardList()
if err != nil {
return
}
for _, forwardItem := range forwardList {
if forwardItem.Remote == remote {
return strconv.Atoi(forwardItem.Local[4:])
}
}
localPort, err := builtin.GetFreePort()
if err != nil {
return
}
local := fmt.Sprintf("tcp:%d", localPort)
command := fmt.Sprintf("host-serial:%s:forward:%s;%s", d.serial, local, remote)
if len(noRebind) != 0 && noRebind[0] {
command = fmt.Sprintf("host-serial:%s:forward:norebind:%s;%s", d.serial, local, remote)
}
_, err = d.adbClient.executeCommand(command, true)
return localPort, nil
}
func (d *Device) ForwardList() (deviceForwardList []DeviceForward, err error) {
var forwardList []DeviceForward
if forwardList, err = d.adbClient.ForwardList(); err != nil {
return nil, err
}
deviceForwardList = make([]DeviceForward, 0, len(deviceForwardList))
for i := range forwardList {
if forwardList[i].Serial == d.serial {
deviceForwardList = append(deviceForwardList, forwardList[i])
}
}
// resp, err := d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:list-forward", d.serial))
return
}
func (d *Device) ForwardKill(localPort int) (err error) {
local := fmt.Sprintf("tcp:%d", localPort)
_, err = d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:killforward:%s", d.serial, local), true)
return
}
func (d *Device) ReverseForward(localPort int, remoteInterface interface{}, noRebind ...bool) (err error) {
var command string
var remote string
local := fmt.Sprintf("tcp:%d", localPort)
switch r := remoteInterface.(type) {
// for unix sockets
case string:
remote = r
case int:
remote = fmt.Sprintf("tcp:%d", r)
}
if len(noRebind) != 0 && noRebind[0] {
command = fmt.Sprintf("reverse:forward:norebind:%s;%s", remote, local)
} else {
command = fmt.Sprintf("reverse:forward:%s;%s", remote, local)
}
_, err = d.executeCommand(command, true)
return
}
func (d *Device) ReverseForwardList() (deviceForwardList []DeviceForward, err error) {
res, err := d.executeCommand("reverse:list-forward")
if err != nil {
return nil, err
}
resStr := string(res)
lines := strings.Split(resStr, "\n")
for _, line := range lines {
groups := strings.Split(line, " ")
if len(groups) == 3 {
deviceForwardList = append(deviceForwardList, DeviceForward{
Reverse: true,
Serial: d.serial,
Remote: groups[1],
Local: groups[2],
})
}
}
return
}
func (d *Device) ReverseForwardKill(remoteInterface interface{}) error {
remote := ""
switch r := remoteInterface.(type) {
case string:
remote = r
case int:
remote = fmt.Sprintf("tcp:%d", r)
}
_, err := d.executeCommand(fmt.Sprintf("reverse:killforward:%s", remote), true)
return err
}
func (d *Device) RunStubCommand(command []byte, processName string) (res string, err error) {
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return "", err
}
defer func() { _ = tp.Close() }()
if err = tp.SendWithCheck(fmt.Sprintf("localabstract:%s", processName)); err != nil {
return "", err
}
if err = tp.SendBytes(command); err != nil {
return "", err
}
lenBuf, err := tp.ReadBytesN(4)
if err != nil {
return "", err
}
length := binary.LittleEndian.Uint32(lenBuf)
result, err := tp.ReadBytesN(int(length) - 4)
if err != nil {
return "", err
}
return string(result), nil
}
func (d *Device) ReverseForwardKillAll() error {
_, err := d.executeCommand("reverse:killforward-all")
return err
}
func (d *Device) RunShellCommand(cmd string, args ...string) (string, error) {
raw, err := d.RunShellCommandWithBytes(cmd, args...)
if err != nil && errors.Cause(err) == nil {
err = errors.Wrap(code.DeviceShellExecError, err.Error())
}
return string(raw), err
}
func (d *Device) RunShellCommandWithBytes(cmd string, args ...string) ([]byte, error) {
if d.HasFeature(FeatShellV2) {
return d.RunShellCommandV2WithBytes(cmd, args...)
}
if len(args) > 0 {
cmd = fmt.Sprintf("%s %s", cmd, strings.Join(args, " "))
}
if strings.TrimSpace(cmd) == "" {
return nil, errors.New("adb shell: command cannot be empty")
}
startTime := time.Now()
defer func() {
// log elapsed seconds for shell execution
log.Debug().Str("cmd",
fmt.Sprintf("adb -s %s shell %s", d.serial, cmd)).
Int64("elapsed(ms)", time.Since(startTime).Milliseconds()).
Msg("run adb shell")
}()
raw, err := d.executeCommand(fmt.Sprintf("shell:%s", cmd))
return raw, err
}
// RunShellCommandV2WithBytes shell v2, 支持后台运行而不会阻断
func (d *Device) RunShellCommandV2WithBytes(cmd string, args ...string) ([]byte, error) {
if len(args) > 0 {
cmd = fmt.Sprintf("%s %s", cmd, strings.Join(args, " "))
}
if strings.TrimSpace(cmd) == "" {
return nil, errors.New("adb shell: command cannot be empty")
}
startTime := time.Now()
defer func() {
// log elapsed seconds for shell execution
log.Debug().Str("cmd",
fmt.Sprintf("adb -s %s shell %s", d.serial, cmd)).
Int64("elapsed(ms)", time.Since(startTime).Milliseconds()).
Msg("run adb shell in v2")
}()
raw, err := d.executeCommand(fmt.Sprintf("shell,v2,raw:%s", cmd))
if err != nil {
return raw, err
}
return d.parseV2CommandWithBytes(raw)
}
func (d *Device) parseV2CommandWithBytes(input []byte) (res []byte, err error) {
if len(input) == 0 {
return input, nil
}
reader := bytes.NewReader(input)
sizeBuf := make([]byte, 4)
var (
resBuf []byte
exitCode int
)
loop:
for {
msgCode, err := reader.ReadByte()
if err != nil {
return input, err
}
switch msgCode {
case 0x01, 0x02: // STDOUT, STDERR
_, err = io.ReadFull(reader, sizeBuf)
if err != nil {
return input, err
}
size := binary.LittleEndian.Uint32(sizeBuf)
if cap(resBuf) < int(size) {
resBuf = make([]byte, int(size))
}
_, err = io.ReadFull(reader, resBuf[:size])
if err != nil {
return input, err
}
res = append(res, resBuf[:size]...)
case 0x03: // EXIT
_, err = io.ReadFull(reader, sizeBuf)
if err != nil {
return input, err
}
size := binary.LittleEndian.Uint32(sizeBuf)
if cap(resBuf) < int(size) {
resBuf = make([]byte, int(size))
}
ec, err := reader.ReadByte()
if err != nil {
return input, err
}
exitCode = int(ec)
break loop
default:
return input, nil
}
}
if exitCode != 0 {
return nil, errors.New(string(res))
}
return res, nil
}
func (d *Device) EnableAdbOverTCP(port ...int) (err error) {
if len(port) == 0 {
port = []int{AdbDaemonPort}
}
_, err = d.executeCommand(fmt.Sprintf("tcpip:%d", port[0]), true)
return
}
func (d *Device) createDeviceTransport(readTimeout ...time.Duration) (tp transport, err error) {
if tp, err = newTransport(fmt.Sprintf("%s:%d", d.adbClient.host, d.adbClient.port), readTimeout...); err != nil {
return transport{}, err
}
err = tp.SendWithCheck(fmt.Sprintf("host:transport:%s", d.serial))
return
}
func (d *Device) executeCommand(command string, onlyVerifyResponse ...bool) (raw []byte, err error) {
if len(onlyVerifyResponse) == 0 {
onlyVerifyResponse = []bool{false}
}
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return nil, err
}
defer func() { _ = tp.Close() }()
if err = tp.SendWithCheck(command); err != nil {
return nil, err
}
if onlyVerifyResponse[0] {
return
}
raw, err = tp.ReadBytesAll()
return
}
func (d *Device) List(remotePath string) (devFileInfos []DeviceFileInfo, err error) {
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return nil, err
}
defer func() { _ = tp.Close() }()
var sync syncTransport
if sync, err = tp.CreateSyncTransport(); err != nil {
return nil, err
}
defer func() { _ = sync.Close() }()
if err = sync.Send("LIST", remotePath); err != nil {
return nil, err
}
devFileInfos = make([]DeviceFileInfo, 0)
var entry DeviceFileInfo
for entry, err = sync.ReadDirectoryEntry(); err == nil; entry, err = sync.ReadDirectoryEntry() {
if entry == (DeviceFileInfo{}) {
break
}
devFileInfos = append(devFileInfos, entry)
}
return
}
func (d *Device) PushFile(localPath, remotePath string, modification ...time.Time) (err error) {
localFile, err := os.Open(localPath)
if err != nil {
return err
}
defer localFile.Close()
if len(modification) == 0 {
var stat os.FileInfo
if stat, err = localFile.Stat(); err != nil {
return err
}
modification = []time.Time{stat.ModTime()}
}
return d.Push(localFile, remotePath, modification[0], DefaultFileMode)
}
func (d *Device) Push(source io.Reader, remotePath string, modification time.Time, mode ...os.FileMode) (err error) {
if len(mode) == 0 {
mode = []os.FileMode{DefaultFileMode}
}
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return err
}
defer func() { _ = tp.Close() }()
var sync syncTransport
if sync, err = tp.CreateSyncTransport(); err != nil {
return err
}
defer func() { _ = sync.Close() }()
data := fmt.Sprintf("%s,%d", remotePath, mode[0])
if err = sync.Send("SEND", data); err != nil {
return err
}
if err = sync.SendStream(source); err != nil {
return
}
if err = sync.SendStatus("DONE", uint32(modification.Unix())); err != nil {
return
}
if err = sync.VerifyStatus(); err != nil {
return
}
return
}
func (d *Device) Pull(remotePath string, dest io.Writer) (err error) {
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return err
}
defer func() { _ = tp.Close() }()
var sync syncTransport
if sync, err = tp.CreateSyncTransport(); err != nil {
return err
}
defer func() { _ = sync.Close() }()
if err = sync.Send("RECV", remotePath); err != nil {
return err
}
err = sync.WriteStream(dest)
return
}
func (d *Device) installViaABBExec(apk io.ReadSeeker, args ...string) (raw []byte, err error) {
var (
tp transport
filesize int64
)
filesize, err = apk.Seek(0, io.SeekEnd)
if err != nil {
return nil, err
}
if tp, err = d.createDeviceTransport(5 * time.Minute); err != nil {
return nil, err
}
defer func() { _ = tp.Close() }()
cmd := "abb_exec:package\x00install\x00-t"
for _, arg := range args {
cmd += "\x00" + arg
}
cmd += fmt.Sprintf("\x00-S\x00%d", filesize)
if err = tp.SendWithCheck(cmd); err != nil {
return nil, err
}
_, err = apk.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
_, err = io.Copy(tp.Conn(), apk)
if err != nil {
return nil, err
}
raw, err = tp.ReadBytesAll()
return
}
func (d *Device) InstallAPK(apkPath string, args ...string) (string, error) {
apkFile, err := os.Open(apkPath)
if err != nil {
return "", errors.Wrap(err, fmt.Sprintf("open apk file %s failed", apkPath))
}
defer apkFile.Close()
haserr := func(ret string) bool {
return strings.Contains(ret, "Failure")
}
if d.HasFeature(FeatAbbExec) {
raw, err := d.installViaABBExec(apkFile)
if err != nil {
return "", fmt.Errorf("error installing: %v", err)
}
if haserr(string(raw)) {
return "", errors.New(string(raw))
}
return string(raw), err
}
remote := fmt.Sprintf("/data/local/tmp/%s.apk", builtin.GenNameWithTimestamp("gadb_remote_%d"))
err = d.Push(apkFile, remote, time.Now())
if err != nil {
return "", fmt.Errorf("error pushing: %v", err)
}
args = append([]string{"install"}, args...)
args = append(args, "-f", remote)
res, err := d.RunShellCommand("pm", args...)
if err != nil {
return "", errors.Wrap(err, "install apk failed")
}
if haserr(res) {
return "", errors.New(res)
}
return res, nil
}
func (d *Device) Uninstall(packageName string, keepData ...bool) (string, error) {
if len(keepData) == 0 {
keepData = []bool{false}
}
packageName = strings.TrimSpace(packageName)
if len(packageName) == 0 {
return "", fmt.Errorf("invalid package name")
}
args := []string{"uninstall"}
if keepData[0] {
args = append(args, "-k")
}
args = append(args, packageName)
return d.RunShellCommand("pm", args...)
}
func (d *Device) ListPackages() ([]string, error) {
args := []string{"list", "packages"}
resRaw, err := d.RunShellCommand("pm", args...)
if err != nil {
return []string{}, err
}
lines := strings.Split(resRaw, "\n")
var packages []string
for _, line := range lines {
packageName := strings.TrimPrefix(line, "package:")
packages = append(packages, packageName)
}
return packages, nil
}
func (d *Device) IsPackageInstalled(packageName string) bool {
packages, err := d.ListPackages()
if err != nil {
return false
}
packageName = strings.TrimSpace(packageName)
if len(packageName) == 0 {
return false
}
return builtin.Contains(packages, packageName)
}
func (d *Device) IsPackageRunning(packageName string) bool {
output, err := d.RunShellCommand("pidof", packageName)
if err != nil {
return false
}
return strings.TrimSpace(output) != ""
}
func (d *Device) ScreenCap() ([]byte, error) {
if d.HasFeature(FeatShellV2) {
return d.RunShellCommandV2WithBytes("screencap", "-p")
}
// for shell v1, screenshot buffer maybe truncated
// thus we firstly save it to local file and then pull it
tempPath := fmt.Sprintf("/data/local/tmp/screenshot_%d.png",
time.Now().Unix())
_, err := d.RunShellCommandWithBytes("screencap", "-p", tempPath)
if err != nil {
return nil, err
}
buffer := bytes.NewBuffer(nil)
err = d.Pull(tempPath, buffer)
return buffer.Bytes(), err
}

366
pkg/gadb/device_test.go Normal file
View File

@@ -0,0 +1,366 @@
//go:build localtest
package gadb
import (
"bytes"
"os"
"reflect"
"strings"
"testing"
"time"
)
var devices []*Device
func setupDevices(t *testing.T) {
var err error
setupClient(t)
devices, err = adbClient.DeviceList()
if err != nil {
t.Fatal(err)
}
}
func TestDevice_State(t *testing.T) {
setupDevices(t)
for i := range devices {
dev := devices[i]
state, err := dev.State()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), state)
resp, err := dev.RunShellCommand("ls")
if err != nil {
t.Fatal(err)
}
t.Log(string(resp))
}
}
func TestDevice_DevicePath(t *testing.T) {
setupDevices(t)
for i := range devices {
dev := devices[i]
devPath, err := dev.DevicePath()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), devPath)
}
}
func TestDevice_Product(t *testing.T) {
setupDevices(t)
for i := range devices {
dev := devices[i]
product, err := dev.Product()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), product)
}
}
func TestDevice_Model(t *testing.T) {
setupDevices(t)
for i := range devices {
dev := devices[i]
model, err := dev.Model()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), model)
}
}
func TestDevice_Brand(t *testing.T) {
setupDevices(t)
for i := range devices {
dev := devices[i]
brand, err := dev.Brand()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), brand)
}
}
func TestDevice_Usb(t *testing.T) {
setupDevices(t)
for i := range devices {
dev := devices[i]
usb, err := dev.Usb()
if err != nil {
t.Fatal(err)
}
isUsb, err := dev.IsUsb()
if err != nil {
t.Fatal(err)
}
t.Log(dev.Serial(), usb, isUsb)
}
}
func TestDevice_DeviceInfo(t *testing.T) {
setupDevices(t)
for i := range devices {
dev := devices[i]
t.Log(dev.DeviceInfo())
}
}
func TestDevice_Forward(t *testing.T) {
setupDevices(t)
for _, device := range devices {
localPort, err := device.Forward(6790)
if err != nil {
t.Fatal(err)
}
err = device.ForwardKill(localPort)
if err != nil {
t.Fatal(err)
}
}
}
func TestDevice_ReverseForward(t *testing.T) {
setupDevices(t)
for _, device := range devices {
localPort := 5005
err := device.ReverseForward(localPort, "localabstract:scrcpy")
if err != nil {
t.Fatal(err)
}
err = device.ReverseForward(localPort, "localabstract:scrcpy1")
if err != nil {
t.Fatal(err)
}
_, err = device.ReverseForwardList()
if err != nil {
t.Fatal(err)
}
err = device.ReverseForwardKill("localabstract:scrcpy1")
if err != nil {
t.Fatal(err)
}
err = device.ReverseForwardKillAll()
if err != nil {
t.Fatal(err)
}
}
}
func TestDevice_ForwardList(t *testing.T) {
setupDevices(t)
for i := range devices {
dev := devices[i]
forwardList, err := dev.ForwardList()
if err != nil {
t.Fatal(err)
}
t.Log(dev.serial, "->", forwardList)
}
}
func TestDevice_ForwardKill(t *testing.T) {
setupDevices(t)
for _, device := range devices {
err := device.ForwardKill(6790)
if err != nil {
t.Fatal(err)
}
}
}
func TestDevice_RunShellCommand(t *testing.T) {
setupDevices(t)
// for i := range devices {
// dev := devices[i]
// // cmdOutput, err := dev.RunShellCommand(`pm list packages | grep "bili"`)
// // cmdOutput, err := dev.RunShellCommand(`pm list packages`, `| grep "bili"`)
// // cmdOutput, err := dev.RunShellCommand("dumpsys activity | grep mFocusedActivity")
// cmdOutput, err := dev.RunShellCommand("monkey", "-p", "tv.danmaku.bili", "-c", "android.intent.category.LAUNCHER", "1")
// if err != nil {
// t.Fatal(dev.serial, err)
// }
// t.Log("\n"+dev.serial, cmdOutput)
// }
for _, dev := range devices {
// cmdOutput, err := dev.RunShellCommand("monkey", "-p", "tv.danmaku.bili", "-c", "android.intent.category.LAUNCHER", "1")
cmdOutput, err := dev.RunShellCommand("ls /sdcard")
// cmdOutput, err := dev.RunShellCommandWithBytes("screencap -p")
if err != nil {
t.Fatal(dev.serial, err)
}
t.Log("\n⬇"+dev.serial+"⬇️\n", cmdOutput)
}
}
func TestDevice_EnableAdbOverTCP(t *testing.T) {
setupDevices(t)
for _, dev := range devices {
err := dev.EnableAdbOverTCP()
if err != nil {
t.Fatal(err)
}
}
}
func TestDevice_List(t *testing.T) {
setupDevices(t)
for _, dev := range devices {
// fileEntries, err := dev.List("/sdcard")
fileEntries, err := dev.List("/sdcard/Download")
if err != nil {
t.Fatal(err)
}
for i := range fileEntries {
t.Log(fileEntries[i].Name, "\t", fileEntries[i].IsDir())
}
}
}
func TestDevice_Push(t *testing.T) {
setupDevices(t)
for _, dev := range devices {
localPath := "test.txt"
err := dev.PushFile(localPath, "/sdcard/Download/push.txt", time.Now())
if err != nil {
t.Fatal(err)
}
err = dev.Push(strings.NewReader("world"), "/sdcard/Download/hello.txt", time.Now())
if err != nil {
t.Fatal(err)
}
}
}
func TestDevice_Pull(t *testing.T) {
setupDevices(t)
for _, dev := range devices {
buffer := bytes.NewBufferString("")
err := dev.Pull("/sdcard/Download/hello.txt", buffer)
if err != nil {
t.Fatal(err)
}
userHomeDir, _ := os.UserHomeDir()
if err = os.WriteFile(userHomeDir+"/Desktop/hello.txt", buffer.Bytes(), DefaultFileMode); err != nil {
t.Fatal(err)
}
}
}
func TestDevice_RunShellCommandBackgroundWithBytes(t *testing.T) {
type fields struct {
adbClient Client
serial string
attrs map[string]string
}
type args struct {
cmd string
args []string
}
tests := []struct {
name string
fields fields
args args
want []byte
wantErr bool
}{
{
name: "runShellCommandBackground",
fields: fields{
adbClient: func() Client {
c, _ := NewClient()
return c
}(),
serial: "63c1ee94",
},
args: args{
cmd: "nohup sleep 10 2>/dev/null 1>/dev/null &",
// cmd: "sleep 10",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := Device{
adbClient: tt.fields.adbClient,
serial: tt.fields.serial,
attrs: tt.fields.attrs,
}
got, err := d.RunShellCommandV2WithBytes(tt.args.cmd, tt.args.args...)
if (err != nil) != tt.wantErr {
t.Errorf("Device.RunShellCommandBackgroundWithBytes() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Device.RunShellCommandBackgroundWithBytes() = %v, want %v", got, tt.want)
}
})
}
}
func TestDevice_InstallAPK(t *testing.T) {
setupDevices(t)
apkPath := "test.apk"
for _, dev := range devices {
res, err := dev.InstallAPK(apkPath)
if err != nil {
t.Fatal(err)
}
t.Log(res)
}
}
func TestDevice_ListPackages(t *testing.T) {
setupDevices(t)
for _, dev := range devices {
res, err := dev.ListPackages()
if err != nil {
t.Fatal(err)
}
t.Log(res)
installed := dev.IsPackageInstalled("io.appium.uiautomator2.server")
if err != nil {
t.Fatal(err)
}
t.Log(installed)
}
}
func TestDevice_HasFeature(t *testing.T) {
setupDevices(t)
for _, dev := range devices {
t.Log(dev.GetFeatures())
}
}

58
pkg/gadb/examples/main.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"log"
"os"
"path/filepath"
"strings"
"github.com/httprunner/httprunner/v5/pkg/gadb"
)
func main() {
adbClient, err := gadb.NewClient()
checkErr(err, "fail to connect adb server")
devices, err := adbClient.DeviceList()
checkErr(err)
if len(devices) == 0 {
log.Fatalln("list of devices is empty")
}
dev := devices[0]
userHomeDir, _ := os.UserHomeDir()
localPath := filepath.Join(userHomeDir, "xxx.apk")
log.Println("starting to push apk")
remotePath := "/data/local/tmp/xuexi_android_10002068.apk"
err = dev.PushFile(localPath, remotePath)
checkErr(err, "adb push")
log.Println("push completed")
log.Println("starting to install apk")
shellOutput, err := dev.RunShellCommand("pm install", remotePath)
checkErr(err, "pm install")
if !strings.Contains(shellOutput, "Success") {
log.Fatalln("fail to install: ", shellOutput)
}
log.Println("install completed")
}
func checkErr(err error, msg ...string) {
if err == nil {
return
}
var output string
if len(msg) != 0 {
output = msg[0] + " "
}
output += err.Error()
log.Fatalln(output)
}

26
pkg/gadb/features.go Normal file
View File

@@ -0,0 +1,26 @@
package gadb
type (
Feature string
Features map[Feature]struct{}
)
var (
FeatSendrecvV2Brotli = Feature("sendrecv_v2_brotli")
FeatRemountShell = Feature("remount_shell")
FeatSendrecvV2 = Feature("sendrecv_v2")
FeatAbbExec = Feature("abb_exec")
FeatFixedPushMkdir = Feature("fixed_push_mkdir")
FeatFixedPushSymlinkTimestamp = Feature("fixed_push_symlink_timestamp")
FeatAbb = Feature("abb")
FeatShellV2 = Feature("shell_v2")
FeatCmd = Feature("cmd")
FeatLsV2 = Feature("ls_v2")
FeatApex = Feature("apex")
FeatStatV2 = Feature("stat_v2")
)
func (fs Features) HasFeature(name Feature) bool {
_, has := fs[name]
return has
}

256
pkg/gadb/sync_transport.go Normal file
View File

@@ -0,0 +1,256 @@
package gadb
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"time"
"github.com/rs/zerolog/log"
)
type syncTransport struct {
sock net.Conn
readTimeout time.Duration
}
// newSyncTransport creates a new sync transport with existed tcp socket connection
func newSyncTransport(sock net.Conn, readTimeout time.Duration) syncTransport {
return syncTransport{sock: sock, readTimeout: readTimeout}
}
func (sync syncTransport) Send(command, data string) (err error) {
if len(command) != 4 {
return errors.New("sync commands must have length 4")
}
msg := bytes.NewBufferString(command)
if err = binary.Write(msg, binary.LittleEndian, int32(len(data))); err != nil {
return fmt.Errorf("sync transport write: %w", err)
}
msg.WriteString(data)
log.Debug().Str("msg", msg.String()).Msg("sync run adb command")
return _send(sync.sock, msg.Bytes())
}
func (sync syncTransport) SendStream(reader io.Reader) (err error) {
syncMaxChunkSize := 64 * 1024
for err == nil {
tmp := make([]byte, syncMaxChunkSize)
var n int
n, err = reader.Read(tmp)
if err == io.EOF {
err = nil
break
}
if err == nil {
err = sync.sendChunk(tmp[:n])
}
}
return
}
func (sync syncTransport) SendStatus(statusCode string, n uint32) (err error) {
msg := bytes.NewBufferString(statusCode)
if err = binary.Write(msg, binary.LittleEndian, n); err != nil {
return fmt.Errorf("sync transport write: %w", err)
}
log.Debug().Str("msg", msg.String()).Msg("sync send adb status")
return _send(sync.sock, msg.Bytes())
}
func (sync syncTransport) sendChunk(buffer []byte) (err error) {
msg := bytes.NewBufferString("DATA")
if err = binary.Write(msg, binary.LittleEndian, int32(len(buffer))); err != nil {
return fmt.Errorf("sync transport write: %w", err)
}
msg.Write(buffer)
return _send(sync.sock, msg.Bytes())
}
func (sync syncTransport) VerifyStatus() (err error) {
var status string
if status, err = sync.ReadStringN(4); err != nil {
return err
}
logs := bytes.NewBufferString(fmt.Sprintf("<-- %s", status))
defer func() {
fmt.Println(logs.String())
}()
var tmpUint32 uint32
if tmpUint32, err = sync.ReadUint32(); err != nil {
return fmt.Errorf("sync transport read (status): %w", err)
}
logs.WriteString(fmt.Sprintf(" %d\t", tmpUint32))
var msg string
if msg, err = sync.ReadStringN(int(tmpUint32)); err != nil {
return err
}
logs.WriteString(msg)
if status == "FAIL" {
err = fmt.Errorf("sync verify status (fail): %s", msg)
return
}
if status != "OKAY" {
err = fmt.Errorf("sync verify status: Unknown error: %s", msg)
return
}
return
}
var syncReadChunkDone = errors.New("sync read chunk done")
func (sync syncTransport) WriteStream(dest io.Writer) (err error) {
var chunk []byte
save := func() error {
if chunk, err = sync.readChunk(); err != nil && err != syncReadChunkDone {
return fmt.Errorf("sync read chunk: %w", err)
}
if err == syncReadChunkDone {
return err
}
if err = _send(dest, chunk); err != nil {
return fmt.Errorf("sync write stream: %w", err)
}
return nil
}
for err == nil {
err = save()
}
if err == syncReadChunkDone {
err = nil
}
return
}
func (sync syncTransport) readChunk() (chunk []byte, err error) {
var status string
if status, err = sync.ReadStringN(4); err != nil {
return nil, err
}
logs := bytes.NewBufferString("")
defer func() {
fmt.Println(logs.String())
}()
var tmpUint32 uint32
if tmpUint32, err = sync.ReadUint32(); err != nil {
return nil, fmt.Errorf("read chunk (length): %w", err)
}
if status == "FAIL" {
logs.WriteString(fmt.Sprintf("<-- %s\t%d\t", status, tmpUint32))
var sError string
if sError, err = sync.ReadStringN(int(tmpUint32)); err != nil {
return nil, fmt.Errorf("read chunk (error message): %w", err)
}
err = fmt.Errorf("status (fail): %s", sError)
logs.WriteString(sError)
return
}
switch status {
case "DONE":
logs.WriteString(fmt.Sprintf("<-- %s", status))
err = syncReadChunkDone
return
case "DATA":
logs.WriteString(fmt.Sprintf("<-- %s\t%d\t", status, tmpUint32))
if chunk, err = sync.ReadBytesN(int(tmpUint32)); err != nil {
return nil, err
}
default:
logs.WriteString(fmt.Sprintf("<-- %s\t%d\t", status, tmpUint32))
err = errors.New("unknown error")
}
logs.WriteString("......")
return
}
func (sync syncTransport) ReadDirectoryEntry() (entry DeviceFileInfo, err error) {
var status string
if status, err = sync.ReadStringN(4); err != nil {
return DeviceFileInfo{}, err
}
logs := bytes.NewBufferString(fmt.Sprintf("<-- %s", status))
defer func() {
fmt.Println(logs.String())
}()
if status == "DONE" {
return
}
logs = bytes.NewBufferString(fmt.Sprintf("<-- %s\t", status))
if err = binary.Read(sync.sock, binary.LittleEndian, &entry.Mode); err != nil {
return DeviceFileInfo{}, fmt.Errorf("sync transport read (mode): %w", err)
}
logs.WriteString(entry.Mode.String() + "\t")
if entry.Size, err = sync.ReadUint32(); err != nil {
return DeviceFileInfo{}, fmt.Errorf("sync transport read (size): %w", err)
}
logs.WriteString(fmt.Sprintf("%10d", entry.Size) + "\t")
var tmpUint32 uint32
if tmpUint32, err = sync.ReadUint32(); err != nil {
return DeviceFileInfo{}, fmt.Errorf("sync transport read (time): %w", err)
}
entry.LastModified = time.Unix(int64(tmpUint32), 0)
logs.WriteString(entry.LastModified.String() + "\t")
if tmpUint32, err = sync.ReadUint32(); err != nil {
return DeviceFileInfo{}, fmt.Errorf("sync transport read (file name length): %w", err)
}
logs.WriteString(fmt.Sprintf("%d\t", tmpUint32))
if entry.Name, err = sync.ReadStringN(int(tmpUint32)); err != nil {
return DeviceFileInfo{}, fmt.Errorf("sync transport read (file name): %w", err)
}
logs.WriteString(entry.Name + "\t")
return
}
func (sync syncTransport) ReadUint32() (n uint32, err error) {
err = binary.Read(sync.sock, binary.LittleEndian, &n)
return
}
func (sync syncTransport) ReadStringN(size int) (s string, err error) {
var raw []byte
if raw, err = sync.ReadBytesN(size); err != nil {
return "", err
}
return string(raw), nil
}
func (sync syncTransport) ReadBytesN(size int) (raw []byte, err error) {
_ = sync.sock.SetReadDeadline(time.Now().Add(time.Second * sync.readTimeout))
return _readN(sync.sock, size)
}
func (sync syncTransport) Close() (err error) {
if sync.sock == nil {
return nil
}
_ = DisableTimeWait(sync.sock.(*net.TCPConn))
return sync.sock.Close()
}

187
pkg/gadb/transport.go Normal file
View File

@@ -0,0 +1,187 @@
package gadb
import (
"fmt"
"io"
"net"
"regexp"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/code"
)
var ErrConnBroken = errors.New("socket connection broken")
var DefaultAdbReadTimeout time.Duration = 300
var regexDeviceOffline = regexp.MustCompile("device .* not found")
type transport struct {
sock net.Conn
readTimeout time.Duration
}
// newTransport creates a new tcp socket connection
func newTransport(address string, readTimeout ...time.Duration) (tp transport, err error) {
if len(readTimeout) == 0 {
readTimeout = []time.Duration{DefaultAdbReadTimeout}
}
tp = transport{
readTimeout: readTimeout[0],
}
tp.sock, err = net.Dial("tcp", address)
if err == nil {
// dial success
return tp, nil
}
// connection refused
if strings.Contains(err.Error(), "connect: connection refused") {
err = errors.Wrap(code.DeviceConnectionError, err.Error())
return
}
// device offline
if regexDeviceOffline.MatchString(err.Error()) {
err = errors.Wrap(code.DeviceOfflineError, err.Error())
return
}
// other connection errors
err = errors.Wrap(code.DeviceConnectionError, err.Error())
return
}
func (t transport) Send(command string) (err error) {
msg := fmt.Sprintf("%04x%s", len(command), command)
return _send(t.sock, []byte(msg))
}
func (t transport) SendBytes(b []byte) (err error) {
return _send(t.sock, b)
}
func (t transport) Conn() net.Conn {
return t.sock
}
func (t transport) VerifyResponse() (err error) {
var status string
if status, err = t.ReadStringN(4); err != nil {
return err
}
if status == "OKAY" {
return nil
}
var sError string
if sError, err = t.UnpackString(); err != nil {
return err
}
log.Warn().Str("status", status).Str("err", sError).
Msg("verify adb response failed")
return errors.New(sError)
}
func (t transport) ReadStringAll() (s string, err error) {
var raw []byte
raw, err = t.ReadBytesAll()
return string(raw), err
}
func (t transport) ReadBytesAll() (raw []byte, err error) {
raw, err = io.ReadAll(t.sock)
return
}
func (t transport) UnpackString() (s string, err error) {
var raw []byte
raw, err = t.UnpackBytes()
return string(raw), err
}
func (t transport) UnpackBytes() (raw []byte, err error) {
var length string
if length, err = t.ReadStringN(4); err != nil {
return nil, err
}
var size int64
if size, err = strconv.ParseInt(length, 16, 64); err != nil {
return nil, err
}
raw, err = t.ReadBytesN(int(size))
return
}
func (t transport) ReadStringN(size int) (s string, err error) {
var raw []byte
if raw, err = t.ReadBytesN(size); err != nil {
return "", err
}
return string(raw), nil
}
func (t transport) ReadBytesN(size int) (raw []byte, err error) {
_ = t.sock.SetReadDeadline(time.Now().Add(time.Second * t.readTimeout))
return _readN(t.sock, size)
}
func (t transport) Close() (err error) {
if t.sock == nil {
return nil
}
_ = DisableTimeWait(t.sock.(*net.TCPConn))
return t.sock.Close()
}
func (t transport) SendWithCheck(command string) (err error) {
if err = t.Send(command); err != nil {
return err
}
return t.VerifyResponse()
}
func (t transport) CreateSyncTransport() (sTp syncTransport, err error) {
if err = t.SendWithCheck("sync:"); err != nil {
return syncTransport{}, err
}
sTp = newSyncTransport(t.sock, t.readTimeout)
return
}
func _send(writer io.Writer, msg []byte) (err error) {
for totalSent := 0; totalSent < len(msg); {
var sent int
if sent, err = writer.Write(msg[totalSent:]); err != nil {
return err
}
if sent == 0 {
return ErrConnBroken
}
totalSent += sent
}
return
}
func _readN(reader io.Reader, size int) (raw []byte, err error) {
raw = make([]byte, 0, size)
for len(raw) < size {
buf := make([]byte, size-len(raw))
var n int
if n, err = io.ReadFull(reader, buf); err != nil {
return nil, err
}
if n == 0 {
return nil, ErrConnBroken
}
raw = append(raw, buf...)
}
return
}

View File

@@ -0,0 +1,25 @@
//go:build localtest
package gadb
import (
"testing"
)
func Test_transport_VerifyResponse(t *testing.T) {
transport, err := newTransport("localhost:5037")
if err != nil {
t.Fatal(err)
}
defer transport.Close()
err = transport.Send("host:version")
if err != nil {
t.Fatal(err)
}
err = transport.VerifyResponse()
if err != nil {
t.Fatal(err)
}
}

9
pkg/gadb/utils.go Normal file
View File

@@ -0,0 +1,9 @@
package gadb
import (
"net"
)
func DisableTimeWait(conn *net.TCPConn) error {
return conn.SetLinger(0)
}