├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── Makefile ├── README.md ├── adb.go ├── adb_test.go ├── cmd ├── adb │ └── main.go ├── demo │ └── demo.go ├── raw-adb │ └── raw-adb.go └── relay │ └── main.go ├── cmd_am.go ├── cmd_am_test.go ├── cmd_df.go ├── cmd_df_test.go ├── cmd_linuxcmd.go ├── cmd_linuxcmd_test.go ├── cmd_pm.go ├── cmd_pm_test.go ├── cmd_process.go ├── cmd_process_test.go ├── cpuinfo.txt ├── debug.go ├── device.go ├── device_descriptor.go ├── device_info.go ├── device_info_test.go ├── device_state.go ├── device_state_test.go ├── device_test.go ├── device_watcher.go ├── device_watcher_test.go ├── devicedescriptortype_string.go ├── devicestate_string.go ├── dialer.go ├── doc.go ├── doc └── API.md ├── executable.go ├── executable_unix.go ├── executable_win.go ├── go.mod ├── go.sum ├── meminfo.txt ├── properties.go ├── properties_test.go ├── server.go ├── server_mock_test.go ├── server_test.go ├── services ├── discovery.go └── discovery_test.go ├── session.go ├── session_test.go ├── settings.go ├── shell.go ├── shell_test.go ├── shell_transport.go ├── sync.go ├── sync_test.go ├── util.go ├── util_test.go └── wire ├── conn.go ├── conn_test.go ├── doc.go ├── errors.go ├── filemode.go ├── sync_conn.go ├── sync_conn_test.go ├── sync_dir.go ├── sync_file.go ├── sync_file_test.go ├── util.go └── util_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.6 5 | - tip 6 | 7 | install: 8 | - make get-deps 9 | 10 | script: 11 | - make test 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | GO ?= go 16 | 17 | # test for go module support 18 | ifeq ($(shell go help mod >/dev/null 2>&1 && echo true), true) 19 | export GO_BUILD=GO111MODULE=on $(GO) build -mod=mod 20 | export GO_TEST=GO111MODULE=on $(GO) test -mod=mod 21 | else 22 | export GO_BUILD=$(GO) build 23 | export GO_TEST=$(GO) test 24 | endif 25 | 26 | 27 | GOOS := $(shell $(GO) env GOOS) 28 | ifeq ($(GOOS),windows) 29 | BIN_EXT := .exe 30 | endif 31 | 32 | #git.woa.com/CloudTesting/CloudServer/taskd 33 | PROJECT := github.com/prife/goadb 34 | BINDIR := /usr/local/bin 35 | 36 | VERSION := $(shell git describe --tags --dirty --always) 37 | VERSION := $(VERSION:v%=%) 38 | TARGET := adb 39 | 40 | # -s -w 41 | GO_LDFLAGS := -X $(PROJECT)/internal/version.Version=$(VERSION) 42 | 43 | BUILD_PATH := $(shell pwd)/build 44 | BUILD_BIN_PATH := $(BUILD_PATH)/bin 45 | 46 | define go-build 47 | $(shell cd `pwd` && $(GO_BUILD) -o $(BUILD_BIN_PATH)/$(shell basename $(1)) $(1)) 48 | @echo > /dev/null 49 | endef 50 | 51 | GINKGO := $(BUILD_BIN_PATH)/ginkgo 52 | GOLANGCI_LINT := $(BUILD_BIN_PATH)/golangci-lint 53 | OUTDIR := $(CURDIR)/_output 54 | 55 | help: 56 | @echo "Usage: make " 57 | @echo 58 | @echo " * 'install' - Install binaries to system locations." 59 | @echo " * 'binaries' - Build emud." 60 | @echo " * 'clean' - Clean artifacts." 61 | 62 | 63 | .PHONY: all 64 | all: | goadb_mac goadb_linux_x86 goadb_linux_arm64 goadb.exe ## Build binary 65 | 66 | .PHONY: goadb_mac 67 | goadb_mac: goadb_mac_x86 goadb_mac_arm64 68 | lipo $(patsubst %, $(OUTDIR)/%, $^) -create -output $(CURDIR)/_output/$@ 69 | 70 | .PHONY: goadb_mac_x86 71 | goadb_mac_x86: 72 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO_BUILD) -o $(CURDIR)/_output/$@ \ 73 | -ldflags '$(GO_LDFLAGS)' \ 74 | $(PROJECT)/cmd/$(TARGET) 75 | 76 | .PHONY: goadb_mac_arm64 77 | goadb_mac_arm64: 78 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO_BUILD) -o $(CURDIR)/_output/$@ \ 79 | -ldflags '$(GO_LDFLAGS)' \ 80 | $(PROJECT)/cmd/$(TARGET) 81 | 82 | ## for linux 83 | .PHONY: goadb_linux_x86 84 | goadb_linux_x86: 85 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO_BUILD) -o $(CURDIR)/_output/$@ \ 86 | -ldflags '$(GO_LDFLAGS)' \ 87 | $(PROJECT)/cmd/$(TARGET) 88 | 89 | .PHONY: goadb_linux_arm64 90 | goadb_linux_arm64: 91 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO_BUILD) -o $(CURDIR)/_output/$@ \ 92 | -ldflags '$(GO_LDFLAGS)' \ 93 | $(PROJECT)/cmd/$(TARGET) 94 | 95 | # for windows 96 | .PHONY: goadb.exe 97 | goadb.exe: 98 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO_BUILD) -o $(CURDIR)/_output/$@ \ 99 | -ldflags '$(GO_LDFLAGS)' \ 100 | $(PROJECT)/cmd/$(TARGET) 101 | 102 | clean: 103 | find . -name \*~ -delete 104 | find . -name \#\* -delete 105 | rm -rf _output/* 106 | 107 | 108 | tidy: 109 | export GO111MODULE=on \ 110 | && $(GO) mod tidy 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goadb 2 | 3 | [![Build Status](https://travis-ci.org/prife/goadb.svg?branch=master)](https://travis-ci.org/prife/goadb) 4 | [![GoDoc](https://godoc.org/github.com/prife/goadb?status.svg)](https://godoc.org/github.com/prife/goadb) 5 | 6 | A Golang library for interacting with the Android Debug Bridge (adb). 7 | 8 | See [demo.go](cmd/demo/demo.go) for usage. 9 | -------------------------------------------------------------------------------- /adb.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/prife/goadb/wire" 11 | ) 12 | 13 | const ( 14 | CommandTimeoutShortDefault = time.Second * 2 15 | CommandTimeoutLongDefault = time.Second * 30 16 | ) 17 | 18 | // Adb communicates with host services on the adb server. 19 | // Eg. 20 | // 21 | // client := adb.New() 22 | // client.ListDevices() 23 | // 24 | // See list of services at https://android.googlesource.com/platform/system/core/+/master/adb/SERVICES.TXT. 25 | // TODO(z): Finish implementing host services. 26 | type Adb struct { 27 | server server 28 | } 29 | 30 | // New creates a new Adb client that uses the default ServerConfig. 31 | func New() (*Adb, error) { 32 | return NewWithConfig(ServerConfig{}) 33 | } 34 | 35 | func NewWithConfig(config ServerConfig) (*Adb, error) { 36 | server, err := newServer(config) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return &Adb{server}, nil 41 | } 42 | 43 | // Dial establishes a connection with the adb server. 44 | func (c *Adb) Dial() (wire.IConn, error) { 45 | return c.server.Dial() 46 | } 47 | 48 | // Starts the adb server if it’s not running. 49 | func (c *Adb) StartServer() error { 50 | return c.server.Start() 51 | } 52 | 53 | func (c *Adb) Device(descriptor DeviceDescriptor) *Device { 54 | return &Device{ 55 | server: c.server, 56 | descriptor: descriptor, 57 | deviceListFunc: c.ListDevices, 58 | CmdTimeoutShort: CommandTimeoutShortDefault, 59 | CmdTimeoutLong: CommandTimeoutLongDefault, 60 | } 61 | } 62 | 63 | func (c *Adb) NewDeviceWatcher() *DeviceWatcher { 64 | return newDeviceWatcher(c.server) 65 | } 66 | 67 | // ServerVersion asks the ADB server for its internal version number. 68 | func (c *Adb) ServerVersion() (int, error) { 69 | resp, err := roundTripSingleResponse(c.server, "host:version") 70 | if err != nil { 71 | return 0, fmt.Errorf("GetServerVersion: %w", err) 72 | } 73 | 74 | version, err := c.parseServerVersion(resp) 75 | if err != nil { 76 | return 0, fmt.Errorf("GetServerVersion: %w", err) 77 | } 78 | return version, nil 79 | } 80 | 81 | func (c *Adb) HostFeatures() (map[string]bool, error) { 82 | resp, err := roundTripSingleResponse(c.server, "host:host-features") 83 | if err != nil { 84 | return nil, err 85 | } 86 | return featuresStrToMap(string(resp)), nil 87 | } 88 | 89 | // KillServer tells the server to quit immediately. 90 | // Corresponds to the command: 91 | // 92 | // adb kill-server 93 | func (c *Adb) KillServer() error { 94 | conn, err := c.server.Dial() 95 | if err != nil { 96 | return fmt.Errorf("KillServer: %w", err) 97 | } 98 | defer conn.Close() 99 | 100 | if err = conn.SendMessage([]byte("host:kill")); err != nil { 101 | return fmt.Errorf("KillServer: %w", err) 102 | } 103 | return nil 104 | } 105 | 106 | // ListDeviceSerials returns the serial numbers of all attached devices. 107 | // Corresponds to the command: 108 | // 109 | // adb devices 110 | func (c *Adb) ListDeviceSerials() ([]string, error) { 111 | resp, err := roundTripSingleResponse(c.server, "host:devices") 112 | if err != nil { 113 | return nil, fmt.Errorf("ListDeviceSerials: %w", err) 114 | } 115 | 116 | devices, err := parseDeviceList(string(resp), parseDeviceShort) 117 | if err != nil { 118 | return nil, fmt.Errorf("ListDeviceSerials: %w", err) 119 | } 120 | 121 | serials := make([]string, len(devices)) 122 | for i, dev := range devices { 123 | serials[i] = dev.Serial 124 | } 125 | return serials, nil 126 | } 127 | 128 | // ListDevices returns the list of connected devices. 129 | // Corresponds to the command: 130 | // 131 | // adb devices -l 132 | func (c *Adb) ListDevices() ([]*DeviceInfo, error) { 133 | resp, err := roundTripSingleResponse(c.server, "host:devices-l") 134 | if err != nil { 135 | return nil, fmt.Errorf("ListDevices: %w", err) 136 | } 137 | 138 | devices, err := parseDeviceList(string(resp), parseDeviceLongE) 139 | if err != nil { 140 | return nil, fmt.Errorf("ListDevices: %w", err) 141 | } 142 | return devices, nil 143 | } 144 | 145 | // Connect connect to a device via TCP/IP 146 | // Corresponds to the command: 147 | // 148 | // adb connect ip:port 149 | func (c *Adb) Connect(addr string) error { 150 | // connect may slow in internet, set 5 second timeout 151 | _, err := roundTripSingleResponseTimeout(c.server, "host:connect:"+addr, time.Second*5) 152 | if err != nil { 153 | return fmt.Errorf("Connect: %w", err) 154 | } 155 | return nil 156 | } 157 | 158 | func (c *Adb) DisconnectAll() error { 159 | _, err := roundTripSingleResponse(c.server, "host:disconnect:") 160 | if err != nil { 161 | return fmt.Errorf("disconnect: %w", err) 162 | } 163 | return nil 164 | } 165 | 166 | func (c *Adb) Disconnect(addr string) error { 167 | _, err := roundTripSingleResponse(c.server, "host:disconnect:"+addr) 168 | if err != nil { 169 | return fmt.Errorf("disconnect: %w", err) 170 | } 171 | return nil 172 | } 173 | 174 | func (c *Adb) ListForward() ([]ForwardEntry, error) { 175 | resp, err := roundTripSingleResponse(c.server, "host:list-forward") 176 | if err != nil { 177 | return nil, err 178 | } 179 | return parseForwardList(resp), nil 180 | } 181 | 182 | // RemoveAllForward 183 | // ---> 184 | // 00000000 30 30 31 34 68 6f 73 74 3a 6b 69 6c 6c 66 6f 72 |0014host:killfor| 185 | // 00000010 77 61 72 64 2d 61 6c 6c |ward-all| 186 | // <--- 187 | // 00000000 4f 4b 41 59 4f 4b 41 59 |OKAYOKAY| 188 | func (c *Adb) RemoveAllForward() (err error) { 189 | conn, err := c.server.Dial() 190 | if err != nil { 191 | return 192 | } 193 | defer conn.Close() 194 | 195 | // 这里没有使用 roundTripSingleResponse,因为它返回 OKAY之后,后面还跟 OKAY 196 | req := "host:killforward-all" 197 | if err = conn.SendMessage([]byte(req)); err != nil { 198 | return err 199 | } 200 | 201 | if _, err = readStatusWithTimeout(conn, req, CommandTimeoutShortDefault); err != nil { 202 | return fmt.Errorf("'%s' failed: %w", req, err) 203 | } 204 | return nil 205 | } 206 | 207 | func (c *Adb) parseServerVersion(versionRaw []byte) (int, error) { 208 | versionStr := string(versionRaw) 209 | version, err := strconv.ParseInt(versionStr, 16, 32) 210 | if err != nil { 211 | return 0, fmt.Errorf("error parsing server version: %s", versionStr) 212 | } 213 | return int(version), nil 214 | } 215 | 216 | func featuresStrToMap(attr string) (features map[string]bool) { 217 | lists := strings.Split(attr, ",") 218 | if len(lists) == 0 { 219 | return 220 | } 221 | features = make(map[string]bool) 222 | for _, f := range lists { 223 | features[f] = true 224 | } 225 | return 226 | } 227 | 228 | type ForwardEntry struct { 229 | Serial string 230 | Local string 231 | Remote string 232 | } 233 | 234 | func parseForwardList(resp []byte) []ForwardEntry { 235 | lines := bytes.Split(resp, []byte("\n")) 236 | deviceForward := make([]ForwardEntry, 0, len(lines)) 237 | 238 | for i := range lines { 239 | line := bytes.TrimSpace(lines[i]) 240 | if len(line) == 0 { 241 | continue 242 | } 243 | fields := bytes.Fields(line) 244 | deviceForward = append(deviceForward, ForwardEntry{Serial: string(fields[0]), Local: string(fields[1]), Remote: string(fields[2])}) 245 | } 246 | return deviceForward 247 | } 248 | -------------------------------------------------------------------------------- /adb_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/prife/goadb/wire" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetServerVersion(t *testing.T) { 11 | s := &MockServer{ 12 | Status: wire.StatusSuccess, 13 | Messages: []string{"000a"}, 14 | } 15 | client := &Adb{s} 16 | 17 | v, err := client.ServerVersion() 18 | assert.Equal(t, "host:version", s.Requests[0]) 19 | assert.NoError(t, err) 20 | assert.Equal(t, 10, v) 21 | } 22 | 23 | func TestAdb_ListForward(t *testing.T) { 24 | _, err := adbclient.ListForward() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | } 29 | 30 | func TestAdb_parseForwardList(t *testing.T) { 31 | resp := "PQY0220A15002880 tcp:5000 tcp:5000\nPQY0220A15002880 tcp:6000 tcp:6000\n" 32 | list := parseForwardList([]byte(resp)) 33 | assert.Len(t, list, 2) 34 | assert.Equal(t, list[0].Serial, "PQY0220A15002880") 35 | assert.Equal(t, list[0].Local, "tcp:5000") 36 | assert.Equal(t, list[0].Remote, "tcp:5000") 37 | assert.Equal(t, list[1].Serial, "PQY0220A15002880") 38 | assert.Equal(t, list[1].Local, "tcp:6000") 39 | assert.Equal(t, list[1].Remote, "tcp:6000") 40 | } 41 | 42 | func TestAdb_RemoveAllForward(t *testing.T) { 43 | // clear all forwards 44 | err := adbclient.RemoveAllForward() 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | } 49 | 50 | func TestDevice_DoForward(t *testing.T) { 51 | d := adbclient.Device(AnyDevice()) 52 | 53 | // clear all forwards 54 | err := adbclient.RemoveAllForward() 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | // forward 60 | err = d.DoForward("tcp:700001", "tcp:7001", false) 61 | assert.Contains(t, err.Error(), "server error: cannot bind listener: bad port number '700001'") 62 | 63 | err = d.DoForward("tcp:5000", "tcp:6000", false) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | err = d.DoForward("tcp:5001", "tcp:6000", false) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | list, err := d.DoListForward() 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | assert.Len(t, list, 2) 78 | assert.Equal(t, list[0].Local, "tcp:5000") 79 | assert.Equal(t, list[0].Remote, "tcp:6000") 80 | 81 | // remove 82 | d.DoRemoveForward(list[0].Local) 83 | list, err = d.DoListForward() 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | assert.Len(t, list, 1) 88 | assert.Equal(t, list[0].Local, "tcp:5001") 89 | assert.Equal(t, list[0].Remote, "tcp:6000") 90 | } 91 | 92 | func TestAdb_Disconnect(t *testing.T) { 93 | err := adbclient.Disconnect("192.168.1.100:5000") 94 | assert.Contains(t, err.Error(), "no such device '192.168.1.100:5000'") 95 | } 96 | 97 | func TestAdb_DisconnectAll(t *testing.T) { 98 | err := adbclient.DisconnectAll() 99 | assert.Nil(t, err) 100 | } 101 | -------------------------------------------------------------------------------- /cmd/adb/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/alecthomas/kingpin/v2" 14 | "github.com/cheggaaa/pb" 15 | adb "github.com/prife/goadb" 16 | "github.com/prife/goadb/wire" 17 | ) 18 | 19 | const StdIoFilename = "-" 20 | 21 | var ( 22 | serial = kingpin.Flag("serial", 23 | "Connect to device by serial number."). 24 | Short('s'). 25 | String() 26 | 27 | shellCommand = kingpin.Command("shell", 28 | "Run a shell command on the device.") 29 | shellCommandArg = shellCommand.Arg("command", 30 | "Command to run on device."). 31 | Strings() 32 | psCommand = kingpin.Command("ps", 33 | "List processes.") 34 | devicesCommand = kingpin.Command("devices", 35 | "List devices.") 36 | devicesLongFlag = devicesCommand.Flag("long", 37 | "Include extra detail about devices."). 38 | Short('l'). 39 | Bool() 40 | 41 | pullCommand = kingpin.Command("pull", 42 | "Pull a file from the device.") 43 | pullProgressFlag = pullCommand.Flag("progress", 44 | "Show progress."). 45 | Short('p'). 46 | Bool() 47 | pullRemoteArg = pullCommand.Arg("remote", 48 | "Path of source file on device."). 49 | Required(). 50 | String() 51 | pullLocalArg = pullCommand.Arg("local", 52 | "Path of destination file. If -, will write to stdout."). 53 | String() 54 | pushCommand = kingpin.Command("push", 55 | "Push a file to the device.") 56 | pushProgressFlag = pushCommand.Flag("progress", 57 | "Show progress."). 58 | Short('p'). 59 | Bool() 60 | pushLocalArg = pushCommand.Arg("local", 61 | "Path of source file. If -, will read from stdin."). 62 | Required(). 63 | String() 64 | pushRemoteArg = pushCommand.Arg("remote", 65 | "Path of destination file on device."). 66 | Required(). 67 | String() 68 | pushCommand2 = kingpin.Command("push2", 69 | "Push to the device.") 70 | pushLocalArg2 = pushCommand2.Arg("local", 71 | "Path of source file. If -, will read from stdin."). 72 | Required(). 73 | String() 74 | pushRemoteArg2 = pushCommand2.Arg("remote", 75 | "Path of destination file on device."). 76 | Required(). 77 | String() 78 | ) 79 | 80 | var client *adb.Adb 81 | 82 | func main() { 83 | var exitCode int 84 | 85 | var err error 86 | client, err = adb.NewWithConfig(adb.ServerConfig{}) 87 | if err != nil { 88 | fmt.Fprintln(os.Stderr, "error:", err) 89 | os.Exit(1) 90 | } 91 | 92 | switch kingpin.Parse() { 93 | case "devices": 94 | exitCode = listDevices(*devicesLongFlag) 95 | case "shell": 96 | exitCode = runShellCommand(*shellCommandArg, parseDevice()) 97 | case "ps": 98 | exitCode = ps(parseDevice()) 99 | case "pull": 100 | exitCode = pull(*pullProgressFlag, *pullRemoteArg, *pullLocalArg, parseDevice()) 101 | case "push": 102 | exitCode = push(*pushProgressFlag, *pushLocalArg, *pushRemoteArg, parseDevice()) 103 | case "push2": 104 | exitCode = push2(parseDevice(), *pushLocalArg2, *pushRemoteArg2) 105 | } 106 | 107 | os.Exit(exitCode) 108 | } 109 | 110 | func parseDevice() adb.DeviceDescriptor { 111 | if *serial != "" { 112 | return adb.DeviceWithSerial(*serial) 113 | } 114 | 115 | return adb.AnyDevice() 116 | } 117 | 118 | func listDevices(long bool) int { 119 | //client := adb.New(server) 120 | devices, err := client.ListDevices() 121 | if err != nil { 122 | fmt.Fprintln(os.Stderr, "error:", err) 123 | } 124 | 125 | for _, device := range devices { 126 | fmt.Printf("%s\t %s ", device.Serial, device.State) 127 | if device.Usb != "" { 128 | fmt.Printf("usb:%s ", device.Usb) 129 | } 130 | 131 | if device.Product != "" { 132 | fmt.Printf("product:%s ", device.Product) 133 | } 134 | 135 | if device.Model != "" { 136 | fmt.Printf("model:%s ", device.Model) 137 | } 138 | 139 | if device.DeviceInfo != "" { 140 | fmt.Printf("device:%s ", device.DeviceInfo) 141 | } 142 | 143 | if device.TransportID != 0 { 144 | fmt.Printf("transport_id:%d ", device.TransportID) 145 | } 146 | fmt.Printf("\n") 147 | } 148 | 149 | return 0 150 | } 151 | 152 | func runShellCommand(commandAndArgs []string, device adb.DeviceDescriptor) int { 153 | if len(commandAndArgs) == 0 { 154 | fmt.Fprintln(os.Stderr, "error: no command") 155 | kingpin.Usage() 156 | return 1 157 | } 158 | 159 | command := commandAndArgs[0] 160 | var args []string 161 | 162 | if len(commandAndArgs) > 1 { 163 | args = commandAndArgs[1:] 164 | } 165 | 166 | client := client.Device(device) 167 | reader, err := client.RunCommand(command, args...) 168 | if err != nil { 169 | fmt.Fprintln(os.Stderr, "error:", err) 170 | return 1 171 | } 172 | _ = reader 173 | fmt.Printf("%s\n", reader) 174 | fmt.Println("--------------") 175 | fmt.Println(hex.Dump(reader)) 176 | // defer reader.Close() 177 | // io.Copy(os.Stdout, reader) 178 | return 0 179 | } 180 | 181 | func ps(device adb.DeviceDescriptor) int { 182 | client := client.Device(device) 183 | list, err := client.ListProcesses(nil) 184 | if err != nil { 185 | panic(err) 186 | return 1 187 | } 188 | for _, name := range list { 189 | fmt.Printf("%-12s%6d%49s%s\n", name.Uid, name.Pid, " ", name.Name) 190 | } 191 | return 0 192 | } 193 | 194 | func pull(showProgress bool, remotePath, localPath string, device adb.DeviceDescriptor) int { 195 | if remotePath == "" { 196 | fmt.Fprintln(os.Stderr, "error: must specify remote file") 197 | kingpin.Usage() 198 | return 1 199 | } 200 | 201 | if localPath == "" { 202 | localPath = filepath.Base(remotePath) 203 | } 204 | 205 | client := client.Device(device) 206 | 207 | info, err := client.Stat(remotePath) 208 | if errors.Is(err, wire.ErrFileNoExist) { 209 | fmt.Fprintln(os.Stderr, "remote file does not exist:", remotePath) 210 | return 1 211 | } else if err != nil { 212 | fmt.Fprintf(os.Stderr, "error reading remote file %s: %s\n", remotePath, err) 213 | return 1 214 | } 215 | 216 | sc, remoteFile, err := client.OpenFileReader(remotePath) 217 | if err != nil { 218 | fmt.Fprintf(os.Stderr, "error opening remote file %s: %v\n", remotePath, err) 219 | return 1 220 | } 221 | defer sc.Close() 222 | 223 | var localFile io.WriteCloser 224 | if localPath == StdIoFilename { 225 | localFile = os.Stdout 226 | } else { 227 | localFile, err = os.Create(localPath) 228 | if err != nil { 229 | fmt.Fprintf(os.Stderr, "error opening local file %s: %s\n", localPath, err) 230 | return 1 231 | } 232 | } 233 | defer localFile.Close() 234 | 235 | if err := copyWithProgressAndStats(localFile, remoteFile, int(info.Size), showProgress); err != nil { 236 | fmt.Fprintln(os.Stderr, "error pulling file:", err) 237 | return 1 238 | } 239 | return 0 240 | } 241 | 242 | func push(showProgress bool, localPath, remotePath string, device adb.DeviceDescriptor) int { 243 | if remotePath == "" { 244 | fmt.Fprintln(os.Stderr, "error: must specify remote file") 245 | kingpin.Usage() 246 | return 1 247 | } 248 | 249 | var ( 250 | localFile io.ReadCloser 251 | size int 252 | perms os.FileMode 253 | mtime time.Time 254 | ) 255 | if localPath == "" || localPath == StdIoFilename { 256 | localFile = os.Stdin 257 | // 0 size will hide the progress bar. 258 | perms = os.FileMode(0660) 259 | mtime = adb.MtimeOfClose 260 | } else { 261 | var err error 262 | localFile, err = os.Open(localPath) 263 | if err != nil { 264 | fmt.Fprintf(os.Stderr, "error opening local file %s: %s\n", localPath, err) 265 | return 1 266 | } 267 | info, err := os.Stat(localPath) 268 | if err != nil { 269 | fmt.Fprintf(os.Stderr, "error reading local file %s: %s\n", localPath, err) 270 | return 1 271 | } 272 | size = int(info.Size()) 273 | perms = info.Mode().Perm() 274 | mtime = info.ModTime() 275 | } 276 | defer localFile.Close() 277 | 278 | client := client.Device(device) 279 | sc, writer, err := client.OpenFileWriter(remotePath, perms, mtime) 280 | if err != nil { 281 | fmt.Fprintf(os.Stderr, "error opening remote file %s: %s\n", remotePath, err) 282 | return 1 283 | } 284 | defer writer.CopyDone() 285 | defer sc.Close() 286 | 287 | if err := copyWithProgressAndStats(writer, localFile, size, showProgress); err != nil { 288 | fmt.Fprintln(os.Stderr, "error pushing file:", err) 289 | return 1 290 | } 291 | return 0 292 | } 293 | 294 | // copyWithProgressAndStats copies src to dst. 295 | // If showProgress is true and size is positive, a progress bar is shown. 296 | // After copying, final stats about the transfer speed and size are shown. 297 | // Progress and stats are printed to stderr. 298 | func copyWithProgressAndStats(dst io.Writer, src io.Reader, size int, showProgress bool) error { 299 | var progress *pb.ProgressBar 300 | if showProgress && size > 0 { 301 | progress = pb.New(size) 302 | // Write to stderr in case dst is stdout. 303 | progress.Output = os.Stderr 304 | progress.ShowSpeed = true 305 | progress.ShowPercent = true 306 | progress.ShowTimeLeft = true 307 | progress.SetUnits(pb.U_BYTES) 308 | progress.Start() 309 | dst = io.MultiWriter(dst, progress) 310 | } 311 | 312 | startTime := time.Now() 313 | copied, err := io.Copy(dst, src) 314 | 315 | if progress != nil { 316 | progress.Finish() 317 | } 318 | 319 | if pathErr, ok := err.(*os.PathError); ok { 320 | if errno, ok := pathErr.Err.(syscall.Errno); ok && errno == syscall.EPIPE { 321 | // Pipe closed. Handle this like an EOF. 322 | err = nil 323 | } 324 | } 325 | if err != nil { 326 | return err 327 | } 328 | 329 | duration := time.Now().Sub(startTime) 330 | rate := int64(float64(copied) / duration.Seconds()) 331 | fmt.Fprintf(os.Stderr, "%d B/s (%d bytes in %s)\n", rate, copied, duration) 332 | 333 | return nil 334 | } 335 | 336 | func push2(descriptor adb.DeviceDescriptor, localPath, remotePath string) int { 337 | device := client.Device(descriptor) 338 | err := device.PushDir(localPath, remotePath, true, func(totalFiles, sentFiles uint64, current string, percent, speed float64, err error) { 339 | if err != nil { 340 | fmt.Printf("[%d/%d] pushing %s, %.2f%%, err:%s\n", sentFiles, totalFiles, current, percent, err.Error()) 341 | } else { 342 | fmt.Printf("[%d/%d] pushing %s, %.2f%%, %.02f MB/s\n", sentFiles, totalFiles, current, percent, speed) 343 | } 344 | }) 345 | if err != nil { 346 | fmt.Fprintf(os.Stderr, "push failed:%v\n", err) 347 | return 1 348 | } 349 | return 0 350 | } 351 | -------------------------------------------------------------------------------- /cmd/demo/demo.go: -------------------------------------------------------------------------------- 1 | // An app demonstrating most of the library's features. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "time" 10 | 11 | adb "github.com/prife/goadb" 12 | ) 13 | 14 | var ( 15 | port = flag.Int("p", adb.AdbPort, "") 16 | 17 | client *adb.Adb 18 | ) 19 | 20 | func main() { 21 | flag.Parse() 22 | 23 | var err error 24 | client, err = adb.NewWithConfig(adb.ServerConfig{ 25 | Port: *port, 26 | }) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | fmt.Println("Starting server…") 31 | client.StartServer() 32 | 33 | serverVersion, err := client.ServerVersion() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | fmt.Println("Server version:", serverVersion) 38 | 39 | devices, err := client.ListDevices() 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | fmt.Println("Devices:") 44 | for _, device := range devices { 45 | fmt.Printf("\t%+v\n", *device) 46 | } 47 | 48 | PrintDeviceInfoAndError(adb.AnyDevice()) 49 | PrintDeviceInfoAndError(adb.AnyLocalDevice()) 50 | PrintDeviceInfoAndError(adb.AnyUsbDevice()) 51 | 52 | serials, err := client.ListDeviceSerials() 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | for _, serial := range serials { 57 | PrintDeviceInfoAndError(adb.DeviceWithSerial(serial)) 58 | } 59 | 60 | fmt.Println() 61 | fmt.Println("Watching for device state changes.") 62 | watcher := client.NewDeviceWatcher() 63 | for event := range watcher.C() { 64 | fmt.Printf("\t[%s]%+v\n", time.Now(), event) 65 | } 66 | if watcher.Err() != nil { 67 | fmt.Println(watcher.Err()) 68 | } 69 | 70 | //fmt.Println("Killing server…") 71 | //client.KillServer() 72 | } 73 | 74 | func PrintDeviceInfoAndError(descriptor adb.DeviceDescriptor) { 75 | device := client.Device(descriptor) 76 | if err := PrintDeviceInfo(device); err != nil { 77 | log.Println(err) 78 | } 79 | } 80 | 81 | func PrintDeviceInfo(device *adb.Device) error { 82 | serialNo, err := device.Serial() 83 | if err != nil { 84 | return err 85 | } 86 | devPath, err := device.DevicePath() 87 | if err != nil { 88 | return err 89 | } 90 | state, err := device.State() 91 | if err != nil { 92 | return err 93 | } 94 | 95 | fmt.Println(device) 96 | fmt.Printf("\tserial no: %s\n", serialNo) 97 | fmt.Printf("\tdevPath: %s\n", devPath) 98 | fmt.Printf("\tstate: %s\n", state) 99 | 100 | cmdOutput, err := device.RunCommand("pwd") 101 | if err != nil { 102 | fmt.Println("\terror running command:", err) 103 | } 104 | fmt.Printf("\tcmd output: %s\n", cmdOutput) 105 | 106 | stat, err := device.Stat("/sdcard") 107 | if err != nil { 108 | fmt.Println("\terror stating /sdcard:", err) 109 | } 110 | fmt.Printf("\tstat \"/sdcard\": %+v\n", stat) 111 | 112 | fmt.Println("\tfiles in \"/\":") 113 | sc, dr, err := device.OpenDirReader("/") 114 | if err != nil { 115 | fmt.Println("\terror listing files:", err) 116 | } else { 117 | defer sc.Close() 118 | for { 119 | dirs, err := dr.ReadDir(1) 120 | if err != nil { 121 | fmt.Println("\terror listing files:", err) 122 | } else { 123 | fmt.Printf("\t%+v\n", dirs[0]) 124 | } 125 | } 126 | } 127 | 128 | fmt.Println("\tnon-existent file:") 129 | stat, err = device.Stat("/supercalifragilisticexpialidocious") 130 | if err != nil { 131 | fmt.Println("\terror:", err) 132 | } else { 133 | fmt.Printf("\tstat: %+v\n", stat) 134 | } 135 | 136 | fmt.Print("\tload avg: ") 137 | sc, loadavgReader, err := device.OpenFileReader("/proc/loadavg") 138 | if err != nil { 139 | fmt.Println("\terror opening file:", err) 140 | } else { 141 | defer sc.Close() 142 | loadAvg, err := io.ReadAll(loadavgReader) 143 | if err != nil { 144 | fmt.Println("\terror reading file:", err) 145 | } else { 146 | fmt.Println(string(loadAvg)) 147 | } 148 | } 149 | 150 | return nil 151 | } 152 | -------------------------------------------------------------------------------- /cmd/raw-adb/raw-adb.go: -------------------------------------------------------------------------------- 1 | // A simple tool for sending raw messages to an adb server. 2 | package main 3 | 4 | import ( 5 | "bufio" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "strings" 12 | 13 | adb "github.com/prife/goadb" 14 | ) 15 | 16 | var port = flag.Int("p", adb.AdbPort, "`port` the adb server is listening on") 17 | 18 | func main() { 19 | flag.Parse() 20 | 21 | fmt.Println("using port", *port) 22 | 23 | printServerVersion() 24 | 25 | for { 26 | line := readLine() 27 | err := doCommand(line) 28 | if err != nil { 29 | fmt.Println("error:", err) 30 | } 31 | } 32 | } 33 | 34 | func printServerVersion() { 35 | err := doCommand("host:version") 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | } 40 | 41 | func readLine() string { 42 | fmt.Print("> ") 43 | line, err := bufio.NewReader(os.Stdin).ReadString('\n') 44 | if err != nil && err != io.EOF { 45 | log.Fatal(err) 46 | } 47 | return strings.TrimSpace(line) 48 | } 49 | 50 | func doCommand(cmd string) error { 51 | server, err := adb.NewWithConfig(adb.ServerConfig{ 52 | Port: *port, 53 | }) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | conn, err := server.Dial() 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | defer conn.Close() 63 | 64 | if err := conn.SendMessage([]byte(cmd)); err != nil { 65 | return err 66 | } 67 | 68 | status, err := conn.ReadStatus("") 69 | if err != nil { 70 | return err 71 | } 72 | 73 | for { 74 | msg, err := conn.ReadMessage() 75 | if err == nil { 76 | fmt.Printf("%s> %s\n", status, msg) 77 | } 78 | if err != io.EOF { 79 | return err 80 | } 81 | } 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /cmd/relay/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "net" 9 | "os" 10 | "path" 11 | "runtime" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func tcp_to_unix(tcp, unix string) error { 17 | listener, err := net.Listen("unix", unix) 18 | if err != nil { 19 | return fmt.Errorf("usbmuxd: fail to listen on: %v, error:%v", unix, err) 20 | } 21 | 22 | os.Chmod(unix, 0777) 23 | log.Debugln("listen on: ") 24 | for { 25 | conn, err := listener.Accept() 26 | if err != nil { 27 | return fmt.Errorf("usbmuxd: fail to listen accept: %v", err) 28 | } 29 | 30 | client, err := net.Dial("tcp", tcp) 31 | go func() { 32 | io.Copy(client, conn) 33 | client.Close() 34 | }() 35 | go func() { 36 | io.Copy(conn, client) 37 | conn.Close() 38 | }() 39 | } 40 | } 41 | 42 | func unix_to_tcp(unix, tcp string) error { 43 | listener, err := net.Listen("tcp", tcp) 44 | if err != nil { 45 | return fmt.Errorf("usbmuxd: fail to listen on: %v, error:%v", tcp, err) 46 | } 47 | 48 | log.Debugln("listen on: ") 49 | for { 50 | conn, err := listener.Accept() 51 | if err != nil { 52 | return fmt.Errorf("usbmuxd: fail to listen accept: %v", err) 53 | } 54 | 55 | client, err := net.Dial("unix", unix) 56 | go func() { 57 | io.Copy(client, conn) 58 | }() 59 | go func() { 60 | io.Copy(conn, client) 61 | }() 62 | } 63 | } 64 | 65 | func tcp_to_tcp(target, listen string) error { 66 | listener, err := net.Listen("tcp4", listen) 67 | if err != nil { 68 | return fmt.Errorf("usbmuxd: fail to listen on: %v, error:%v", listen, err) 69 | } 70 | 71 | log.Debugln("listen on: ") 72 | for { 73 | conn, err := listener.Accept() 74 | if err != nil { 75 | return fmt.Errorf("usbmuxd: fail to listen accept: %v", err) 76 | } 77 | 78 | client, err := net.Dial("tcp", target) 79 | go func() { 80 | buf := make([]byte, 1024*1024) 81 | for { 82 | n, err := conn.Read(buf) 83 | if n > 0 { 84 | fmt.Printf("--->\n%s\n", hex.Dump(buf[:n])) 85 | client.Write(buf[:n]) 86 | } 87 | 88 | if err != nil { 89 | client.Close() 90 | break 91 | } 92 | } 93 | }() 94 | go func() { 95 | buf := make([]byte, 1024*1024) 96 | for { 97 | n, err := client.Read(buf) 98 | if n > 0 { 99 | fmt.Printf("<---\n%s\n", hex.Dump(buf[:n])) 100 | conn.Write(buf[:n]) 101 | } 102 | 103 | if err != nil { 104 | conn.Close() 105 | break 106 | } 107 | } 108 | }() 109 | } 110 | } 111 | 112 | var mode = flag.String("mode", "tcp2tcp", "adb server port") 113 | 114 | func initLog() { 115 | log.SetFormatter(&log.JSONFormatter{}) 116 | log.SetReportCaller(true) 117 | log.SetFormatter(&log.TextFormatter{ 118 | CallerPrettyfier: func(f *runtime.Frame) (string, string) { 119 | filename := path.Base(f.File) 120 | return "", fmt.Sprintf("%s:%d", filename, f.Line) 121 | }, 122 | }) 123 | log.SetLevel(log.InfoLevel) 124 | } 125 | 126 | func main() { 127 | flag.Parse() 128 | 129 | initLog() 130 | 131 | switch *mode { 132 | case "unix": 133 | panic(tcp_to_unix("127.0.0.1:27015", "/var/run/usbmuxd")) 134 | case "tcp": 135 | panic(unix_to_tcp("/var/run/usbmuxd", "127.0.0.1:27015")) 136 | case "tcp2tcp": 137 | panic(tcp_to_tcp("127.0.0.1:8711", "127.0.0.1:8710")) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /cmd_am.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | ) 9 | 10 | var ( 11 | activityRegrex = regexp.MustCompile(`\b(\w+(\.\w+)*)\/([\.\w]+)`) 12 | ) 13 | 14 | type Activity struct { 15 | Fullname string 16 | Package string 17 | Component string 18 | } 19 | 20 | // UnpackActivity extract / from bytes 21 | func UnpackActivity(resp []byte) (l []Activity) { 22 | matches := activityRegrex.FindAllSubmatch(resp, -1) 23 | m := make(map[string]interface{}) 24 | for _, match := range matches { 25 | fullname := string(match[0]) 26 | if _, ok := m[fullname]; !ok { 27 | m[fullname] = struct{}{} 28 | l = append(l, Activity{Fullname: fullname, Package: string(match[1]), Component: string(match[3])}) 29 | } 30 | } 31 | return 32 | } 33 | 34 | // $ adb shell am 35 | // force-stop [--user | all | current] 36 | // Completely stop the given application package. 37 | // stop-app [--user | all | current] 38 | // Stop an app and all of its services. Unlike `force-stop` this does 39 | // not cancel the app's scheduled alarms and jobs. 40 | // kill [--user | all | current] 41 | // Kill all background processes associated with the given application. 42 | // 43 | // $ adb shell 'dumpsys activity activities | grep ResumedActivity' 44 | // Android 14 [passed] 45 | // topResumedActivity=ActivityRecord{18aea91 u0 com.android.settings/.Settings t84} 46 | // ResumedActivity: ActivityRecord{18aea91 u0 com.android.settings/.Settings t84} 47 | // 48 | // Android 5.1 [passed] 49 | // mResumedActivity: ActivityRecord{2f5cd8d4 u0 com.oppo.launcher/.Launcher t9} 50 | 51 | // GetCurrentActivity get current focused activity 52 | // TODO: may not support multi display. 53 | // references: 54 | // https://stackoverflow.com/questions/13193592/getting-the-name-of-the-current-activity-via-adb 55 | func (d *Device) GetCurrentActivity() (app []Activity, err error) { 56 | resp, err := d.RunCommandTimeout(d.CmdTimeoutLong, "dumpsys activity activities | grep ResumedActivity") 57 | if err != nil { 58 | return // tcp error 59 | } 60 | 61 | // err maybe nil, check response to determine error 62 | if len(resp) > 0 { 63 | app = UnpackActivity(resp) 64 | if len(app) == 0 { 65 | return nil, fmt.Errorf("can't found current activity") 66 | } 67 | } 68 | return 69 | } 70 | 71 | // LaunchAppByMonkey launch app by it's package name with monkey 72 | // 73 | // $ adb shell monkey -p com.android.settings 1 74 | // Android 5.1 75 | // Events injected: 1 76 | // ## Network stats: elapsed time=50ms (0ms mobile, 0ms wifi, 50ms not connected) 77 | // 78 | // Android 14 79 | // bash arg: -p 80 | // bash arg: com.android.settings 81 | // bash arg: 1 82 | // args: [-p, com.android.settings, 1] 83 | // arg: "-p" 84 | // arg: "com.android.settings" 85 | // arg: "1" 86 | // data="com.android.settings" 87 | // Events injected: 1 88 | // ## Network stats: elapsed time=56ms (0ms mobile, 0ms wifi, 56ms not connected) 89 | // 90 | // $ adb shell monkey -p com.android.settings1111 1 91 | // ... 92 | // ** No activities found to run, monkey aborted. 93 | func (d *Device) LaunchAppByMonkey(packageName string) (resp []byte, err error) { 94 | // https://stackoverflow.com/questions/4567904/how-to-start-an-application-using-android-adb-tools 95 | cmd := "monkey -p " + packageName + " 1" 96 | resp, err = d.RunCommandTimeout(d.CmdTimeoutLong, cmd) 97 | if err != nil { 98 | return // tcp error 99 | } 100 | 101 | // err maybe nil, check response to determine error 102 | if bytes.Contains(resp, []byte("Events injected: ")) { 103 | return 104 | } else if bytes.Contains(resp, []byte("No activities found to run, monkey aborted")) { 105 | err = errors.New("no activities found") 106 | return 107 | } 108 | err = errors.New("unrecognized error") 109 | return 110 | } 111 | 112 | // Android 12 113 | // HWNOH:/ $ am start -n com.EpicLRT.ActionRPGSample/com.epicgames.ue4.SplashActivity 114 | // Starting: Intent { cmp=com.EpicLRT.ActionRPGSample/com.epicgames.ue4.SplashActivity } 115 | // HWNOH:/ $ am start -n com.EpicLRT.ActionRPGSample/com.epicgames.ue4.SplashActivity1 116 | // Starting: Intent { cmp=com.EpicLRT.ActionRPGSample/com.epicgames.ue4.SplashActivity1 } 117 | // Error type 3 118 | // Error: Activity class {com.EpicLRT.ActionRPGSample/com.epicgames.ue4.SplashActivity1} does not exist. 119 | func (d *Device) AmStart(pkgActivityName string) error { 120 | resp, err := d.RunCommandTimeout(d.CmdTimeoutLong, "am start -n "+pkgActivityName) 121 | if err != nil { 122 | return err // tcp error 123 | } 124 | // err maybe nil, check response to determine error 125 | if bytes.Contains(resp, []byte("Error: ")) { 126 | return errors.New(string(resp)) 127 | } else { 128 | return nil 129 | } 130 | } 131 | 132 | // ForceStopPackage force-stop app 133 | // Android 14: don't need permission 134 | func (d *Device) AmForceStop(packageName string) (err error) { 135 | resp, err := d.RunCommandTimeout(d.CmdTimeoutLong, "am force-stop "+packageName) 136 | if err != nil { 137 | return err // tcp error 138 | } 139 | 140 | // err maybe nil, check response to determine error 141 | if len(resp) == 0 { 142 | return 143 | } 144 | 145 | err = errors.New(string(resp)) 146 | return 147 | } 148 | -------------------------------------------------------------------------------- /cmd_am_test.go: -------------------------------------------------------------------------------- 1 | package adb_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | adb "github.com/prife/goadb" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDevice_GetCurrentActivity(t *testing.T) { 12 | assert.NotNil(t, adbclient) 13 | d := adbclient.Device(adb.AnyDevice()) 14 | list, err := d.GetCurrentActivity() 15 | assert.Nil(t, err) 16 | for _, l := range list { 17 | fmt.Println(l) 18 | } 19 | } 20 | 21 | func TestDevice_ForceStopApp(t *testing.T) { 22 | assert.NotNil(t, adbclient) 23 | d := adbclient.Device(adb.AnyDevice()) 24 | list, err := d.GetCurrentActivity() 25 | assert.Nil(t, err) 26 | err = d.AmForceStop(list[0].Package) 27 | assert.Nil(t, err) 28 | } 29 | 30 | func TestDevice_StartApp(t *testing.T) { 31 | assert.NotNil(t, adbclient) 32 | d := adbclient.Device(adb.AnyDevice()) 33 | _, err := d.LaunchAppByMonkey("com.android.settings") 34 | assert.Nil(t, err) 35 | } 36 | 37 | func TestDevice_StartApp2(t *testing.T) { 38 | assert.NotNil(t, adbclient) 39 | d := adbclient.Device(adb.AnyDevice()) 40 | _, err := d.LaunchAppByMonkey("com.EpicLRT.ActionRPGSample") 41 | assert.Nil(t, err) 42 | } 43 | 44 | func TestUnpackActivity(t *testing.T) { 45 | str := ` 46 | topResumedActivity=ActivityRecord{18aea91 u0 com.android.settings/.Settings t84} 47 | ResumedActivity: ActivityRecord{18aea91 u0 com.android.settings/.Settings t84} 48 | ` 49 | l := adb.UnpackActivity([]byte(str)) 50 | assert.Equal(t, len(l), 1) 51 | assert.Equal(t, l[0].Fullname, "com.android.settings/.Settings") 52 | assert.Equal(t, l[0].Package, "com.android.settings") 53 | assert.Equal(t, l[0].Component, ".Settings") 54 | 55 | str = ` 56 | topResumedActivity=ActivityRecord{18aea91 u0 com.android.settings/com.android.settings.Settings t84} 57 | ResumedActivity: ActivityRecord{18aea91 u0 com.android.settings/com.android.settings.Settings t84} 58 | ` 59 | l = adb.UnpackActivity([]byte(str)) 60 | assert.True(t, len(l) == 1) 61 | assert.Equal(t, l[0].Fullname, "com.android.settings/com.android.settings.Settings") 62 | assert.Equal(t, l[0].Package, "com.android.settings") 63 | assert.Equal(t, l[0].Component, "com.android.settings.Settings") 64 | 65 | str = ` 66 | ResumedActivity: ActivityRecord{18aea91 u0 ab/cd t84} 67 | ` 68 | l = adb.UnpackActivity([]byte(str)) 69 | assert.True(t, len(l) == 1) 70 | assert.Equal(t, l[0].Fullname, "ab/cd") 71 | assert.Equal(t, l[0].Package, "ab") 72 | assert.Equal(t, l[0].Component, "cd") 73 | 74 | str = ` 75 | ResumedActivity: ActivityRecord{18aea91 u0 a/b t84} 76 | ResumedActivity: ActivityRecord{18aea91 u0 c/d t84} 77 | ` 78 | l = adb.UnpackActivity([]byte(str)) 79 | assert.True(t, len(l) == 2) 80 | assert.Equal(t, l[0].Fullname, "a/b") 81 | assert.Equal(t, l[0].Package, "a") 82 | assert.Equal(t, l[0].Component, "b") 83 | 84 | assert.Equal(t, l[1].Fullname, "c/d") 85 | assert.Equal(t, l[1].Package, "c") 86 | assert.Equal(t, l[1].Component, "d") 87 | } 88 | -------------------------------------------------------------------------------- /cmd_df.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | ) 9 | 10 | type DfEntry struct { 11 | FileSystem string // v1: mount-point, v2: device node 12 | Size float64 //bytes 13 | Used float64 //bytes 14 | Avail float64 15 | MountedOn string // v1: mount-point, v2: mount-point 16 | } 17 | 18 | // parseDfNumber 0, 0.0K, 956.5M, 2.7G 19 | func parseDfNumber(element string) (float64, error) { 20 | size := len(element) 21 | switch element[size-1] { 22 | case 'K': 23 | value, err := strconv.ParseFloat(string(element[:size-1]), 64) 24 | if err != nil { 25 | return 0, err 26 | } 27 | return value * 1024, nil 28 | case 'M': 29 | value, err := strconv.ParseFloat(string(element[:size-1]), 64) 30 | if err != nil { 31 | return 0, err 32 | } 33 | return value * 1024 * 1024, nil 34 | case 'G': 35 | value, err := strconv.ParseFloat(string(element[:size-1]), 64) 36 | if err != nil { 37 | return 0, err 38 | } 39 | return value * 1024 * 1024 * 1024, nil 40 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 41 | return strconv.ParseFloat(string(element), 64) 42 | default: 43 | return 0, fmt.Errorf("unknown suffix: %s", element) 44 | } 45 | } 46 | 47 | /* 48 | Android 6.0 49 | ----------- 50 | 51 | shell@HWMYA-L6737:/ $ df -h 52 | Filesystem Size Used Free Blksize 53 | -h: No such file or directory 54 | 1|shell@HWMYA-L6737:/ $ 55 | 56 | shell@HWMYA-L6737:/ $ df 57 | Filesystem Size Used Free Blksize 58 | /dev 956.5M 148.0K 956.3M 4096 59 | /sys/fs/cgroup 956.5M 12.0K 956.5M 4096 60 | /mnt 956.5M 0.0K 956.5M 4096 61 | /system 2.9G 2.6G 333.5M 4096 62 | /cache 192.8M 156.0K 192.7M 4096 63 | /protect_f 5.8M 60.0K 5.8M 4096 64 | /protect_s 9.8M 56.0K 9.7M 4096 65 | /nvdata 27.5M 2.0M 25.5M 4096 66 | /cust 93.6M 3.1M 90.5M 4096 67 | /log 59.0M 9.4M 49.6M 4096 68 | /storage 956.5M 0.0K 956.5M 4096 69 | /data 10.9G 6.3G 4.6G 4096 70 | /mnt/runtime/default/emulated: Permission denied 71 | /storage/emulated 10.9G 6.3G 4.6G 4096 72 | /mnt/runtime/read/emulated: Permission denied 73 | /mnt/runtime/write/emulated: Permission denied 74 | */ 75 | var ( 76 | // Filesystem Size Used Free Blksize 77 | // /dev 956.5M 148.0K 956.3M 4096 78 | // /storage 956.5M 0.0K 956.5M 4096 79 | dfV1Regrex = regexp.MustCompile(`(?m)^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+\d+\s*$`) 80 | ) 81 | 82 | // unpackDfV1 83 | func unpackDfV1(resp []byte) (names []DfEntry) { 84 | matches := dfV1Regrex.FindAllSubmatch(resp, -1) 85 | for _, match := range matches { 86 | size, err := parseDfNumber(string(match[2])) 87 | if err != nil { 88 | continue 89 | } 90 | used, err := parseDfNumber(string(match[3])) 91 | if err != nil { 92 | continue 93 | } 94 | avail, err := parseDfNumber(string(match[4])) 95 | if err != nil { 96 | continue 97 | } 98 | 99 | d := DfEntry{ 100 | FileSystem: string(match[1]), 101 | Size: size, 102 | Used: used, 103 | Avail: avail, 104 | MountedOn: string(match[1]), 105 | } 106 | names = append(names, d) 107 | } 108 | return 109 | } 110 | 111 | /* 112 | Android 9 113 | --------- 114 | sagit:/ $ df --help 115 | usage: df [-HPkhi] [-t type] [FILESYSTEM ...] 116 | 117 | The "disk free" command shows total/used/available disk space for 118 | each filesystem listed on the command line, or all currently mounted 119 | filesystems. 120 | 121 | -a Show all (including /proc and friends) 122 | -P The SUSv3 "Pedantic" option 123 | -k Sets units back to 1024 bytes (the default without -P) 124 | -h Human readable output (K=1024) 125 | -H Human readable output (k=1000) 126 | -i Show inodes instead of blocks 127 | -t type Display only filesystems of this type 128 | 129 | Pedantic provides a slightly less useful output format dictated by Posix, 130 | and sets the units to 512 bytes instead of the default 1024 bytes. 131 | 132 | sagit:/ $ df 133 | Filesystem 1K-blocks Used Available Use% Mounted on 134 | rootfs 2828340 6328 2822012 1% / 135 | tmpfs 2912464 804 2911660 1% /dev 136 | tmpfs 2912464 0 2912464 0% /mnt 137 | /dev/block/dm-0 5079888 3378472 1685032 67% /system 138 | none 2912464 0 2912464 0% /sys/fs/cgroup 139 | /dev/block/sda17 115609024 35907960 79553608 32% /data 140 | /dev/block/sde37 12016 10256 1436 88% /system/vendor/dsp 141 | /dev/block/sde42 825240 242940 565916 31% /cust 142 | /dev/block/sda16 124912 1936 120356 2% /cache 143 | /dev/block/sda10 5092 160 4932 4% /dev/logfs 144 | /data/media 115609024 35907960 79553608 32% /storage/emulated 145 | 146 | sagit:/ $ df -h 147 | Filesystem Size Used Avail Use% Mounted on 148 | rootfs 2.6G 6.1M 2.6G 1% / 149 | tmpfs 2.7G 804K 2.7G 1% /dev 150 | tmpfs 2.7G 0 2.7G 0% /mnt 151 | /dev/block/dm-0 4.8G 3.2G 1.6G 67% /system 152 | none 2.7G 0 2.7G 0% /sys/fs/cgroup 153 | /dev/block/sda17 110G 34G 76G 32% /data 154 | /dev/block/sde37 12M 10M 1.4M 88% /system/vendor/dsp 155 | /dev/block/sde42 806M 237M 553M 31% /cust 156 | /dev/block/sda16 122M 1.8M 118M 2% /cache 157 | /dev/block/sda10 4.9M 160K 4.8M 4% /dev/logfs 158 | /data/media 110G 34G 76G 32% /storage/emulated 159 | */ 160 | 161 | var ( 162 | // Filesystem Size Used Avail Use% Mounted on 163 | // /dev/block/sda17 110G 34G 76G 32% /data 164 | dfV2Regrex = regexp.MustCompile(`(?m)^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$`) 165 | ) 166 | 167 | // unpackDfV2 168 | func unpackDfV2(resp []byte) (names []DfEntry) { 169 | matches := dfV2Regrex.FindAllSubmatch(resp, -1) 170 | for _, match := range matches { 171 | size, err := parseDfNumber(string(match[2])) 172 | if err != nil { 173 | continue 174 | } 175 | used, err := parseDfNumber(string(match[3])) 176 | if err != nil { 177 | continue 178 | } 179 | avail, err := parseDfNumber(string(match[4])) 180 | if err != nil { 181 | continue 182 | } 183 | 184 | d := DfEntry{ 185 | FileSystem: string(match[1]), 186 | Size: size, 187 | Used: used, 188 | Avail: avail, 189 | MountedOn: string(match[6]), 190 | } 191 | names = append(names, d) 192 | } 193 | return 194 | } 195 | 196 | // DF adb shell df 197 | // After Android 6+, /storage/emulated and /data are on the same partition, they have same information 198 | // Android 5.x, they may be not same 199 | // please see comments in cmd_df_test.go 200 | // in general, check '/data' in MountedOn, which is supported on Android 5.x ~ Android14 201 | func (d *Device) DF() (list []DfEntry, err error) { 202 | // detect wether support df -h or not 203 | resp, err := d.RunCommand("df", "-h") 204 | if err != nil { 205 | return 206 | } 207 | 208 | // if received too few bytes, means 'df -h' is not supported 209 | if len(resp) < 128 { 210 | // <= Android 6.x 211 | resp, err = d.RunCommand("df") 212 | if err != nil { 213 | return 214 | } 215 | list = unpackDfV1(resp) 216 | } else { 217 | list = unpackDfV2(resp) 218 | } 219 | 220 | if len(list) == 0 { 221 | return nil, errors.New(string(resp)) 222 | } 223 | return 224 | } 225 | 226 | // 使用最大分区作为磁盘大小的近似 227 | func (d *Device) GetDiskSize() (sizeInBytes uint64, err error) { 228 | list, err := d.DF() 229 | if err != nil { 230 | return 231 | } 232 | 233 | for _, l := range list { 234 | if uint64(l.Size) > sizeInBytes { 235 | sizeInBytes = uint64(l.Size) 236 | } 237 | } 238 | return 239 | } 240 | -------------------------------------------------------------------------------- /cmd_linuxcmd.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // The `/proc/uptime` file contains two values that represent the system's uptime and idle time (in seconds) since it started. 17 | // In the output you provided, `52784.18 409860.90`: 18 | // - The first value `52784.18` indicates that the system has been running for 52784.18 seconds (approximately 14.66 hours). 19 | // - The second value `409860.90` indicates that the system's idle time during this period is 409860.90 seconds (approximately 113.85 hours). 20 | // It's worth noting that the idle time may be greater than the actual uptime because each core of a multi-core processor calculates idle time. 21 | // For example, a dual-core processor being idle for 1 second will count as 2 seconds of idle time. 22 | func parseUptime(resp []byte) (uptime float64, err error) { 23 | list := bytes.Fields(resp) 24 | if len(list) != 2 { 25 | err = fmt.Errorf("invalid uptime:%s", resp) 26 | return 27 | } 28 | return strconv.ParseFloat(string(list[0]), 64) 29 | } 30 | 31 | func (d *Device) Uptime() (uptime float64, err error) { 32 | // detect wether support df -h or not 33 | resp, err := d.RunCommand("cat", "/proc/uptime") 34 | if err != nil { 35 | return 36 | } 37 | return parseUptime(resp) 38 | } 39 | 40 | type LinuxVersion struct { 41 | Version string 42 | Built time.Time 43 | Raw []byte 44 | } 45 | 46 | var ( 47 | kernelRegrex = regexp.MustCompile(`\d+\.\d+\.\d+`) 48 | ) 49 | 50 | func parseUname(resp []byte) (info LinuxVersion, err error) { 51 | version := kernelRegrex.Find(resp) 52 | if version == nil { 53 | err = fmt.Errorf("version not found") 54 | return 55 | } 56 | 57 | sep := []byte("SMP PREEMPT") 58 | sepIndex := bytes.Index(resp, sep) 59 | if sepIndex < 0 { 60 | err = fmt.Errorf("%s not found", sep) 61 | return 62 | } 63 | 64 | const layout = "Mon Jan 2 15:04:05 MST 2006" 65 | time, err := time.Parse(layout, string(bytes.TrimSpace(resp[sepIndex+len(sep):]))) 66 | if err != nil { 67 | return 68 | } 69 | info.Version = string(version) 70 | info.Built = time 71 | info.Raw = resp 72 | return 73 | } 74 | 75 | func (d *Device) Uname() (version LinuxVersion, err error) { 76 | // detect wether support df -h or not 77 | resp, err := d.RunCommand("cat", "/proc/version") 78 | if err != nil { 79 | return 80 | } 81 | return parseUname(resp) 82 | } 83 | 84 | type GpuInfo struct { 85 | Vendor string 86 | Model string 87 | OpenGLVersion string 88 | } 89 | 90 | var ( 91 | // "GLES: Qualcomm, Adreno (TM) 618, OpenGL ES 3.2 V@415.0 (GIT@663be55, I724753c5e3, 1573037262) (Date:11/06/19)" 92 | // "GLES: ARM, Mali-G78, OpenGL ES 3.2 v1.r34p0-01eac0.a1b116bd871d46ef040e8feef9ed691e" 93 | gpuRegrex = regexp.MustCompile(`GLES:\s*(\w+),\s*([^,]+),\s*(OpenGL ES [0-9.]+)`) 94 | ) 95 | 96 | func parseGpu(resp []byte) (info GpuInfo, err error) { 97 | match := gpuRegrex.FindSubmatch(resp) 98 | if len(match) == 0 { 99 | err = fmt.Errorf("can't found GLES: %s", resp) 100 | return 101 | } 102 | 103 | info.Vendor = string(match[1]) 104 | info.Model = string(match[2]) 105 | info.OpenGLVersion = string(match[3]) 106 | return 107 | } 108 | 109 | func (d *Device) GetGpuAndOpenGL() (des GpuInfo, err error) { 110 | glstr, err := d.RunCommand("dumpsys SurfaceFlinger | grep GLES") 111 | if err != nil { 112 | return 113 | } 114 | 115 | return parseGpu(glstr) 116 | } 117 | 118 | var ( 119 | etherRegex = regexp.MustCompile(`\s*link/ether\s+(\S+)`) 120 | inetRegex = regexp.MustCompile(`\s*(inet6?)\s+(\S+)`) 121 | ) 122 | 123 | type EtherInfo struct { 124 | Name string 125 | LinkAddr string // mac 126 | Ipv4 []byte 127 | Ipv6 []byte 128 | } 129 | 130 | func (e EtherInfo) String() string { 131 | var a strings.Builder 132 | a.Write([]byte(e.Name)) 133 | a.WriteString(" link/addr=" + e.LinkAddr) 134 | if e.Ipv4 != nil { 135 | a.WriteString(" inet=" + string(e.Ipv4)) 136 | } 137 | if e.Ipv6 != nil { 138 | a.WriteString(" inet6=" + string(e.Ipv6)) 139 | } 140 | return a.String() 141 | } 142 | 143 | func parseIpAddressWlan0(resp []byte) (info EtherInfo, err error) { 144 | match := etherRegex.FindSubmatch(resp) 145 | if len(match) == 0 { 146 | err = fmt.Errorf("no linkaddr found") 147 | return 148 | } 149 | info.LinkAddr = string(match[1]) 150 | 151 | matches := inetRegex.FindAllSubmatch(resp, -1) 152 | if len(matches) == 0 { 153 | return 154 | } 155 | for _, match := range matches { 156 | if string(match[1]) == "inet" { 157 | info.Ipv4 = bytes.Clone(match[2]) 158 | } else if string(match[1]) == "inet6" { 159 | info.Ipv6 = bytes.Clone(match[2]) 160 | } 161 | } 162 | return 163 | } 164 | 165 | // GetWlanInfo adb shell ip address show wlan0 166 | func (d *Device) GetWlanInfo() (info EtherInfo, err error) { 167 | resp, err := d.RunCommand("ip address show wlan0") 168 | if err != nil { 169 | return 170 | } 171 | 172 | info, err = parseIpAddressWlan0(resp) 173 | info.Name = "wlan0" 174 | return 175 | } 176 | 177 | var ( 178 | meminfoRegex = regexp.MustCompile(`(?m)\s*(\S+)\:\s*(\d+)\s*kB\s*$`) 179 | ) 180 | 181 | // in kB 182 | func parseMemoryInfo(resp []byte) (info map[string]uint64, err error) { 183 | matches := meminfoRegex.FindAllSubmatch(resp, -1) 184 | if len(matches) == 0 { 185 | return 186 | } 187 | 188 | info = make(map[string]uint64) 189 | for _, match := range matches { 190 | v, err := strconv.ParseInt(string(match[2]), 0, 64) 191 | if err != nil { 192 | continue 193 | } 194 | info[string(match[1])] = uint64(v) 195 | } 196 | return 197 | } 198 | 199 | // GetMemoryTotal 200 | func (d *Device) GetMemoryTotal() (totalInKb uint64, err error) { 201 | resp, err := d.RunCommand("cat /proc/meminfo") 202 | if err != nil { 203 | return 204 | } 205 | 206 | info, err := parseMemoryInfo(resp) 207 | if err != nil { 208 | return 209 | } 210 | totalInKb, ok := info["MemTotal"] 211 | if !ok { 212 | err = fmt.Errorf("no MemTotal found") 213 | return 214 | } 215 | return 216 | } 217 | 218 | type Screen struct { 219 | Width int 220 | Height int 221 | } 222 | 223 | type DisplaySizeInfo struct { 224 | Physical Screen 225 | Override Screen 226 | } 227 | 228 | var ( 229 | rectRegex = regexp.MustCompile(`(\d+)x(\d+)`) 230 | ) 231 | 232 | // HWALP:/ $ wm size 233 | // Physical size: 1440x2560 234 | // Override size: 720x1280 235 | func parseDisplaySize(resp []byte) (display DisplaySizeInfo, err error) { 236 | lines := bytes.Split(resp, []byte("\n")) 237 | 238 | var found bool 239 | for _, line := range lines { 240 | matches := rectRegex.FindSubmatch(line) 241 | if len(matches) == 0 { 242 | continue 243 | } 244 | width, _ := strconv.ParseInt(string(matches[1]), 0, 32) 245 | Height, _ := strconv.ParseInt(string(matches[2]), 0, 32) 246 | if bytes.Contains(line, []byte("Physical")) { 247 | display.Physical.Width = int(width) 248 | display.Physical.Height = int(Height) 249 | found = true 250 | } else if bytes.Contains(line, []byte("Override")) { 251 | display.Override.Width = int(width) 252 | display.Override.Height = int(Height) 253 | found = true 254 | } 255 | } 256 | 257 | if !found { 258 | err = fmt.Errorf("parse failed") 259 | } 260 | return 261 | } 262 | 263 | // GetDisplayDefault wm size 264 | func (d *Device) GetDefaultDisplaySize() (display DisplaySizeInfo, err error) { 265 | resp, err := d.RunCommand("wm size") 266 | if err != nil { 267 | return 268 | } 269 | return parseDisplaySize(resp) 270 | } 271 | 272 | type CpuInfo struct { 273 | // Name is the product name of this CPU. 274 | Name string 275 | // Vendor is the vendor of this CPU. 276 | Vendor string 277 | // Architecture is the architecture that this CPU implements. 278 | Architecture string 279 | // Cores is the number of cores in this CPU. 280 | Cores uint32 281 | Frequency float64 282 | } 283 | 284 | var ( 285 | cpuInfoRegex = regexp.MustCompile(`(?m)(\w+\s*\w+)\s*:\s*(\S+)\s*$`) 286 | ) 287 | 288 | type CpuInfoProp struct { 289 | Key string 290 | Value string 291 | } 292 | 293 | func parseCpuInfo(resp []byte) (cpuInfo []CpuInfoProp, err error) { 294 | matches := cpuInfoRegex.FindAllSubmatch(resp, -1) 295 | if len(matches) == 0 { 296 | err = fmt.Errorf("invalid data") 297 | return 298 | } 299 | 300 | for _, match := range matches { 301 | // fmt.Printf("[%s] : [%s]\n", match[1], match[2]) 302 | cpuInfo = append(cpuInfo, CpuInfoProp{Key: string(match[1]), Value: string(match[2])}) 303 | } 304 | return 305 | } 306 | 307 | // GetCpuInfo get cpu information 308 | func (d *Device) GetCpuInfo() (cpuInfo CpuInfo, err error) { 309 | resp, err := d.RunCommand("cat /proc/cpuinfo") 310 | if err != nil { 311 | return 312 | } 313 | infos, err := parseCpuInfo(resp) 314 | if err != nil { 315 | return 316 | } 317 | for _, kv := range infos { 318 | switch kv.Key { 319 | case "Hardware": // optional 320 | cpuInfo.Name = kv.Value 321 | case "processor": 322 | number, _ := strconv.Atoi(kv.Value) 323 | cpuInfo.Cores = uint32(number) + 1 324 | case "CPU architecture": 325 | arch, _ := strconv.Atoi(kv.Value) 326 | if arch >= 8 { 327 | cpuInfo.Architecture = "arm64" 328 | } 329 | } 330 | } 331 | 332 | // get cores 333 | // coreInfo, err := device.RunCommand("ls", "/sys/devices/system/cpu/") 334 | 335 | // get frequency 336 | freqInfo, err := d.RunCommand("cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq") 337 | if err != nil { 338 | return 339 | } 340 | freqTotal, _ := strconv.ParseUint(string(bytes.TrimSpace(freqInfo)), 10, 32) 341 | freq := float64(freqTotal/100000) / 10.0 342 | cpuInfo.Frequency = freq 343 | return 344 | } 345 | 346 | // Reboot the device 347 | func (d *Device) Reboot(ctx context.Context, waitToBootCompleted bool) error { 348 | _, err := d.RunCommand("reboot") 349 | if errors.Is(err, os.ErrDeadlineExceeded) { 350 | // pass 351 | // err is "read tcp 127.0.0.1:65357->127.0.0.1:5037: i/o timeout" 352 | } else if err == io.EOF { 353 | // pass 354 | } else if err != nil { 355 | return fmt.Errorf("reboot failed: %w", err) 356 | } 357 | 358 | // make sure adb disconnected 359 | ctx1, cancel := context.WithTimeout(ctx, time.Second*30) 360 | defer cancel() 361 | for { 362 | if state, _ := d.State(); state == StateInvalid || state == StateOffline { 363 | break 364 | } 365 | 366 | select { 367 | case <-ctx1.Done(): 368 | return fmt.Errorf("reboot check disconnected failed: %w", ctx1.Err()) 369 | case <-time.After(2 * time.Second): 370 | } 371 | } 372 | 373 | if !waitToBootCompleted { 374 | return nil 375 | } 376 | 377 | // wait to boot complete 378 | ctx2, cancel := context.WithTimeout(ctx, time.Second*90) 379 | defer cancel() 380 | for { 381 | if state, _ := d.State(); state == StateOnline { 382 | if booted, _ := d.BootCompleted(); booted { 383 | return nil 384 | } 385 | } 386 | 387 | select { 388 | case <-ctx2.Done(): 389 | return fmt.Errorf("reboot check booted failed: %w", ctx2.Err()) 390 | case <-time.After(2 * time.Second): 391 | } 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /cmd_linuxcmd_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_parseUptime(t *testing.T) { 14 | upttimestr := "73681.99 69586.45" 15 | uptime, err := parseUptime([]byte(upttimestr)) 16 | assert.Nil(t, err) 17 | assert.Equal(t, uptime, float64(73681.99)) 18 | 19 | upttimestr = "\x0D\x0A73681.99 69586.45\x0D\x0A" 20 | uptime, err = parseUptime([]byte(upttimestr)) 21 | assert.Nil(t, err) 22 | assert.Equal(t, uptime, float64(73681.99)) 23 | 24 | upttimestr = "\x0D\x0A73681.99 69586.45\x0D\x0A" 25 | uptime, err = parseUptime([]byte(upttimestr)) 26 | assert.Nil(t, err) 27 | assert.Equal(t, uptime, float64(73681.99)) 28 | } 29 | 30 | func TestDevice_Uptime(t *testing.T) { 31 | assert.NotNil(t, adbclient) 32 | d := adbclient.Device(AnyDevice()) 33 | uptime, err := d.Uptime() 34 | assert.Nil(t, err) 35 | fmt.Println(uptime / 3600) 36 | } 37 | 38 | func Test_parseUname(t *testing.T) { 39 | // xiaomi 6, Android 9 40 | versionStr := "Linux version 4.4.153-perf+ (builder@c3-miui-ota-bd114.bj) (gcc version 4.9.x 20150123 (prerelease) (GCC) ) #1 SMP PREEMPT Thu Mar 5 11:28:37 CST 2020" 41 | info, err := parseUname([]byte(versionStr)) 42 | assert.Nil(t, err) 43 | assert.Equal(t, info.Version, "4.4.153") 44 | built, _ := time.Parse("Mon Jan 2 15:04:05 MST 2006", "Thu Mar 5 11:28:37 CST 2020") 45 | assert.Equal(t, info.Built, built) 46 | 47 | // huawei hormonyOS 2.0 48 | versionStr = "Linux version 4.14.116 (HarmonyOS@localhost) (Android (5484270 based on r353983c) clang version 9.0.3 (https://android.googlesource.com/toolchain/clang 745b335211bb9eadfa6aa6301f84715cee4b37c5) (https://android.googlesource.com/toolchain/llvm 60cf23e54e46c807513f7a36d0a7b777920b5881) (based on LLVM 9.0.3svn)) #1 SMP PREEMPT Tue Mar 22 17:09:22 CST 2022" 49 | info, err = parseUname([]byte(versionStr)) 50 | assert.Nil(t, err) 51 | assert.Equal(t, info.Version, "4.14.116") 52 | built, _ = time.Parse("Mon Jan 2 15:04:05 MST 2006", "Tue Mar 22 17:09:22 CST 2022") 53 | assert.Equal(t, info.Built, built) 54 | 55 | // a59, android5.1 56 | versionStr = "\x0d\x0aLinux version 3.10.72+ (root@ubuntu-121-147) (gcc version 4.9 20140514 (mtk-20150408) (GCC) ) #1 SMP PREEMPT Wed Dec 18 20:06:03 CST 2019\x0d\x0a" 57 | info, err = parseUname([]byte(versionStr)) 58 | assert.Nil(t, err) 59 | assert.Equal(t, info.Version, "3.10.72") 60 | built, _ = time.Parse("Mon Jan 2 15:04:05 MST 2006", "Wed Dec 18 20:06:03 CST 2019") 61 | assert.Equal(t, info.Built, built) 62 | } 63 | 64 | func TestDevice_Uname(t *testing.T) { 65 | assert.NotNil(t, adbclient) 66 | d := adbclient.Device(AnyDevice()) 67 | info, err := d.Uname() 68 | assert.Nil(t, err) 69 | fmt.Println(info) 70 | } 71 | 72 | func Test_parseGpu(t *testing.T) { 73 | gpuStr := "GLES: Qualcomm, Adreno (TM) 618, OpenGL ES 3.2 V@415.0 (GIT@663be55, I724753c5e3, 1573037262) (Date:11/06/19)" 74 | info, err := parseGpu([]byte(gpuStr)) 75 | assert.Nil(t, err) 76 | assert.Equal(t, info, GpuInfo{ 77 | Vendor: "Qualcomm", 78 | Model: "Adreno (TM) 618", 79 | OpenGLVersion: "OpenGL ES 3.2", 80 | }) 81 | 82 | gpuStr = "GLES: ARM, Mali-G78, OpenGL ES 3.2 v1.r34p0-01eac0.a1b116bd871d46ef040e8feef9ed691e" 83 | info, err = parseGpu([]byte(gpuStr)) 84 | assert.Nil(t, err) 85 | assert.Equal(t, info, GpuInfo{ 86 | Vendor: "ARM", 87 | Model: "Mali-G78", 88 | OpenGLVersion: "OpenGL ES 3.2", 89 | }) 90 | 91 | gpuStr = `------------RE GLES------------ 92 | GLES: Qualcomm, Adreno (TM) 750, OpenGL ES 3.2 V@0762.10 (GIT@1394a2c7a8, Id12349e41b, 1708672982) (Date:02/23/24) 93 | ` 94 | info, err = parseGpu([]byte(gpuStr)) 95 | assert.Nil(t, err) 96 | assert.Equal(t, info, GpuInfo{ 97 | Vendor: "Qualcomm", 98 | Model: "Adreno (TM) 750", 99 | OpenGLVersion: "OpenGL ES 3.2", 100 | }) 101 | } 102 | 103 | func Test_parseIpAddressWlan0(t *testing.T) { 104 | // shell@A33:/ $ 105 | output := []byte(` 106 | 29: wlan0: mtu 1500 qdisc mq state DOWN qlen 1000 107 | link/ether 2c:5b:b8:e5:d6:12 brd ff:ff:ff:ff:ff:ff 108 | `) 109 | m, _ := parseIpAddressWlan0(output) 110 | assert.Equal(t, m.LinkAddr, "2c:5b:b8:e5:d6:12") 111 | assert.True(t, m.Ipv4 == nil) 112 | assert.True(t, m.Ipv6 == nil) 113 | 114 | // Android 14 115 | output = []byte(` 116 | 24: wlan0: mtu 1500 qdisc htb state UP group default qlen 3000 117 | link/ether 62:7b:0f:61:b2:d6 brd ff:ff:ff:ff:ff:ff 118 | inet 192.168.31.222/24 brd 192.168.31.255 scope global wlan0 119 | valid_lft forever preferred_lft forever 120 | inet6 fe80::607b:fff:fe61:b2d6/64 scope link 121 | valid_lft forever preferred_lft forever`) 122 | m, _ = parseIpAddressWlan0(output) 123 | assert.Equal(t, m.LinkAddr, "62:7b:0f:61:b2:d6") 124 | assert.Equal(t, m.Ipv4, []byte("192.168.31.222/24")) 125 | assert.Equal(t, m.Ipv6, []byte("fe80::607b:fff:fe61:b2d6/64")) 126 | } 127 | 128 | func TestDevice_GetWlanInfo(t *testing.T) { 129 | assert.NotNil(t, adbclient) 130 | d := adbclient.Device(AnyDevice()) 131 | info, err := d.GetWlanInfo() 132 | assert.Nil(t, err) 133 | fmt.Println(info) 134 | } 135 | 136 | func Test_parseMemoryInfo(t *testing.T) { 137 | output := []byte(` 138 | MemTotal: 23794344 kB 139 | MemFree: 430780 kB 140 | MemAvailable: 17117864 kB 141 | Buffers: 8628 kB 142 | Cached: 12901756 kB 143 | SwapCached: 27376 kB 144 | SecPageTables: 0 kB 145 | NFS_Unstable: 0 kB 146 | Bounce: 0 kB 147 | WritebackTmp: 0 kB 148 | `) 149 | 150 | info, err := parseMemoryInfo(output) 151 | assert.Nil(t, err) 152 | assert.Equal(t, len(info), 10) 153 | for k, v := range info { 154 | fmt.Printf("%-16s %9d kB\n", k, v) 155 | } 156 | 157 | assert.Equal(t, info["MemTotal"], uint64(23794344)) 158 | assert.Equal(t, info["MemFree"], uint64(430780)) 159 | assert.Equal(t, info["MemAvailable"], uint64(17117864)) 160 | } 161 | 162 | func Test_parseMemoryInfoFile(t *testing.T) { 163 | output, err := os.ReadFile("meminfo.txt") 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | 168 | info, err := parseMemoryInfo(output) 169 | assert.Nil(t, err) 170 | assert.Equal(t, len(info), 34) 171 | for k, v := range info { 172 | fmt.Printf("%-16s %9d kB\n", k, v) 173 | } 174 | 175 | assert.Equal(t, info["MemTotal"], uint64(3681628)) 176 | assert.Equal(t, info["MemFree"], uint64(273652)) 177 | assert.Equal(t, info["MemAvailable"], uint64(2243756)) 178 | } 179 | 180 | func Test_parseDisplaySize(t *testing.T) { 181 | output := []byte(` 182 | Physical size: 1440x2560 183 | Override size: 720x1280 184 | `) 185 | info, err := parseDisplaySize(output) 186 | assert.Nil(t, err) 187 | assert.Equal(t, info.Physical.Width, 1440) 188 | assert.Equal(t, info.Physical.Height, 2560) 189 | assert.Equal(t, info.Override.Width, 720) 190 | assert.Equal(t, info.Override.Height, 1280) 191 | 192 | output = []byte("Physical size: 1440x2560\r\nOverride size: 720x1280\r\n\r\n") 193 | info, err = parseDisplaySize(output) 194 | assert.Nil(t, err) 195 | assert.Equal(t, info.Physical.Width, 1440) 196 | assert.Equal(t, info.Physical.Height, 2560) 197 | assert.Equal(t, info.Override.Width, 720) 198 | assert.Equal(t, info.Override.Height, 1280) 199 | } 200 | 201 | func Test_parseCpuInfo(t *testing.T) { 202 | output := []byte(` 203 | Processor : AArch64 Processor rev 2 (aarch64) 204 | processor : 0 205 | BogoMIPS : 3.84 206 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid 207 | CPU implementer : 0x41 208 | CPU architecture: 8 209 | CPU variant : 0x0 210 | CPU part : 0xd03 211 | CPU revision : 4 212 | 213 | processor : 1 214 | BogoMIPS : 3.84 215 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid 216 | CPU implementer : 0x41 217 | CPU architecture: 8 218 | CPU variant : 0x0 219 | CPU part : 0xd03 220 | CPU revision : 4 221 | 222 | processor : 2 223 | BogoMIPS : 3.84 224 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid 225 | CPU implementer : 0x41 226 | CPU architecture: 8 227 | CPU variant : 0x0 228 | CPU part : 0xd03 229 | CPU revision : 4 230 | 231 | processor : 3 232 | BogoMIPS : 3.84 233 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid 234 | CPU implementer : 0x41 235 | CPU architecture: 8 236 | CPU variant : 0x0 237 | CPU part : 0xd03 238 | CPU revision : 4 239 | 240 | processor : 4 241 | BogoMIPS : 3.84 242 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid 243 | CPU implementer : 0x41 244 | CPU architecture: 8 245 | CPU variant : 0x0 246 | CPU part : 0xd09 247 | CPU revision : 2 248 | 249 | processor : 5 250 | BogoMIPS : 3.84 251 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid 252 | CPU implementer : 0x41 253 | CPU architecture: 8 254 | CPU variant : 0x0 255 | CPU part : 0xd09 256 | CPU revision : 2 257 | 258 | processor : 6 259 | BogoMIPS : 3.84 260 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid 261 | CPU implementer : 0x41 262 | CPU architecture: 8 263 | CPU variant : 0x0 264 | CPU part : 0xd09 265 | CPU revision : 2 266 | 267 | processor : 7 268 | BogoMIPS : 3.84 269 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid 270 | CPU implementer : 0x41 271 | CPU architecture: 8 272 | CPU variant : 0x0 273 | CPU part : 0xd09 274 | CPU revision : 2 275 | 276 | Hardware : Hisilicon Kirin970 277 | `) 278 | info, err := parseCpuInfo(output) 279 | _ = info 280 | _ = err 281 | } 282 | 283 | func Test_parseCpuInfoFile(t *testing.T) { 284 | output, err := os.ReadFile("cpuinfo.txt") 285 | if err != nil { 286 | t.Fatal(err) 287 | } 288 | 289 | info, err := parseCpuInfo(output) 290 | assert.Nil(t, err) 291 | fmt.Printf("info %#v\n", info) 292 | } 293 | 294 | func TestDevice_Reboot(t *testing.T) { 295 | assert.NotNil(t, adbclient) 296 | d := adbclient.Device(AnyDevice()) 297 | err := d.Reboot(context.TODO(), false) 298 | assert.Nil(t, err) 299 | 300 | } 301 | 302 | func TestDevice_Reboot2(t *testing.T) { 303 | lists, err := adbclient.ListDeviceSerials() 304 | assert.Nil(t, err) 305 | assert.Greater(t, len(lists), 0) 306 | 307 | d2 := adbclient.Device(DeviceWithSerial(lists[0])) 308 | err = d2.Reboot(context.TODO(), true) 309 | assert.Nil(t, err) 310 | } 311 | -------------------------------------------------------------------------------- /cmd_pm.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | ErrSecurityException = errors.New("JavaSecurityException") 13 | ) 14 | 15 | // PmListPackages adb shell pm 16 | // list packages [-f] [-d] [-e] [-s] [-3] [-i] [-l] [-u] [-U] 17 | // 18 | // [--show-versioncode] [--apex-only] [--factory-only] 19 | // [--uid UID] [--user USER_ID] [FILTER] 20 | // Prints all packages; optionally only those whose name contains 21 | // the text in FILTER. Options are: 22 | // -f: see their associated file 23 | // -a: all known packages (but excluding APEXes) 24 | // -d: filter to only show disabled packages 25 | // -e: filter to only show enabled packages 26 | // -s: filter to only show system packages 27 | // -3: filter to only show third party packages 28 | // -i: see the installer for the packages 29 | // -l: ignored (used for compatibility with older releases) 30 | // -U: also show the package UID 31 | // -u: also include uninstalled packages 32 | // --show-versioncode: also show the version code 33 | // --apex-only: only show APEX packages 34 | // --factory-only: only show system packages excluding updates 35 | // --uid UID: filter to only show packages with the given UID 36 | // --user USER_ID: only list packages belonging to the given user 37 | // --match-libraries: include packages that declare static shared and SDK libraries 38 | func (d *Device) PmListPackages(thirdParty bool) (names []string, err error) { 39 | args := []string{"list", "packages"} 40 | if thirdParty { 41 | args = append(args, "-3") 42 | } 43 | 44 | list, err := d.RunCommandTimeout(d.CmdTimeoutLong, "pm", args...) 45 | if err != nil { 46 | return nil, fmt.Errorf("pm "+strings.Join(args, " ")+": %w", err) 47 | } 48 | 49 | lines := bytes.Split(list, []byte("\n")) 50 | for _, line := range lines { 51 | pos := bytes.Index(line, []byte("package:")) 52 | if pos >= 0 { 53 | l := bytes.TrimSpace(line[pos+8:]) // cut `package:` 54 | names = append(names, string(l)) 55 | } 56 | } 57 | return 58 | } 59 | 60 | // PmClear clear app 61 | // Android 5.1 62 | // shell:pm clear 63 | // 00000000 53 75 63 63 65 73 73 0d 0a |Success..| 64 | func (d *Device) PmClear(packageName string) (err error) { 65 | resp, err := d.RunCommandTimeout(d.CmdTimeoutLong, "pm clear "+packageName) 66 | if err != nil { 67 | return err // always tcp error 68 | } 69 | 70 | resp = bytes.TrimSpace(resp) 71 | // err maybe nil, check response to determine error 72 | if bytes.Equal(resp, []byte("Success")) { 73 | return nil 74 | } 75 | 76 | err = errors.New(string(resp)) 77 | if bytes.Contains(resp, []byte("does not have permission android.permission.CLEAR_APP_USER_DATA to clear data of package")) { 78 | // https://blog.csdn.net/shandong_chu/article/details/105144785 79 | // 关闭开发者选项中“权限监控”可消除此错误 80 | return fmt.Errorf("%w: %w", ErrSecurityException, err) 81 | } 82 | return 83 | } 84 | 85 | // PmUninstall uninstall package 86 | // HWALP:/ $ pm uninstall com.tencent.wetest.demo 87 | // Success 88 | // HWALP:/ $ pm uninstall non-existed-app 89 | // Failure [DELETE_FAILED_INTERNAL_ERROR] 90 | func (d *Device) PmUninstall(packageName string) (err error) { 91 | resp, err := d.RunCommandTimeout(d.CmdTimeoutLong, "pm uninstall "+packageName) 92 | if err != nil { 93 | return err // always tcp error 94 | } 95 | 96 | resp = bytes.TrimSpace(resp) 97 | // err maybe nil, check response to determine error 98 | if bytes.Equal(resp, []byte("Success")) { 99 | return nil 100 | } else if bytes.Contains(resp, []byte("Failure")) { 101 | return errors.New(string(resp)) 102 | } 103 | return errors.New("unknown error: " + string(resp)) 104 | } 105 | 106 | // Android 12 / Harmony OS 4 107 | // 108 | // HWNOH:/data/local/tmp $ pm install multi-touch.apk 109 | // Success 110 | // 111 | // HWNOH:/sdcard $ pm install multi-touch.apk 112 | // avc: denied { read } for scontext=u:r:system_server:s0 tcontext=u:object_r:fuse:s0 tclass=file permissive=0 113 | // System server has no access to read file context u:object_r:fuse:s0 (from path /storage/emulated/0/multi-touch.apk, context u:r:system_server:s0) 114 | // Error: Unable to open file: multi-touch.apk 115 | // Consider using a file under /data/local/tmp/ 116 | // Error: Can't open file: multi-touch.apk 117 | // Exception occurred while executing 'install': 118 | // java.lang.IllegalArgumentException: Error: Can't open file: multi-touch.apk 119 | // at com.android.server.pm.PackageManagerShellCommand.setParamsSize(PackageManagerShellCommand.java:604) 120 | // at com.android.server.pm.PackageManagerShellCommand.doRunInstall(PackageManagerShellCommand.java:1427) 121 | // at com.android.server.pm.PackageManagerShellCommand.runInstall(PackageManagerShellCommand.java:1393) 122 | // at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:209) 123 | // at com.android.modules.utils.BasicShellCommandHandler.exec(BasicShellCommandHandler.java:97) 124 | // at android.os.ShellCommand.exec(ShellCommand.java:38) 125 | // at com.android.server.pm.PackageManagerService.onShellCommand(PackageManagerService.java:29176) 126 | // at android.os.Binder.shellCommand(Binder.java:962) 127 | // at android.os.Binder.onTransact(Binder.java:846) 128 | // at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:5090) 129 | // at com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:10014) 130 | // at com.android.server.pm.HwPackageManagerService.onTransact(HwPackageManagerService.java:527) 131 | // at android.os.Binder.execTransactInternal(Binder.java:1197) 132 | // at android.os.Binder.execTransact(Binder.java:1156) 133 | // 255|HWNOH:/sdcard $ 134 | 135 | func (d *Device) PmInstall(ctx context.Context, apkPath string, reinstall bool, grantPermission bool, 136 | allowDowngrade bool) error { 137 | var args string 138 | if reinstall { 139 | args += "-r " 140 | } 141 | if grantPermission { 142 | args += "-g " 143 | } 144 | if allowDowngrade { 145 | args += "-d " 146 | } 147 | 148 | resp, err := d.RunCommandOutputCtx(ctx, "pm install "+args+apkPath) 149 | if err != nil { 150 | return fmt.Errorf("'pm install %s' failed: %w", args+apkPath, err) 151 | } 152 | 153 | resp = bytes.TrimSpace(resp) 154 | // err maybe nil, check response to determine error 155 | if bytes.Equal(resp, []byte("Success")) { 156 | return nil 157 | } 158 | return errors.New(string(resp)) 159 | } 160 | -------------------------------------------------------------------------------- /cmd_pm_test.go: -------------------------------------------------------------------------------- 1 | package adb_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | adb "github.com/prife/goadb" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var ( 14 | adbclient, _ = adb.NewWithConfig(adb.ServerConfig{}) 15 | ) 16 | 17 | func TestDevice_PmListPackages(t *testing.T) { 18 | assert.NotNil(t, adbclient) 19 | d := adbclient.Device(adb.AnyDevice()) 20 | list, err := d.PmListPackages(true) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | for _, name := range list { 26 | fmt.Println(name) 27 | } 28 | } 29 | 30 | // TestDevice_ClearPackageData 31 | /* 32 | Exception occurred while executing 'clear': 33 | java.lang.SecurityException: PID 6593 does not have permission android.permission.CLEAR_APP_USER_DATA to clear data of package com.heytap.smarthome 34 | at com.android.server.am.ActivityManagerService.clearApplicationUserData(ActivityManagerService.java:3986) 35 | at com.android.server.pm.PackageManagerShellCommand.runClear(PackageManagerShellCommand.java:2473) 36 | at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:277) 37 | at com.android.modules.utils.BasicShellCommandHandler.exec(BasicShellCommandHandler.java:97) 38 | at android.os.ShellCommand.exec(ShellCommand.java:38) 39 | at com.android.server.pm.PackageManagerService$IPackageManagerImpl.onShellCommand(PackageManagerService.java:6823) 40 | at android.os.Binder.shellCommand(Binder.java:1092) 41 | at android.os.Binder.onTransact(Binder.java:912) 42 | at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:4352) 43 | at com.android.server.pm.PackageManagerService$IPackageManagerImpl.onTransact(PackageManagerService.java:6807) 44 | at android.os.Binder.execTransactInternal(Binder.java:1392) 45 | at android.os.Binder.execTransact(Binder.java:1299) 46 | */ 47 | func TestDevice_PmClear(t *testing.T) { 48 | assert.NotNil(t, adbclient) 49 | d := adbclient.Device(adb.AnyDevice()) 50 | list, err := d.PmListPackages(true) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | assert.True(t, len(list) > 0) 55 | 56 | packageName := list[0] 57 | fmt.Println("pm clear", packageName) 58 | 59 | err = d.PmClear(list[0]) 60 | if err != nil { 61 | assert.ErrorIs(t, err, adb.ErrSecurityException) 62 | fmt.Println(err) 63 | } 64 | } 65 | 66 | func TestDevice_PmUninstall(t *testing.T) { 67 | assert.NotNil(t, adbclient) 68 | d := adbclient.Device(adb.AnyDevice()) 69 | err := d.PmUninstall("non-existed-app") 70 | assert.True(t, strings.Contains(err.Error(), "DELETE_FAILED_INTERNAL_ERROR")) 71 | 72 | err = d.PmUninstall("com.tencent.wetestdemo") 73 | assert.Nil(t, err) 74 | } 75 | 76 | func TestDevice_PmInstall(t *testing.T) { 77 | assert.NotNil(t, adbclient) 78 | d := adbclient.Device(adb.AnyDevice()) 79 | err := d.PmInstall(context.TODO(), "/data/local/tmp/WeTestDemo.apk", true, true, true) 80 | assert.Nil(t, err) 81 | } 82 | -------------------------------------------------------------------------------- /cmd_process.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // Android 14: adb shell,v2:ps == adb shell ps -A 13 | // $ adb shell ps | head -n 5 14 | // USER PID PPID VSZ RSS WCHAN ADDR S NAME 15 | // root 8350 1428 6065932 108244 0 0 S com.oplus.ndsf 16 | // root 1 0 2334848 15576 0 0 S init 17 | // root 2 0 0 0 0 0 S [kthreadd] 18 | // root 3 2 0 0 0 0 I [rcu_gp] 19 | // 20 | // Android 8.0 (From now, support `ps -A`) 21 | // $ adb shell ps --help 22 | // usage: ps [-AadefLlnwZ] [-gG GROUP,] [-k FIELD,] [-o FIELD,] [-p PID,] [-t TTY,] [-uU USER,] 23 | // ... 24 | // $ adb shell,v2:ps == adb shell ps -A 25 | // ct@ubuntu:~$ adb shell ps | head -n 5 26 | // USER PID PPID VSZ RSS WCHAN ADDR S NAME 27 | // root 1 0 19652 1700 SyS_epoll_wait 0 S init 28 | // root 2 0 0 0 kthreadd 0 S [kthreadd] 29 | // root 3 2 0 0 smpboot_thread_fn 0 S [ksoftirqd/0] 30 | // root 6 2 0 0 diag_socket_read 0 S [kworker/u8:0] 31 | // 32 | // $ adb shell 33 | // OP5929L1:/ $ ps # only a few processes on current session 34 | // USER PID PPID VSZ RSS WCHAN ADDR S NAME 35 | // shell 30463 15653 2130476 6084 __arm64_s+ 0 S sh 36 | // shell 30540 30463 2153560 5924 0 0 R ps 37 | // 38 | // 39 | // Android 7.0 40 | // $ adb shell ps --help 41 | // bad pid '--help' 42 | // 43 | // $ adb shell ps -A 44 | // bad pid '-A' 45 | // 46 | // $ adb shell ps | head -n 5 47 | // USER PID PPID VSIZE RSS WCHAN PC NAME 48 | // root 1 0 16332 1348 SyS_epoll_ 0000000000 S /init 49 | // root 2 0 0 0 kthreadd 0000000000 S kthreadd 50 | // root 3 2 0 0 smpboot_th 0000000000 S ksoftirqd/0 51 | // root 5 2 0 0 worker_thr 0000000000 S kworker/0:0H 52 | 53 | // Android 5.1 54 | // $ adb shell ps --help 55 | // USER PID PPID VSIZE RSS WCHAN PC NAM 56 | // 57 | // $ adb shell ps -A 58 | // USER PID PPID VSIZE RSS WCHAN PC NAM 59 | // 60 | // $ adb shell ps 61 | // USER PID PPID VSIZE RSS WCHAN PC NAME 62 | // root 1 0 17096 932 ffffffff 00000000 S /init 63 | // root 2 0 0 0 ffffffff 00000000 S kthreadd 64 | // root 3 2 0 0 ffffffff 00000000 S ksoftirqd/0 65 | // root 5 2 0 0 ffffffff 00000000 S kworker/0:0H 66 | 67 | var ( 68 | ErrNotPermitted = errors.New("NotPermitted") 69 | ErrNoSuchProcess = errors.New("NoSuchProcess") 70 | ) 71 | 72 | type Process struct { 73 | Uid string 74 | Pid int 75 | PPid int 76 | Name string 77 | } 78 | 79 | type ProcessFilter func(p Process) bool 80 | 81 | var ( 82 | // android 8+ root 845 2 0 0 0 0 S [irq/227-q6v5 wdog] 83 | // android 5.1 root 845 2 0 0 0 0 S kworker/2:0H^M 84 | psRegrex = regexp.MustCompile(`(?m)^(\S+)\s+(\d+)\s+(\d+)\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S+\s*\S+)\s*$`) 85 | ) 86 | 87 | func unpackProcess(resp []byte, filter ProcessFilter) (names []Process) { 88 | matches := psRegrex.FindAllSubmatch(resp, -1) 89 | for _, match := range matches { 90 | pid, err := strconv.Atoi(string(match[2])) 91 | if err != nil { 92 | continue 93 | } 94 | ppid, err := strconv.Atoi(string(match[3])) 95 | if err != nil { 96 | continue 97 | } 98 | 99 | p := Process{Uid: string(match[1]), Pid: pid, PPid: ppid, Name: string(match[4])} 100 | if filter == nil || filter(p) { 101 | names = append(names, p) 102 | } 103 | } 104 | return 105 | } 106 | 107 | // ListProcesses run adb shell ps 108 | func (d *Device) ListProcesses(filter ProcessFilter) (names []Process, err error) { 109 | // detect wether support ps -A or not 110 | resp, err := d.RunCommand("ps", "-A") 111 | if err != nil { 112 | return 113 | } 114 | 115 | // Android 5.1: USER PID PPID VSIZE RSS WCHAN PC NAM 116 | // Android 7.1: bad pid '-A' 117 | // if received too few bytes, means 'ps -A' is not supported 118 | if len(resp) < 256 { 119 | // <= Android 7.x 120 | resp, err = d.RunCommand("ps") 121 | if err != nil { 122 | return 123 | } 124 | } 125 | 126 | names = unpackProcess(resp, filter) 127 | if len(names) == 0 { 128 | return nil, errors.New(string(resp)) 129 | } 130 | return 131 | } 132 | 133 | func (d *Device) ListProcessGroup(filter ProcessFilter) (list map[Process][]Process, err error) { 134 | l, err := d.ListProcesses(nil) 135 | if err != nil { 136 | return 137 | } 138 | 139 | list = make(map[Process][]Process) 140 | for _, p := range l { 141 | if filter(p) { 142 | list[p] = make([]Process, 0) 143 | } 144 | } 145 | 146 | for _, p := range l { 147 | for key, value := range list { 148 | if p.PPid == key.Pid { 149 | list[key] = append(value, p) 150 | } 151 | } 152 | } 153 | return 154 | } 155 | 156 | // Android 7.x 157 | // PD1619:/ $ pidof --help 158 | // usage: pidof [-s] [-o omitpid[,omitpid...]] [NAME]... 159 | // Print the PIDs of all processes with the given names. 160 | // -s single shot, only return one pid. 161 | // -o omit PID(s) 162 | // 163 | // PD1619:/ $ pidof 555555 164 | // 1|PD1619:/ $ 165 | // 166 | // Android 5.1 167 | // shell@A33:/ $ pidof 55555 168 | // system/bin/sh: pidof: not found 169 | 170 | // FindPids find pid 171 | func (d *Device) PidOf(name string, match bool) (list []Process, err error) { 172 | return d.ListProcesses(func(p Process) bool { 173 | return (match && p.Name == name) || (!match && strings.Contains(p.Name, name)) 174 | }) 175 | } 176 | 177 | func (d *Device) PidGroupOf(name string, match bool) (list map[Process][]Process, err error) { 178 | return d.ListProcessGroup(func(p Process) bool { 179 | return (match && p.Name == name) || (!match && strings.Contains(p.Name, name)) 180 | }) 181 | } 182 | 183 | // Android 14 184 | // $ kill 584 185 | // /system/bin/sh: kill: 584: Operation not permitted 186 | // 187 | // OP5929L1:/ $ kill 12006 188 | // /system/bin/sh: kill: 12006: No such process 189 | 190 | // KillPidGroup kill process and it's children processes 191 | func (d *Device) KillPids(list []int, signal int) (err error) { 192 | args := make([]string, len(list)) 193 | if signal > 0 { 194 | args = append(args, "-"+strconv.Itoa(signal)) 195 | } 196 | for _, pid := range list { 197 | args = append(args, strconv.Itoa(pid)) 198 | } 199 | 200 | resp, err := d.RunCommand("kill", args...) 201 | if len(resp) > 0 { 202 | err = errors.New(string(resp)) 203 | if bytes.Contains(resp, []byte("Operation not permitted")) { 204 | err = fmt.Errorf("%w: %w", ErrNotPermitted, err) 205 | } else if bytes.Contains(resp, []byte("No such process")) { 206 | err = fmt.Errorf("%w: %w", ErrNoSuchProcess, err) 207 | } 208 | } 209 | return 210 | } 211 | 212 | // KillPidGroup kill process and it's children processes 213 | func (d *Device) KillPidGroupOf(name string, match bool) (killed map[Process][]Process, err error) { 214 | killed, err = d.ListProcessGroup(func(p Process) bool { 215 | return (match && p.Name == name) || (!match && strings.Contains(p.Name, name)) 216 | }) 217 | if err != nil { 218 | return 219 | } 220 | 221 | var pids []int 222 | for p, pl := range killed { 223 | pids = append(pids, p.Pid) 224 | for _, child := range pl { 225 | pids = append(pids, child.Pid) 226 | } 227 | } 228 | err = d.KillPids(pids, 9) 229 | return 230 | } 231 | -------------------------------------------------------------------------------- /cmd_process_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var ( 11 | adbclient, _ = NewWithConfig(ServerConfig{}) 12 | ) 13 | 14 | func Test_unpackProcess(t *testing.T) { 15 | str := `USER PID PPID VSIZE RSS WCHAN PC NAME 16 | root 1 0 17096 932 ffffffff 00000000 S /init 17 | root 2 0 0 0 ffffffff 00000000 S kthreadd 18 | root 3 2 0 0 ffffffff 00000000 S ksoftirqd/0 19 | root 5 2 0 0 ffffffff 00000000 S kworker/0:0H` 20 | l := unpackProcess([]byte(str), nil) 21 | assert.Equal(t, len(l), 4) 22 | assert.Equal(t, l[0].Uid, "root") 23 | assert.Equal(t, l[0].Pid, 1) 24 | assert.Equal(t, l[0].PPid, 0) 25 | assert.Equal(t, l[0].Name, "/init") 26 | 27 | assert.Equal(t, l[3].Uid, "root") 28 | assert.Equal(t, l[3].Pid, 5) 29 | assert.Equal(t, l[3].PPid, 2) 30 | assert.Equal(t, l[3].Name, "kworker/0:0H") 31 | 32 | // Android14 33 | andriod14 := `USER PID PPID VSZ RSS WCHAN ADDR S NAME 34 | root 1 0 19652 1700 SyS_epoll_wait 0 S init 35 | root 2 0 0 0 kthreadd 0 S [kthreadd] 36 | root 3 2 0 0 smpboot_thread_fn 0 S [ksoftirqd/0] 37 | root 845 2 0 0 0 0 S [irq/227-q6v5 wdog]` 38 | l = unpackProcess([]byte(andriod14), nil) 39 | assert.Equal(t, len(l), 4) 40 | assert.Equal(t, l[3].Uid, "root") 41 | assert.Equal(t, l[3].Pid, 845) 42 | assert.Equal(t, l[3].PPid, 2) 43 | assert.Equal(t, l[3].Name, "[irq/227-q6v5 wdog]") 44 | 45 | // only non root processes 46 | l = unpackProcess([]byte(andriod14), func(p Process) bool { 47 | return p.Name != "root" 48 | }) 49 | assert.Equal(t, len(l), 4) 50 | 51 | // only 52 | l = unpackProcess([]byte(andriod14), func(p Process) bool { 53 | return p.Pid == 845 54 | }) 55 | assert.Equal(t, len(l), 1) 56 | fmt.Println("filter pid:", l) 57 | 58 | l = unpackProcess([]byte(andriod14), func(p Process) bool { 59 | return p.PPid == 2 60 | }) 61 | assert.Equal(t, len(l), 2) 62 | fmt.Println("filter ppid:", l) 63 | } 64 | 65 | func TestDevice_ListProcesses(t *testing.T) { 66 | assert.NotNil(t, adbclient) 67 | d := adbclient.Device(AnyDevice()) 68 | list, err := d.ListProcesses(nil) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | for _, name := range list { 73 | fmt.Printf("%-12s%6d%49s%s\n", name.Uid, name.Pid, " ", name.Name) 74 | } 75 | } 76 | 77 | // Android 5.1 passed 78 | func TestDevice_PidGroupOf(t *testing.T) { 79 | assert.NotNil(t, adbclient) 80 | d := adbclient.Device(DeviceWithSerial("4d639ca1")) 81 | list, err := d.PidGroupOf("zygote", true) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | assert.Equal(t, len(list), 1) 86 | for p, pl := range list { 87 | assert.Greater(t, len(pl), 1) 88 | 89 | fmt.Printf("%-12s%6d %s\n", p.Uid, p.Pid, p.Name) 90 | for _, pp := range pl { 91 | fmt.Printf(" %-12s%6d %s\n", pp.Uid, pp.Pid, pp.Name) 92 | } 93 | } 94 | } 95 | 96 | func TestDevice_PidGroupOfAndroid14(t *testing.T) { 97 | assert.NotNil(t, adbclient) 98 | d := adbclient.Device(DeviceWithSerial("79f63fb7")) 99 | list, err := d.PidGroupOf("zygote", false) // zygote64, zygote, webview_zygote 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | assert.Greater(t, len(list), 0) 105 | for p, pl := range list { 106 | fmt.Printf("%-12s%6d %s\n", p.Uid, p.Pid, p.Name) 107 | for _, pp := range pl { 108 | fmt.Printf(" %-12s%6d %s\n", pp.Uid, pp.Pid, pp.Name) 109 | } 110 | } 111 | } 112 | 113 | func TestDevice_KillPids(t *testing.T) { 114 | assert.NotNil(t, adbclient) 115 | d := adbclient.Device(AnyDevice()) 116 | list, err := d.PidOf("com.android.settings", true) 117 | assert.Nil(t, err) 118 | assert.Equal(t, len(list), 1) 119 | err = d.KillPids([]int{list[0].Pid}, 9) 120 | assert.ErrorIs(t, err, ErrNotPermitted) 121 | 122 | err = d.KillPids([]int{100000}, 9) 123 | assert.ErrorIs(t, err, ErrNoSuchProcess) 124 | } 125 | 126 | func TestDevice_KillPidGroupOf(t *testing.T) { 127 | assert.NotNil(t, adbclient) 128 | d := adbclient.Device(AnyDevice()) 129 | list, err := d.KillPidGroupOf("com.android.settings", true) 130 | assert.Equal(t, len(list), 1) 131 | assert.ErrorIs(t, err, ErrNotPermitted) 132 | } 133 | -------------------------------------------------------------------------------- /cpuinfo.txt: -------------------------------------------------------------------------------- 1 | Processor : AArch64 Processor rev 4 (aarch64) 2 | processor : 0 3 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 4 | CPU implementer : 0x41 5 | CPU architecture: 8 6 | CPU variant : 0x0 7 | CPU part : 0xd03 8 | CPU revision : 4 9 | 10 | processor : 1 11 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 12 | CPU implementer : 0x41 13 | CPU architecture: 8 14 | CPU variant : 0x0 15 | CPU part : 0xd03 16 | CPU revision : 4 17 | 18 | processor : 2 19 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 20 | CPU implementer : 0x41 21 | CPU architecture: 8 22 | CPU variant : 0x0 23 | CPU part : 0xd03 24 | CPU revision : 4 25 | 26 | processor : 3 27 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 28 | CPU implementer : 0x41 29 | CPU architecture: 8 30 | CPU variant : 0x0 31 | CPU part : 0xd03 32 | CPU revision : 4 33 | 34 | processor : 4 35 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 36 | CPU implementer : 0x41 37 | CPU architecture: 8 38 | CPU variant : 0x0 39 | CPU part : 0xd03 40 | CPU revision : 4 41 | 42 | processor : 5 43 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 44 | CPU implementer : 0x41 45 | CPU architecture: 8 46 | CPU variant : 0x0 47 | CPU part : 0xd03 48 | CPU revision : 4 49 | 50 | processor : 6 51 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 52 | CPU implementer : 0x41 53 | CPU architecture: 8 54 | CPU variant : 0x0 55 | CPU part : 0xd03 56 | CPU revision : 4 57 | 58 | processor : 7 59 | Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 60 | CPU implementer : 0x41 61 | CPU architecture: 8 62 | CPU variant : 0x0 63 | CPU part : 0xd03 64 | CPU revision : 4 65 | 66 | Hardware : Qualcomm Technologies, Inc MSM8953 67 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import "log" 4 | 5 | var debugFlag = false 6 | 7 | // SetDebug set debug mode 8 | func SetDebug(debug bool) { 9 | debugFlag = debug 10 | } 11 | 12 | func debugLog(msg string) { 13 | if !debugFlag { 14 | return 15 | } 16 | log.Println("[DEBUG] [gadb] " + msg) 17 | } 18 | -------------------------------------------------------------------------------- /device_descriptor.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import "fmt" 4 | 5 | //go:generate stringer -type=deviceDescriptorType 6 | type deviceDescriptorType int 7 | 8 | const ( 9 | // host:transport-any and host: 10 | DeviceAny deviceDescriptorType = iota 11 | // host:transport: and host-serial:: 12 | DeviceSerial 13 | // host:transport-usb and host-usb: 14 | DeviceUsb 15 | // host:transport-local and host-local: 16 | DeviceLocal 17 | ) 18 | 19 | type DeviceDescriptor struct { 20 | descriptorType deviceDescriptorType 21 | 22 | // Only used if Type is DeviceSerial. 23 | serial string 24 | } 25 | 26 | func AnyDevice() DeviceDescriptor { 27 | return DeviceDescriptor{descriptorType: DeviceAny} 28 | } 29 | 30 | func AnyUsbDevice() DeviceDescriptor { 31 | return DeviceDescriptor{descriptorType: DeviceUsb} 32 | } 33 | 34 | func AnyLocalDevice() DeviceDescriptor { 35 | return DeviceDescriptor{descriptorType: DeviceLocal} 36 | } 37 | 38 | func DeviceWithSerial(serial string) DeviceDescriptor { 39 | return DeviceDescriptor{ 40 | descriptorType: DeviceSerial, 41 | serial: serial, 42 | } 43 | } 44 | 45 | func (d DeviceDescriptor) String() string { 46 | if d.descriptorType == DeviceSerial { 47 | return fmt.Sprintf("%s[%s]", d.descriptorType, d.serial) 48 | } 49 | return d.descriptorType.String() 50 | } 51 | 52 | func (d DeviceDescriptor) getHostPrefix() string { 53 | switch d.descriptorType { 54 | case DeviceAny: 55 | return "host" 56 | case DeviceUsb: 57 | return "host-usb" 58 | case DeviceLocal: 59 | return "host-local" 60 | case DeviceSerial: 61 | return fmt.Sprintf("host-serial:%s", d.serial) 62 | default: 63 | panic(fmt.Sprintf("invalid DeviceDescriptorType: %v", d.descriptorType)) 64 | } 65 | } 66 | 67 | func (d DeviceDescriptor) getTransportDescriptor() string { 68 | switch d.descriptorType { 69 | case DeviceAny: 70 | return "transport-any" 71 | case DeviceUsb: 72 | return "transport-usb" 73 | case DeviceLocal: 74 | return "transport-local" 75 | case DeviceSerial: 76 | return fmt.Sprintf("transport:%s", d.serial) 77 | default: 78 | panic(fmt.Sprintf("invalid DeviceDescriptorType: %v", d.descriptorType)) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /device_info.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/prife/goadb/wire" 11 | ) 12 | 13 | type DeviceInfo struct { 14 | // Always set. 15 | Serial string 16 | 17 | State string 18 | // Product, device, and model are not set in the short form. 19 | Product string 20 | Model string 21 | DeviceInfo string 22 | TransportID int 23 | 24 | // Only set for devices connected via USB. 25 | Usb string 26 | } 27 | 28 | // IsUsb returns true if the device is connected via USB. 29 | func (d *DeviceInfo) IsUsb() bool { 30 | return d.Usb != "" 31 | } 32 | 33 | func newDevice(serial, state string, attrs map[string]string) (*DeviceInfo, error) { 34 | if serial == "" { 35 | return nil, fmt.Errorf("%w: device serial cannot be blank", wire.ErrAssertion) 36 | } 37 | 38 | var tid int 39 | tidstr, ok := attrs["transport_id"] 40 | if ok { 41 | value, err := strconv.Atoi(tidstr) 42 | if err == nil { 43 | tid = value 44 | } 45 | } 46 | 47 | return &DeviceInfo{ 48 | Serial: serial, 49 | State: state, 50 | Product: attrs["product"], 51 | Model: attrs["model"], 52 | DeviceInfo: attrs["device"], 53 | Usb: attrs["usb"], 54 | TransportID: tid, 55 | }, nil 56 | } 57 | 58 | func parseDeviceList(list string, lineParseFunc func(string) (*DeviceInfo, error)) ([]*DeviceInfo, error) { 59 | devices := []*DeviceInfo{} 60 | scanner := bufio.NewScanner(strings.NewReader(list)) 61 | 62 | for scanner.Scan() { 63 | device, err := lineParseFunc(scanner.Text()) 64 | if err != nil { 65 | return nil, err 66 | } 67 | devices = append(devices, device) 68 | } 69 | 70 | return devices, nil 71 | } 72 | 73 | func parseDeviceShort(line string) (*DeviceInfo, error) { 74 | fields := strings.Fields(line) 75 | if len(fields) != 2 { 76 | return nil, fmt.Errorf("%w: malformed device line, expected 2 fields but found %d", wire.ErrParse, len(fields)) 77 | } 78 | 79 | return newDevice(fields[0], fields[1], map[string]string{}) 80 | } 81 | 82 | func readBuff(buf *bytes.Buffer, toSpace bool) ([]byte, error) { 83 | cbuf := buf.Bytes() 84 | 85 | for i, c := range cbuf { 86 | if toSpace { 87 | if c == '\t' || c == ' ' { 88 | return buf.Next(i), nil 89 | } 90 | } else { 91 | if !(c == '\t' || c == ' ') { 92 | return buf.Next(i), nil 93 | } 94 | } 95 | } 96 | return nil, fmt.Errorf("not found") 97 | } 98 | 99 | func parseDeviceLongE(line string) (*DeviceInfo, error) { 100 | invalidErr := fmt.Errorf("%w: invalid line:%s", wire.ErrParse, line) 101 | buf := bytes.NewBufferString(strings.TrimSpace(line)) 102 | 103 | // Read serial 104 | serial, err := readBuff(buf, true) 105 | if err != nil { 106 | return nil, invalidErr 107 | } 108 | // skip spaces 109 | if _, err = readBuff(buf, false); err != nil { 110 | return nil, invalidErr 111 | } 112 | 113 | // Read state 114 | state, err := readBuff(buf, true) 115 | if err != nil { 116 | return nil, invalidErr 117 | } 118 | if _, err = readBuff(buf, false); err != nil { 119 | return nil, invalidErr 120 | } 121 | 122 | // Read attributes 123 | attrs := map[string]string{} 124 | // get the first 'key' 125 | rbuf, err := buf.ReadBytes(':') 126 | if err != nil { 127 | return nil, invalidErr 128 | } 129 | key := string(rbuf[:len(rbuf)-1]) 130 | for { 131 | // get the value 132 | rbuf, err = buf.ReadBytes(':') 133 | if err != nil { 134 | value := string(rbuf) 135 | attrs[key] = value 136 | break 137 | } 138 | bi := bytes.LastIndexByte(rbuf, ' ') 139 | if bi < 0 { 140 | return nil, invalidErr 141 | } 142 | value := string(bytes.TrimSpace(rbuf[:bi])) 143 | attrs[key] = value 144 | 145 | // get the next key 146 | key = string(rbuf[bi+1 : len(rbuf)-1]) 147 | } 148 | return newDevice(string(serial), string(state), attrs) 149 | } 150 | 151 | func parseDeviceLong(line string) (*DeviceInfo, error) { 152 | fields := strings.Fields(line) 153 | 154 | attrs := parseDeviceAttributes(fields[2:]) 155 | return newDevice(fields[0], fields[1], attrs) 156 | } 157 | 158 | func parseDeviceAttributes(fields []string) map[string]string { 159 | attrs := map[string]string{} 160 | for _, field := range fields { 161 | key, val := parseKeyVal(field) 162 | attrs[key] = val 163 | } 164 | return attrs 165 | } 166 | 167 | // Parses a key:val pair and returns key, val. 168 | func parseKeyVal(pair string) (string, string) { 169 | split := strings.Split(pair, ":") 170 | return split[0], split[1] 171 | } 172 | -------------------------------------------------------------------------------- /device_info_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ParseDeviceList(t *testing.T) { 10 | devs, err := parseDeviceList(`192.168.56.101:5555 device 11 | 05856558`, parseDeviceShort) 12 | 13 | assert.NoError(t, err) 14 | assert.Len(t, devs, 2) 15 | assert.Equal(t, "192.168.56.101:5555", devs[0].Serial) 16 | assert.Equal(t, "05856558", devs[1].Serial) 17 | } 18 | 19 | func TestParseDeviceShort(t *testing.T) { 20 | dev, err := parseDeviceShort("192.168.56.101:5555 device\n") 21 | assert.NoError(t, err) 22 | assert.Equal(t, &DeviceInfo{ 23 | Serial: "192.168.56.101:5555", 24 | State: "device"}, dev) 25 | } 26 | 27 | func TestParseDeviceLong(t *testing.T) { 28 | dev, err := parseDeviceLong("SERIAL device product:PRODUCT model:MODEL device:DEVICE\n") 29 | assert.NoError(t, err) 30 | assert.Equal(t, &DeviceInfo{ 31 | Serial: "SERIAL", 32 | State: "device", 33 | Product: "PRODUCT", 34 | Model: "MODEL", 35 | DeviceInfo: "DEVICE"}, dev) 36 | } 37 | 38 | func TestParseDeviceLongUnauthorized(t *testing.T) { 39 | dev, err := parseDeviceLong("SERIAL unauthorized usb:1234 transport_id:8") 40 | assert.NoError(t, err) 41 | assert.Equal(t, &DeviceInfo{ 42 | Serial: "SERIAL", 43 | State: "unauthorized", 44 | Usb: "1234", 45 | TransportID: 8}, dev) 46 | } 47 | 48 | func TestParseDeviceLongUsb(t *testing.T) { 49 | dev, err := parseDeviceLong("SERIAL device usb:1234 product:PRODUCT model:MODEL device:DEVICE \n") 50 | assert.NoError(t, err) 51 | assert.Equal(t, &DeviceInfo{ 52 | Serial: "SERIAL", 53 | State: "device", 54 | Product: "PRODUCT", 55 | Model: "MODEL", 56 | DeviceInfo: "DEVICE", 57 | Usb: "1234"}, dev) 58 | } 59 | 60 | func Test_parseDeviceLongE(t *testing.T) { 61 | tests := []struct { 62 | line string 63 | want *DeviceInfo 64 | }{ 65 | { 66 | "SERIAL device product:PRODUCT model:MODEL device:DEVICE", &DeviceInfo{ 67 | Serial: "SERIAL", 68 | State: "device", 69 | Product: "PRODUCT", 70 | Model: "MODEL", 71 | DeviceInfo: "DEVICE", 72 | }, 73 | }, 74 | { 75 | "UYT5T18414003349 unauthorized usb:1114112X transport_id:23", &DeviceInfo{ 76 | Serial: "UYT5T18414003349", 77 | State: "unauthorized", 78 | Usb: "1114112X", 79 | TransportID: 23, 80 | }, 81 | }, 82 | { 83 | "UYT5T18414003349 device usb:1114112X product:ALP_AL00 model:ALP_AL00 device:HWALP transport_id:23", &DeviceInfo{ 84 | Serial: "UYT5T18414003349", 85 | State: "device", 86 | Usb: "1114112X", 87 | Product: "ALP_AL00", 88 | Model: "ALP_AL00", 89 | DeviceInfo: "HWALP", 90 | TransportID: 23, 91 | }, 92 | }, 93 | { 94 | "UYT5T18414003349 device usb:1114112X product:ALP AL00 model:ALP AL00 device:HWALP transport_id:23", &DeviceInfo{ 95 | Serial: "UYT5T18414003349", 96 | State: "device", 97 | Usb: "1114112X", 98 | Product: "ALP AL00", 99 | Model: "ALP AL00", 100 | DeviceInfo: "HWALP", 101 | TransportID: 23, 102 | }, 103 | }, 104 | { 105 | "119.29.201.189:41012 offline product:PRODUCT model:MODEL device:DEVICE transport_id:24", &DeviceInfo{ 106 | Serial: "119.29.201.189:41012", 107 | State: "offline", 108 | Product: "PRODUCT", 109 | Model: "MODEL", 110 | DeviceInfo: "DEVICE", 111 | TransportID: 24, 112 | }, 113 | }, 114 | } 115 | 116 | for _, tt := range tests { 117 | dev, err := parseDeviceLongE(tt.line) 118 | assert.NoError(t, err) 119 | assert.Equal(t, tt.want, dev) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /device_state.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/prife/goadb/wire" 7 | ) 8 | 9 | // DeviceState represents one of the 3 possible states adb will report devices. 10 | // A device can be communicated with when it's in StateOnline. 11 | // A USB device will make the following state transitions: 12 | // Plugged in: StateDisconnected->StateOffline->StateOnline 13 | // Unplugged: StateOnline->StateDisconnected 14 | 15 | //go:generate stringer -type=DeviceState 16 | type DeviceState int8 17 | 18 | const ( 19 | StateInvalid DeviceState = iota 20 | StateUnauthorized 21 | StateAuthorizing 22 | StateDisconnected 23 | StateOffline 24 | StateOnline 25 | StateHost 26 | ) 27 | 28 | var deviceStateStrings = map[string]DeviceState{ 29 | "": StateDisconnected, 30 | "offline": StateOffline, 31 | "device": StateOnline, 32 | "unauthorized": StateUnauthorized, 33 | "authorizing": StateAuthorizing, 34 | "host": StateHost, 35 | } 36 | 37 | func parseDeviceState(str string) (DeviceState, error) { 38 | state, ok := deviceStateStrings[str] 39 | if !ok { 40 | return StateInvalid, fmt.Errorf("%w: invalid device state: '%s'", wire.ErrParse, str) 41 | } 42 | return state, nil 43 | } 44 | -------------------------------------------------------------------------------- /device_state_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseDeviceState(t *testing.T) { 11 | for _, test := range []struct { 12 | String string 13 | WantState DeviceState 14 | WantName string 15 | WantError error // Compared by Error() message. 16 | }{ 17 | {"", StateDisconnected, "StateDisconnected", nil}, 18 | {"offline", StateOffline, "StateOffline", nil}, 19 | {"device", StateOnline, "StateOnline", nil}, 20 | {"unauthorized", StateUnauthorized, "StateUnauthorized", nil}, 21 | {"bad", StateInvalid, "StateInvalid", errors.New(`ParseError: invalid device state: 'bad'`)}, 22 | } { 23 | state, err := parseDeviceState(test.String) 24 | if test.WantError == nil { 25 | assert.NoError(t, err) 26 | } else { 27 | assert.EqualError(t, err, test.WantError.Error()) 28 | } 29 | assert.Equal(t, test.WantState, state) 30 | assert.Equal(t, test.WantName, state.String()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /device_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/prife/goadb/wire" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestGetAttribute(t *testing.T) { 14 | s := &MockServer{ 15 | Status: wire.StatusSuccess, 16 | Messages: []string{"value"}, 17 | } 18 | client := (&Adb{s}).Device(DeviceWithSerial("serial")) 19 | 20 | v, err := client.getAttribute("attr") 21 | assert.Equal(t, "host-serial:serial:attr", s.Requests[0]) 22 | assert.NoError(t, err) 23 | assert.Equal(t, "value", v) 24 | } 25 | 26 | func TestGetDeviceInfo(t *testing.T) { 27 | deviceLister := func() ([]*DeviceInfo, error) { 28 | return []*DeviceInfo{ 29 | &DeviceInfo{ 30 | Serial: "abc", 31 | Product: "Foo", 32 | }, 33 | &DeviceInfo{ 34 | Serial: "def", 35 | Product: "Bar", 36 | }, 37 | }, nil 38 | } 39 | 40 | client := newDeviceClientWithDeviceLister("abc", deviceLister) 41 | device, err := client.DeviceInfo() 42 | assert.NoError(t, err) 43 | assert.Equal(t, "Foo", device.Product) 44 | 45 | client = newDeviceClientWithDeviceLister("def", deviceLister) 46 | device, err = client.DeviceInfo() 47 | assert.NoError(t, err) 48 | assert.Equal(t, "Bar", device.Product) 49 | 50 | client = newDeviceClientWithDeviceLister("serial", deviceLister) 51 | device, err = client.DeviceInfo() 52 | assert.True(t, errors.Is(err, wire.ErrDeviceNotFound)) 53 | assert.EqualError(t, errors.Unwrap(err), 54 | "DeviceNotFound: device list doesn't contain serial serial") 55 | assert.Nil(t, device) 56 | } 57 | 58 | func newDeviceClientWithDeviceLister(serial string, deviceLister func() ([]*DeviceInfo, error)) *Device { 59 | client := (&Adb{&MockServer{ 60 | Status: wire.StatusSuccess, 61 | Messages: []string{serial}, 62 | }}).Device(DeviceWithSerial(serial)) 63 | client.deviceListFunc = deviceLister 64 | return client 65 | } 66 | 67 | func TestRunCommandNoArgs(t *testing.T) { 68 | buf := bytes.NewBuffer([]byte("output")) 69 | s := &MockServer{ 70 | Status: wire.StatusSuccess, 71 | // Messages: []string{"output"}, 72 | mockConn: mockConn{ 73 | Buffer: buf, 74 | }, 75 | } 76 | client := (&Adb{s}).Device(AnyDevice()) 77 | 78 | v, err := client.RunCommand("cmd") 79 | assert.Equal(t, "host:transport-any", s.Requests[0]) 80 | assert.Equal(t, "shell:cmd", s.Requests[1]) 81 | assert.NoError(t, err) 82 | assert.Equal(t, "output", string(v)) 83 | } 84 | 85 | func TestPrepareCommandLineNoArgs(t *testing.T) { 86 | result, err := prepareCommandLine("cmd") 87 | assert.NoError(t, err) 88 | assert.Equal(t, "cmd", result) 89 | } 90 | 91 | func TestPrepareCommandLineEmptyCommand(t *testing.T) { 92 | _, err := prepareCommandLine("") 93 | assert.True(t, errors.Is(err, wire.ErrAssertion)) 94 | assert.Contains(t, err.Error(), "command cannot be empty") 95 | } 96 | 97 | func TestPrepareCommandLineBlankCommand(t *testing.T) { 98 | _, err := prepareCommandLine(" ") 99 | assert.True(t, errors.Is(err, wire.ErrAssertion)) 100 | assert.Contains(t, err.Error(), "command cannot be empty") 101 | } 102 | 103 | func TestPrepareCommandLineCleanArgs(t *testing.T) { 104 | result, err := prepareCommandLine("cmd", "arg1", "arg2") 105 | assert.NoError(t, err) 106 | assert.Equal(t, "cmd arg1 arg2", result) 107 | } 108 | 109 | func TestPrepareCommandLineArgWithWhitespaceQuotes(t *testing.T) { 110 | result, err := prepareCommandLine("cmd", "arg with spaces") 111 | assert.NoError(t, err) 112 | assert.Equal(t, "cmd \"arg with spaces\"", result) 113 | } 114 | 115 | func TestPrepareCommandLineArgWithDoubleQuoteFails(t *testing.T) { 116 | _, err := prepareCommandLine("cmd", "quoted\"arg") 117 | assert.True(t, errors.Is(err, wire.ErrParse)) 118 | assert.Contains(t, err.Error(), "arg at index 0 contains an invalid double quote: quoted\"arg") 119 | } 120 | 121 | func Test_featuresStrToMap(t *testing.T) { 122 | str := "shell_v2,cmd,stat_v2,ls_v2,fixed_push_mkdir,apex,abb,fixed_push_symlink_timestamp,abb_exec,remount_shell,track_app,sendrecv_v2,sendrecv_v2_brotli,sendrecv_v2_lz4,sendrecv_v2_zstd,sendrecv_v2_dry_run_send,openscreen_mdns,delayed_ack" 123 | features := featuresStrToMap(str) 124 | assert.Equal(t, len(features), 18) 125 | // for k, _ := range features { 126 | // fmt.Println(k) 127 | // } 128 | } 129 | 130 | func TestDevice_State(t *testing.T) { 131 | assert.NotNil(t, adbclient) 132 | d := adbclient.Device(DeviceWithSerial("not-existed")) 133 | state, err := d.State() 134 | assert.Equal(t, state, StateInvalid) 135 | assert.ErrorIs(t, err, wire.ErrDeviceNotFound) 136 | 137 | d2 := adbclient.Device(AnyDevice()) 138 | state, err = d2.State() 139 | fmt.Printf("state:%v, err=%v", state, err) 140 | assert.Equal(t, state, StateInvalid) 141 | assert.Contains(t, err.Error(), "no devices/emulators found") 142 | } 143 | -------------------------------------------------------------------------------- /device_watcher.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "runtime" 9 | "strings" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/prife/goadb/wire" 14 | ) 15 | 16 | // DeviceWatcher publishes device status change events. 17 | // If the server dies while listening for events, it restarts the server. 18 | type DeviceWatcher struct { 19 | *deviceWatcherImpl 20 | } 21 | 22 | // DeviceStateChangedEvent represents a device state transition. 23 | // Contains the device’s old and new states, but also provides methods to query the 24 | // type of state transition. 25 | type DeviceStateChangedEvent struct { 26 | Serial string 27 | OldState DeviceState 28 | NewState DeviceState 29 | } 30 | 31 | // CameOnline returns true if this event represents a device coming online. 32 | func (s DeviceStateChangedEvent) CameOnline() bool { 33 | return s.OldState != StateOnline && s.NewState == StateOnline 34 | } 35 | 36 | // WentOffline returns true if this event represents a device going offline. 37 | func (s DeviceStateChangedEvent) WentOffline() bool { 38 | return s.OldState == StateOnline && s.NewState != StateOnline 39 | } 40 | 41 | type deviceWatcherImpl struct { 42 | server server 43 | 44 | // If an error occurs, it is stored here and eventChan is close immediately after. 45 | err atomic.Value 46 | 47 | eventChan chan DeviceStateChangedEvent 48 | } 49 | 50 | func newDeviceWatcher(server server) *DeviceWatcher { 51 | watcher := &DeviceWatcher{&deviceWatcherImpl{ 52 | server: server, 53 | eventChan: make(chan DeviceStateChangedEvent), 54 | }} 55 | 56 | runtime.SetFinalizer(watcher, func(watcher *DeviceWatcher) { 57 | watcher.Shutdown() 58 | }) 59 | 60 | go publishDevices(watcher.deviceWatcherImpl) 61 | 62 | return watcher 63 | } 64 | 65 | // C returns a channel than can be received on to get events. 66 | // If an unrecoverable error occurs, or Shutdown is called, the channel will be closed. 67 | func (w *DeviceWatcher) C() <-chan DeviceStateChangedEvent { 68 | return w.eventChan 69 | } 70 | 71 | // Err returns the error that caused the channel returned by C to be closed, if C is closed. 72 | // If C is not closed, its return value is undefined. 73 | func (w *DeviceWatcher) Err() error { 74 | if err, ok := w.err.Load().(error); ok { 75 | return err 76 | } 77 | return nil 78 | } 79 | 80 | // Shutdown stops the watcher from listening for events and closes the channel returned 81 | // from C. 82 | func (w *DeviceWatcher) Shutdown() { 83 | // TODO(z): Implement. 84 | } 85 | 86 | func (w *deviceWatcherImpl) reportErr(err error) { 87 | w.err.Store(err) 88 | } 89 | 90 | // publishDevices reads device lists from scanner, calculates diffs, and publishes events on 91 | // eventChan. 92 | // Returns when scanner returns an error. 93 | // Doesn't refer directly to a *DeviceWatcher so it can be GCed (which will, 94 | // in turn, close Scanner and stop this goroutine). 95 | // 96 | // TODO: to support shutdown, spawn a new goroutine each time a server connection is established. 97 | // This goroutine should read messages and send them to a message channel. Can write errors directly 98 | // to errVal. publishDevicesUntilError should take the msg chan and the scanner and select on the msg chan and stop chan, and if the stop 99 | // chan sends, close the scanner and return true. If the msg chan closes, just return false. 100 | // publishDevices can look at ret val: if false and err == EOF, reconnect. If false and other error, report err 101 | // and abort. If true, report no error and stop. 102 | func publishDevices(watcher *deviceWatcherImpl) { 103 | defer close(watcher.eventChan) 104 | 105 | var lastKnownStates map[string]DeviceState 106 | finished := false 107 | 108 | for { 109 | scanner, err := connectToTrackDevices(watcher.server) 110 | if err != nil { 111 | watcher.reportErr(err) 112 | return 113 | } 114 | 115 | finished, err = publishDevicesUntilError(scanner, watcher.eventChan, &lastKnownStates) 116 | 117 | if finished { 118 | scanner.Close() 119 | return 120 | } 121 | 122 | if errors.Is(err, wire.ErrConnectionReset) { 123 | // The server died, restart and reconnect. 124 | if realServer, ok := watcher.server.(*realServer); ok { 125 | if !realServer.config.AutoStart { 126 | watcher.reportErr(fmt.Errorf("server killed")) 127 | return 128 | } 129 | } 130 | 131 | // report all devices removed 132 | for serial, deviceState := range lastKnownStates { 133 | watcher.eventChan <- DeviceStateChangedEvent{serial, deviceState, StateDisconnected} 134 | } 135 | lastKnownStates = nil 136 | 137 | // Delay by a random [0ms, 500ms) in case multiple DeviceWatchers are trying to 138 | // start the same server. 139 | delay := time.Duration(rand.Intn(500)) * time.Millisecond 140 | 141 | log.Printf("[DeviceWatcher] server died, restarting in %s…", delay) 142 | time.Sleep(delay) 143 | if err := watcher.server.Start(); err != nil { 144 | log.Println("[DeviceWatcher] error restarting server, giving up") 145 | watcher.reportErr(err) 146 | return 147 | } // Else server should be running, continue listening. 148 | } else { 149 | // Unknown error, don't retry. 150 | watcher.reportErr(err) 151 | return 152 | } 153 | } 154 | } 155 | 156 | func connectToTrackDevices(server server) (wire.Scanner, error) { 157 | conn, err := server.Dial() 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | if err := conn.SendMessage([]byte("host:track-devices")); err != nil { 163 | conn.Close() 164 | return nil, err 165 | } 166 | 167 | if _, err := conn.ReadStatus("host:track-devices"); err != nil { 168 | conn.Close() 169 | return nil, err 170 | } 171 | 172 | return conn, nil 173 | } 174 | 175 | func publishDevicesUntilError(scanner wire.Scanner, eventChan chan<- DeviceStateChangedEvent, lastKnownStates *map[string]DeviceState) (finished bool, err error) { 176 | for { 177 | msg, err := scanner.ReadMessage() 178 | if err != nil { 179 | return false, err 180 | } 181 | 182 | deviceStates, err := parseDeviceStates(string(msg)) 183 | if err != nil { 184 | return false, err 185 | } 186 | 187 | for _, event := range calculateStateDiffs(*lastKnownStates, deviceStates) { 188 | eventChan <- event 189 | } 190 | *lastKnownStates = deviceStates 191 | } 192 | } 193 | 194 | func parseDeviceStates(msg string) (states map[string]DeviceState, err error) { 195 | states = make(map[string]DeviceState) 196 | 197 | for lineNum, line := range strings.Split(msg, "\n") { 198 | if len(line) == 0 { 199 | continue 200 | } 201 | 202 | fields := strings.Split(line, "\t") 203 | if len(fields) != 2 { 204 | err = fmt.Errorf("%w: invalid device state line %d: %s", wire.ErrParse, lineNum, line) 205 | return 206 | } 207 | 208 | serial, stateString := fields[0], fields[1] 209 | var state DeviceState 210 | state, err = parseDeviceState(stateString) 211 | states[serial] = state 212 | } 213 | 214 | return 215 | } 216 | 217 | func calculateStateDiffs(oldStates, newStates map[string]DeviceState) (events []DeviceStateChangedEvent) { 218 | for serial, newState := range newStates { 219 | if oldState, ok := oldStates[serial]; ok { 220 | // Device present in both lists: state changed. 221 | if oldState != newState { 222 | events = append(events, DeviceStateChangedEvent{serial, oldState, newState}) 223 | } 224 | } else { 225 | // Device only present in new list: device added. 226 | events = append(events, DeviceStateChangedEvent{serial, StateDisconnected, newState}) 227 | } 228 | } 229 | 230 | for serial, oldState := range oldStates { 231 | if _, ok := newStates[serial]; !ok { 232 | // Device only present in old list: device removed. 233 | events = append(events, DeviceStateChangedEvent{serial, oldState, StateDisconnected}) 234 | } 235 | } 236 | 237 | return events 238 | } 239 | -------------------------------------------------------------------------------- /device_watcher_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/prife/goadb/wire" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestParseDeviceStatesSingle(t *testing.T) { 13 | states, err := parseDeviceStates(`192.168.56.101:5555 offline 14 | `) 15 | 16 | assert.NoError(t, err) 17 | assert.Len(t, states, 1) 18 | assert.Equal(t, StateOffline, states["192.168.56.101:5555"]) 19 | } 20 | 21 | func TestParseDeviceStatesMultiple(t *testing.T) { 22 | states, err := parseDeviceStates(`192.168.56.101:5555 offline 23 | 0x0x0x0x device 24 | `) 25 | 26 | assert.NoError(t, err) 27 | assert.Len(t, states, 2) 28 | assert.Equal(t, StateOffline, states["192.168.56.101:5555"]) 29 | assert.Equal(t, StateOnline, states["0x0x0x0x"]) 30 | } 31 | 32 | func TestParseDeviceStatesMalformed(t *testing.T) { 33 | _, err := parseDeviceStates(`192.168.56.101:5555 offline 34 | 0x0x0x0x 35 | `) 36 | 37 | assert.True(t, errors.Is(err, wire.ErrParse)) 38 | assert.EqualError(t, err, "ParseError: invalid device state line 1: 0x0x0x0x") 39 | } 40 | 41 | func TestCalculateStateDiffsUnchangedEmpty(t *testing.T) { 42 | oldStates := map[string]DeviceState{} 43 | newStates := map[string]DeviceState{} 44 | 45 | diffs := calculateStateDiffs(oldStates, newStates) 46 | 47 | assert.Empty(t, diffs) 48 | } 49 | 50 | func TestCalculateStateDiffsUnchangedNonEmpty(t *testing.T) { 51 | oldStates := map[string]DeviceState{ 52 | "1": StateOnline, 53 | "2": StateOnline, 54 | } 55 | newStates := map[string]DeviceState{ 56 | "1": StateOnline, 57 | "2": StateOnline, 58 | } 59 | 60 | diffs := calculateStateDiffs(oldStates, newStates) 61 | 62 | assert.Empty(t, diffs) 63 | } 64 | 65 | func TestCalculateStateDiffsOneAdded(t *testing.T) { 66 | oldStates := map[string]DeviceState{} 67 | newStates := map[string]DeviceState{ 68 | "serial": StateOffline, 69 | } 70 | 71 | diffs := calculateStateDiffs(oldStates, newStates) 72 | 73 | assertContainsOnly(t, []DeviceStateChangedEvent{ 74 | DeviceStateChangedEvent{"serial", StateDisconnected, StateOffline}, 75 | }, diffs) 76 | } 77 | 78 | func TestCalculateStateDiffsOneRemoved(t *testing.T) { 79 | oldStates := map[string]DeviceState{ 80 | "serial": StateOffline, 81 | } 82 | newStates := map[string]DeviceState{} 83 | 84 | diffs := calculateStateDiffs(oldStates, newStates) 85 | 86 | assertContainsOnly(t, []DeviceStateChangedEvent{ 87 | DeviceStateChangedEvent{"serial", StateOffline, StateDisconnected}, 88 | }, diffs) 89 | } 90 | 91 | func TestCalculateStateDiffsOneAddedOneUnchanged(t *testing.T) { 92 | oldStates := map[string]DeviceState{ 93 | "1": StateOnline, 94 | } 95 | newStates := map[string]DeviceState{ 96 | "1": StateOnline, 97 | "2": StateOffline, 98 | } 99 | 100 | diffs := calculateStateDiffs(oldStates, newStates) 101 | 102 | assertContainsOnly(t, []DeviceStateChangedEvent{ 103 | DeviceStateChangedEvent{"2", StateDisconnected, StateOffline}, 104 | }, diffs) 105 | } 106 | 107 | func TestCalculateStateDiffsOneRemovedOneUnchanged(t *testing.T) { 108 | oldStates := map[string]DeviceState{ 109 | "1": StateOffline, 110 | "2": StateOnline, 111 | } 112 | newStates := map[string]DeviceState{ 113 | "2": StateOnline, 114 | } 115 | 116 | diffs := calculateStateDiffs(oldStates, newStates) 117 | 118 | assertContainsOnly(t, []DeviceStateChangedEvent{ 119 | DeviceStateChangedEvent{"1", StateOffline, StateDisconnected}, 120 | }, diffs) 121 | } 122 | 123 | func TestCalculateStateDiffsOneAddedOneRemoved(t *testing.T) { 124 | oldStates := map[string]DeviceState{ 125 | "1": StateOffline, 126 | } 127 | newStates := map[string]DeviceState{ 128 | "2": StateOffline, 129 | } 130 | 131 | diffs := calculateStateDiffs(oldStates, newStates) 132 | 133 | assertContainsOnly(t, []DeviceStateChangedEvent{ 134 | DeviceStateChangedEvent{"1", StateOffline, StateDisconnected}, 135 | DeviceStateChangedEvent{"2", StateDisconnected, StateOffline}, 136 | }, diffs) 137 | } 138 | 139 | func TestCalculateStateDiffsOneChangedOneUnchanged(t *testing.T) { 140 | oldStates := map[string]DeviceState{ 141 | "1": StateOffline, 142 | "2": StateOnline, 143 | } 144 | newStates := map[string]DeviceState{ 145 | "1": StateOnline, 146 | "2": StateOnline, 147 | } 148 | 149 | diffs := calculateStateDiffs(oldStates, newStates) 150 | 151 | assertContainsOnly(t, []DeviceStateChangedEvent{ 152 | DeviceStateChangedEvent{"1", StateOffline, StateOnline}, 153 | }, diffs) 154 | } 155 | 156 | func TestCalculateStateDiffsMultipleChanged(t *testing.T) { 157 | oldStates := map[string]DeviceState{ 158 | "1": StateOffline, 159 | "2": StateOnline, 160 | } 161 | newStates := map[string]DeviceState{ 162 | "1": StateOnline, 163 | "2": StateOffline, 164 | } 165 | 166 | diffs := calculateStateDiffs(oldStates, newStates) 167 | 168 | assertContainsOnly(t, []DeviceStateChangedEvent{ 169 | DeviceStateChangedEvent{"1", StateOffline, StateOnline}, 170 | DeviceStateChangedEvent{"2", StateOnline, StateOffline}, 171 | }, diffs) 172 | } 173 | 174 | func TestCalculateStateDiffsOneAddedOneRemovedOneChanged(t *testing.T) { 175 | oldStates := map[string]DeviceState{ 176 | "1": StateOffline, 177 | "2": StateOffline, 178 | } 179 | newStates := map[string]DeviceState{ 180 | "1": StateOnline, 181 | "3": StateOffline, 182 | } 183 | 184 | diffs := calculateStateDiffs(oldStates, newStates) 185 | 186 | assertContainsOnly(t, []DeviceStateChangedEvent{ 187 | DeviceStateChangedEvent{"1", StateOffline, StateOnline}, 188 | DeviceStateChangedEvent{"2", StateOffline, StateDisconnected}, 189 | DeviceStateChangedEvent{"3", StateDisconnected, StateOffline}, 190 | }, diffs) 191 | } 192 | 193 | func TestCameOnline(t *testing.T) { 194 | assert.True(t, DeviceStateChangedEvent{"", StateDisconnected, StateOnline}.CameOnline()) 195 | assert.True(t, DeviceStateChangedEvent{"", StateOffline, StateOnline}.CameOnline()) 196 | assert.False(t, DeviceStateChangedEvent{"", StateOnline, StateOffline}.CameOnline()) 197 | assert.False(t, DeviceStateChangedEvent{"", StateOnline, StateDisconnected}.CameOnline()) 198 | assert.False(t, DeviceStateChangedEvent{"", StateOffline, StateDisconnected}.CameOnline()) 199 | } 200 | 201 | func TestWentOffline(t *testing.T) { 202 | assert.True(t, DeviceStateChangedEvent{"", StateOnline, StateDisconnected}.WentOffline()) 203 | assert.True(t, DeviceStateChangedEvent{"", StateOnline, StateOffline}.WentOffline()) 204 | assert.False(t, DeviceStateChangedEvent{"", StateOffline, StateOnline}.WentOffline()) 205 | assert.False(t, DeviceStateChangedEvent{"", StateDisconnected, StateOnline}.WentOffline()) 206 | assert.False(t, DeviceStateChangedEvent{"", StateOffline, StateDisconnected}.WentOffline()) 207 | } 208 | 209 | func TestPublishDevicesRestartsServer(t *testing.T) { 210 | server := &MockServer{ 211 | Status: wire.StatusSuccess, 212 | Errs: []error{ 213 | nil, nil, nil, // Successful dial. 214 | fmt.Errorf("%w: failed first read", wire.ErrConnectionReset), 215 | fmt.Errorf("%w: failed redial", wire.ErrServerNotAvailable), 216 | }, 217 | } 218 | watcher := deviceWatcherImpl{ 219 | server: server, 220 | eventChan: make(chan DeviceStateChangedEvent), 221 | } 222 | 223 | publishDevices(&watcher) 224 | 225 | assert.Equal(t, []string{"host:track-devices"}, server.Requests) 226 | assert.Equal(t, []string{"Dial", "SendMessage", "ReadStatus", "ReadMessage", "Start", "Dial"}, server.Trace) 227 | err := watcher.err.Load().(error) 228 | assert.True(t, errors.Is(err, wire.ErrServerNotAvailable)) 229 | } 230 | 231 | func assertContainsOnly(t *testing.T, expected, actual []DeviceStateChangedEvent) { 232 | assert.Len(t, actual, len(expected)) 233 | for _, expectedEntry := range expected { 234 | assertContains(t, expectedEntry, actual) 235 | } 236 | } 237 | 238 | func assertContains(t *testing.T, expectedEntry DeviceStateChangedEvent, actual []DeviceStateChangedEvent) { 239 | for _, actualEntry := range actual { 240 | if expectedEntry == actualEntry { 241 | return 242 | } 243 | } 244 | assert.Fail(t, "expected to find %+v in %+v", expectedEntry, actual) 245 | } 246 | -------------------------------------------------------------------------------- /devicedescriptortype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=deviceDescriptorType"; DO NOT EDIT. 2 | 3 | package adb 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[DeviceAny-0] 12 | _ = x[DeviceSerial-1] 13 | _ = x[DeviceUsb-2] 14 | _ = x[DeviceLocal-3] 15 | } 16 | 17 | const _deviceDescriptorType_name = "DeviceAnyDeviceSerialDeviceUsbDeviceLocal" 18 | 19 | var _deviceDescriptorType_index = [...]uint8{0, 9, 21, 30, 41} 20 | 21 | func (i deviceDescriptorType) String() string { 22 | if i < 0 || i >= deviceDescriptorType(len(_deviceDescriptorType_index)-1) { 23 | return "deviceDescriptorType(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | return _deviceDescriptorType_name[_deviceDescriptorType_index[i]:_deviceDescriptorType_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /devicestate_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=DeviceState"; DO NOT EDIT. 2 | 3 | package adb 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[StateInvalid-0] 12 | _ = x[StateUnauthorized-1] 13 | _ = x[StateAuthorizing-2] 14 | _ = x[StateDisconnected-3] 15 | _ = x[StateOffline-4] 16 | _ = x[StateOnline-5] 17 | _ = x[StateHost-6] 18 | } 19 | 20 | const _DeviceState_name = "StateInvalidStateUnauthorizedStateAuthorizingStateDisconnectedStateOfflineStateOnlineStateHost" 21 | 22 | var _DeviceState_index = [...]uint8{0, 12, 29, 45, 62, 74, 85, 94} 23 | 24 | func (i DeviceState) String() string { 25 | if i < 0 || i >= DeviceState(len(_DeviceState_index)-1) { 26 | return "DeviceState(" + strconv.FormatInt(int64(i), 10) + ")" 27 | } 28 | return _DeviceState_name[_DeviceState_index[i]:_DeviceState_index[i+1]] 29 | } 30 | -------------------------------------------------------------------------------- /dialer.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | 8 | "github.com/prife/goadb/wire" 9 | ) 10 | 11 | // Dialer knows how to create connections to an adb server. 12 | type Dialer interface { 13 | Dial(address string, timeout time.Duration) (*wire.Conn, error) 14 | } 15 | 16 | type tcpDialer struct{} 17 | 18 | // DialTimeout connects to the adb server on the host and port set on the netDialer. 19 | // The zero-value will connect to the default, localhost:5037. 20 | func (tcpDialer) Dial(address string, timeout time.Duration) (*wire.Conn, error) { 21 | netConn, err := net.DialTimeout("tcp", address, timeout) 22 | if err != nil { 23 | return nil, fmt.Errorf("%w: error dialing %s", wire.ErrServerNotAvailable, address) 24 | } 25 | 26 | return wire.NewConn(netConn), nil 27 | } 28 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // package adb is a Go interface to the Android Debug Bridge (adb). 2 | // See cmd/demo/demo.go for an example of how to use this library. 3 | // The client/server spec is defined at https://android.googlesource.com/platform/system/core/+/master/adb/OVERVIEW.TXT. 4 | // WARNING This library is under heavy development, and its API is likely to change without notice. 5 | package adb 6 | 7 | // TODO(z): Write method-specific examples. 8 | -------------------------------------------------------------------------------- /doc/API.md: -------------------------------------------------------------------------------- 1 | # API说明 2 | 3 | ## 设备上线/离线监控 4 | 5 | 当未授权手机插入时,会发送以下消息 6 | ``` 7 | time="2023-11-21T17:16:09+08:00" level=info msg="adb-monitor: {Serial:6d0a931b OldState:StateDisconnected NewState:StateOffline}" 8 | time="2023-11-21T17:16:09+08:00" level=info msg="adb-monitor: {Serial:6d0a931b OldState:StateOffline NewState:StateAuthorizing}" 9 | time="2023-11-21T17:16:09+08:00" level=info msg="adb-monitor: {Serial:6d0a931b OldState:StateAuthorizing NewState:StateUnauthorized}" 10 | ``` 11 | 12 | 此时如点击授权,则会继续发送以下消息 13 | ``` 14 | time="2023-11-21T17:16:30+08:00" level=info msg="adb-monitor: {Serial:6d0a931b OldState:StateUnauthorized NewState:StateOffline}" 15 | time="2023-11-21T17:16:30+08:00" level=info msg="adb-monitor: {Serial:6d0a931b OldState:StateOffline NewState:StateOnline}" 16 | ``` 17 | 18 | 当接入一台已授权的手机插入时,会顺序发送以下消息 19 | ``` 20 | "monitor: {Serial:PQY0220A15002880 OldState:StateDisconnected NewState:StateOffline}" 21 | "monitor: {Serial:PQY0220A15002880 OldState:StateOffline NewState:StateAuthorizing}" 22 | "monitor: {Serial:PQY0220A15002880 OldState:StateAuthorizing NewState:StateOffline}" 23 | "monitor: {Serial:PQY0220A15002880 OldState:StateOffline NewState:StateOnline}" 24 | ``` 25 | 26 | 设备离线 27 | 28 | ``` 29 | time="2023-11-21T17:16:58+08:00" level=info msg="adb-monitor: {Serial:6d0a931b OldState:StateOnline NewState:StateOffline}" 30 | time="2023-11-21T17:16:58+08:00" level=info msg="adb-monitor: {Serial:6d0a931b OldState:StateOffline NewState:StateDisconnected}" 31 | ``` 32 | 33 | 总结:只需监听以下连个状态变化,其他变化可以忽略 34 | 1. OldState:StateOnline NewState:StateOffline,则触发离线回调 35 | 2. OldState:StateOffline NewState:StateOnline,则触发上线回调 -------------------------------------------------------------------------------- /executable.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | // isExecutable function calls the isExecutableOnPlatform function. 4 | // Implementation the isExecutableOnPlatform function is provided in 5 | // execuatble_win.go for windows and 6 | // executable_unix.go for unix. 7 | func isExecutable(path string) error { 8 | return isExecutableOnPlatform(path) 9 | } 10 | -------------------------------------------------------------------------------- /executable_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || linux || netbsd || openbsd 2 | // +build darwin freebsd linux netbsd openbsd 3 | 4 | package adb 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | func isExecutableOnPlatform(path string) error { 9 | return unix.Access(path, unix.X_OK) 10 | } 11 | -------------------------------------------------------------------------------- /executable_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package adb 5 | 6 | import ( 7 | "errors" 8 | "strings" 9 | ) 10 | 11 | func isExecutableOnPlatform(path string) error { 12 | if strings.HasSuffix(path, ".exe") || strings.HasSuffix(path, ".cmd") || 13 | strings.HasSuffix(path, ".bat") { 14 | return nil 15 | } 16 | return errors.New("not an executable") 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prife/goadb 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/alecthomas/kingpin/v2 v2.3.2 7 | github.com/cheggaaa/pb v1.0.29 8 | github.com/prife/gomlib v0.0.4 9 | github.com/sirupsen/logrus v1.9.3 10 | github.com/stretchr/testify v1.8.4 11 | golang.org/x/sys v0.14.0 12 | ) 13 | 14 | require ( 15 | github.com/Masterminds/semver v1.5.0 16 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/mattn/go-runewidth v0.0.4 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 2 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 3 | github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= 4 | github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 5 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 6 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 7 | github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= 8 | github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 13 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 14 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 15 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 16 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 17 | github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= 18 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 19 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= 20 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/prife/gomlib v0.0.4 h1:IuOfq0XuxtZrtXPFLuq6pzTOu4z4Q3NfvUx0TmEV9XI= 24 | github.com/prife/gomlib v0.0.4/go.mod h1:kbAWWKS19Lc2mQyghlWAm+iRHCAj3cx62nepIsycbgk= 25 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 26 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 28 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 29 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 30 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 31 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 32 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 33 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 34 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 35 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= 38 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 42 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /meminfo.txt: -------------------------------------------------------------------------------- 1 | MemTotal: 3681628 kB 2 | MemFree: 273652 kB 3 | MemAvailable: 2243756 kB 4 | Buffers: 103184 kB 5 | Cached: 1855852 kB 6 | SwapCached: 10848 kB 7 | Active: 1663412 kB 8 | Inactive: 970892 kB 9 | Active(anon): 534180 kB 10 | Inactive(anon): 149568 kB 11 | Active(file): 1129232 kB 12 | Inactive(file): 821324 kB 13 | Unevictable: 0 kB 14 | Mlocked: 0 kB 15 | SwapTotal: 524284 kB 16 | SwapFree: 256136 kB 17 | Dirty: 8 kB 18 | Writeback: 0 kB 19 | AnonPages: 671720 kB 20 | Mapped: 440704 kB 21 | Shmem: 8480 kB 22 | Slab: 233524 kB 23 | SReclaimable: 118728 kB 24 | SUnreclaim: 114796 kB 25 | KernelStack: 34784 kB 26 | PageTables: 38080 kB 27 | NFS_Unstable: 0 kB 28 | Bounce: 0 kB 29 | WritebackTmp: 0 kB 30 | CommitLimit: 2365096 kB 31 | Committed_AS: 73155452 kB 32 | VmallocTotal: 258998208 kB 33 | VmallocUsed: 215108 kB 34 | VmallocChunk: 258623492 kB 35 | -------------------------------------------------------------------------------- /properties.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | "strconv" 9 | 10 | "github.com/Masterminds/semver" 11 | ) 12 | 13 | const ( 14 | PropSysBootCompleted = "sys.boot_completed" 15 | PropSerial = "ro.serialno" 16 | PropProductName = "ro.product.name" 17 | PropProductBrand = "ro.product.brand" 18 | PropProductModel = "ro.product.model" 19 | PropProductManu = "ro.product.manufacturer" 20 | PropProductCpuAbi = "ro.product.cpu.abi" 21 | PropBuildVersionSdk = "ro.build.version.sdk" // api level 22 | PropProductBuildVersionSdk = "ro.product.build.version.sdk" // api level 23 | PropBuildVersionRelease = "ro.build.version.release" // android os version 24 | // for vendor 25 | PropHwPlatformVersion = "hw_sc.build.platform.version" 26 | ) 27 | 28 | var ( 29 | devicePropertyRegex = regexp.MustCompile(`(?m)\[(\S+)\]:\s*\[(.*)\]\s*$`) 30 | // devicePropertyRegex = regexp.MustCompile(`\[([\s\S]*?)\]: \[([\s\S]*?)\]\r?`) 31 | ) 32 | 33 | var ( 34 | ErrNotFound = errors.New("NotFound") 35 | ) 36 | 37 | type PropertiesFilter func(k, v string) bool 38 | type AndroidProperties map[string]string 39 | 40 | func parseDeviceProperties(resp []byte, filter PropertiesFilter) map[string]string { 41 | matches := devicePropertyRegex.FindAllSubmatch(resp, -1) 42 | properties := make(map[string]string) 43 | for _, match := range matches { 44 | if len(match) < 3 { 45 | continue 46 | } 47 | key := string(match[1]) 48 | value := string(match[2]) 49 | 50 | if filter == nil || filter(key, value) { 51 | properties[key] = value 52 | } 53 | } 54 | return properties 55 | } 56 | 57 | // GetProperties adb shell getprop 58 | func (d *Device) GetProperties(filter PropertiesFilter) (properties AndroidProperties, err error) { 59 | resp, err := d.RunCommand("getprop") 60 | if err != nil { 61 | return 62 | } 63 | 64 | properties = parseDeviceProperties(resp, filter) 65 | if len(properties) == 0 { 66 | err = fmt.Errorf("not found any properties") 67 | } 68 | return 69 | } 70 | 71 | func (d *Device) GetProperty(name string) (value string, err error) { 72 | resp, err := d.RunCommand("getprop", name) 73 | if err != nil { 74 | return 75 | } 76 | value = string(bytes.TrimSpace(resp)) 77 | return 78 | } 79 | 80 | func (d *Device) BootCompleted() (bool, error) { 81 | booted, err := d.GetProperty(PropSysBootCompleted) 82 | if err != nil { 83 | return false, err 84 | } 85 | return booted == "1", nil 86 | } 87 | 88 | // SetProperty adb shell setprop 89 | func (d *Device) SetProperty(key, value string) (err error) { 90 | resp, err := d.RunCommand("setprop", key, value) 91 | if err != nil { 92 | return fmt.Errorf("'setprop %s %s' failed: %w", key, value, err) 93 | } 94 | 95 | resp = bytes.TrimSpace(resp) 96 | if len(resp) > 0 { 97 | err = fmt.Errorf("'setprop %s %s' failed: %s", key, value, resp) 98 | } 99 | return 100 | } 101 | 102 | func (a AndroidProperties) GetMapValue(key string) (string, error) { 103 | if v, ok := a[key]; ok { 104 | return v, nil 105 | } else { 106 | return "", fmt.Errorf("getprop %s: %w", key, ErrNotFound) 107 | } 108 | } 109 | 110 | func (a AndroidProperties) Serial() (string, error) { 111 | return a.GetMapValue(PropSerial) 112 | } 113 | 114 | func (a AndroidProperties) ProductName() (string, error) { 115 | return a.GetMapValue(PropProductName) 116 | } 117 | 118 | func (a AndroidProperties) ProductBrand() (string, error) { 119 | return a.GetMapValue(PropProductBrand) 120 | } 121 | 122 | func (a AndroidProperties) ProductManufacturer() (string, error) { 123 | return a.GetMapValue(PropProductManu) 124 | } 125 | 126 | func (a AndroidProperties) ProductModel() (string, error) { 127 | return a.GetMapValue(PropProductModel) 128 | } 129 | 130 | func (a AndroidProperties) CpuAbi() (string, error) { 131 | return a.GetMapValue(PropProductCpuAbi) 132 | } 133 | 134 | func (a AndroidProperties) SdkLevel() (int, error) { 135 | sdkstr, err := a.GetMapValue(PropBuildVersionSdk) 136 | if err != nil { 137 | sdkstr, err = a.GetMapValue(PropProductBuildVersionSdk) 138 | if err != nil { 139 | return -1, fmt.Errorf("neither %s nor %s prop found", PropBuildVersionSdk, PropProductBuildVersionSdk) 140 | } 141 | } 142 | v, err := strconv.Atoi(sdkstr) 143 | if err != nil { 144 | return 0, fmt.Errorf("parse 'getprop %s': %w", PropProductBuildVersionSdk, err) 145 | } 146 | return v, nil 147 | } 148 | 149 | func (a AndroidProperties) BuildVersion() (version *semver.Version, err error) { 150 | versionStr, err := a.GetMapValue(PropBuildVersionRelease) 151 | if err != nil { 152 | return 153 | } 154 | return semver.NewVersion(versionStr) 155 | } 156 | -------------------------------------------------------------------------------- /properties_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_parseDeviceProperties(t *testing.T) { 12 | props := []byte(` 13 | [ro.vendor.build.date]: [Thu Apr 18 22:16:50 CST 2024] 14 | [ro.vendor.build.date.utc]: [1713449810] 15 | [ro.vendor.build.fingerprint]: [OnePlus/PJD110/OP5929L1:14/UKQ1.230924.001/U.17a89d1_3ff5_3ff4:user/release-keys] 16 | [ro.build.description]: [msm8916_64-user 5.1.1 LMY47V eng.root.20161104.171401 dev-keys] 17 | `) 18 | 19 | m := parseDeviceProperties(props, nil) 20 | assert.Equal(t, len(m), 4) 21 | for k, v := range m { 22 | fmt.Printf("[%s]: [%s]\n", k, v) 23 | } 24 | } 25 | 26 | func TestDevice_GetProperites(t *testing.T) { 27 | assert.NotNil(t, adbclient) 28 | d := adbclient.Device(AnyDevice()) 29 | m, err := d.GetProperties(func(k, v string) bool { 30 | return strings.HasPrefix(k, "ro.") 31 | }) 32 | assert.Nil(t, err) 33 | // for k, v := range m { 34 | // fmt.Printf("[%s]: [%s]\n", k, v) 35 | // } 36 | 37 | level, err := m.SdkLevel() 38 | assert.Nil(t, err) 39 | fmt.Println("level:", level) 40 | 41 | buildVersion, err := m.BuildVersion() 42 | assert.Nil(t, err) 43 | fmt.Println("BuildVersion:", buildVersion) 44 | 45 | manu, err := m.ProductManufacturer() 46 | assert.Nil(t, err) 47 | fmt.Println("manu:", manu) 48 | 49 | brand, err := m.ProductBrand() 50 | assert.Nil(t, err) 51 | fmt.Println("brand:", brand) 52 | 53 | model, err := m.ProductModel() 54 | assert.Nil(t, err) 55 | fmt.Println("model:", model) 56 | 57 | name, err := m.ProductName() 58 | assert.Nil(t, err) 59 | fmt.Println("name:", name) 60 | 61 | serial, err := m.Serial() 62 | assert.Nil(t, err) 63 | fmt.Println("serial:", serial) 64 | 65 | booted, err := d.BootCompleted() 66 | assert.Nil(t, err) 67 | fmt.Println("booted:", booted) 68 | } 69 | 70 | func TestDevice_SetProperty(t *testing.T) { 71 | assert.NotNil(t, adbclient) 72 | d := adbclient.Device(AnyDevice()) 73 | err := d.SetProperty(PropProductName, "hello") 74 | assert.Contains(t, err.Error(), "Failed to set property") 75 | 76 | // err = d.SetProperty("hello.test", "hello") 77 | // assert.Nil(t, err) 78 | 79 | // value, err := d.GetProperty("hello.test") 80 | // assert.Nil(t, err) 81 | // assert.Equal(t, value, "hello") 82 | } 83 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | "time" 10 | 11 | "github.com/prife/goadb/wire" 12 | ) 13 | 14 | const ( 15 | AdbExecutableName = "adb" 16 | 17 | // Default port the adb server listens on. 18 | AdbPort = 5037 19 | DialTimeoutDefault = time.Second * 3 20 | ) 21 | 22 | type ServerConfig struct { 23 | // Dialer used to connect to the adb server. 24 | Dialer 25 | // Path to the adb executable. If empty, the PATH environment variable will be searched. 26 | PathToAdb string 27 | AutoStart bool 28 | DialTimeout time.Duration 29 | // Host and port the adb server is listening on. If not specified, will use the default port on localhost. 30 | Host string 31 | Port int 32 | fs *filesystem 33 | } 34 | 35 | // Server knows how to start the adb server and connect to it. 36 | type server interface { 37 | Start() error 38 | Dial() (wire.IConn, error) 39 | } 40 | 41 | func roundTripSingleResponse(s server, req string) (resp []byte, err error) { 42 | return roundTripSingleResponseTimeout(s, req, time.Second) 43 | } 44 | 45 | func roundTripSingleResponseTimeout(s server, req string, timeout time.Duration) (resp []byte, err error) { 46 | conn, err := s.Dial() 47 | if err != nil { 48 | return 49 | } 50 | defer conn.Close() 51 | 52 | if err = conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { 53 | return 54 | } 55 | if resp, err = conn.RoundTripSingleResponse([]byte(req)); err != nil { 56 | return 57 | } 58 | if err = conn.SetReadDeadline(time.Time{}); err != nil { 59 | return 60 | } 61 | return 62 | } 63 | 64 | type realServer struct { 65 | config ServerConfig 66 | 67 | // Caches Host:Port so they don't have to be concatenated for every dial. 68 | address string 69 | } 70 | 71 | func newServer(config ServerConfig) (server, error) { 72 | if config.Dialer == nil { 73 | config.Dialer = tcpDialer{} 74 | } 75 | 76 | if config.Host == "" { 77 | config.Host = "127.0.0.1" 78 | } 79 | if config.Port == 0 { 80 | config.Port = AdbPort 81 | } 82 | 83 | if config.fs == nil { 84 | config.fs = localFilesystem 85 | } 86 | 87 | if config.DialTimeout == 0 { 88 | config.DialTimeout = DialTimeoutDefault 89 | } 90 | 91 | if config.PathToAdb == "" { 92 | path, err := config.fs.LookPath(AdbExecutableName) 93 | if err != nil { 94 | return nil, fmt.Errorf("%w: could not find %s in PATH", wire.ErrServerNotAvailable, AdbExecutableName) 95 | } 96 | config.PathToAdb = path 97 | } 98 | if err := config.fs.IsExecutableFile(config.PathToAdb); err != nil { 99 | return nil, fmt.Errorf("%w: invalid adb executable: %s, err: %w", wire.ErrServerNotAvailable, config.PathToAdb, err) 100 | } 101 | 102 | return &realServer{ 103 | config: config, 104 | address: fmt.Sprintf("%s:%d", config.Host, config.Port), 105 | }, nil 106 | } 107 | 108 | // Dial tries to connect to the server. If the first attempt fails, tries starting the server before 109 | // retrying. If the second attempt fails, returns the error. 110 | func (s *realServer) Dial() (wire.IConn, error) { 111 | conn, err := s.config.Dial(s.address, s.config.DialTimeout) 112 | if err != nil { 113 | if !s.config.AutoStart { 114 | return nil, err 115 | } 116 | // Attempt to start the server and try again. 117 | if err = s.Start(); err != nil { 118 | return nil, fmt.Errorf("%w: error starting server for dial, err:%w", wire.ErrServerNotAvailable, err) 119 | } 120 | 121 | conn, err = s.config.Dial(s.address, s.config.DialTimeout) 122 | if err != nil { 123 | return nil, err 124 | } 125 | } 126 | return conn, nil 127 | } 128 | 129 | // StartServer ensures there is a server running. 130 | func (s *realServer) Start() error { 131 | output, err := s.config.fs.CmdCombinedOutput(s.config.PathToAdb /*"-L", fmt.Sprintf("tcp:%s", s.address),*/, "start-server") 132 | outputStr := strings.TrimSpace(string(output)) 133 | if err != nil { 134 | return fmt.Errorf("%w: error starting server: %w\noutput:\n%s", wire.ErrServerNotAvailable, err, outputStr) 135 | } 136 | return nil 137 | } 138 | 139 | // filesystem abstracts interactions with the local filesystem for testability. 140 | type filesystem struct { 141 | // Wraps exec.LookPath. 142 | LookPath func(string) (string, error) 143 | 144 | // Returns nil if path is a regular file and executable by the current user. 145 | IsExecutableFile func(path string) error 146 | 147 | // Wraps exec.Command().CombinedOutput() 148 | CmdCombinedOutput func(name string, arg ...string) ([]byte, error) 149 | } 150 | 151 | var localFilesystem = &filesystem{ 152 | LookPath: exec.LookPath, 153 | IsExecutableFile: func(path string) error { 154 | info, err := os.Stat(path) 155 | if err != nil { 156 | return err 157 | } 158 | if !info.Mode().IsRegular() { 159 | return errors.New("not a regular file") 160 | } 161 | return isExecutable(path) 162 | }, 163 | CmdCombinedOutput: func(name string, arg ...string) ([]byte, error) { 164 | return exec.Command(name, arg...).CombinedOutput() 165 | }, 166 | } 167 | -------------------------------------------------------------------------------- /server_mock_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | "strings" 8 | "time" 9 | 10 | "github.com/prife/goadb/wire" 11 | ) 12 | 13 | // MockServer implements Server, Scanner, and Sender. 14 | type MockServer struct { 15 | mockConn 16 | // Each time an operation is performed, if this slice is non-empty, the head element 17 | // of this slice is returned and removed from the slice. If the head is nil, it is removed 18 | // but not returned. 19 | Errs []error 20 | 21 | Status string 22 | 23 | // Messages are returned from read calls in order, each preceded by a length header. 24 | Messages []string 25 | nextMsgIndex int 26 | 27 | // Each message passed to a send call is appended to this slice. 28 | Requests []string 29 | 30 | // Each time an operation is performed, its name is appended to this slice. 31 | Trace []string 32 | } 33 | 34 | var _ server = &MockServer{ 35 | mockConn: mockConn{ 36 | Buffer: bytes.NewBuffer(nil), 37 | rbuf: bytes.NewBuffer(nil), 38 | }, 39 | } 40 | 41 | func (s *MockServer) Dial() (wire.IConn, error) { 42 | s.logMethod("Dial") 43 | if err := s.getNextErrToReturn(); err != nil { 44 | return nil, err 45 | } 46 | return s, nil 47 | } 48 | 49 | func (s *MockServer) Start() error { 50 | s.logMethod("Start") 51 | return nil 52 | } 53 | 54 | func (s *MockServer) RoundTripSingleResponse(req []byte) (resp []byte, err error) { 55 | if err = s.SendMessage(req); err != nil { 56 | return nil, err 57 | } 58 | 59 | if _, err = s.ReadStatus(string(req)); err != nil { 60 | return nil, err 61 | } 62 | 63 | return s.ReadMessage() 64 | } 65 | 66 | func (s *MockServer) ReadStatus(req string) (string, error) { 67 | s.logMethod("ReadStatus") 68 | if err := s.getNextErrToReturn(); err != nil { 69 | return "", err 70 | } 71 | return s.Status, nil 72 | } 73 | 74 | func (s *MockServer) ReadMessage() ([]byte, error) { 75 | s.logMethod("ReadMessage") 76 | if err := s.getNextErrToReturn(); err != nil { 77 | return nil, err 78 | } 79 | if s.nextMsgIndex >= len(s.Messages) { 80 | return nil, io.EOF 81 | } 82 | 83 | s.nextMsgIndex++ 84 | return []byte(s.Messages[s.nextMsgIndex-1]), nil 85 | } 86 | 87 | func (s *MockServer) ReadUntilEof() ([]byte, error) { 88 | s.logMethod("ReadUntilEof") 89 | if err := s.getNextErrToReturn(); err != nil { 90 | return nil, err 91 | } 92 | 93 | var data []string 94 | for ; s.nextMsgIndex < len(s.Messages); s.nextMsgIndex++ { 95 | data = append(data, s.Messages[s.nextMsgIndex]) 96 | } 97 | return []byte(strings.Join(data, "")), nil 98 | } 99 | 100 | func (s *MockServer) SendMessage(msg []byte) error { 101 | s.logMethod("SendMessage") 102 | if err := s.getNextErrToReturn(); err != nil { 103 | return err 104 | } 105 | s.Requests = append(s.Requests, string(msg)) 106 | return nil 107 | } 108 | 109 | func (s *MockServer) Close() error { 110 | s.logMethod("Close") 111 | if err := s.getNextErrToReturn(); err != nil { 112 | return err 113 | } 114 | return nil 115 | } 116 | 117 | func (s *MockServer) getNextErrToReturn() (err error) { 118 | if len(s.Errs) > 0 { 119 | err = s.Errs[0] 120 | s.Errs = s.Errs[1:] 121 | } 122 | return 123 | } 124 | 125 | func (s *MockServer) logMethod(name string) { 126 | s.Trace = append(s.Trace, name) 127 | } 128 | 129 | // mockConn is a wrapper around a bytes.Buffer that implements net.Conn 130 | type mockConn struct { 131 | // io.ReadWriter 132 | *bytes.Buffer // write buffer 133 | rbuf *bytes.Buffer 134 | } 135 | 136 | func (b *mockConn) Read(p []byte) (n int, err error) { 137 | if b.rbuf != nil { 138 | return b.rbuf.Read(p) 139 | } 140 | 141 | return b.Buffer.Read(p) 142 | } 143 | 144 | func (b *mockConn) Write(p []byte) (n int, err error) { 145 | return b.Buffer.Write(p) 146 | } 147 | 148 | func (b *mockConn) Close() error { 149 | // No-op. 150 | return nil 151 | } 152 | 153 | func (b *mockConn) LocalAddr() net.Addr { 154 | return nil 155 | } 156 | func (b *mockConn) RemoteAddr() net.Addr { 157 | return nil 158 | } 159 | 160 | func (b *mockConn) SetDeadline(t time.Time) error { 161 | return nil 162 | } 163 | 164 | func (b *mockConn) SetReadDeadline(t time.Time) error { 165 | return nil 166 | } 167 | 168 | func (b *mockConn) SetWriteDeadline(t time.Time) error { 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/prife/goadb/wire" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewServer_ZeroConfig(t *testing.T) { 13 | config := ServerConfig{fs: &filesystem{ 14 | LookPath: func(name string) (string, error) { 15 | if name == AdbExecutableName { 16 | return "/bin/adb", nil 17 | } 18 | return "", fmt.Errorf("invalid name: %s", name) 19 | }, 20 | IsExecutableFile: func(path string) error { 21 | if path == "/bin/adb" { 22 | return nil 23 | } 24 | return fmt.Errorf("wrong path: %s", path) 25 | }, 26 | }} 27 | 28 | serverIf, err := newServer(config) 29 | server := serverIf.(*realServer) 30 | assert.NoError(t, err) 31 | assert.IsType(t, tcpDialer{}, server.config.Dialer) 32 | assert.Equal(t, "127.0.0.1", server.config.Host) 33 | assert.Equal(t, AdbPort, server.config.Port) 34 | assert.Equal(t, fmt.Sprintf("127.0.0.1:%d", AdbPort), server.address) 35 | assert.Equal(t, "/bin/adb", server.config.PathToAdb) 36 | } 37 | 38 | type MockDialer struct{} 39 | 40 | func (d MockDialer) Dial(address string, timeout time.Duration) (*wire.Conn, error) { 41 | return nil, nil 42 | } 43 | 44 | func TestNewServer_CustomConfig(t *testing.T) { 45 | config := ServerConfig{ 46 | Dialer: MockDialer{}, 47 | Host: "foobar", 48 | Port: 1, 49 | PathToAdb: "/bin/adb", 50 | fs: &filesystem{ 51 | IsExecutableFile: func(path string) error { 52 | if path == "/bin/adb" { 53 | return nil 54 | } 55 | return fmt.Errorf("wrong path: %s", path) 56 | }, 57 | }, 58 | } 59 | 60 | serverIf, err := newServer(config) 61 | server := serverIf.(*realServer) 62 | assert.NoError(t, err) 63 | assert.IsType(t, MockDialer{}, server.config.Dialer) 64 | assert.Equal(t, "foobar", server.config.Host) 65 | assert.Equal(t, 1, server.config.Port) 66 | assert.Equal(t, fmt.Sprintf("foobar:1"), server.address) 67 | assert.Equal(t, "/bin/adb", server.config.PathToAdb) 68 | } 69 | 70 | func TestNewServer_AdbNotFound(t *testing.T) { 71 | config := ServerConfig{fs: &filesystem{ 72 | LookPath: func(name string) (string, error) { 73 | return "", fmt.Errorf("executable not found: %s", name) 74 | }, 75 | }} 76 | 77 | _, err := newServer(config) 78 | assert.EqualError(t, err, "ServerNotAvailable: could not find adb in PATH") 79 | } 80 | -------------------------------------------------------------------------------- /services/discovery.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | adb "github.com/prife/goadb" 7 | "github.com/prife/gomlib" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func InitAdb() (cli *adb.Adb, err error) { 12 | serverConfig := adb.ServerConfig{ 13 | AutoStart: true, 14 | Host: "127.0.0.1", 15 | Port: 5037, 16 | } 17 | 18 | /* 19 | out, err := exec.Command("adb", "start-server").CombinedOutput() 20 | if err != nil { 21 | panic(out) 22 | } 23 | */ 24 | 25 | cli, err = adb.NewWithConfig(serverConfig) 26 | if err != nil { 27 | log.Errorln(err) 28 | return 29 | } 30 | 31 | err = cli.StartServer() 32 | if err != nil { 33 | log.Errorln(err) 34 | return 35 | } 36 | return 37 | } 38 | 39 | func Monitor(ctx context.Context, r *gomlib.Registry) (err error) { 40 | client, err := InitAdb() 41 | if err != nil { 42 | return 43 | } 44 | 45 | watcher := client.NewDeviceWatcher() 46 | for event := range watcher.C() { 47 | log.Infof("adb-monitor: %+v", event) 48 | switch event.NewState { 49 | case adb.StateOnline: 50 | case adb.StateInvalid, 51 | adb.StateUnauthorized, 52 | adb.StateAuthorizing, 53 | adb.StateDisconnected, 54 | adb.StateOffline, 55 | adb.StateHost: 56 | default: 57 | log.Fatalf("adb-monitor: unknown listen message type: %#v", event) 58 | } 59 | } 60 | 61 | return watcher.Err() 62 | } 63 | -------------------------------------------------------------------------------- /services/discovery_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/prife/goadb/services" 8 | "github.com/prife/gomlib" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func TestMonitor(t *testing.T) { 13 | // ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 14 | // defer cancel() 15 | ctx := context.Background() 16 | 17 | registry := gomlib.NewRegistry() 18 | registry.Listen(gomlib.NewDeviceListener( 19 | func(ctx context.Context, d gomlib.DeviceEntry) { 20 | log.Infof("--> added device:%#v", d) 21 | }, func(ctx context.Context, d gomlib.DeviceEntry) { 22 | log.Infof("--> removed device:%#v", d) 23 | }, 24 | )) 25 | 26 | ch := make(chan error) 27 | go func() { 28 | log.Infoln("=== adb monitor begin === ") 29 | err := services.Monitor(ctx, registry) 30 | log.Errorln("=== adb monitor quit ===", err) 31 | ch <- err 32 | }() 33 | 34 | select { 35 | case <-ctx.Done(): 36 | case err := <-ch: 37 | t.Fatalf("quit:%v", err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The ChromiumOS Authors 2 | // Use of this source code is governed by a MIT License that can be 3 | // found in the LICENSE file. 4 | 5 | package adb 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "os" 13 | 14 | "github.com/prife/goadb/wire" 15 | ) 16 | 17 | // A Session represents a connection to a remote command or shell. 18 | type Session struct { 19 | // Stdin specifies the remote process's standard input. 20 | // If Stdin is nil, the remote process reads from an empty 21 | // bytes.Buffer. 22 | Stdin io.Reader 23 | 24 | // Stdout and Stderr specify the remote process's standard 25 | // output and error. 26 | // 27 | // If either is nil, Run connects the corresponding file 28 | // descriptor to an instance of io.Discard. There is a 29 | // fixed amount of buffering that is shared for the two streams. 30 | // If either blocks it may eventually cause the remote 31 | // command to block. 32 | Stdout io.Writer 33 | Stderr io.Writer 34 | 35 | transport *wire.Conn 36 | errorChan chan error 37 | abort bool 38 | handlesToClose []io.Closer 39 | } 40 | 41 | // ExitMissingError is returned if a session is torn down cleanly, but the server sends no confirmation of the exit status. 42 | type ExitMissingError struct{} 43 | 44 | // Error returns error message. 45 | func (e *ExitMissingError) Error() string { 46 | return "command exited without exit status" 47 | } 48 | 49 | // An ExitError reports unsuccessful completion of a remote command. 50 | type ExitError struct { 51 | Waitmsg 52 | } 53 | 54 | func (e *ExitError) Error() string { 55 | return fmt.Sprintf("unexpected error code %d", e.exitStatus) 56 | } 57 | 58 | // Waitmsg stores the information about an exited remote command as reported by Wait. 59 | type Waitmsg struct { 60 | exitStatus int 61 | } 62 | 63 | // ExitStatus returns the exit status of the remote command. 64 | func (w Waitmsg) ExitStatus() int { 65 | return w.exitStatus 66 | } 67 | 68 | // NewSession opens a new Session for this client. (A session is a remote execution of a program.) 69 | func (d *Device) NewSession() (*Session, error) { 70 | conn, err := d.dialDevice(d.CmdTimeoutShort) 71 | if err != nil { 72 | return nil, fmt.Errorf("failed to create transport: %w", err) 73 | } 74 | return &Session{transport: conn.(*wire.Conn)}, nil 75 | } 76 | 77 | // Close frees resources associated with this Session, and aborts any running command. 78 | func (s *Session) Close() error { 79 | var err error 80 | s.abort = true 81 | if s.transport != nil { 82 | err = s.transport.Close() 83 | } 84 | s.transport = nil 85 | err = errors.Join(err, s.closeFiles()) 86 | return err 87 | } 88 | 89 | func (s *Session) closeFiles() error { 90 | var err error 91 | for _, f := range s.handlesToClose { 92 | fErr := f.Close() 93 | err = errors.Join(err, fErr) 94 | } 95 | s.handlesToClose = nil 96 | return err 97 | } 98 | 99 | // CombinedOutput runs cmd on the remote host and returns its combined standard output and standard error. 100 | func (s *Session) CombinedOutput(cmd string) ([]byte, error) { 101 | if s.Stdout != nil { 102 | return nil, errors.New("can't set Stdout and call CombinedOutput()") 103 | } 104 | if s.Stderr != nil { 105 | return nil, errors.New("can't set Stdout and call CombinedOutput()") 106 | } 107 | var output bytes.Buffer 108 | s.Stdout = &output 109 | s.Stderr = &output 110 | if err := s.Run(cmd); err != nil { 111 | return output.Bytes(), err 112 | } 113 | return output.Bytes(), nil 114 | } 115 | 116 | // Output runs cmd on the remote host and returns its standard output. 117 | func (s *Session) Output(cmd string) ([]byte, error) { 118 | if s.Stdout != nil { 119 | return nil, errors.New("can't set Stdout and call Output()") 120 | } 121 | var output bytes.Buffer 122 | s.Stdout = &output 123 | if err := s.Run(cmd); err != nil { 124 | return output.Bytes(), err 125 | } 126 | return output.Bytes(), nil 127 | } 128 | 129 | // Run runs cmd on the remote host. 130 | func (s *Session) Run(cmd string) error { 131 | if err := s.Start(cmd); err != nil { 132 | return err 133 | } 134 | if err := s.Wait(); err != nil { 135 | return err 136 | } 137 | return nil 138 | } 139 | 140 | // Start runs cmd on the remote host. 141 | func (s *Session) Start(cmd string) error { 142 | if s.errorChan != nil { 143 | return errors.New("Start() already called") 144 | } 145 | 146 | req := fmt.Sprintf("shell,v2,raw:%s", cmd) 147 | if err := s.transport.SendMessage([]byte(req)); err != nil { 148 | return fmt.Errorf("failed to send shell cmd: %w", err) 149 | } 150 | 151 | if _, err := readStatusWithTimeout(s.transport, req, CommandTimeoutShortDefault); err != nil { 152 | s.transport.Close() 153 | return fmt.Errorf("failed to verify shell cmd: %w", err) 154 | } 155 | 156 | shellTp := newShellTransport(s.transport.Conn, 0) 157 | s.errorChan = make(chan error) 158 | s.abort = false 159 | // Copy stdin to remote command 160 | if s.Stdin != nil { 161 | go func() { 162 | buffer := make([]byte, 1024) 163 | for !s.abort { 164 | n, err := s.Stdin.Read(buffer) 165 | if err == io.EOF { 166 | if err := shellTp.Send(shellCloseStdin, []byte{}); err != nil { 167 | s.errorChan <- fmt.Errorf("failed to close stdin: %w", err) 168 | } 169 | return 170 | } 171 | if err != nil { 172 | s.errorChan <- fmt.Errorf("failed to copy stdin: %w", err) 173 | return 174 | } 175 | shellTp.Send(shellStdin, buffer[0:n]) 176 | } 177 | }() 178 | } else { 179 | if err := shellTp.Send(shellCloseStdin, []byte{}); err != nil { 180 | return fmt.Errorf("failed to close stdin: %w", err) 181 | } 182 | } 183 | go func() { 184 | for !s.abort { 185 | msgType, msg, err := shellTp.Read() 186 | if err == io.EOF { 187 | break 188 | } 189 | if err != nil { 190 | s.errorChan <- fmt.Errorf("failed to read shell msg: %w", err) 191 | return 192 | } 193 | switch msgType { 194 | case shellStdout: // stdout 195 | if s.Stdout != nil { 196 | if _, err := s.Stdout.Write(msg); err != nil { 197 | s.errorChan <- fmt.Errorf("failed to write stdout: %w", err) 198 | return 199 | } 200 | } 201 | case shellStderr: // stderr 202 | if s.Stderr != nil { 203 | if _, err := s.Stderr.Write(msg); err != nil { 204 | s.errorChan <- fmt.Errorf("failed to write stderr: %w", err) 205 | return 206 | } 207 | } 208 | case shellExit: // exit 209 | exitCode := int(msg[0]) 210 | err := s.closeFiles() 211 | if err != nil { 212 | s.errorChan <- fmt.Errorf("failed to close files: %w", err) 213 | } 214 | if exitCode == 0 { 215 | s.errorChan <- nil 216 | } else { 217 | s.errorChan <- &ExitError{ 218 | Waitmsg: Waitmsg{exitStatus: exitCode}, 219 | } 220 | } 221 | return 222 | default: 223 | s.errorChan <- fmt.Errorf("unexpected shell message %d: %w", msgType, err) 224 | return 225 | } 226 | } 227 | s.errorChan <- &ExitMissingError{} 228 | }() 229 | return nil 230 | } 231 | 232 | // StderrPipe returns a pipe that will be connected to the remote command's standard error when the command starts. 233 | func (s *Session) StderrPipe() (io.Reader, error) { 234 | if s.Stderr != nil { 235 | return nil, errors.New("can't set Stderr and call StderrPipe()") 236 | } 237 | if s.errorChan != nil { 238 | return nil, errors.New("StderrPipe called after Start()") 239 | } 240 | pr, pw, err := os.Pipe() 241 | if err != nil { 242 | return nil, err 243 | } 244 | s.Stderr = pw 245 | s.handlesToClose = append(s.handlesToClose, pw) 246 | return pr, nil 247 | } 248 | 249 | // StdinPipe returns a pipe that will be connected to the remote command's standard input when the command starts. 250 | func (s *Session) StdinPipe() (io.WriteCloser, error) { 251 | if s.Stdin != nil { 252 | return nil, errors.New("can't set Stdin and call StdinPipe()") 253 | } 254 | if s.errorChan != nil { 255 | return nil, errors.New("StdinPipe called after Start()") 256 | } 257 | pr, pw, err := os.Pipe() 258 | if err != nil { 259 | return nil, err 260 | } 261 | s.Stdin = pr 262 | s.handlesToClose = append(s.handlesToClose, pr) 263 | return pw, nil 264 | } 265 | 266 | // StdoutPipe returns a pipe that will be connected to the remote command's standard output when the command starts. 267 | func (s *Session) StdoutPipe() (io.Reader, error) { 268 | if s.Stdout != nil { 269 | return nil, errors.New("can't set Stdout and call StdoutPipe()") 270 | } 271 | if s.errorChan != nil { 272 | return nil, errors.New("StdoutPipe called after Start()") 273 | } 274 | pr, pw, err := os.Pipe() 275 | if err != nil { 276 | return nil, err 277 | } 278 | s.Stdout = pw 279 | s.handlesToClose = append(s.handlesToClose, pw) 280 | return pr, nil 281 | } 282 | 283 | // Wait waits for the remote command to exit. 284 | func (s *Session) Wait() error { 285 | if s.errorChan == nil { 286 | return errors.New("Wait() called before Start()") 287 | } 288 | if s.abort { 289 | return errors.New("Wait() called twice or after Close()") 290 | } 291 | backgroundErr := <-s.errorChan 292 | if err := s.Close(); err != nil { 293 | return errors.Join(backgroundErr, err) 294 | } 295 | return backgroundErr 296 | } 297 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The ChromiumOS Authors 2 | // Use of this source code is governed by a MIT License that can be 3 | // found in the LICENSE file. 4 | 5 | package adb_test 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "testing" 11 | 12 | adb "github.com/prife/goadb" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestDevice_Session(t *testing.T) { 17 | d := adbclient.Device(adb.AnyDevice()) 18 | session, err := d.NewSession() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | defer session.Close() 23 | 24 | out, err := session.CombinedOutput("date") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | fmt.Println(string(out)) 29 | } 30 | 31 | func TestDevice_SessionNonExistedCommand(t *testing.T) { 32 | d := adbclient.Device(adb.AnyDevice()) 33 | session, err := d.NewSession() 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | defer session.Close() 38 | 39 | out, err := session.CombinedOutput("non-existed-cmd") 40 | assert.NotNil(t, err) 41 | assert.Equal(t, err.Error(), "unexpected error code 127") 42 | fmt.Println(string(out)) 43 | } 44 | 45 | func TestDevice_SessionLogcat(t *testing.T) { 46 | d := adbclient.Device(adb.AnyDevice()) 47 | session, err := d.NewSession() 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | defer session.Close() 52 | session.Stdout = os.Stdout 53 | session.Stderr = os.Stderr 54 | err = session.Run("logcat") 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /settings.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func checkNameValid(name string) bool { 8 | return !(name == "" || name == "null" || strings.Contains(strings.ToLower(name), "error")) 9 | } 10 | 11 | // see: https://stackoverflow.com/questions/16704597/how-do-you-get-the-user-defined-device-name-in-android 12 | func (d *Device) GetDeviceName() (name string, err error) { 13 | // fist try 14 | resp, err := d.RunCommand("settings get global device_name") 15 | if err != nil { 16 | return 17 | } 18 | name = string(resp) 19 | if checkNameValid(name) { 20 | return 21 | } 22 | 23 | // try again 24 | resp, err = d.RunCommand("settings get secure bluetooth_name") 25 | if err != nil { 26 | return 27 | } 28 | name = string(resp) 29 | if checkNameValid(name) { 30 | return 31 | } 32 | 33 | // final try 34 | name, err = d.GetProperty(PropProductName) 35 | return 36 | } 37 | 38 | func (d *Device) SetAccelerometerRotation(enable bool) error { 39 | var value string 40 | if enable { 41 | value = "1" 42 | } else { 43 | value = "0" 44 | } 45 | _, err := d.RunCommand("settings put system accelerometer_rotation " + value) 46 | return err 47 | } 48 | -------------------------------------------------------------------------------- /shell.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net" 9 | "time" 10 | 11 | "github.com/prife/goadb/wire" 12 | ) 13 | 14 | // RunShellCommand runs the specified commands on a shell on the device. 15 | // From the Android docs: 16 | // 17 | // Run 'command arg1 arg2 ...' in a shell on the device, and return 18 | // its output and error streams. Note that arguments must be separated 19 | // by spaces. If an argument contains a space, it must be quoted with 20 | // double-quotes. Arguments cannot contain double quotes or things 21 | // will go very wrong. 22 | // Note that this is the non-interactive version of "adb shell" 23 | // 24 | // Source: https://android.googlesource.com/platform/system/core/+/master/adb/SERVICES.TXT 25 | // This method quotes the arguments for you, and will return an error if any of them 26 | // contain double quotes. 27 | // 28 | // shell:echo 1 29 | // 00000000 31 0a |1.| 30 | // shell:echo 12 31 | // 00000000 31 32 0a |12.| 32 | // 33 | // shell,v2:echo 1 34 | // 00000000 01 02 00 00 00 31 0a 03 01 00 00 00 00 |.....1.......| 35 | // shell,v2:echo 12 36 | // 00000000 01 03 00 00 00 31 32 0a 03 01 00 00 00 00 |.....12.......| 37 | // 38 | // shell,v2:pm list pacakges -3 39 | // 00000000 01 df 06 00 00 70 61 63 6b 61 67 65 3a 63 6f 6d |.....package:com| 40 | // ... 41 | // 000006e0 65 61 64 0a 03 01 00 00 00 00 |ead.......| 42 | // 43 | // shell,v2:pm clear kage:com.heytap.smarthome 44 | // 00000000 02 9f 04 00 00 0a 45 78 63 65 70 74 69 6f 6e 20 |......Exception | 45 | // ........ 46 | // 000004a0 39 39 29 0a 03 01 00 00 00 ff |99).......| 47 | // 48 | // v2协议,在应用输出的开头包裹了5个字符,其中的第2~5个字符似乎是小端表示的4字节长度 49 | // 在应用输出的结尾包裹了6个字符,似乎总是 03 01 00 00 00 [00 or ff] 50 | // 参考:https://stackoverflow.com/questions/13578416/read-binary-stdout-data-like-screencap-data-from-adb-shell 51 | func (c *Device) RunShellCommand(v2 bool, cmd string, args ...string) (fn net.Conn, err error) { 52 | cmd, err = prepareCommandLine(cmd, args...) 53 | if err != nil { 54 | return nil, wrapClientError(err, c, "RunCommand") 55 | } 56 | 57 | conn, err := c.dialDevice(c.CmdTimeoutShort) 58 | if err != nil { 59 | return nil, wrapClientError(err, c, "RunCommand") 60 | } 61 | 62 | var req string 63 | if v2 { 64 | req = fmt.Sprintf("shell,v2:%s", cmd) 65 | } else { 66 | req = fmt.Sprintf("shell:%s", cmd) 67 | } 68 | // req := fmt.Sprintf("shell,v2:%s", cmd) 69 | // req := fmt.Sprintf("shell,v2,TERM=xterm-256color:%s", cmd) 70 | 71 | // Shell responses are special, they don't include a length header. 72 | // We read until the stream is closed. 73 | // So, we can't use conn.RoundTripSingleResponse. 74 | // fmt.Println("run command: ", req) 75 | if err = conn.SendMessage([]byte(req)); err != nil { 76 | conn.Close() 77 | return nil, wrapClientError(err, c, "RunCommand") 78 | } 79 | 80 | if _, err = readStatusWithTimeout(conn, req, c.CmdTimeoutShort); err != nil { 81 | conn.Close() 82 | return nil, wrapClientError(err, c, "RunCommand") 83 | } 84 | 85 | return conn, wrapClientError(err, c, "RunCommand") 86 | } 87 | 88 | func (c *Device) RunCommandTimeout(timeout time.Duration, cmd string, args ...string) (resp []byte, err error) { 89 | conn, err := c.RunShellCommand(false, cmd, args...) 90 | if err != nil { 91 | return nil, err 92 | } 93 | defer conn.Close() 94 | 95 | // set read timeout 96 | if timeout > 0 { 97 | if err = conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { 98 | return nil, wrapClientError(err, c, "RunCommand") 99 | } 100 | } 101 | resp, err = io.ReadAll(conn) 102 | if err != nil { 103 | return 104 | } 105 | // fmt.Println(hex.Dump(resp)) 106 | // fmt.Println("----------------") 107 | // fmt.Printf("%s", resp) 108 | return 109 | } 110 | 111 | // RunCommand default timeout is CommandTimeoutShortDefault which is 2 seconds, be careful 112 | func (c *Device) RunCommand(cmd string, args ...string) ([]byte, error) { 113 | return c.RunCommandTimeout(c.CmdTimeoutShort, cmd, args...) 114 | } 115 | 116 | // RunCommandCtx wrap RunShellCommand with context 117 | func (c *Device) RunCommandCtx(ctx context.Context, writer io.Writer, cmd string, args ...string) error { 118 | conn, err := c.RunShellCommand(false, cmd, args...) 119 | if err != nil { 120 | return err 121 | } 122 | defer conn.Close() 123 | buf := make([]byte, wire.SyncMaxChunkSize) 124 | ch := make(chan error, 2) 125 | go func() { 126 | for { 127 | n, err := conn.Read(buf) 128 | if writer != nil && n > 0 { 129 | writer.Write(buf[:n]) 130 | } 131 | 132 | // shell v1 协议无法确认 connection 结束的真正原因,实际测试效果如下: 133 | // 1. 手机 adb 连接正常,程序正常结束,err返回 EOF 134 | // 2. 手机 adb 连接正常,kill 掉正在执行的程序,err 也会返回 EOF 135 | // 3. 如果进程执行中,断开 USB 线,err 还会返回 EOF 136 | // 综上: 需要支持 v2 协议,才有可能区分上述三种情况。 137 | if err != nil { 138 | // fmt.Println("err:", err) 139 | ch <- io.EOF 140 | return 141 | } 142 | } 143 | }() 144 | 145 | select { 146 | case <-ctx.Done(): 147 | return ctx.Err() 148 | case err := <-ch: 149 | if err == io.EOF { 150 | return nil 151 | } 152 | return err 153 | } 154 | } 155 | 156 | func (c *Device) RunCommandOutputCtx(ctx context.Context, cmd string, args ...string) ([]byte, error) { 157 | buf := &bytes.Buffer{} 158 | err := c.RunCommandCtx(ctx, buf, cmd, args...) 159 | return buf.Bytes(), err 160 | } 161 | -------------------------------------------------------------------------------- /shell_test.go: -------------------------------------------------------------------------------- 1 | package adb_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | adb "github.com/prife/goadb" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestDevice_RunCommandCtx(t *testing.T) { 13 | assert.NotNil(t, adbclient) 14 | d := adbclient.Device(adb.AnyDevice()) 15 | out, err := d.RunCommandOutputCtx(context.TODO(), "/data/local/tmp/test") 16 | assert.Nil(t, err) 17 | fmt.Printf("out:[%s]\n", out) 18 | } 19 | -------------------------------------------------------------------------------- /shell_transport.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The ChromiumOS Authors 2 | // Use of this source code is governed by a MIT License that can be 3 | // found in the LICENSE file. 4 | 5 | package adb 6 | 7 | import ( 8 | "bytes" 9 | "encoding/binary" 10 | "fmt" 11 | "io" 12 | "net" 13 | "time" 14 | ) 15 | 16 | type shellTransport struct { 17 | sock net.Conn 18 | readTimeout time.Duration 19 | } 20 | 21 | // Shell protocol message types. 22 | type shellMessageType byte 23 | 24 | const ( 25 | shellStdin shellMessageType = 0 26 | shellStdout shellMessageType = 1 27 | shellStderr shellMessageType = 2 28 | shellExit shellMessageType = 3 29 | shellCloseStdin shellMessageType = 4 30 | ) 31 | 32 | func newShellTransport(sock net.Conn, readTimeout time.Duration) shellTransport { 33 | return shellTransport{sock: sock, readTimeout: readTimeout} 34 | } 35 | 36 | // Send creates and sends a packet over the shell protocol. 37 | func (s *shellTransport) Send(command shellMessageType, data []byte) (err error) { 38 | msg := new(bytes.Buffer) 39 | if err := msg.WriteByte(byte(command)); err != nil { 40 | return fmt.Errorf("shell transport write: %w", err) 41 | } 42 | if err := binary.Write(msg, binary.LittleEndian, int32(len(data))); err != nil { 43 | return fmt.Errorf("shell transport write: %w", err) 44 | } 45 | if _, err := msg.Write(data); err != nil { 46 | return fmt.Errorf("shell transport write: %w", err) 47 | } 48 | 49 | debugLog(fmt.Sprintf("--> %v", msg.Bytes())) 50 | _, err = s.sock.Write(msg.Bytes()) 51 | return 52 | } 53 | 54 | func (s *shellTransport) Read() (command shellMessageType, data []byte, err error) { 55 | err = binary.Read(s.sock, binary.LittleEndian, &command) 56 | if err == io.EOF { 57 | return 255, nil, err 58 | } 59 | if err != nil { 60 | return 255, nil, fmt.Errorf("failed to read response msg type: %w", err) 61 | } 62 | var msgLen uint32 63 | err = binary.Read(s.sock, binary.LittleEndian, &msgLen) 64 | if err != nil { 65 | return command, nil, fmt.Errorf("failed to read response msg len: %w", err) 66 | } 67 | 68 | data = make([]byte, int(msgLen)) 69 | _, err = io.ReadFull(s.sock, data) 70 | if err != nil { 71 | return command, data, fmt.Errorf("failed to read response msg body: %w", err) 72 | } 73 | return command, data, nil 74 | } 75 | 76 | func (s *shellTransport) Close() (err error) { 77 | if s.sock == nil { 78 | return nil 79 | } 80 | return s.sock.Close() 81 | } 82 | -------------------------------------------------------------------------------- /sync.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io/fs" 9 | "os" 10 | "path/filepath" 11 | "time" 12 | 13 | "github.com/prife/goadb/wire" 14 | ) 15 | 16 | func ListAllSubDirs(localDir string) (list []string, err error) { 17 | err = filepath.WalkDir(localDir, func(path string, d fs.DirEntry, err error) error { 18 | if err != nil { 19 | return err 20 | } 21 | if path == localDir { 22 | return nil 23 | } 24 | // ignore special file 25 | if d.IsDir() { 26 | relativePath, _ := filepath.Rel(localDir, path) 27 | list = append(list, relativePath) 28 | } 29 | return nil 30 | }) 31 | return 32 | } 33 | 34 | // adb shell mkdir 35 | // # Android 14 36 | // 37 | // OP5929L1:/ $ mkdir /a /b /c 38 | // mkdir: '/a': Read-only file system 39 | // mkdir: '/b': Read-only file system 40 | // mkdir: '/c': Read-only file system 41 | // 1|OP5929L1:/ $ 42 | // 43 | // OP5929L1:/ $ mkdir /sdcard/a /sdcard/b /sdcard/c 44 | // OP5929L1:/ $ mkdir /sdcard/a /sdcard/b /sdcard/c 45 | // mkdir: '/sdcard/a': File exists 46 | // mkdir: '/sdcard/b': File exists 47 | // mkdir: '/sdcard/c': File exists 48 | // 1|OP5929L1:/ $ 49 | // 50 | // OP5929L1:/ $ mkdir /data/a /data/b /data/c 51 | // mkdir: '/data/a': Permission denied 52 | // mkdir: '/data/b': Permission denied 53 | // mkdir: '/data/c': Permission denied 54 | // 1|OP5929L1:/ $ 55 | // 56 | // OP5929L1:/ $ mkdir /sd/a /sd/b /sd/c 57 | // mkdir: '/sd/a': No such file or directory 58 | // mkdir: '/sd/b': No such file or directory 59 | // mkdir: '/sd/c': No such file or directory 60 | // 61 | // # Android 5.1 62 | // 63 | // shell@A33:/ $ mkdir /sdcard/a /sdcard/b /sdcard/c 64 | // shell@A33:/ $ mkdir /sdcard/a /sdcard/b /sdcard/c 65 | // mkdir failed for /sdcard/a, File exists 66 | // 255|shell@A33:/ $ 67 | // 68 | // shell@A33:/ $ mkdir /sd/a /sd/b /sd/c 69 | // mkdir failed for /sd/a, No such file or directory 70 | // 255|shell@A33:/ $ 71 | // 72 | // shell@A33:/ $ mkdir /a /b /c 73 | // mkdir failed for /a, Read-only file system 74 | // 255|shell@A33:/ $ 75 | // 76 | // shell@A33:/ $ mkdir /data/a /data/b /data/c 77 | // mkdir failed for /data/a, Permission denied 78 | // 255|shell@A33:/ $ 79 | func filterFileExistedError(resp []byte) (errs []error) { 80 | lines := bytes.Split(resp, []byte("\n")) 81 | for _, line := range lines { 82 | line := bytes.TrimSpace(line) 83 | if len(line) > 0 && !bytes.HasSuffix(line, []byte("File exists")) { 84 | errs = append(errs, errors.New(string(line))) 85 | } 86 | } 87 | return 88 | } 89 | 90 | func (c *Device) Mkdirs(list []string) error { 91 | return c.MkdirsWithParent(list, false) 92 | } 93 | 94 | // adb shell mkdir [-p] ... 95 | func (c *Device) MkdirsWithParent(list []string, withParent bool) error { 96 | var commands []string 97 | var commandsLen int 98 | 99 | var errs []error 100 | if withParent { 101 | commands = append(commands, "-p") 102 | } 103 | for _, l := range list { 104 | // adb 这里的长度是32768,但是由于wire/conn.go 中判断最大长度为 MaxPayloadV1Length 4096 105 | // 因此这里使用 4000 106 | if commandsLen+len(l) > 4000 { 107 | resp, err := c.RunCommandTimeout(time.Second*15, "mkdir", commands...) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | if len(resp) > 0 { 113 | // fmt.Println("resp:", string(resp)) 114 | errs = append(errs, filterFileExistedError(resp)...) 115 | } 116 | commands = make([]string, 0) 117 | if withParent { 118 | commands = append(commands, "-p") 119 | } 120 | commandsLen = 0 121 | } 122 | 123 | commands = append(commands, l) 124 | commandsLen = commandsLen + len(l) + 1 // and one space 125 | } 126 | 127 | if commandsLen > 0 { 128 | resp, err := c.RunCommandTimeout(time.Second*15, "mkdir", commands...) 129 | if err != nil { 130 | return err 131 | } 132 | if len(resp) > 0 { 133 | errs = append(errs, filterFileExistedError(resp)...) 134 | } 135 | } 136 | 137 | if len(errs) > 0 { 138 | return errors.Join(errs...) 139 | } 140 | return nil 141 | } 142 | 143 | // Rm run `adb shell rm -rf xx xx` 144 | // it returns is meaning less in most cases, so just ignore error is ok 145 | func (c *Device) Rm(list []string) error { 146 | var commands []string 147 | var commandsLen int 148 | 149 | var errs []error 150 | 151 | commands = append(commands, "-rf") 152 | for _, l := range list { 153 | if commandsLen+len(l) > (32768 - 7) { // len('rm -rf ') == 6 154 | resp, err := c.RunCommandTimeout(time.Second*15, "rm", commands...) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | if len(resp) > 0 { 160 | errs = append(errs, errors.New(string(resp))) 161 | } 162 | 163 | // reset commands 164 | commands = make([]string, 0) 165 | commands = append(commands, "-rf") 166 | commandsLen = 0 167 | } 168 | 169 | commands = append(commands, l) 170 | commandsLen = commandsLen + len(l) + 1 // and one space 171 | } 172 | 173 | if commandsLen > 0 { 174 | resp, err := c.RunCommandTimeout(time.Second*15, "rm", commands...) 175 | if err != nil { 176 | return err 177 | } 178 | if len(resp) > 0 { 179 | errs = append(errs, errors.New(string(resp))) 180 | } 181 | } 182 | return errors.Join(errs...) 183 | } 184 | 185 | func (c *Device) PushFile(localPath, remotePath string, handler wire.SyncFileHandler) error { 186 | return c.PushFileCtx(context.Background(), localPath, remotePath, handler) 187 | } 188 | 189 | func (c *Device) PushFileCtx(ctx context.Context, localPath, remotePath string, handler wire.SyncFileHandler) error { 190 | linfo, err := os.Lstat(localPath) 191 | if err != nil { 192 | return err 193 | } 194 | if !linfo.Mode().IsRegular() { 195 | return fmt.Errorf("not regular file: %s", localPath) 196 | } 197 | 198 | // features, err := c.DeviceFeatures() 199 | // if err != nil { 200 | // return fmt.Errorf("get device features: %w", err) 201 | // } 202 | 203 | fconn, err := c.NewSyncConn() 204 | if err != nil { 205 | return err 206 | } 207 | defer fconn.Close() 208 | 209 | // if remotePath is dir, just append src file name 210 | rinfo, err := fconn.Stat(remotePath) 211 | if err == nil && rinfo.Mode.IsDir() { 212 | remotePath = remotePath + "/" + linfo.Name() 213 | } 214 | 215 | var syncHandler func(n uint64) 216 | if handler != nil { 217 | total := uint64(linfo.Size()) 218 | sent := uint64(0) 219 | startTime := time.Now() 220 | percent := 0 221 | syncHandler = func(n uint64) { 222 | sent += n 223 | curPercent := float64(sent) / float64(total) * 100 224 | if int(curPercent) > percent { 225 | speedMBPerSecond := float64(sent) * float64(time.Second) / 1024.0 / 1024.0 / (float64(time.Since(startTime))) 226 | handler(total, sent, curPercent, speedMBPerSecond) 227 | } 228 | percent = int(curPercent) 229 | } 230 | } 231 | 232 | ch := make(chan error, 2) 233 | go func() { 234 | err := fconn.PushFile(localPath, remotePath, syncHandler) 235 | ch <- err 236 | }() 237 | 238 | select { 239 | case <-ctx.Done(): 240 | return fmt.Errorf("push failed by ctx done: %w", ctx.Err()) 241 | case err := <-ch: 242 | if err != nil { 243 | return fmt.Errorf("push failed: %w", err) 244 | } 245 | return nil 246 | } 247 | } 248 | 249 | // PushDir support push dir 250 | // push 文件夹: 251 | // adb push src-dir dest-dir具有两种行为,与cp命令效果一致 252 | // 1.如果'dest-dir'路径不存在,会创建'dest-dir',其内容与`src-dir`完全一致 253 | // 2.如果'dest-dir'路径存在,会创建'dest-dir/src-dir',其内容与`src-dir`完全一致 254 | // 255 | // 本函数行为如下 256 | // 当 withSrcDir 为 true,永远会在手机上创建src-dir 257 | // 当 withSrcDir 为 false,则仅会 src-dir 的子文件/目录推送到目标文件夹下 258 | func (c *Device) PushDir(local, remote string, withSrcDir bool, handler wire.SyncHandler) (err error) { 259 | return c.PushDirCtx(context.Background(), local, remote, withSrcDir, handler) 260 | } 261 | 262 | func (c *Device) PushDirCtx(ctx context.Context, local, remote string, withSrcDir bool, handler wire.SyncHandler) (err error) { 263 | // Android 12 之后,push 可能遇到文件夹权限问题,解决办法 264 | // 1. 先在手机上创建所有文件夹,如果失败则直接返回错误 265 | // 2. 再推送文件 266 | if err := MakeDirs(c, local, remote, withSrcDir); err != nil { 267 | return err 268 | } 269 | 270 | // push files 271 | fconn, err := c.NewSyncConn() 272 | if err != nil { 273 | return err 274 | } 275 | defer fconn.Close() 276 | 277 | ch := make(chan error, 2) 278 | go func() { 279 | err := fconn.PushDir(withSrcDir, local, remote, handler) 280 | ch <- err 281 | }() 282 | 283 | select { 284 | case <-ctx.Done(): 285 | return fmt.Errorf("push failed by ctx done: %w", ctx.Err()) 286 | case err := <-ch: 287 | if err != nil { 288 | return fmt.Errorf("push failed: %w", err) 289 | } 290 | return nil 291 | } 292 | } 293 | 294 | func MakeDirs(c *Device, local string, remote string, withSrcDir bool) (err error) { 295 | local, err = filepath.Abs(local) 296 | if err != nil { 297 | return fmt.Errorf("pushd: get abs path of %s failed: %w", local, err) 298 | } 299 | var baseName string 300 | if withSrcDir { 301 | // use filepath.Base, not path.Base 302 | // https://stackoverflow.com/questions/48050724/how-to-get-correct-file-base-name-on-windows-using-golang 303 | baseName = filepath.Base(local) 304 | } 305 | 306 | linfo, err := os.Lstat(local) 307 | if err != nil { 308 | return err 309 | } 310 | 311 | if !linfo.IsDir() { 312 | return fmt.Errorf("not dir: %s", local) 313 | } 314 | 315 | // mkdir sub dirs 316 | subdirs, err := ListAllSubDirs(local) 317 | if err != nil { 318 | return 319 | } 320 | remoteSubDirs := make([]string, 1+len(subdirs)) 321 | if baseName != "" { 322 | remoteSubDirs[0] = remote + "/" + baseName 323 | for i, d := range subdirs { 324 | remoteSubDirs[i+1] = remote + "/" + baseName + "/" + d 325 | } 326 | } else { 327 | remoteSubDirs[0] = remote 328 | for i, d := range subdirs { 329 | remoteSubDirs[i+1] = remote + "/" + d 330 | } 331 | } 332 | err = c.MkdirsWithParent(remoteSubDirs, true) 333 | if err != nil { 334 | // 当创建很多文件夹时(比如推送游戏资源包到手机中),可能会返回一个超长的错误,截断处理 335 | errStr := err.Error() 336 | if len(errStr) > 1024 { 337 | errStr = errStr[:1024] 338 | } 339 | return fmt.Errorf("mkdirs failed: %s", errStr) 340 | } 341 | return nil 342 | } 343 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | whitespaceRegex = regexp.MustCompile(`^\s*$`) 11 | ) 12 | 13 | func containsWhitespace(str string) bool { 14 | return strings.ContainsAny(str, " \t\v") 15 | } 16 | 17 | func isBlank(str string) bool { 18 | return whitespaceRegex.MatchString(str) 19 | } 20 | 21 | func wrapClientError(err error, client *Device, operation string, args ...interface{}) error { 22 | if err == nil { 23 | return nil 24 | } 25 | 26 | return fmt.Errorf("%s on %s, err: %w", fmt.Sprintf(operation, args...), client.descriptor.serial, err) 27 | } 28 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestContainsWhitespaceYes(t *testing.T) { 10 | assert.True(t, containsWhitespace("hello world")) 11 | } 12 | 13 | func TestContainsWhitespaceNo(t *testing.T) { 14 | assert.False(t, containsWhitespace("hello")) 15 | } 16 | 17 | func TestIsBlankWhenEmpty(t *testing.T) { 18 | assert.True(t, isBlank("")) 19 | } 20 | 21 | func TestIsBlankWhenJustWhitespace(t *testing.T) { 22 | assert.True(t, isBlank(" \t")) 23 | } 24 | 25 | func TestIsBlankNo(t *testing.T) { 26 | assert.False(t, isBlank(" h ")) 27 | } 28 | -------------------------------------------------------------------------------- /wire/conn.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "strconv" 8 | ) 9 | 10 | const ( 11 | // The official implementation of adb imposes an undocumented 255-byte limit 12 | // on messages. 13 | MaxPayloadV1Length = 4 * 1024 14 | MaxPayload = 1024 * 1024 15 | // Chunks cannot be longer than 64k. 16 | SyncMaxChunkSize = 64 * 1024 17 | 18 | // StatusCodes are returned by the server. If the code indicates failure, the next message will be the error. 19 | StatusSuccess string = "OKAY" 20 | StatusFailure string = "FAIL" 21 | ) 22 | 23 | type StatusReader interface { 24 | // Reads a 4-byte status string and returns it. 25 | // If the status string is StatusFailure, reads the error message from the server 26 | // and returns it as an AdbError. 27 | ReadStatus(req string) (string, error) 28 | } 29 | 30 | // Sender sends messages to the server. 31 | type Sender interface { 32 | SendMessage(msg []byte) error 33 | } 34 | 35 | // Scanner reads tokens from a server. 36 | type Scanner interface { 37 | io.Closer 38 | StatusReader 39 | ReadMessage() ([]byte, error) 40 | ReadUntilEof() ([]byte, error) 41 | // NewSyncScanner() SyncScanner 42 | } 43 | 44 | type IConn interface { 45 | net.Conn 46 | Sender 47 | Scanner 48 | RoundTripSingleResponse(req []byte) (resp []byte, err error) 49 | } 50 | 51 | // Conn is a normal connection to an adb server. 52 | // For most cases, usage looks something like: 53 | // 54 | // conn := wire.Dial() 55 | // conn.SendMessage(data) 56 | // conn.ReadStatus() == StatusSuccess || StatusFailure 57 | // conn.ReadMessage() 58 | // conn.Close() 59 | // 60 | // For some messages, the server will return more than one message (but still a single 61 | // status). Generally, after calling ReadStatus once, you should call ReadMessage until 62 | // it returns an io.EOF error. Note: the protocol docs seem to suggest that connections will be 63 | // kept open for multiple commands, but this is not the case. The official client closes 64 | // a connection immediately after its read the response, in most cases. The docs might be 65 | // referring to the connection between the adb server and the device, but I haven't confirmed 66 | // that. 67 | // For most commands, the server will close the connection after sending the response. 68 | // You should still always call Close() when you're done with the connection. 69 | type Conn struct { 70 | net.Conn 71 | rbuf []byte 72 | } 73 | 74 | func NewConn(conn net.Conn) *Conn { 75 | return &Conn{ 76 | Conn: conn, 77 | rbuf: make([]byte, 4), 78 | } 79 | } 80 | 81 | var _ Sender = &Conn{} 82 | var _ Scanner = &Conn{} 83 | 84 | // NewSyncConn returns connection that can operate in sync mode. 85 | // The connection must already have been switched (by sending the sync command 86 | // to a specific device), or the return connection will return an error. 87 | func (c *Conn) NewSyncConn() *SyncConn { 88 | return &SyncConn{c.Conn, make([]byte, 8), make([]byte, 8)} 89 | } 90 | 91 | func (s *Conn) SendMessage(msg []byte) error { 92 | if len(msg) > MaxPayloadV1Length { 93 | return fmt.Errorf("message length exceeds maximum:%d", len(msg)) 94 | } 95 | 96 | // FIXME: when message is very large, if cost heavy 97 | lengthAndMsg := fmt.Sprintf("%04x%s", len(msg), msg) 98 | _, err := s.Write([]byte(lengthAndMsg)) 99 | return err 100 | } 101 | 102 | // RoundTripSingleResponse sends a message to the server, and reads a single 103 | // message response. If the response has a failure status code, returns it as an error. 104 | func (conn *Conn) RoundTripSingleResponse(req []byte) (resp []byte, err error) { 105 | if err = conn.SendMessage(req); err != nil { 106 | return nil, err 107 | } 108 | 109 | if _, err = conn.ReadStatus(string(req)); err != nil { 110 | return nil, err 111 | } 112 | 113 | return conn.ReadMessage() 114 | } 115 | 116 | func (s *Conn) ReadStatus(req string) (string, error) { 117 | return readStatusFailureAsError(s, s.rbuf, req) 118 | } 119 | 120 | func (s *Conn) ReadMessage() ([]byte, error) { 121 | return readMessage(s, s.rbuf) 122 | } 123 | 124 | func (s *Conn) ReadUntilEof() ([]byte, error) { 125 | data, err := io.ReadAll(s) 126 | if err != nil { 127 | return nil, fmt.Errorf("error reading until EOF: %w", err) 128 | } 129 | return data, nil 130 | } 131 | 132 | func (conn *Conn) Close() error { 133 | if err := conn.Conn.Close(); err != nil { 134 | return fmt.Errorf("error closing connection: %w", err) 135 | } 136 | return nil 137 | } 138 | 139 | // TODO(zach): All EOF errors returned from networking calls should use ConnectionResetError. 140 | func isFailureStatus(status string) bool { 141 | return status == StatusFailure 142 | } 143 | 144 | // Reads the status, and if failure, reads the message and returns it as an error. 145 | // If the status is success, doesn't read the message. 146 | // req is just used to populate the AdbError, and can be nil. 147 | func readStatusFailureAsError(r io.Reader, buf []byte, req string) (string, error) { 148 | // read 4 bytes 149 | if len(buf) < 4 { 150 | buf = make([]byte, 4) 151 | } 152 | n, err := io.ReadFull(r, buf[:4]) 153 | if err == io.ErrUnexpectedEOF { 154 | return "", fmt.Errorf("error reading status for %s: %w", req, errIncompleteMessage(req, n, 4)) 155 | } else if err != nil { 156 | return "", fmt.Errorf("error reading status for %s: %w", req, err) 157 | } 158 | 159 | status := string(buf[:4]) 160 | if isFailureStatus(status) { 161 | msg, err := readMessage(r, buf) 162 | if err != nil { 163 | return "", fmt.Errorf("server returned error for %s, but couldn't read the error message, %w", req, err) 164 | } 165 | 166 | return "", adbServerError(req, string(msg)) 167 | } 168 | 169 | return status, nil 170 | } 171 | 172 | // readMessage reads a 4-byte hex string from r, then reads length bytes and returns them. 173 | func readMessage(r io.Reader, buf []byte) ([]byte, error) { 174 | // read 4 bytes 175 | if len(buf) < 4 { 176 | buf = make([]byte, 4) 177 | } 178 | length, err := readHexLength(r, buf) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | // read length buf 184 | if int(length) > len(buf) { 185 | buf = make([]byte, length) 186 | } 187 | n, err := io.ReadFull(r, buf[:length]) 188 | if err == io.ErrUnexpectedEOF { 189 | return buf[:n], errIncompleteMessage("message data", n, int(length)) 190 | } else if err != nil { 191 | return buf[:n], fmt.Errorf("error reading message data: %w", err) 192 | } 193 | return buf[:n], nil 194 | } 195 | 196 | // readHexLength reads the next 4 bytes from r as an ASCII hex-encoded length and parses them into an int. 197 | func readHexLength(r io.Reader, buf []byte) (int, error) { 198 | n, err := io.ReadFull(r, buf) 199 | if err != nil { 200 | return 0, errIncompleteMessage("length", n, 4) 201 | } 202 | 203 | length, err := strconv.ParseInt(string(buf[:n]), 16, 64) 204 | if err != nil { 205 | return 0, fmt.Errorf("%w: could not parse hex length:%v", ErrAssertion, buf[:n]) 206 | } 207 | return int(length), nil 208 | } 209 | -------------------------------------------------------------------------------- /wire/conn_test.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "io" 8 | "net" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestReadStatusOkay(t *testing.T) { 16 | s := newEofReader("OKAYd") 17 | status, err := readStatusFailureAsError(s, nil, "") 18 | assert.NoError(t, err) 19 | assert.False(t, isFailureStatus(status)) 20 | assertNotEof(t, s) 21 | } 22 | 23 | func TestReadIncompleteStatus(t *testing.T) { 24 | s := newEofReader("oka") 25 | _, err := readStatusFailureAsError(s, nil, "") 26 | assert.Contains(t, err.Error(), "error reading status for ") 27 | assert.Equal(t, errors.Unwrap(err), errIncompleteMessage("", 3, 4)) 28 | assertEof(t, s) 29 | } 30 | 31 | func TestReadFailureIncompleteStatus(t *testing.T) { 32 | s := newEofReader("FAIL") 33 | _, err := readStatusFailureAsError(s, nil, "req") 34 | assert.Contains(t, err.Error(), "server returned error for req, but couldn't read the error message") 35 | assertEof(t, s) 36 | } 37 | 38 | func TestReadFailureEmptyStatus(t *testing.T) { 39 | s := newEofReader("FAIL0000") 40 | _, err := readStatusFailureAsError(s, nil, "") 41 | assert.EqualError(t, err, "AdbError: request , server error: ") 42 | assertEof(t, s) 43 | } 44 | 45 | func TestReadFailureStatus(t *testing.T) { 46 | s := newEofReader("FAIL0004fail") 47 | _, err := readStatusFailureAsError(s, nil, "") 48 | assert.EqualError(t, err, "AdbError: request , server error: fail") 49 | assertEof(t, s) 50 | } 51 | 52 | func TestReadMessage(t *testing.T) { 53 | s := newEofReader("0005hello") 54 | msg, err := readMessage(s, nil) 55 | assert.NoError(t, err) 56 | assert.Len(t, msg, 5) 57 | assert.Equal(t, "hello", string(msg)) 58 | assertEof(t, s) 59 | } 60 | 61 | func TestReadMessageWithExtraData(t *testing.T) { 62 | s := newEofReader("0005hellothere") 63 | msg, err := readMessage(s, nil) 64 | assert.NoError(t, err) 65 | assert.Len(t, msg, 5) 66 | assert.Equal(t, "hello", string(msg)) 67 | assertNotEof(t, s) 68 | } 69 | 70 | func TestReadLongerMessage(t *testing.T) { 71 | s := newEofReader("001b192.168.56.101:5555 device\n") 72 | msg, err := readMessage(s, nil) 73 | assert.NoError(t, err) 74 | assert.Len(t, msg, 27) 75 | assert.Equal(t, "192.168.56.101:5555 device\n", string(msg)) 76 | assertEof(t, s) 77 | } 78 | 79 | func TestReadEmptyMessage(t *testing.T) { 80 | s := newEofReader("0000") 81 | msg, err := readMessage(s, nil) 82 | assert.NoError(t, err) 83 | assert.Equal(t, "", string(msg)) 84 | assertEof(t, s) 85 | } 86 | 87 | func TestReadIncompleteMessage(t *testing.T) { 88 | s := newEofReader("0005hel") 89 | msg, err := readMessage(s, nil) 90 | assert.Error(t, err) 91 | assert.Equal(t, errIncompleteMessage("message data", 3, 5), err) 92 | assert.Equal(t, "hel", string(msg)) 93 | assertEof(t, s) 94 | } 95 | 96 | func TestReadLength(t *testing.T) { 97 | s := newEofReader("000a") 98 | l, err := readHexLength(s, make([]byte, 4)) 99 | assert.NoError(t, err) 100 | assert.Equal(t, 10, l) 101 | assertEof(t, s) 102 | } 103 | 104 | func TestReadLengthIncompleteLength(t *testing.T) { 105 | s := newEofReader("aaa") 106 | _, err := readHexLength(s, make([]byte, 4)) 107 | assert.Equal(t, errIncompleteMessage("length", 3, 4), err) 108 | assertEof(t, s) 109 | } 110 | 111 | func assertEof(t *testing.T, r io.Reader) { 112 | msg, err := readMessage(r, nil) 113 | assert.True(t, errors.Is(err, ErrConnectionReset)) 114 | assert.Nil(t, msg) 115 | } 116 | 117 | func assertNotEof(t *testing.T, r io.Reader) { 118 | n, err := r.Read(make([]byte, 1)) 119 | assert.Equal(t, 1, n) 120 | assert.NoError(t, err) 121 | } 122 | 123 | // newEofBuffer returns a bytes.Buffer of str that returns an EOF error 124 | // at the end of input, instead of just returning 0 bytes read. 125 | func newEofReader(str string) io.ReadCloser { 126 | limitReader := io.LimitReader(bytes.NewBufferString(str), int64(len(str))) 127 | bufReader := bufio.NewReader(limitReader) 128 | return io.NopCloser(bufReader) 129 | } 130 | 131 | func TestWriteMessage(t *testing.T) { 132 | s, b := newTestSender() 133 | err := s.SendMessage([]byte("hello")) 134 | assert.NoError(t, err) 135 | assert.Equal(t, "0005hello", b.String()) 136 | } 137 | 138 | func TestWriteEmptyMessage(t *testing.T) { 139 | s, b := newTestSender() 140 | err := s.SendMessage([]byte("")) 141 | assert.NoError(t, err) 142 | assert.Equal(t, "0000", b.String()) 143 | } 144 | 145 | func newTestSender() (Sender, *mockConn) { 146 | var buf bytes.Buffer 147 | w := makeMockConnBuf(&buf) 148 | return NewConn(w), w 149 | } 150 | 151 | // mockConn is a wrapper around a bytes.Buffer that implements net.Conn 152 | type mockConn struct { 153 | // io.ReadWriter 154 | *bytes.Buffer // write buffer 155 | rbuf *bytes.Buffer 156 | } 157 | 158 | func makeMockConnStr(str string) net.Conn { 159 | w := &mockConn{ 160 | Buffer: bytes.NewBufferString(str), 161 | } 162 | return w 163 | } 164 | 165 | func makeMockConnBuf(buf *bytes.Buffer) *mockConn { 166 | w := &mockConn{ 167 | Buffer: buf, 168 | } 169 | return w 170 | } 171 | 172 | func makeMockConnBytes(b []byte) net.Conn { 173 | w := &mockConn{ 174 | Buffer: bytes.NewBuffer(b), 175 | } 176 | return w 177 | } 178 | 179 | func makeMockConn2(str string, buf *bytes.Buffer) net.Conn { 180 | w := &mockConn{ 181 | rbuf: bytes.NewBufferString(str), 182 | Buffer: buf, 183 | } 184 | return w 185 | } 186 | 187 | func (b *mockConn) Read(p []byte) (n int, err error) { 188 | if b.rbuf != nil { 189 | return b.rbuf.Read(p) 190 | } 191 | 192 | return b.Buffer.Read(p) 193 | } 194 | 195 | func (b *mockConn) Write(p []byte) (n int, err error) { 196 | return b.Buffer.Write(p) 197 | } 198 | 199 | func (b *mockConn) Close() error { 200 | // No-op. 201 | return nil 202 | } 203 | 204 | func (b *mockConn) LocalAddr() net.Addr { 205 | return nil 206 | } 207 | func (b *mockConn) RemoteAddr() net.Addr { 208 | return nil 209 | } 210 | 211 | func (b *mockConn) SetDeadline(t time.Time) error { 212 | return nil 213 | } 214 | 215 | func (b *mockConn) SetReadDeadline(t time.Time) error { 216 | return nil 217 | } 218 | 219 | func (b *mockConn) SetWriteDeadline(t time.Time) error { 220 | return nil 221 | } 222 | -------------------------------------------------------------------------------- /wire/doc.go: -------------------------------------------------------------------------------- 1 | // Package wire implements the low-level part of the client/server wire protocol. 2 | // It also implements the "sync" wire format for file transfers. 3 | // This package is not intended to be used directly. adb.Adb and adb.Device 4 | // use it to abstract away the bit-twiddling details of the protocol. You should only ever 5 | // need to work with the goadb package. Also, this package's API may change more frequently 6 | // than goadb's. 7 | // The protocol spec can be found at 8 | // https://android.googlesource.com/platform/system/core/+/master/adb/OVERVIEW.TXT. 9 | 10 | package wire 11 | -------------------------------------------------------------------------------- /wire/errors.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrAssertion = errors.New("AssertionError") 9 | ErrParse = errors.New("ParseError") 10 | // ErrServerNotAvailable the server was not available on the requested port. 11 | ErrServerNotAvailable = errors.New("ServerNotAvailable") 12 | // ErrNetwork general network error communicating with the server. 13 | ErrNetwork = errors.New("Network") 14 | // ErrConnectionReset the connection to the server was reset in the middle of an operation. Server probably died. 15 | ErrConnectionReset = errors.New("ConnectionReset") 16 | // ErrAdb The server returned an error message, but we couldn't parse it. 17 | ErrAdb = errors.New("AdbError") 18 | // ErrDeviceNotFound the server returned a "device not found" error. 19 | ErrDeviceNotFound = errors.New("DeviceNotFound") 20 | // ErrFileNoExist tried to perform an operation on a path that doesn't exist on the device. 21 | ErrFileNoExist = errors.New("FileNoExist") 22 | ) 23 | -------------------------------------------------------------------------------- /wire/filemode.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import "os" 4 | 5 | // ADB file modes seem to only be 16 bits. 6 | // Values are taken from http://linux.die.net/include/bits/stat.h. 7 | const ( 8 | ModeDir uint32 = 0040000 9 | ModeSymlink = 0120000 10 | ModeSocket = 0140000 11 | ModeFifo = 0010000 12 | ModeCharDevice = 0020000 13 | ) 14 | 15 | func ParseFileModeFromAdb(modeFromSync uint32) (filemode os.FileMode) { 16 | // The ADB filemode uses the permission bits defined in Go's os package, but 17 | // we need to parse the other bits manually. 18 | switch { 19 | case modeFromSync&ModeSymlink == ModeSymlink: 20 | filemode = os.ModeSymlink 21 | case modeFromSync&ModeDir == ModeDir: 22 | filemode = os.ModeDir 23 | case modeFromSync&ModeSocket == ModeSocket: 24 | filemode = os.ModeSocket 25 | case modeFromSync&ModeFifo == ModeFifo: 26 | filemode = os.ModeNamedPipe 27 | case modeFromSync&ModeCharDevice == ModeCharDevice: 28 | filemode = os.ModeCharDevice 29 | } 30 | 31 | filemode |= os.FileMode(modeFromSync).Perm() 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /wire/sync_conn_test.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "math" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | var ( 17 | someTime = time.Date(2015, 04, 12, 20, 7, 51, 0, time.UTC) 18 | // The little-endian encoding of someTime.Unix() 19 | someTimeEncoded = []byte{151, 208, 42, 85} 20 | ) 21 | 22 | func packLstatV1(mode os.FileMode, size int32, mtime time.Time) []byte { 23 | var b bytes.Buffer 24 | b.Write([]byte("STAT")) 25 | binary.Write(&b, binary.LittleEndian, mode) 26 | binary.Write(&b, binary.LittleEndian, size) 27 | binary.Write(&b, binary.LittleEndian, mtime.Unix()) 28 | return b.Bytes() 29 | } 30 | 31 | func TestStatValid(t *testing.T) { 32 | var buf bytes.Buffer 33 | conn := NewSyncConn(makeMockConnBuf(&buf)) 34 | 35 | var mode os.FileMode = 0777 36 | 37 | statV1 := packLstatV1(mode, 4, someTime) 38 | conn.Write(statV1) 39 | entry, err := conn.Stat("/thing") 40 | assert.NoError(t, err) 41 | require.NotNil(t, entry) 42 | assert.Equal(t, mode, entry.Mode, "expected os.FileMode %s, got %s", mode, entry.Mode) 43 | assert.Equal(t, int32(4), entry.Size) 44 | assert.Equal(t, someTime, entry.ModifiedAt) 45 | assert.Equal(t, "", entry.Name) 46 | } 47 | 48 | func TestStatBadResponse(t *testing.T) { 49 | var buf bytes.Buffer 50 | conn := NewSyncConn(makeMockConnBuf(&buf)) 51 | conn.SendRequest([]byte("SPAT"), nil) 52 | entry, err := conn.Stat("/") 53 | assert.Nil(t, entry) 54 | assert.Error(t, err) 55 | } 56 | 57 | func TestStatNoExist(t *testing.T) { 58 | var buf bytes.Buffer 59 | conn := NewSyncConn(makeMockConnBuf(&buf)) 60 | statV1 := packLstatV1(0, 0, time.Unix(0, 0).UTC()) 61 | conn.Write(statV1) 62 | entry, err := conn.Stat("/") 63 | assert.Nil(t, entry) 64 | assert.True(t, errors.Is(err, ErrFileNoExist)) 65 | } 66 | 67 | func TestSyncSendOctetString(t *testing.T) { 68 | var buf bytes.Buffer 69 | s := NewSyncConn(makeMockConnBuf(&buf)) 70 | err := s.SendRequest([]byte("helo"), nil) 71 | assert.NoError(t, err) 72 | assert.Equal(t, "helo\x00\x00\x00\x00", buf.String()) 73 | } 74 | 75 | func TestSyncSendOctetStringTooLong(t *testing.T) { 76 | var buf bytes.Buffer 77 | s := NewSyncConn(makeMockConnBuf(&buf)) 78 | err := s.SendRequest([]byte("hello"), nil) 79 | assert.EqualError(t, err, "AssertionError: octet string must be exactly 4 bytes: 'hello'") 80 | } 81 | 82 | func TestSyncReadString(t *testing.T) { 83 | s := NewSyncConn(makeMockConnStr("\005\000\000\000hello")) 84 | str, err := s.ReadBytes(nil) 85 | assert.NoError(t, err) 86 | assert.Equal(t, "hello", string(str)) 87 | } 88 | 89 | func TestSyncReadStringTooShort(t *testing.T) { 90 | s := NewSyncConn(makeMockConnStr("\005\000\000\000h")) 91 | _, err := s.ReadBytes(nil) 92 | assert.Equal(t, errIncompleteMessage("bytes", 1, 5), err) 93 | } 94 | 95 | func TestSyncSendBytes(t *testing.T) { 96 | var buf bytes.Buffer 97 | s := NewSyncConn(makeMockConnBuf(&buf)) 98 | err := s.SendRequest([]byte(ID_DATA), []byte("hello")) 99 | assert.NoError(t, err) 100 | assert.Equal(t, "DATA\005\000\000\000hello", buf.String()) 101 | } 102 | 103 | func TestSyncReadBytes(t *testing.T) { 104 | s := NewSyncConn(makeMockConnStr("\005\000\000\000helloworld")) 105 | 106 | buf, err := s.ReadBytes(nil) 107 | assert.NoError(t, err) 108 | assert.Equal(t, "hello", string(buf)) 109 | } 110 | 111 | func TestMath(t *testing.T) { 112 | a := int(math.Ceil(0)) 113 | assert.Equal(t, a, 0) 114 | } 115 | -------------------------------------------------------------------------------- /wire/sync_dir.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "time" 8 | ) 9 | 10 | // DirEntry holds information about a directory entry on a device. 11 | type DirEntry struct { 12 | Name string 13 | Mode os.FileMode 14 | Size int32 15 | ModifiedAt time.Time 16 | } 17 | 18 | func (entry DirEntry) String() string { 19 | return fmt.Sprintf("%s %12d %v %s", entry.Mode.String(), entry.Size, entry.ModifiedAt, entry.Name) 20 | } 21 | 22 | // SyncDirReader iterates over directory entries. 23 | type SyncDirReader struct { 24 | syncConn *SyncConn 25 | eof bool 26 | } 27 | 28 | // ReadDir work same as os.ReadDir 29 | // if n = -1, reads all the remaining directory entries into a slice 30 | // If err is non-nil, result will contain any entries read until the error occurred. 31 | // At the end of a directory, the error is io.EOF. 32 | func (dr *SyncDirReader) ReadDir(n int) (entries []*DirEntry, err error) { 33 | if dr.eof { 34 | return nil, io.EOF 35 | } 36 | 37 | // to iterator when n = -1, just cast it to uint32 in loop 38 | for i := uint32(0); i < uint32(n); i++ { 39 | entry, done, err2 := dr.syncConn.readDentV1() 40 | if err2 != nil { 41 | err = err2 42 | return 43 | } 44 | if done { 45 | dr.eof = true 46 | err = io.EOF 47 | return 48 | } 49 | 50 | entries = append(entries, entry) 51 | } 52 | 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /wire/sync_file.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | ) 8 | 9 | // SyncFileReader wraps a SyncConn that has requested to receive a file. 10 | type SyncFileReader struct { 11 | // Reader used to read data from the adb connection. 12 | syncConn *SyncConn 13 | toRead int 14 | eof bool 15 | } 16 | 17 | var _ io.Reader = &SyncFileReader{} 18 | 19 | func newSyncFileReader(s *SyncConn) (r *SyncFileReader) { 20 | r = &SyncFileReader{ 21 | syncConn: s, 22 | } 23 | return 24 | } 25 | 26 | func (r *SyncFileReader) Read(buf []byte) (n int, err error) { 27 | if r.eof { 28 | return 0, io.EOF 29 | } 30 | 31 | var length int32 32 | if r.toRead == 0 { 33 | length, err = r.syncConn.ReadNextChunkSize() 34 | if err == io.EOF { 35 | r.eof = true 36 | return 0, err 37 | } else if err != nil { 38 | return 0, err 39 | } else { 40 | r.toRead = int(length) 41 | } 42 | } 43 | 44 | // need read `r.toRead` bytes 45 | if len(buf) >= int(r.toRead) { 46 | n, err = io.ReadFull(r.syncConn, buf[:r.toRead]) 47 | } else { 48 | n, err = io.ReadFull(r.syncConn, buf) 49 | } 50 | 51 | r.toRead = r.toRead - n 52 | return 53 | } 54 | 55 | // SyncFileWriter wraps a SyncConn that has requested to send a file. 56 | type SyncFileWriter struct { 57 | // The modification time to write in the footer. 58 | // If 0, use the current time. 59 | mtime time.Time 60 | 61 | // Reader used to read data from the adb connection. 62 | syncConn *SyncConn 63 | } 64 | 65 | var _ io.Writer = &SyncFileWriter{} 66 | 67 | func newSyncFileWriter(s *SyncConn, mtime time.Time) *SyncFileWriter { 68 | return &SyncFileWriter{ 69 | mtime: mtime, 70 | syncConn: s, 71 | } 72 | } 73 | 74 | // Write writes the min of (len(buf), 64k). 75 | func (w *SyncFileWriter) Write(buf []byte) (n int, err error) { 76 | written := 0 77 | 78 | // If buf > 64k we'll have to send multiple chunks. 79 | // TODO Refactor this into something that can coalesce smaller writes into a single chukn. 80 | for len(buf) > 0 { 81 | // Writes < 64k have a one-to-one mapping to chunks. 82 | // If buffer is larger than the max, we'll return the max size and leave it up to the 83 | // caller to handle correctly. 84 | partialBuf := buf 85 | if len(partialBuf) > SyncMaxChunkSize { 86 | partialBuf = partialBuf[:SyncMaxChunkSize] 87 | } 88 | 89 | if err := w.syncConn.SendRequest([]byte(ID_DATA), partialBuf); err != nil { 90 | return written, err 91 | } 92 | written += len(partialBuf) 93 | buf = buf[len(partialBuf):] 94 | } 95 | 96 | return written, nil 97 | } 98 | 99 | func (w *SyncFileWriter) CopyDone() error { 100 | if w.mtime.IsZero() { 101 | w.mtime = time.Now() 102 | } 103 | 104 | if err := w.syncConn.SendDone(w.mtime); err != nil { 105 | return fmt.Errorf("error sending done chunk to close stream: %w", err) 106 | } 107 | 108 | if status, err := w.syncConn.ReadStatus(""); err != nil { 109 | return fmt.Errorf("error reading status, should receive 'ID_OKAY': %w", err) 110 | } else if status == ID_OKAY { 111 | return nil 112 | } else { 113 | fmt.Println("sync-send with resp status: ", status) 114 | } 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /wire/sync_file_test.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "io" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestReadNextChunk(t *testing.T) { 16 | s := NewSyncConn(makeMockConnStr( 17 | "DATA\006\000\000\000hello DATA\005\000\000\000worldDONE\000\000\000\000")) 18 | 19 | // Read 1st chunk 20 | reader, err := s.ReadNextChunkSize() 21 | assert.NoError(t, err) 22 | assert.Equal(t, int32(6), reader) 23 | buf := make([]byte, 10) 24 | n, err := s.Read(buf[:reader]) 25 | assert.NoError(t, err) 26 | assert.Equal(t, 6, n) 27 | assert.Equal(t, "hello ", string(buf[:6])) 28 | 29 | // Read 2nd chunk 30 | reader, err = s.ReadNextChunkSize() 31 | assert.NoError(t, err) 32 | assert.Equal(t, int32(5), reader) 33 | buf = make([]byte, 10) 34 | n, err = s.Read(buf[:reader]) 35 | assert.NoError(t, err) 36 | assert.Equal(t, 5, n) 37 | assert.Equal(t, "world", string(buf[:5])) 38 | 39 | // Read DONE 40 | _, err = s.ReadNextChunkSize() 41 | assert.Equal(t, io.EOF, err) 42 | } 43 | func TestReadNextChunkInvalidChunkId(t *testing.T) { 44 | s := NewSyncConn(makeMockConnStr( 45 | "ATAD\006\000\000\000hello ")) 46 | 47 | // Read 1st chunk 48 | _, err := s.ReadNextChunkSize() 49 | assert.EqualError(t, err, "AssertionError: expected chunk id 'DATA' or 'DONE', but got 'ATAD'") 50 | } 51 | 52 | func TestReadMultipleCalls(t *testing.T) { 53 | s := NewSyncConn(makeMockConnStr( 54 | "DATA\006\000\000\000hello DATA\005\000\000\000worldDONE\000\000\000\000")) 55 | reader := newSyncFileReader(s) 56 | 57 | firstByte := make([]byte, 1) 58 | _, err := io.ReadFull(reader, firstByte) 59 | assert.NoError(t, err) 60 | assert.Equal(t, "h", string(firstByte)) 61 | 62 | restFirstChunkBytes := make([]byte, 5) 63 | _, err = io.ReadFull(reader, restFirstChunkBytes) 64 | assert.NoError(t, err) 65 | assert.Equal(t, "ello ", string(restFirstChunkBytes)) 66 | 67 | secondChunkBytes := make([]byte, 5) 68 | _, err = io.ReadFull(reader, secondChunkBytes) 69 | assert.NoError(t, err) 70 | assert.Equal(t, "world", string(secondChunkBytes)) 71 | 72 | _, err = io.ReadFull(reader, make([]byte, 5)) 73 | assert.Equal(t, io.EOF, err) 74 | } 75 | 76 | func TestReadAll(t *testing.T) { 77 | s := NewSyncConn(makeMockConnStr( 78 | "DATA\006\000\000\000hello DATA\005\000\000\000worldDONE\000\000\000\000")) 79 | reader := newSyncFileReader(s) 80 | buf := make([]byte, 20) 81 | _, err := io.ReadFull(reader, buf) 82 | assert.Equal(t, io.ErrUnexpectedEOF, err) 83 | assert.Equal(t, "hello world\000", string(buf[:12])) 84 | } 85 | 86 | func TestReadError(t *testing.T) { 87 | s := NewSyncConn(makeMockConnStr( 88 | "FAIL\004\000\000\000fail")) 89 | r := newSyncFileReader(s) 90 | _, err := r.Read(nil) 91 | assert.EqualError(t, err, "AdbError: request read-chunk, server error: fail") 92 | } 93 | 94 | func TestReadEmpty(t *testing.T) { 95 | s := NewSyncConn(makeMockConnStr("DONE\000\000\000\000")) 96 | r := newSyncFileReader(s) 97 | data, err := io.ReadAll(r) 98 | assert.NoError(t, err) 99 | assert.Empty(t, data) 100 | // Multiple read calls that return EOF is a valid case. 101 | for i := 0; i < 5; i++ { 102 | data, err := io.ReadAll(r) 103 | assert.NoError(t, err) // io.ReadAll treat an EOF to nil 104 | assert.Empty(t, data) 105 | } 106 | } 107 | 108 | func TestReadErrorNotFound(t *testing.T) { 109 | s := NewSyncConn(makeMockConnStr( 110 | "FAIL\031\000\000\000No such file or directory")) 111 | r := newSyncFileReader(s) 112 | _, err := r.Read(nil) 113 | assert.True(t, errors.Is(err, ErrFileNoExist)) 114 | assert.EqualError(t, err, "FileNoExist: no such file or directory") 115 | } 116 | 117 | //////////////////////////////////////////////////////////////////// 118 | // writer 119 | 120 | func TestFileWriterWriteSingleChunk(t *testing.T) { 121 | var buf bytes.Buffer 122 | syncConn := NewSyncConn(makeMockConn2("OKAY\x00\x00\x00\x00", &buf)) 123 | writer := newSyncFileWriter(syncConn, time.Time{}) 124 | 125 | n, err := writer.Write([]byte("hello")) 126 | assert.NoError(t, err) 127 | assert.Equal(t, 5, n) 128 | 129 | assert.Equal(t, "DATA\005\000\000\000hello", buf.String()) 130 | } 131 | 132 | func TestFileWriterWriteMultiChunk(t *testing.T) { 133 | var buf bytes.Buffer 134 | syncConn := NewSyncConn(makeMockConn2("OKAY\x00\x00\x00\x00", &buf)) 135 | writer := newSyncFileWriter(syncConn, time.Time{}) 136 | 137 | n, err := writer.Write([]byte("hello")) 138 | assert.NoError(t, err) 139 | assert.Equal(t, 5, n) 140 | 141 | n, err = writer.Write([]byte(" world")) 142 | assert.NoError(t, err) 143 | assert.Equal(t, 6, n) 144 | 145 | assert.Equal(t, "DATA\005\000\000\000helloDATA\006\000\000\000 world", buf.String()) 146 | } 147 | 148 | func TestFileWriterWriteLargeChunk(t *testing.T) { 149 | var buf bytes.Buffer 150 | syncConn := NewSyncConn(makeMockConn2("OKAY\x00\x00\x00\x00", &buf)) 151 | writer := newSyncFileWriter(syncConn, time.Time{}) 152 | 153 | // Send just enough data to get 2 chunks. 154 | data := make([]byte, SyncMaxChunkSize+1) 155 | n, err := writer.Write(data) 156 | 157 | assert.NoError(t, err) 158 | assert.Equal(t, SyncMaxChunkSize+1, n) 159 | assert.Equal(t, 8+8+SyncMaxChunkSize+1, buf.Len()) 160 | 161 | // First header. 162 | chunk := buf.Bytes()[:8+SyncMaxChunkSize] 163 | expectedHeader := []byte("DATA----") 164 | binary.LittleEndian.PutUint32(expectedHeader[4:], SyncMaxChunkSize) 165 | assert.Equal(t, expectedHeader, chunk[:8]) 166 | assert.Equal(t, data[:SyncMaxChunkSize], chunk[8:]) 167 | 168 | // Second header. 169 | chunk = buf.Bytes()[SyncMaxChunkSize+8 : SyncMaxChunkSize+8+1] 170 | expectedHeader = []byte("DATA\000\000\000\000") 171 | binary.LittleEndian.PutUint32(expectedHeader[4:], 1) 172 | assert.Equal(t, expectedHeader, chunk[:8]) 173 | } 174 | 175 | func TestFileWriterCloseEmpty(t *testing.T) { 176 | var buf bytes.Buffer 177 | mtime := time.Unix(1, 0) 178 | syncConn := NewSyncConn(makeMockConn2("OKAY\x00\x00\x00\x00", &buf)) 179 | writer := newSyncFileWriter(syncConn, mtime) 180 | 181 | assert.NoError(t, writer.CopyDone()) 182 | 183 | assert.Equal(t, "DONE\x01\x00\x00\x00", buf.String()) 184 | } 185 | 186 | func TestFileWriterWriteClose(t *testing.T) { 187 | var buf bytes.Buffer 188 | mtime := time.Unix(1, 0) 189 | syncConn := NewSyncConn(makeMockConn2("OKAY\x00\x00\x00\x00", &buf)) 190 | writer := newSyncFileWriter(syncConn, mtime) 191 | 192 | writer.Write([]byte("hello")) 193 | 194 | assert.NoError(t, writer.CopyDone()) 195 | assert.Equal(t, "DATA\005\000\000\000helloDONE\x01\x00\x00\x00", buf.String()) 196 | } 197 | 198 | func TestFileWriterCloseAutoMtime(t *testing.T) { 199 | var buf bytes.Buffer 200 | syncConn := NewSyncConn(makeMockConn2("OKAY\x00\x00\x00\x00", &buf)) 201 | writer := newSyncFileWriter(syncConn, time.Time{}) 202 | 203 | assert.NoError(t, writer.CopyDone()) 204 | assert.Len(t, buf.String(), 8) 205 | assert.True(t, strings.HasPrefix(buf.String(), ID_DONE)) 206 | 207 | mtimeBytes := buf.Bytes()[4:] 208 | mtimeActual := time.Unix(int64(binary.LittleEndian.Uint32(mtimeBytes)), 0) 209 | 210 | // Delta has to be a whole second since adb only supports second granularity for mtimes. 211 | assert.WithinDuration(t, time.Now(), mtimeActual, 1*time.Second) 212 | } 213 | -------------------------------------------------------------------------------- /wire/util.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | // "errors" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "sync" 9 | ) 10 | 11 | // ErrorResponseDetails is an error message returned by the server for a particular request. 12 | type ErrorResponseDetails struct { 13 | Request string 14 | ServerMsg string 15 | } 16 | 17 | // deviceNotFoundMessagePattern matches all possible error messages returned by adb servers to 18 | // report that a matching device was not found. Used to set the DeviceNotFound error code on 19 | // error values. 20 | // 21 | // Old servers send "device not found", and newer ones "device 'serial' not found". 22 | var deviceNotFoundMessagePattern = regexp.MustCompile(`device( '.*')? not found`) 23 | 24 | func adbServerError(request string, serverMsg string) error { 25 | if deviceNotFoundMessagePattern.MatchString(serverMsg) { 26 | return fmt.Errorf("%w: request %s, server error: %s", ErrDeviceNotFound, request, serverMsg) 27 | } 28 | return fmt.Errorf("%w: request %s, server error: %s", ErrAdb, request, serverMsg) 29 | } 30 | 31 | func errIncompleteMessage(description string, actual int, expected int) error { 32 | return fmt.Errorf("%w: incomplete %s: read %d bytes, expecting %d", ErrConnectionReset, description, actual, expected) 33 | } 34 | 35 | // MultiCloseable wraps c in a ReadWriteCloser that can be safely closed multiple times. 36 | func MultiCloseable(c io.ReadWriteCloser) io.ReadWriteCloser { 37 | return &multiCloseable{ReadWriteCloser: c} 38 | } 39 | 40 | type multiCloseable struct { 41 | io.ReadWriteCloser 42 | closeOnce sync.Once 43 | err error 44 | } 45 | 46 | func (c *multiCloseable) Close() error { 47 | c.closeOnce.Do(func() { 48 | c.err = c.ReadWriteCloser.Close() 49 | }) 50 | return c.err 51 | } 52 | -------------------------------------------------------------------------------- /wire/util_test.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAdbServerError_NoRequest(t *testing.T) { 11 | err := adbServerError("", "fail") 12 | assert.True(t, errors.Is(err, ErrAdb)) 13 | assert.EqualError(t, err, "AdbError: request , server error: fail") 14 | } 15 | 16 | func TestAdbServerError_WithRequest(t *testing.T) { 17 | err := adbServerError("polite", "fail") 18 | assert.True(t, errors.Is(err, ErrAdb)) 19 | assert.EqualError(t, err, "AdbError: request polite, server error: fail") 20 | } 21 | 22 | func TestAdbServerError_DeviceNotFound(t *testing.T) { 23 | err := adbServerError("", "device not found") 24 | assert.True(t, errors.Is(err, ErrDeviceNotFound)) 25 | assert.EqualError(t, err, "DeviceNotFound: request , server error: device not found") 26 | } 27 | 28 | func TestAdbServerError_DeviceSerialNotFound(t *testing.T) { 29 | err := adbServerError("", "device 'LGV4801c74eccd' not found") 30 | assert.True(t, errors.Is(err, ErrDeviceNotFound)) 31 | assert.EqualError(t, err, "DeviceNotFound: request , server error: device 'LGV4801c74eccd' not found") 32 | } 33 | --------------------------------------------------------------------------------