├── .gitignore ├── .goreleaser.yml ├── .travis.yml ├── LICENSE ├── README.md ├── adb.go ├── adb ├── client.go ├── client_test.go ├── conn.go ├── descriptor.go ├── device.go ├── device_test.go ├── filemode.go ├── packet.go ├── reader.go ├── send2adb.sh ├── server.go ├── tcpusb.go ├── tcpusb_test.go └── writer.go ├── adb_test.go ├── client.go ├── go.mod ├── install.go ├── main.go ├── pidcat.go ├── screenshot.go ├── scripts └── tcp-proxy.py ├── tunnel-test └── main.go ├── tunnel └── tunnel.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | go.sum 15 | 16 | # User defined 17 | fa 18 | *.apk 19 | .DS_Store 20 | dist/ 21 | *.png 22 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | brew: 2 | github: 3 | owner: codeskyblue 4 | name: homebrew-tap 5 | builds: 6 | - goos: 7 | - linux 8 | - darwin 9 | - windows 10 | goarch: 11 | - amd64 12 | - "386" 13 | goarm: 14 | - "6" 15 | main: . 16 | archive: 17 | format: zip 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.11" 4 | env: 5 | - GO111MODULE=on 6 | script: 7 | - go test -v 8 | deploy: 9 | - provider: script 10 | skip_cleanup: true 11 | script: curl -sL https://git.io/goreleaser | bash 12 | on: 13 | tags: true 14 | condition: $TRAVIS_OS_NAME = linux 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 shengxiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fa = fast adb 2 | [![Build Status](https://travis-ci.org/codeskyblue/fa.svg?branch=master)](https://travis-ci.org/codeskyblue/fa) 3 | [![GoDoc](https://godoc.org/github.com/codeskyblue/fa/adb?status.svg)](https://godoc.org/github.com/codeskyblue/fa/adb) 4 | 5 | `fa` is a command line tool that wraps `adb` in order to extend it with extra features and commands that make working with Android easier. 6 | 7 | This project is still in developing, please be careful when use in production. 8 | 9 | ## Features 10 | - [x] show device selection when multi device connected 11 | - [x] screenshot 12 | - [x] install support http url 13 | - [x] support launch after install apk 14 | - [x] support `fa devices --json` 15 | - [x] support `fa shell` 16 | - [x] colorful logcat and filter with package name 17 | - [ ] install apk and auto click confirm 18 | - [ ] check device health status 19 | - [ ] show current app 20 | - [ ] unlock device 21 | - [ ] reset device state, clean up installed packages 22 | - [ ] show wlan (ip,mac,signal), enable and disable it 23 | - [x] share device to public web 24 | - [ ] fa share-server 25 | - [ ] install ipa support 26 | - [x] fahub service 27 | 28 | ## Install 29 | **For mac** 30 | 31 | ```bash 32 | brew install codeskyblue/tap/fa 33 | ``` 34 | 35 | **For windows and linux** 36 | 37 | download binary from [**releases**](https://github.com/codeskyblue/fa/releases) 38 | 39 | ## Usage 40 | ### Show version 41 | 42 | ```bash 43 | $ fa version 44 | fa version v0.0.5 # just example 45 | adb server version 28 46 | ``` 47 | 48 | ### Show devices 49 | - [x] Remove header `List of devices attached` to make it easy parse 50 | 51 | ```bash 52 | $ fa devices 53 | 3578298f device 54 | 55 | $ fa devices --json 56 | [ 57 | {"serial": "3578298f", "status": "device"} 58 | ] 59 | ``` 60 | 61 | ### Run adb command with device select 62 | if multi device connected, `fa` will give you list of devices to choose. 63 | 64 | ```bash 65 | $ fa adb shell 66 | @ select device 67 | > 3aff8912 Smartion 68 | vv12afvv Google Nexus 5 69 | {selected 3aff8912} 70 | shell $ 71 | ``` 72 | 73 | `-s` option and `$ANDROID_SERIAL` is also supported, but if you known serial, maybe use `adb` directly is better. 74 | 75 | ```bash 76 | $ fa -s 3578298f adb shell pwd 77 | / 78 | $ ANDROID_SERIAL=3578298 fa adb shell pwd 79 | / 80 | ``` 81 | 82 | ### Screenshot 83 | only `png` format now. 84 | 85 | ```bash 86 | fa screenshot -o screenshot.png 87 | ``` 88 | 89 | ### Install APK 90 | 91 | ```bash 92 | fa install ApiDemos-debug.apk # from local file 93 | fa install http://example.org/demo.apk # from URL 94 | fa install -l ApiDemos-debug.apk # launch after install 95 | fa install -f ApiDemos-debug.apk # uninstall before install 96 | ``` 97 | 98 | Show debug info when install 99 | 100 | ```bash 101 | $ fa -d install --force --launch https://github.com/appium/java-client/raw/master/src/test/java/io/appium/java_client/ApiDemos-debug.apk 102 | Downloading ApiDemos-debug.apk... 103 | 2.94 MiB / 2.94 MiB [================================] 100.00% 282.47 KiB/s 10s 104 | Download saved to ApiDemos-debug.apk 105 | + adb -s 0123456789ABCDEF uninstall io.appium.android.apis 106 | + adb -s 0123456789ABCDEF install ApiDemos-debug.apk 107 | ApiDemos-debug.apk: 1 file pushed. 4.8 MB/s (3084877 bytes in 0.609s) 108 | pkg: /data/local/tmp/ApiDemos-debug.apk 109 | Success 110 | Launch io.appium.android.apis ... 111 | + adb -s 0123456789ABCDEF shell am start -n io.appium.android.apis/.ApiDemos 112 | ``` 113 | 114 | ### App 115 | ``` 116 | $ fa app list # show all app package names 117 | $ fa app list -3 # only show third party packages 118 | ``` 119 | 120 | ### Shell 121 | Like `adb shell`, run `fa shell` will open a terminal 122 | 123 | The difference is `fa shell` will add `/data/local/tmp` into `$PATH` 124 | So if you have binary `busybox` in `/data/local/tmp`, 125 | You can just run 126 | 127 | ``` 128 | $ fa shell busybox ls 129 | # using adb shell you have to 130 | $ adb shell /data/local/tmp/busybox ls 131 | ``` 132 | 133 | ### Watch 134 | Trace device `came online` and `went offline` 135 | 136 | ```bash 137 | $ fa watch 138 | 3578298f offline 139 | 3578298f device 140 | ``` 141 | 142 | Hook script when device online 143 | 144 | ```bash 145 | # on windows 146 | $ fa watch --online-hook-cmd="echo %SERIAL%" 147 | 148 | # on linux 149 | $ fa watch --online-hook-cmd="echo \$SERIAL" 150 | ``` 151 | 152 | This subcommand seems not very stable, raise issue when you find something wrong. 153 | 154 | ### Share 155 | Share local device 156 | 157 | ```bash 158 | # share in localnet 159 | $ fa share 160 | Connect with: adb connect 10.0.0.1:6174 161 | 162 | # share to public ip 163 | $ fa share --tunnel 164 | ______ __ 165 | /_ __/_ _____ ___ ___ / / 166 | / / / // / _ \/ _ \/ -_) / 167 | /_/ \_,_/_//_/_//_/\__/_/ v0.2.11 168 | 169 | Expose local servers to internet securely 170 | https://labstack.com/docs/tunnel 171 | ________________________________O/_______ 172 | O\ 173 | ⇨ routing traffic from tcp://leo.labstack.me:10081 174 | ``` 175 | 176 | When use tunnel, you should use `adb connect leo.labstack.me:10081` 177 | 178 | Then you can use adb to do anything just like device plugged in your computer. 179 | 180 | ### Pidcat (logcat) 181 | Current implementation is wrapper of [pidcat.py](https://github.com/JakeWharton/pidcat) 182 | So use this feature, you need python installed. 183 | 184 | ```bash 185 | $ fa help pidcat 186 | USAGE: 187 | fa pidcat [package-name ...] 188 | 189 | OPTIONS: 190 | --current filter logcat by current running app 191 | --clear clear the entire log before running 192 | --min-level value, -l value Minimum level to be displayed {V,D,I,W,E,F} 193 | --tag value, -t value filter output by specified tag(s) 194 | --ignore-tag value, -i value filter output by ignoring specified tag(s) 195 | ``` 196 | 197 | The pidcat is very beautiful. 198 | 199 | ![pidcat](https://github.com/JakeWharton/pidcat/raw/master/screen.png) 200 | 201 | ## Thanks for these Articles and Codes 202 | - 203 | - 204 | - [Facebook One World Project](https://code.fb.com/android/managing-resources-for-large-scale-testing/) 205 | - [Facebook Device Lab](https://code.fb.com/android/the-mobile-device-lab-at-the-prineville-data-center/) 206 | - Article reverse ssh tunnling 207 | - [openstf/adbkit](https://github.com/openstf/adbkit) 208 | - [ADB Source Code](https://github.com/aosp-mirror/platform_system_core/blob/master/adb) 209 | - ADB Protocols [OVERVIEW.TXT](https://github.com/aosp-mirror/platform_system_core/blob/master/adb/OVERVIEW.TXT) [SERVICES.TXT](https://github.com/aosp-mirror/platform_system_core/blob/master/adb/SERVICES.TXT) [SYNC.TXT](https://github.com/aosp-mirror/platform_system_core/blob/master/adb/SYNC.TXT) 210 | - [JakeWharton/pidcat](https://github.com/JakeWharton/pidcat) 211 | - 212 | - 213 | 214 | 215 | Libs might be useful 216 | 217 | - 218 | - 219 | - 220 | - 221 | - 222 | - SSH Tunnel 223 | - Easy SSH servers in Golang 224 | 225 | ## LICENSE 226 | [MIT](LICENSE) 227 | -------------------------------------------------------------------------------- /adb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | "syscall" 14 | 15 | shellquote "github.com/kballard/go-shellquote" 16 | ) 17 | 18 | const ( 19 | _OKAY = "OKAY" 20 | _FAIL = "FAIL" 21 | ) 22 | 23 | func adbCommand(serial string, args ...string) *exec.Cmd { 24 | if debug { 25 | fmt.Println("+ adb", "-s", serial, strings.Join(args, " ")) 26 | } 27 | c := exec.Command(adbPath(), args...) 28 | c.Env = append(os.Environ(), "ANDROID_SERIAL="+serial) 29 | return c 30 | } 31 | 32 | func runCommand(name string, args ...string) (err error) { 33 | if filepath.Base(name) == name { 34 | name, err = exec.LookPath(name) 35 | if err != nil { 36 | return err 37 | } 38 | } 39 | procAttr := new(os.ProcAttr) 40 | procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr} 41 | proc, err := os.StartProcess(name, append([]string{name}, args...), procAttr) 42 | if err != nil { 43 | return err 44 | } 45 | procState, err := proc.Wait() 46 | if err != nil { 47 | return err 48 | } 49 | ws, ok := procState.Sys().(syscall.WaitStatus) 50 | if !ok { 51 | return errors.New("exit code unknown") 52 | } 53 | exitCode := ws.ExitStatus() 54 | if exitCode == 0 { 55 | return nil 56 | } 57 | return errors.New("exit code " + strconv.Itoa(exitCode)) 58 | } 59 | 60 | func panicError(e error) { 61 | if e != nil { 62 | panic(e) 63 | } 64 | } 65 | 66 | type AdbConnection struct { 67 | net.Conn 68 | } 69 | 70 | func (conn *AdbConnection) WritePacket(data string) error { 71 | pktData := fmt.Sprintf("%04x%s", len(data), data) 72 | _, err := conn.Write([]byte(pktData)) 73 | if err != nil { 74 | return err 75 | } 76 | return conn.respCheck() 77 | } 78 | 79 | func (conn *AdbConnection) readN(n int) (v string, err error) { 80 | buf := make([]byte, n) 81 | _, err = io.ReadFull(conn, buf) 82 | if err != nil { 83 | return 84 | } 85 | return string(buf), nil 86 | } 87 | 88 | // respCheck check OKAY, or FAIL 89 | func (conn *AdbConnection) respCheck() error { 90 | status, err := conn.readN(4) 91 | if err != nil { 92 | return err 93 | } 94 | switch status { 95 | case _OKAY: 96 | return nil 97 | case _FAIL: 98 | data, err := conn.readString() 99 | if err != nil { 100 | return err 101 | } 102 | return errors.New(data) 103 | default: 104 | return fmt.Errorf("Unexpected response: %s, should be OKAY or FAIL", strconv.Quote(status)) 105 | } 106 | } 107 | 108 | func (conn *AdbConnection) readString() (string, error) { 109 | hexlen, err := conn.readN(4) 110 | if err != nil { 111 | return "", err 112 | } 113 | var length int 114 | _, err = fmt.Sscanf(hexlen, "%04x", &length) 115 | if err != nil { 116 | return "", err 117 | } 118 | return conn.readN(length) 119 | } 120 | 121 | type AdbDevice struct { 122 | *AdbClient 123 | Serial string 124 | } 125 | 126 | func (c *AdbDevice) OpenShell(cmd string) (rw io.ReadWriteCloser, err error) { 127 | conn, err := c.newConnection() 128 | if err != nil { 129 | return 130 | } 131 | err = conn.WritePacket("host:transport:" + c.Serial) 132 | if err != nil { 133 | return 134 | } 135 | err = conn.WritePacket("shell:" + cmd) //shellquote.Join(args...)) // + " ; echo :$?") 136 | if err != nil { 137 | return 138 | } 139 | return conn, nil 140 | } 141 | 142 | // OpenCommand accept list of args return combined output reader 143 | func (c *AdbDevice) OpenCommand(args ...string) (reader io.ReadWriteCloser, err error) { 144 | return c.OpenShell(shellquote.Join(args...)) 145 | } 146 | 147 | func (c *AdbDevice) RunCommand(args ...string) (exitCode int, err error) { 148 | // TODO 149 | reader, err := c.OpenCommand(args...) 150 | if err != nil { 151 | return 152 | } 153 | defer reader.Close() 154 | _, err = io.Copy(os.Stdout, reader) 155 | return 156 | } 157 | 158 | func (c *AdbDevice) SerialNo() (string, error) { 159 | conn, err := c.newConnection() 160 | if err != nil { 161 | return "", err 162 | } 163 | 164 | err = conn.WritePacket("host-serial:" + c.Serial + ":get-serialno") 165 | if err != nil { 166 | return "", err 167 | } 168 | return conn.readString() 169 | } 170 | -------------------------------------------------------------------------------- /adb/client.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os/exec" 7 | "strings" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type Client struct { 14 | Addr string 15 | } 16 | 17 | func NewClient(addr string) *Client { 18 | if addr == "" { 19 | addr = "127.0.0.1:5037" 20 | } 21 | return &Client{ 22 | Addr: addr, 23 | } 24 | } 25 | 26 | func (c *Client) dial() (conn *ADBConn, err error) { 27 | nc, err := net.DialTimeout("tcp", c.Addr, 2*time.Second) 28 | if err != nil { 29 | if err = c.StartServer(); err != nil { 30 | err = errors.Wrap(err, "adb start-server") 31 | return 32 | } 33 | nc, err = net.DialTimeout("tcp", c.Addr, 2*time.Second) 34 | } 35 | return NewADBConn(nc), err 36 | } 37 | 38 | func (c *Client) roundTrip(data string) (conn *ADBConn, err error) { 39 | conn, err = c.dial() 40 | if err != nil { 41 | return 42 | } 43 | if len(data) > 0 { 44 | err = conn.Encode([]byte(data)) 45 | } 46 | return 47 | } 48 | 49 | func (c *Client) roundTripSingleResponse(data string) (string, error) { 50 | conn, err := c.roundTrip(data) 51 | if err != nil { 52 | return "", err 53 | } 54 | defer conn.Close() 55 | if err := conn.CheckOKAY(); err != nil { 56 | return "", err 57 | } 58 | return conn.DecodeString() 59 | } 60 | 61 | // ServerVersion returns int. 39 means 1.0.39 62 | func (c *Client) ServerVersion() (v int, err error) { 63 | verstr, err := c.roundTripSingleResponse("host:version") 64 | if err != nil { 65 | return 66 | } 67 | _, err = fmt.Sscanf(verstr, "%x", &v) 68 | return 69 | } 70 | 71 | type DeviceState string 72 | 73 | const ( 74 | StateUnauthorized = DeviceState("unauthorized") 75 | StateDisconnected = DeviceState("disconnected") 76 | StateOffline = DeviceState("offline") 77 | StateOnline = DeviceState("device") 78 | ) 79 | 80 | // ListDevices returns the list of connected devices 81 | func (c *Client) ListDevices() (devs []*Device, err error) { 82 | lines, err := c.roundTripSingleResponse("host:devices") 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | devs = make([]*Device, 0) 88 | for _, line := range strings.Split(lines, "\n") { 89 | parts := strings.SplitN(line, "\t", 2) 90 | if len(parts) != 2 { 91 | continue 92 | } 93 | devs = append(devs, c.Device(DeviceWithSerial(parts[0]))) 94 | } 95 | return 96 | } 97 | 98 | func (c *Client) StartServer() (err error) { 99 | cmd := exec.Command("adb", "start-server") 100 | return cmd.Run() 101 | } 102 | 103 | // KillServer tells the server to quit immediately 104 | func (c *Client) KillServer() error { 105 | conn, err := c.roundTrip("host:kill") 106 | if err != nil { 107 | if _, ok := err.(net.Error); ok { // adb is already stopped if connection refused 108 | return nil 109 | } 110 | return err 111 | } 112 | defer conn.Close() 113 | return conn.CheckOKAY() 114 | } 115 | 116 | func (c *Client) Device(descriptor DeviceDescriptor) *Device { 117 | return &Device{ 118 | client: c, 119 | descriptor: descriptor, 120 | } 121 | } 122 | 123 | func (c *Client) DeviceWithSerial(serial string) *Device { 124 | return c.Device(DeviceWithSerial(serial)) 125 | } 126 | -------------------------------------------------------------------------------- /adb/client_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var client = NewClient("127.0.0.1:5037") 11 | 12 | func TestServerVersion(t *testing.T) { 13 | version, err := client.ServerVersion() 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | t.Log(version) 18 | } 19 | 20 | func TestDevices(t *testing.T) { 21 | devs, err := client.ListDevices() 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | t.Log(devs) 26 | } 27 | 28 | func TestKillServer(t *testing.T) { 29 | err := client.KillServer() 30 | assert.NoError(t, err) 31 | time.Sleep(2 * time.Second) 32 | } 33 | 34 | func TestDeviceStat(t *testing.T) { 35 | device := client.Device(AnyUsbDevice()) 36 | info, err := device.Stat("/data/local/tmp/minicap") 37 | if !assert.NoError(t, err) { 38 | return 39 | } 40 | t.Log(info.Name(), info.Mode().String(), info.Size(), info.ModTime()) 41 | } 42 | 43 | func TestDeviceRunCommand(t *testing.T) { 44 | device := client.Device(AnyUsbDevice()) 45 | output, err := device.RunCommand("pwd") 46 | if !assert.NoError(t, err) { 47 | return 48 | } 49 | assert.Equal(t, "/\n", output) 50 | } 51 | -------------------------------------------------------------------------------- /adb/conn.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "net" 9 | "regexp" 10 | "strconv" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type ADBConn struct { 16 | rw io.ReadWriter 17 | io.Closer 18 | err error 19 | } 20 | 21 | func NewADBConn(conn net.Conn) *ADBConn { 22 | proxyRW := debugProxyConn{ 23 | R: bufio.NewReader(conn), 24 | W: conn, 25 | Debug: false} 26 | 27 | return &ADBConn{ 28 | rw: proxyRW, 29 | Closer: conn, 30 | } 31 | } 32 | 33 | func (conn *ADBConn) Err() error { 34 | return conn.err 35 | } 36 | 37 | func (conn *ADBConn) Read(p []byte) (n int, err error) { 38 | if conn.err != nil { 39 | return 0, conn.err 40 | } 41 | n, err = conn.rw.Read(p) 42 | conn.err = err 43 | return 44 | } 45 | 46 | func (conn *ADBConn) Write(p []byte) (n int, err error) { 47 | if conn.err != nil { 48 | return 0, conn.err 49 | } 50 | n, err = conn.rw.Write(p) 51 | conn.err = err 52 | return 53 | } 54 | 55 | func (conn *ADBConn) Encode(v []byte) error { 56 | val := string(v) 57 | return conn.EncodeString(val) 58 | } 59 | 60 | func (conn *ADBConn) EncodeString(s string) error { 61 | data := fmt.Sprintf("%04x%s", len(s), s) 62 | _, err := conn.Write([]byte(data)) 63 | return err 64 | } 65 | 66 | // write data with little endian 67 | func (conn *ADBConn) WriteLE(v interface{}) error { 68 | return binary.Write(conn, binary.LittleEndian, v) 69 | } 70 | 71 | func (conn *ADBConn) WriteString(s string) (int, error) { 72 | return conn.Write([]byte(s)) 73 | } 74 | 75 | // WriteObjects according to type 76 | func (conn *ADBConn) WriteObjects(objs ...interface{}) error { 77 | var err error 78 | for _, obj := range objs { 79 | switch obj.(type) { 80 | case string: 81 | _, err = conn.WriteString(obj.(string)) 82 | case uint32, int32, uint16, int16: 83 | err = conn.WriteLE(obj) 84 | default: 85 | err = fmt.Errorf("Unsupported type: %t", obj) 86 | } 87 | if err != nil { 88 | return err 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | func (conn *ADBConn) ReadUint32() (i uint32, err error) { 95 | err = binary.Read(conn, binary.LittleEndian, &i) 96 | return 97 | } 98 | 99 | func (conn *ADBConn) ReadN(n int) (data []byte, err error) { 100 | buf := make([]byte, n) 101 | _, err = io.ReadFull(conn, buf) 102 | if err != nil { 103 | return 104 | } 105 | return buf, nil 106 | } 107 | 108 | func (conn *ADBConn) ReadNString(n int) (data string, err error) { 109 | bdata, err := conn.ReadN(n) 110 | return string(bdata), err 111 | } 112 | 113 | func (conn *ADBConn) DecodeString() (string, error) { 114 | hexlen, err := conn.ReadNString(4) 115 | if err != nil { 116 | return "", err 117 | } 118 | var length int 119 | _, err = fmt.Sscanf(hexlen, "%04x", &length) 120 | if err != nil { 121 | return "", err 122 | } 123 | return conn.ReadNString(length) 124 | } 125 | 126 | // CheckOKAY check OKAY, or FAIL 127 | func (conn *ADBConn) CheckOKAY() error { 128 | status, _ := conn.ReadNString(4) 129 | switch status { 130 | case _OKAY: 131 | return nil 132 | case _FAIL: 133 | data, err := conn.DecodeString() 134 | if err != nil { 135 | return err 136 | } 137 | return errors.Wrap(errors.New(data), "respCheck") 138 | default: 139 | return fmt.Errorf("Unexpected response: %s, should be OKAY or FAIL", strconv.Quote(status)) 140 | } 141 | } 142 | 143 | type debugProxyConn struct { 144 | R io.Reader 145 | W io.Writer 146 | Debug bool 147 | } 148 | 149 | func (px debugProxyConn) Write(data []byte) (int, error) { 150 | if px.Debug { 151 | m := regexp.MustCompile(`^[-:/0-9a-zA-Z ]+$`) 152 | if m.Match(data) { 153 | fmt.Printf("-> %q\n", string(data)) 154 | } else { 155 | fmt.Printf("-> \\x%x\n", reverseBytes(data)) 156 | } 157 | } 158 | return px.W.Write(data) 159 | } 160 | 161 | func reverseBytes(b []byte) []byte { 162 | out := make([]byte, len(b)) 163 | for i, c := range b { 164 | out[len(b)-i-1] = c 165 | } 166 | return out 167 | } 168 | 169 | func (px debugProxyConn) Read(data []byte) (int, error) { 170 | n, err := px.R.Read(data) 171 | if px.Debug { 172 | m := regexp.MustCompile(`^[-:/0-9a-zA-Z ]+$`) 173 | if m.Match(data[0:n]) { 174 | fmt.Printf("<---- %q\n", string(data[0:n])) 175 | } else { 176 | fmt.Printf("<---- \\x%x\n", reverseBytes(data[0:n])) 177 | } 178 | } 179 | return n, err 180 | } 181 | -------------------------------------------------------------------------------- /adb/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 | func (d deviceDescriptorType) String() string { 20 | switch d { 21 | case DeviceAny: 22 | return "deviceAny" 23 | case DeviceSerial: 24 | return "deviceSerial" 25 | case DeviceUsb: 26 | return "deviceUsb" 27 | case DeviceLocal: 28 | return "deviceLocal" 29 | default: 30 | return "deviceUnknown" 31 | } 32 | } 33 | 34 | type DeviceDescriptor struct { 35 | descriptorType deviceDescriptorType 36 | 37 | // Only used if Type is DeviceSerial. 38 | serial string 39 | } 40 | 41 | func AnyDevice() DeviceDescriptor { 42 | return DeviceDescriptor{descriptorType: DeviceAny} 43 | } 44 | 45 | func AnyUsbDevice() DeviceDescriptor { 46 | return DeviceDescriptor{descriptorType: DeviceUsb} 47 | } 48 | 49 | func AnyLocalDevice() DeviceDescriptor { 50 | return DeviceDescriptor{descriptorType: DeviceLocal} 51 | } 52 | 53 | func DeviceWithSerial(serial string) DeviceDescriptor { 54 | return DeviceDescriptor{ 55 | descriptorType: DeviceSerial, 56 | serial: serial, 57 | } 58 | } 59 | 60 | func (d DeviceDescriptor) String() string { 61 | if d.descriptorType == DeviceSerial { 62 | return fmt.Sprintf("%s[%s]", d.descriptorType, d.serial) 63 | } 64 | return d.descriptorType.String() 65 | } 66 | 67 | func (d DeviceDescriptor) getHostPrefix() string { 68 | switch d.descriptorType { 69 | case DeviceAny: 70 | return "host" 71 | case DeviceUsb: 72 | return "host-usb" 73 | case DeviceLocal: 74 | return "host-local" 75 | case DeviceSerial: 76 | return fmt.Sprintf("host-serial:%s", d.serial) 77 | default: 78 | panic(fmt.Sprintf("invalid DeviceDescriptorType: %v", d.descriptorType)) 79 | } 80 | } 81 | 82 | func (d DeviceDescriptor) getTransportDescriptor() string { 83 | switch d.descriptorType { 84 | case DeviceAny: 85 | return "transport-any" 86 | case DeviceUsb: 87 | return "transport-usb" 88 | case DeviceLocal: 89 | return "transport-local" 90 | case DeviceSerial: 91 | return fmt.Sprintf("transport:%s", d.serial) 92 | default: 93 | panic(fmt.Sprintf("invalid DeviceDescriptorType: %v", d.descriptorType)) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /adb/device.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net" 8 | "os" 9 | "regexp" 10 | "strings" 11 | "time" 12 | 13 | shellquote "github.com/kballard/go-shellquote" 14 | ) 15 | 16 | // Device 17 | type Device struct { 18 | descriptor DeviceDescriptor 19 | client *Client 20 | } 21 | 22 | func (d *Device) String() string { 23 | return d.descriptor.String() 24 | // return fmt.Sprintf("%s:%v", ad.serial, ad.State) 25 | } 26 | 27 | func (d *Device) Serial() (serial string, err error) { 28 | return 29 | } 30 | 31 | // OpenTransport is a low level function 32 | // Connect to adbd.exe and send :transport and check OKAY 33 | // conn should be Close after using 34 | func (d *Device) OpenTransport() (conn *ADBConn, err error) { 35 | req := "host:" + d.descriptor.getTransportDescriptor() 36 | conn, err = d.client.roundTrip(req) 37 | if err != nil { 38 | return 39 | } 40 | conn.CheckOKAY() 41 | if conn.Err() != nil { 42 | conn.Close() 43 | } 44 | return conn, conn.Err() 45 | } 46 | 47 | func (d *Device) OpenShell(cmd string) (rwc io.ReadWriteCloser, err error) { 48 | req := "host:" + d.descriptor.getTransportDescriptor() 49 | conn, err := d.client.roundTrip(req) 50 | if err != nil { 51 | return 52 | } 53 | conn.CheckOKAY() 54 | conn.EncodeString("shell:" + cmd) 55 | conn.CheckOKAY() 56 | if conn.Err() != nil { 57 | conn.Close() 58 | } 59 | return conn, conn.Err() 60 | } 61 | 62 | func (d *Device) RunCommand(args ...string) (output string, err error) { 63 | cmd := shellquote.Join(args...) 64 | rwc, err := d.OpenShell(cmd) 65 | if err != nil { 66 | return 67 | } 68 | data, err := ioutil.ReadAll(rwc) 69 | if err != nil { 70 | return 71 | } 72 | return string(data), err 73 | } 74 | 75 | // ServeTCP acts as adbd(Daemon) for adb connect 76 | func (d *Device) ServeTCP(in net.Conn) { 77 | NewSession(in, d).Serve() // conn will be Closed inside 78 | } 79 | 80 | type adbFileInfo struct { 81 | name string 82 | mode os.FileMode 83 | size uint32 84 | mtime time.Time 85 | } 86 | 87 | func (f *adbFileInfo) Name() string { 88 | return f.name 89 | } 90 | 91 | func (f *adbFileInfo) Size() int64 { 92 | return int64(f.size) 93 | } 94 | func (f *adbFileInfo) Mode() os.FileMode { 95 | return f.mode 96 | } 97 | 98 | func (f *adbFileInfo) ModTime() time.Time { 99 | return f.mtime 100 | } 101 | 102 | func (f *adbFileInfo) IsDir() bool { 103 | return f.mode.IsDir() 104 | } 105 | 106 | func (f *adbFileInfo) Sys() interface{} { 107 | return nil 108 | } 109 | 110 | func (d *Device) Stat(path string) (info os.FileInfo, err error) { 111 | req := "host:" + d.descriptor.getTransportDescriptor() 112 | conn, err := d.client.roundTrip(req) 113 | if err != nil { 114 | return 115 | } 116 | defer conn.Close() 117 | if err = conn.CheckOKAY(); err != nil { 118 | return 119 | } 120 | conn.EncodeString("sync:") 121 | conn.CheckOKAY() 122 | conn.WriteObjects("STAT", uint32(len(path)), path) 123 | 124 | id, err := conn.ReadNString(4) 125 | if err != nil { 126 | return 127 | } 128 | if id != "STAT" { 129 | return nil, fmt.Errorf("Invalid status: %q", id) 130 | } 131 | adbMode, _ := conn.ReadUint32() 132 | size, _ := conn.ReadUint32() 133 | seconds, err := conn.ReadUint32() 134 | if err != nil { 135 | return nil, err 136 | } 137 | return &adbFileInfo{ 138 | name: path, 139 | size: size, 140 | mtime: time.Unix(int64(seconds), 0).Local(), 141 | mode: fileModeFromAdb(adbMode), 142 | }, nil 143 | } 144 | 145 | type PropValue string 146 | 147 | func (p PropValue) Bool() bool { 148 | return p == "true" 149 | } 150 | 151 | var propertyRE = regexp.MustCompile(`\[(.+)\]: \[(.+)\]`) 152 | 153 | func (ad *Device) Properties() (props map[string]PropValue, err error) { 154 | props = make(map[string]PropValue) 155 | output, err := ad.RunCommand("getprop") 156 | if err != nil { 157 | return 158 | } 159 | for _, line := range strings.Split(output, "\n") { 160 | parts := propertyRE.FindStringSubmatch(line) 161 | if len(parts) != 3 { 162 | continue 163 | } 164 | key, val := parts[1], parts[2] 165 | props[key] = PropValue(val) 166 | } 167 | return 168 | } 169 | -------------------------------------------------------------------------------- /adb/device_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDeviceProperties(t *testing.T) { 10 | device := NewClient("").Device(AnyDevice()) 11 | props, err := device.Properties() 12 | if assert.NoError(t, err) { 13 | t.Log(props["ro.product.name"]) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /adb/filemode.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import "os" 4 | 5 | // The status information extracted from man 2 stat 6 | 7 | // S_IFMT 0170000 /* type of file */ 8 | 9 | // S_IFIFO 0010000 /* named pipe (fifo) */ 10 | // S_IFCHR 0020000 /* character special */ 11 | // S_IFDIR 0040000 /* directory */ 12 | // S_IFBLK 0060000 /* block special */ 13 | 14 | // S_IFREG 0100000 /* regular */ 15 | // S_IFLNK 0120000 /* symbolic link */ 16 | // S_IFSOCK 0140000 /* socket */ 17 | // S_IFWHT 0160000 /* whiteout */ 18 | 19 | // S_ISUID 0004000 /* set user id on execution */ 20 | // S_ISGID 0002000 /* set group id on execution */ 21 | // S_ISVTX 0001000 /* save swapped text even after use */ 22 | // S_IRUSR 0000400 /* read permission, owner */ 23 | // S_IWUSR 0000200 /* write permission, owner */ 24 | // S_IXUSR 0000100 /* execute/search permission, owner */ 25 | 26 | const ( 27 | ModeDir uint32 = 0040000 28 | ModeSocket = 0140000 29 | ModeSymlink = 0120000 30 | ModeRegular = 0100000 31 | ModeNamedPipe = 0010000 32 | ModeCharDevice = 0020000 33 | ModeSetuid = 0004000 34 | ModeSetgid = 0002000 35 | ModePerm = 0000777 36 | ) 37 | 38 | func maskMatch(m uint32, mask uint32) bool { 39 | return m&mask == mask 40 | } 41 | 42 | var _modeMatches = map[uint32]os.FileMode{ 43 | ModeDir: os.ModeDir, 44 | ModeSocket: os.ModeSocket, 45 | ModeSymlink: os.ModeSymlink, 46 | ModeNamedPipe: os.ModeNamedPipe, 47 | ModeCharDevice: os.ModeCharDevice, 48 | ModeSetuid: os.ModeSetuid, 49 | ModeSetgid: os.ModeSetgid, 50 | } 51 | 52 | func fileModeFromAdb(m uint32) os.FileMode { 53 | mode := os.FileMode(m & ModePerm) 54 | for statMask, modeMask := range _modeMatches { 55 | if m&statMask == statMask { 56 | mode |= modeMask 57 | } 58 | } 59 | return os.FileMode(mode) 60 | } 61 | 62 | func fileModeToAdb(mode os.FileMode) uint32 { 63 | m := uint32(mode) & ModePerm 64 | if mode.IsRegular() { 65 | m |= ModeRegular 66 | } 67 | for statMask, mask := range _modeMatches { 68 | if mode&mask == mask { 69 | m |= statMask 70 | } 71 | } 72 | return m 73 | } 74 | -------------------------------------------------------------------------------- /adb/packet.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | "os" 10 | "strconv" 11 | ) 12 | 13 | func swapUint32(n uint32) uint32 { 14 | var i uint32 15 | buf := bytes.NewBuffer(nil) 16 | binary.Write(buf, binary.LittleEndian, n) 17 | binary.Read(buf, binary.BigEndian, &i) 18 | return i 19 | } 20 | 21 | // Packet is a meta for adb connect 22 | type Packet struct { 23 | Command string 24 | Arg0 uint32 25 | Arg1 uint32 26 | Body []byte 27 | } 28 | 29 | func (pkt Packet) magic() []byte { 30 | return xorBytes([]byte(pkt.Command), []byte{0xff, 0xff, 0xff, 0xff}) 31 | } 32 | 33 | func (pkt Packet) checksum() uint32 { 34 | sum := uint32(0) 35 | for _, c := range pkt.Body { 36 | sum += uint32(c) 37 | } 38 | return sum 39 | } 40 | 41 | func (pkt Packet) length() uint32 { 42 | return uint32(len(pkt.Body)) 43 | } 44 | 45 | func (pkt Packet) BodySkipNull() []byte { 46 | if len(pkt.Body) >= 1 && pkt.Body[len(pkt.Body)-1] == byte(0) { 47 | return pkt.Body[0 : len(pkt.Body)-1] 48 | } 49 | return pkt.Body 50 | } 51 | 52 | func (pkt Packet) EncodeToBytes() []byte { 53 | payload := pkt.Body // append(pkt.Body, byte(0x00)) 54 | buf := bytes.NewBuffer(make([]byte, 0, 24+pkt.length())) 55 | if len(pkt.Command) != 4 { 56 | panic("Invalid command " + strconv.Quote(pkt.Command)) 57 | } 58 | binary.Write(buf, binary.LittleEndian, []byte(pkt.Command)) 59 | binary.Write(buf, binary.LittleEndian, pkt.Arg0) 60 | binary.Write(buf, binary.LittleEndian, pkt.Arg1) 61 | binary.Write(buf, binary.LittleEndian, pkt.length()) 62 | binary.Write(buf, binary.LittleEndian, pkt.checksum()) 63 | binary.Write(buf, binary.LittleEndian, pkt.magic()) 64 | buf.Write(payload) 65 | return buf.Bytes() 66 | } 67 | 68 | func (pkt Packet) WriteTo(wr io.Writer) (n int, err error) { 69 | return wr.Write(pkt.EncodeToBytes()) 70 | } 71 | 72 | func (pkt Packet) DumpToStdout() { 73 | fmt.Printf("cmd:%s arg0:%d arg1:%d\n", pkt.Command, pkt.Arg0, pkt.Arg1) 74 | dumper := hex.Dumper(os.Stdout) 75 | dumper.Write(pkt.Body) 76 | dumper.Close() 77 | } 78 | -------------------------------------------------------------------------------- /adb/reader.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | 10 | "github.com/qiniu/log" 11 | ) 12 | 13 | const ( 14 | _SYNC = "SYNC" 15 | _CNXN = "CNXN" 16 | _OPEN = "OPEN" 17 | _OKAY = "OKAY" 18 | _CLSE = "CLSE" 19 | _WRTE = "WRTE" 20 | _AUTH = "AUTH" 21 | _FAIL = "FAIL" 22 | 23 | UINT16_MAX = 0xFFFF 24 | UINT32_MAX = 0xFFFFFFFF 25 | 26 | AUTH_TOKEN = 1 27 | AUTH_SIGNATURE = 2 28 | AUTH_RSAPUBLICKEY = 3 29 | 30 | TOKEN_LENGTH = 20 31 | ) 32 | 33 | var ( 34 | ErrChecksum = errors.New("adb: checksum error") 35 | ErrCheckMagic = errors.New("adb: magic error") 36 | ) 37 | 38 | func calculateChecksum(data []byte) uint32 { 39 | sum := uint32(0) 40 | for _, c := range data { 41 | sum += uint32(c) 42 | } 43 | return sum 44 | } 45 | 46 | func xorBytes(a, b []byte) []byte { 47 | if len(a) != len(b) { 48 | panic(fmt.Sprintf("xorBytes a:%x b:%x have different size", a, b)) 49 | } 50 | dst := make([]byte, len(a)) 51 | for i := 0; i < len(a); i++ { 52 | dst[i] = a[i] ^ b[i] 53 | } 54 | return dst 55 | } 56 | 57 | type PacketReader struct { 58 | C chan Packet 59 | reader io.Reader 60 | err error 61 | } 62 | 63 | func NewPacketReader(reader io.Reader) *PacketReader { 64 | pr := &PacketReader{ 65 | C: make(chan Packet, 1), 66 | reader: reader, 67 | } 68 | go pr.drain() 69 | return pr 70 | } 71 | 72 | func (p *PacketReader) Err() error { 73 | return p.err 74 | } 75 | 76 | type errReader struct{} 77 | 78 | func (e errReader) Read(p []byte) (int, error) { 79 | return 0, errors.New("package already read error") 80 | } 81 | 82 | func (p *PacketReader) r() io.Reader { 83 | if p.err != nil { // use p.err to short error checks 84 | return errReader{} 85 | } 86 | return p.reader 87 | } 88 | 89 | func (p *PacketReader) drain() { 90 | defer close(p.C) 91 | for { 92 | pkt, err := p.readPacket() 93 | if err != nil { 94 | break 95 | } 96 | if p.Err() != nil { 97 | log.Println("packet read error", p.Err()) 98 | break 99 | } 100 | p.C <- pkt 101 | } 102 | } 103 | 104 | // Receive packet example 105 | // 00000000 43 4e 58 4e 01 00 00 01 00 00 10 00 23 00 00 00 |CNXN........#...| 106 | // 00000010 3c 0d 00 00 bc b1 a7 b1 68 6f 73 74 3a 3a 66 65 |<.......host::fe| 107 | // 00000020 61 74 75 72 65 73 3d 63 6d 64 2c 73 74 61 74 5f |atures=cmd,stat_| 108 | // 00000030 76 32 2c 73 68 65 6c 6c 5f 76 32 |v2,shell_v2| 109 | func (p *PacketReader) readPacket() (pkt Packet, err error) { 110 | pkt = Packet{ 111 | Command: p.readStringN(4), 112 | Arg0: p.readUint32(), 113 | Arg1: p.readUint32(), 114 | } 115 | 116 | var ( 117 | length = p.readUint32() 118 | checksum = p.readUint32() 119 | magic = p.readN(4) 120 | ) 121 | 122 | pkt.Body = p.readN(int(length)) 123 | 124 | if p.err != nil { 125 | return 126 | } 127 | if !bytes.Equal(xorBytes([]byte(pkt.Command), magic), []byte{0xff, 0xff, 0xff, 0xff}) { 128 | p.err = ErrCheckMagic 129 | log.Printf("%x %x %x", []byte(pkt.Command), magic, xorBytes([]byte(pkt.Command), magic)) 130 | return 131 | } 132 | // log.Printf("cmd:%s, arg0:%x, arg1:%x, len:%d, check:%x, magic:%x", 133 | // pkt.Command, pkt.Arg0, pkt.Arg1, length, checksum, magic) 134 | if calculateChecksum(pkt.Body) != checksum { 135 | p.err = ErrChecksum 136 | } 137 | return pkt, p.err 138 | } 139 | 140 | func (p *PacketReader) readN(n int) []byte { 141 | buf := make([]byte, n) 142 | _, p.err = io.ReadFull(p.r(), buf) 143 | return buf 144 | } 145 | 146 | func (p *PacketReader) readStringN(n int) string { 147 | return string(p.readN(n)) 148 | } 149 | 150 | func (p *PacketReader) readInt32() int32 { 151 | var i int32 152 | p.err = binary.Read(p.r(), binary.LittleEndian, &i) 153 | return i 154 | } 155 | 156 | func (p *PacketReader) readUint32() uint32 { 157 | var i uint32 158 | p.err = binary.Read(p.r(), binary.LittleEndian, &i) 159 | return i 160 | } 161 | -------------------------------------------------------------------------------- /adb/send2adb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | 4 | set -e 5 | 6 | 7 | while read -p "@? " CMD 8 | do 9 | FULLCMD=$(printf "%04x%s" ${#CMD} "${CMD}") 10 | echo -n "$FULLCMD" 11 | done 12 | 13 | exit 0 14 | #echo "SEND: $FULLCMD" 15 | echo -n "${PREFIX}$FULLCMD" | nc localhost 5037 16 | echo "" 17 | -------------------------------------------------------------------------------- /adb/server.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | 7 | "github.com/qiniu/log" 8 | ) 9 | 10 | // ADBDaemon implement service for command: adb connect 11 | type ADBDaemon struct { 12 | device *Device 13 | remotes map[string]net.Conn 14 | mu sync.Mutex 15 | } 16 | 17 | func NewADBDaemon(device *Device) *ADBDaemon { 18 | return &ADBDaemon{ 19 | device: device, 20 | remotes: make(map[string]net.Conn), 21 | } 22 | } 23 | 24 | func (s *ADBDaemon) ListenAndServe(addr string) error { 25 | ln, err := net.Listen("tcp", addr) 26 | if err != nil { 27 | return err 28 | } 29 | return s.Serve(ln) 30 | } 31 | 32 | func (s *ADBDaemon) Serve(ln net.Listener) error { 33 | defer ln.Close() 34 | for { 35 | conn, err := ln.Accept() 36 | if err != nil { 37 | return err 38 | } 39 | remoteAddress := conn.RemoteAddr().String() 40 | log.Infof("Incomming request from: %v", remoteAddress) 41 | 42 | s.mu.Lock() 43 | s.remotes[remoteAddress] = conn 44 | s.mu.Unlock() 45 | 46 | go func() { 47 | s.device.ServeTCP(conn) 48 | 49 | s.mu.Lock() 50 | delete(s.remotes, remoteAddress) 51 | s.mu.Unlock() 52 | }() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /adb/tcpusb.go: -------------------------------------------------------------------------------- 1 | // Ref link 2 | // https://github.com/openstf/adbkit/blob/master/src/adb/tcpusb/socket.coffee 3 | package adb 4 | 5 | import ( 6 | "crypto/rand" 7 | "encoding/base64" 8 | "fmt" 9 | "net" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/qiniu/log" 15 | ) 16 | 17 | // Session created when adb connected 18 | type Session struct { // adbSession 19 | device *Device 20 | conn net.Conn 21 | signature []byte 22 | err error 23 | token []byte 24 | version uint32 25 | maxPayload uint32 26 | remoteAddress string 27 | services map[uint32]*TransportService 28 | 29 | mu sync.Mutex 30 | tmpLocalIdLock sync.Mutex 31 | tmpLocalId uint32 32 | } 33 | 34 | func NewSession(conn net.Conn, device *Device) *Session { 35 | // generate challenge 36 | token := make([]byte, TOKEN_LENGTH) 37 | rand.Read(token) 38 | log.Println("Create challenge", base64.StdEncoding.EncodeToString(token)) 39 | 40 | return &Session{ 41 | device: device, 42 | conn: conn, 43 | token: token, 44 | version: 1, 45 | remoteAddress: conn.RemoteAddr().String(), 46 | services: make(map[uint32]*TransportService), 47 | } 48 | } 49 | 50 | func (s *Session) nextLocalId() uint32 { 51 | s.tmpLocalIdLock.Lock() 52 | defer s.tmpLocalIdLock.Unlock() 53 | s.tmpLocalId += 1 54 | return s.tmpLocalId 55 | } 56 | 57 | func (s *Session) writePacket(cmd string, arg0, arg1 uint32, body []byte) error { 58 | s.mu.Lock() 59 | defer s.mu.Unlock() // FIXME(ssx): need to improve performance 60 | if s.err != nil { 61 | return s.err 62 | } 63 | _, s.err = Packet{ 64 | Command: cmd, 65 | Arg0: arg0, 66 | Arg1: arg1, 67 | Body: body, 68 | }.WriteTo(s.conn) 69 | return s.err 70 | } 71 | 72 | func (s *Session) Serve() { 73 | defer s.conn.Close() 74 | pr := NewPacketReader(s.conn) 75 | 76 | for pkt := range pr.C { 77 | switch pkt.Command { 78 | case _CNXN: 79 | s.onConnection(pkt) 80 | case _AUTH: 81 | s.onAuth(pkt) 82 | case _OPEN: 83 | s.onOpen(pkt) 84 | case _OKAY, _WRTE: 85 | s.forwardServicePacket(pkt) 86 | case _CLSE: 87 | s.forwardServicePacket(pkt) 88 | s.mu.Lock() 89 | delete(s.services, pkt.Arg1) 90 | s.mu.Unlock() 91 | default: 92 | s.err = errors.New("unknown cmd: " + pkt.Command) 93 | } 94 | if s.err != nil { 95 | log.Printf("unexpect err: %v", s.err) 96 | break 97 | } 98 | } 99 | log.Println("session closed") 100 | } 101 | 102 | func (sess *Session) onConnection(pkt Packet) { 103 | sess.version = pkt.Arg0 104 | log.Printf("Version: %x", pkt.Arg0) 105 | maxPayload := pkt.Arg1 106 | // log.Println("MaxPayload:", maxPayload) 107 | if maxPayload > 0xFFFF { // UINT16_MAX 108 | maxPayload = 0xFFFF 109 | } 110 | sess.maxPayload = maxPayload 111 | // log.Println("MaxPayload:", maxPayload) 112 | sess.err = sess.writePacket(_AUTH, AUTH_TOKEN, 0, sess.token) 113 | // pkt.DumpToStdout() 114 | } 115 | 116 | func (sess *Session) authVerified() { 117 | version := swapUint32(1) 118 | // FIXME(ssx): need device.Properties() 119 | props, _ := sess.device.Properties() 120 | connProps := make([]string, 0, 3) 121 | for _, propName := range []string{ 122 | "ro.product.name", 123 | "ro.product.model", 124 | "ro.product.device", 125 | } { 126 | connProps = append(connProps, fmt.Sprintf("%s=%s", propName, props[propName])) 127 | } 128 | // connProps = append(connProps, "features=cmd") //,stat_v2,shell_v2") 129 | deviceBanner := "device" 130 | payload := fmt.Sprintf("%s::%s", deviceBanner, strings.Join(connProps, ";")) 131 | // id := "device::;;\x00" 132 | sess.err = sess.writePacket(_CNXN, version, sess.maxPayload, []byte(payload)) 133 | Packet{_CNXN, sess.version, sess.maxPayload, []byte(payload)}.DumpToStdout() 134 | } 135 | 136 | func (sess *Session) onAuth(pkt Packet) { 137 | log.Println("Handle AUTH") 138 | switch pkt.Arg0 { 139 | case AUTH_SIGNATURE: 140 | sess.signature = pkt.Body 141 | // The real logic is 142 | // If already have rsa_publickey, then verify signature, send CNXN if passed 143 | // If no rsa pubkey, then send AUTH to request it 144 | // Check signature again and send CNXN if passed 145 | log.Printf("Receive signature: %s", base64.StdEncoding.EncodeToString(pkt.Body)) 146 | // sess.err = sess.writePacket(_AUTH, AUTH_TOKEN, 0, sess.token) 147 | sess.authVerified() 148 | case AUTH_RSAPUBLICKEY: 149 | if sess.signature == nil { 150 | sess.err = errors.New("Public key sent before signature") 151 | return 152 | } 153 | log.Printf("Receive public key: %s", pkt.Body) 154 | // TODO(ssx): parse public key from body and verify signature 155 | // pkt.DumpToStdout() 156 | log.Println("receive RSA PublicKey") 157 | // pkt.DumpToStdout() 158 | // send deviceId 159 | // time.Sleep(10 * time.Second) 160 | // sess.err = errors.New("retry") 161 | // adb 1.0.40 will show "failed to authenticate to x.x.x.x:5555" 162 | // but actually connected. 163 | // sess.authVerified() 164 | default: 165 | sess.err = fmt.Errorf("unknown authentication method: %d", pkt.Arg0) 166 | } 167 | } 168 | 169 | func (sess *Session) onOpen(pkt Packet) { 170 | remoteId := pkt.Arg0 171 | localId := sess.nextLocalId() 172 | if len(pkt.Body) < 2 { 173 | sess.err = errors.New("empty service name") 174 | return // Not throw error ? 175 | } 176 | name := string(pkt.BodySkipNull()) 177 | log.Infof("Calling #%s, remoteId: %d, localId: %d", name, remoteId, localId) 178 | 179 | service := &TransportService{ 180 | localId: localId, 181 | remoteId: remoteId, 182 | sess: sess, 183 | } 184 | 185 | sess.mu.Lock() 186 | sess.services[localId] = service 187 | sess.mu.Unlock() 188 | 189 | service.handle(pkt) 190 | // pkt.DumpToStdout() 191 | } 192 | 193 | func (sess *Session) forwardServicePacket(pkt Packet) { 194 | sess.mu.Lock() 195 | service, ok := sess.services[pkt.Arg1] // localId 196 | sess.mu.Unlock() 197 | if !ok { 198 | log.Warnf("Receive packet of already closed service: localId: %d", pkt.Arg1) 199 | return 200 | } 201 | service.handle(pkt) 202 | } 203 | 204 | type TransportService struct { 205 | sess *Session 206 | device *Device 207 | transport *ADBConn 208 | localId, remoteId uint32 209 | opened bool 210 | ended bool 211 | once sync.Once 212 | } 213 | 214 | func (t *TransportService) handle(pkt Packet) { 215 | switch pkt.Command { 216 | case _OPEN: 217 | t.handleOpenPacket(pkt) 218 | case _OKAY: 219 | // Just ingore 220 | case _WRTE: 221 | t.handleWritePacket(pkt) 222 | case _CLSE: 223 | t.handleClosePacket(pkt) 224 | } 225 | } 226 | 227 | func (t *TransportService) writeError(message string) { 228 | t.writePacket(_WRTE, []byte("FAIL"+fmt.Sprintf( 229 | "%04x%s", len(message), message, 230 | ))) 231 | } 232 | 233 | func (t *TransportService) handleOpenPacket(pkt Packet) { 234 | t.writePacket(_OKAY, nil) 235 | 236 | serviceName := string(pkt.BodySkipNull()) 237 | if strings.HasPrefix(serviceName, "reverse:") { 238 | failMessage := "reverse service not supported" 239 | t.writeError(failMessage) 240 | t.end() 241 | return 242 | } 243 | 244 | var err error 245 | t.transport, err = t.sess.device.OpenTransport() 246 | if err != nil { 247 | t.end() 248 | return 249 | } 250 | t.transport.Encode([]byte(serviceName)) 251 | 252 | if err := t.transport.CheckOKAY(); err != nil { 253 | t.writeError(err.Error()) 254 | t.end() 255 | return 256 | } 257 | 258 | go func() { 259 | buf := make([]byte, t.sess.maxPayload) 260 | for { 261 | n, err := t.transport.Read(buf) 262 | if n > 0 { 263 | t.writePacket(_WRTE, buf[0:n]) 264 | } 265 | if err != nil { 266 | t.end() 267 | break 268 | } 269 | } 270 | }() 271 | } 272 | 273 | func (t *TransportService) handleWritePacket(pkt Packet) { 274 | t.writePacket(_OKAY, nil) 275 | t.transport.Write(pkt.Body) 276 | } 277 | 278 | func (t *TransportService) handleClosePacket(pkt Packet) { 279 | t.end() 280 | } 281 | 282 | func (t *TransportService) end() { 283 | t.once.Do(func() { 284 | t.ended = true 285 | if t.transport != nil { 286 | t.transport.Close() 287 | } 288 | t.writePacket(_CLSE, nil) 289 | }) 290 | } 291 | 292 | func (t *TransportService) writePacket(oper string, data []byte) { 293 | t.sess.writePacket(oper, t.localId, t.remoteId, data) 294 | } 295 | -------------------------------------------------------------------------------- /adb/tcpusb_test.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | // func TestTcpUsb(t *testing.T) { 4 | // t.Log("adb connect localhost:9000") 5 | // err := RunAdbServer("12345678") 6 | // if err != nil { 7 | // t.Fatal(err) 8 | // } 9 | // } 10 | 11 | // func TestSliceBytes(t *testing.T) { 12 | // buf := make([]byte, 0) 13 | // t.Log(buf[0 : math.Max len(buf)-1]) 14 | // } 15 | -------------------------------------------------------------------------------- /adb/writer.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import "io" 4 | 5 | type PacketWriter struct { 6 | wr io.Writer 7 | } 8 | 9 | func NewPacketWriter(w io.Writer) *PacketWriter { 10 | return &PacketWriter{ 11 | wr: w, 12 | } 13 | } 14 | 15 | func (p *PacketWriter) WritePacket(pkt Packet) error { 16 | _, err := p.wr.Write(pkt.EncodeToBytes()) 17 | return err 18 | } 19 | -------------------------------------------------------------------------------- /adb_test.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | 3 | package main 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestAdbVersion(t *testing.T) { 12 | version, err := DefaultAdbClient.Version() 13 | if err != nil { 14 | panic(err) 15 | } 16 | t.Logf("version: %s", version) 17 | } 18 | 19 | func TestAdbShell(t *testing.T) { 20 | t.Log("Shell Test") 21 | d := DefaultAdbClient.DeviceWithSerial("0123456789ABCDEF") 22 | rd, err := d.OpenShell("pwd") 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | io.Copy(os.Stdout, rd) 27 | // output, exitCode, err := DefaultAdbClient.Shell("pwd") 28 | // if err != nil { 29 | // t.Log(err) 30 | // } 31 | // t.Log(output) 32 | // t.Log(exitCode) 33 | } 34 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "os/exec" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | type AdbClient struct { 13 | Addr string 14 | } 15 | 16 | func NewAdbClient() *AdbClient { 17 | return &AdbClient{ 18 | Addr: defaultHost + ":" + strconv.Itoa(defaultPort), 19 | } 20 | } 21 | 22 | var DefaultAdbClient = &AdbClient{ 23 | Addr: "127.0.0.1:5037", 24 | } 25 | 26 | func (c *AdbClient) newConnection() (conn *AdbConnection, err error) { 27 | netConn, err := net.Dial("tcp", c.Addr) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &AdbConnection{netConn}, nil 32 | } 33 | 34 | // Version return 4 size string 35 | func (c *AdbClient) Version() (string, error) { 36 | ver, err := c.rawVersion() 37 | if err == nil { 38 | return ver, nil 39 | } 40 | exec.Command(adbPath(), "start-server").Run() 41 | return c.rawVersion() 42 | } 43 | 44 | // Plugged in: StateDisconnected->StateOffline->StateOnline 45 | // Unplugged: StateOnline->StateDisconnected 46 | type DeviceState string 47 | 48 | const ( 49 | StateInvalid = "" 50 | StateDisconnected = "disconnected" 51 | StateOffline = "offline" 52 | StateOnline = "device" 53 | StateUnauthorized = "unauthorized" 54 | ) 55 | 56 | func newDeviceState(s string) DeviceState { 57 | switch s { 58 | case "device": 59 | return StateOnline 60 | case "offline": 61 | return StateOffline 62 | case "disconnected": 63 | return StateDisconnected 64 | case "unauthorized": 65 | return StateUnauthorized 66 | default: 67 | return StateInvalid 68 | } 69 | } 70 | 71 | type DeviceStateChangedEvent struct { 72 | Serial string 73 | OldState DeviceState 74 | NewState DeviceState 75 | } 76 | 77 | func (s DeviceStateChangedEvent) String() string { 78 | return fmt.Sprintf("%s: %s->%s", s.Serial, s.OldState, s.NewState) 79 | } 80 | 81 | // CameOnline returns true if this event represents a device coming online. 82 | func (s DeviceStateChangedEvent) CameOnline() bool { 83 | return s.OldState != StateOnline && s.NewState == StateOnline 84 | } 85 | 86 | // WentOffline returns true if this event represents a device going offline. 87 | func (s DeviceStateChangedEvent) WentOffline() bool { 88 | return s.OldState == StateOnline && s.NewState != StateOnline 89 | } 90 | 91 | func (c *AdbClient) Watch() (C chan DeviceStateChangedEvent, err error) { 92 | C = make(chan DeviceStateChangedEvent, 0) 93 | conn, err := c.newConnection() 94 | if err != nil { 95 | return 96 | } 97 | conn.WritePacket("host:track-devices") 98 | go func() { 99 | defer close(C) 100 | var lastKnownStates = make(map[string]DeviceState) 101 | for { 102 | line, err := conn.readString() 103 | if err != nil { 104 | break 105 | } 106 | line = strings.TrimSpace(line) 107 | log.Println("TRACK", strconv.Quote(line)) 108 | if line == "" { 109 | continue 110 | } 111 | parts := strings.Split(line, "\t") 112 | if len(parts) != 2 { 113 | continue 114 | } 115 | serial, state := parts[0], newDeviceState(parts[1]) 116 | 117 | C <- DeviceStateChangedEvent{ 118 | Serial: serial, 119 | OldState: lastKnownStates[serial], 120 | NewState: state, 121 | } 122 | lastKnownStates[serial] = state 123 | } 124 | }() 125 | return 126 | } 127 | 128 | // Version returns adb server version 129 | func (c *AdbClient) rawVersion() (string, error) { 130 | conn, err := c.newConnection() 131 | if err != nil { 132 | return "", err 133 | } 134 | defer conn.Close() 135 | if err := conn.WritePacket("host:version"); err != nil { 136 | return "", err 137 | } 138 | return conn.readString() 139 | } 140 | 141 | func (c *AdbClient) DeviceWithSerial(serial string) *AdbDevice { 142 | return &AdbDevice{ 143 | AdbClient: c, 144 | Serial: serial, 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codeskyblue/fa 2 | 3 | require ( 4 | github.com/cavaliercoder/grab v2.0.0+incompatible 5 | github.com/fatih/color v1.7.0 // indirect 6 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 7 | github.com/labstack/tunnel-client v0.2.12 8 | github.com/manifoldco/promptui v0.3.2 9 | github.com/mattn/go-runewidth v0.0.3 // indirect 10 | github.com/mattn/go-tty v0.0.0-20181127064339-e4f871175a2f 11 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 12 | github.com/pkg/errors v0.8.0 13 | github.com/qiniu/log v0.0.0-20140728010919-a304a74568d6 14 | github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5 15 | github.com/stretchr/testify v1.2.2 16 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 17 | golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 // indirect 18 | golang.org/x/tools v0.0.0-20181214171254-3c39ce7b6105 // indirect 19 | gopkg.in/cheggaaa/pb.v1 v1.0.27 20 | gopkg.in/resty.v1 v1.10.1 21 | gopkg.in/urfave/cli.v1 v1.20.0 22 | ) 23 | -------------------------------------------------------------------------------- /install.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "regexp" 9 | "strings" 10 | "time" 11 | 12 | "github.com/cavaliercoder/grab" 13 | "github.com/pkg/errors" 14 | "github.com/shogo82148/androidbinary/apk" 15 | pb "gopkg.in/cheggaaa/pb.v1" 16 | cli "gopkg.in/urfave/cli.v1" 17 | ) 18 | 19 | func httpDownload(dst string, url string) (resp *grab.Response, err error) { 20 | client := grab.NewClient() 21 | req, err := grab.NewRequest(dst, url) 22 | if err != nil { 23 | return nil, err 24 | } 25 | // start download 26 | resp = client.Do(req) 27 | fmt.Printf("Downloading %v...\n", resp.Filename) 28 | 29 | // start UI loop 30 | t := time.NewTicker(500 * time.Millisecond) 31 | defer t.Stop() 32 | 33 | bar := pb.New(int(resp.Size)) 34 | bar.SetMaxWidth(80) 35 | bar.ShowSpeed = true 36 | bar.ShowTimeLeft = false 37 | bar.SetUnits(pb.U_BYTES) 38 | bar.Start() 39 | 40 | Loop: 41 | for { 42 | select { 43 | case <-t.C: 44 | bar.Set(int(resp.BytesComplete())) 45 | case <-resp.Done: 46 | bar.Set(int(resp.Size)) 47 | bar.Finish() 48 | break Loop 49 | } 50 | } 51 | // check for errors 52 | if err := resp.Err(); err != nil { 53 | return nil, errors.Wrap(err, "download failed") 54 | } 55 | fmt.Println("Download saved to", resp.Filename) 56 | return resp, err 57 | } 58 | 59 | func actInstall(ctx *cli.Context) error { 60 | if !ctx.Args().Present() { 61 | return errors.New("apkfile or apkurl should provided") 62 | } 63 | serial, err := chooseOne() 64 | if err != nil { 65 | return err 66 | } 67 | arg := ctx.Args().First() 68 | 69 | // download apk 70 | apkpath := arg 71 | if regexp.MustCompile(`^https?://`).MatchString(arg) { 72 | resp, err := httpDownload(".", arg) 73 | if err != nil { 74 | return err 75 | } 76 | apkpath = resp.Filename 77 | } 78 | 79 | // parse apk 80 | pkg, err := apk.OpenFile(apkpath) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | // handle --force 86 | if ctx.Bool("force") { 87 | pkgName := pkg.PackageName() 88 | adbCommand(serial, "uninstall", pkgName).Run() 89 | } 90 | 91 | // install 92 | outBuffer := bytes.NewBuffer(nil) 93 | c := adbCommand(serial, "install", "-r", apkpath) 94 | c.Stdout = io.MultiWriter(os.Stdout, outBuffer) 95 | c.Stderr = os.Stderr 96 | 97 | if err := c.Run(); err != nil { 98 | return err 99 | } 100 | 101 | if strings.Contains(outBuffer.String(), "Failure") { 102 | return errors.New("install failed") 103 | } 104 | if ctx.Bool("launch") { 105 | packageName := pkg.PackageName() 106 | mainActivity, er := pkg.MainActivity() 107 | if er != nil { 108 | fmt.Println("apk have no main-activity") 109 | return nil 110 | } 111 | if !strings.Contains(mainActivity, ".") { 112 | mainActivity = "." + mainActivity 113 | } 114 | fmt.Println("Launch app", packageName, "...") 115 | adbCommand(serial, "shell", "am", "start", "-n", packageName+"/"+mainActivity).Run() 116 | } 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "math/rand" 11 | "net" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "regexp" 16 | "runtime" 17 | "strconv" 18 | "strings" 19 | "syscall" 20 | "time" 21 | 22 | shellquote "github.com/kballard/go-shellquote" 23 | tty "github.com/mattn/go-tty" 24 | 25 | "github.com/codeskyblue/fa/adb" 26 | "github.com/codeskyblue/fa/tunnel" 27 | "github.com/manifoldco/promptui" 28 | cli "gopkg.in/urfave/cli.v1" 29 | ) 30 | 31 | var ( 32 | version = "develop" 33 | debug = false 34 | defaultSerial string 35 | defaultHost string 36 | defaultPort int 37 | ) 38 | 39 | type Device struct { 40 | Serial string `json:"serial"` 41 | Status string `json:"status"` 42 | Description string `json:"-"` 43 | } 44 | 45 | func (d *Device) String() string { 46 | return d.Serial 47 | } 48 | 49 | func shortDeviceInfo(s string) string { 50 | fields := strings.Fields(s) 51 | props := make(map[string]string, 4) 52 | for _, v := range fields { 53 | kv := strings.SplitN(v, ":", 2) 54 | if len(kv) != 2 { 55 | continue 56 | } 57 | props[kv[0]] = kv[1] 58 | } 59 | if props["model"] != "" { 60 | return props["model"] 61 | } 62 | return s 63 | } 64 | 65 | func listDevices() (ds []Device, err error) { 66 | output, err := exec.Command("adb", "devices").CombinedOutput() 67 | if err != nil { 68 | return 69 | } 70 | re := regexp.MustCompile(`(?m)^([^\s]+)\s+(device|offline|unauthorized)\s*$`) 71 | matches := re.FindAllStringSubmatch(string(output), -1) 72 | ds = make([]Device, 0, len(matches)) 73 | for _, m := range matches { 74 | status := m[2] 75 | ds = append(ds, Device{ 76 | Serial: m[1], 77 | Status: status, 78 | }) 79 | } 80 | return 81 | } 82 | 83 | func listDetailedDevices() (ds []Device, err error) { 84 | output, err := exec.Command("adb", "devices", "-l").CombinedOutput() 85 | if err != nil { 86 | return 87 | } 88 | re := regexp.MustCompile(`(?m)^([^\s]+)\s+device\s+(.+)$`) 89 | matches := re.FindAllStringSubmatch(string(output), -1) 90 | for _, m := range matches { 91 | desc := shortDeviceInfo(m[2]) 92 | ds = append(ds, Device{ 93 | Serial: m[1], 94 | Description: desc, 95 | }) 96 | } 97 | return 98 | } 99 | 100 | func choose(devices []Device) Device { 101 | if defaultSerial != "" { 102 | return Device{Serial: defaultSerial} 103 | } 104 | if len(devices) == 1 { 105 | return devices[0] 106 | } 107 | templates := &promptui.SelectTemplates{ 108 | Label: "✨ {{ . | green}}", //"{{ . }}?", 109 | Active: "➤ {{ .Serial | cyan }} {{ .Description | faint }}", 110 | Inactive: " {{ .Serial | faint }} {{ .Description | faint }}", 111 | } 112 | prompt := promptui.Select{ 113 | Label: "Select device", 114 | Items: devices, 115 | Templates: templates, 116 | } 117 | 118 | i, _, err := prompt.Run() 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | return devices[i] 123 | } 124 | 125 | func chooseOne() (serial string, err error) { 126 | devices, err := listDetailedDevices() 127 | if err != nil { 128 | return 129 | } 130 | if len(devices) == 0 { 131 | err = errors.New("no devices/emulators found") 132 | return 133 | } 134 | d := choose(devices) 135 | return d.Serial, nil 136 | } 137 | 138 | func adbWrap(args ...string) { 139 | serial, err := chooseOne() 140 | if err != nil { 141 | log.Fatal(err) 142 | } 143 | cmd := exec.Command(adbPath(), args...) 144 | cmd.Env = append(os.Environ(), "ANDROID_SERIAL="+serial) 145 | cmd.Stdout = os.Stdout 146 | cmd.Stderr = os.Stderr 147 | cmd.Stdin = os.Stdin 148 | err = cmd.Run() 149 | if err != nil { 150 | if exiterr, ok := err.(*exec.ExitError); ok { 151 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 152 | os.Exit(status.ExitStatus()) 153 | } 154 | } 155 | log.Fatal(err) 156 | } 157 | } 158 | 159 | func adbPath() string { 160 | exeName := "adb" 161 | if runtime.GOOS == "windows" { 162 | exeName += ".exe" 163 | } 164 | path, err := exec.LookPath(exeName) 165 | if err != nil { 166 | panic(err) 167 | } 168 | return path 169 | } 170 | 171 | func main() { 172 | app := cli.NewApp() 173 | app.Version = version 174 | app.Usage = "fa (fast adb) helps you win at adb" 175 | app.Authors = []cli.Author{ 176 | cli.Author{ 177 | Name: "codeskyblue", 178 | Email: "codeskyblue@gmail.com", 179 | }, 180 | } 181 | app.Flags = []cli.Flag{ 182 | cli.BoolFlag{ 183 | Name: "debug, d", 184 | Usage: "show debug info", 185 | Destination: &debug, 186 | }, 187 | cli.StringFlag{ 188 | Name: "serial, s", 189 | Usage: "use device with given serial", 190 | EnvVar: "ANDROID_SERIAL", 191 | Destination: &defaultSerial, 192 | }, 193 | cli.StringFlag{ 194 | Name: "host, H", 195 | Usage: "name of adb server host", 196 | Value: "localhost", 197 | Destination: &defaultHost, 198 | }, 199 | cli.IntFlag{ 200 | Name: "port, P", 201 | Usage: "port of adb server", 202 | Value: 5037, 203 | Destination: &defaultPort, 204 | }, 205 | } 206 | app.Commands = []cli.Command{ 207 | { 208 | Name: "adb", 209 | Usage: "exec adb with device select", 210 | SkipFlagParsing: true, 211 | Action: func(ctx *cli.Context) error { 212 | adbWrap(ctx.Args()...) 213 | return nil 214 | }, 215 | }, 216 | { 217 | Name: "version", 218 | Usage: "show version", 219 | Action: func(ctx *cli.Context) error { 220 | fmt.Printf("fa version %s\n", version) 221 | adbVersion, err := NewAdbClient().Version() 222 | if err != nil { 223 | fmt.Printf("adb version err: %v\n", err) 224 | return err 225 | } 226 | fmt.Println("adb path", adbPath()) 227 | fmt.Println("adb server version", adbVersion) 228 | return nil 229 | // output, err := exec.Command(adbPath(), "version").Output() 230 | // for _, line := range strings.Split(string(output), "\n") { 231 | // fmt.Println(" " + line) 232 | // } 233 | // return err 234 | }, 235 | }, 236 | { 237 | Name: "devices", 238 | Usage: "show connected devices", 239 | Flags: []cli.Flag{ 240 | cli.BoolFlag{ 241 | Name: "json", 242 | Usage: "output json format", 243 | }, 244 | }, 245 | Action: func(ctx *cli.Context) error { 246 | ds, err := listDevices() 247 | if err != nil { 248 | return err 249 | } 250 | if ctx.Bool("json") { 251 | data, _ := json.MarshalIndent(ds, "", " ") 252 | fmt.Println(string(data)) 253 | } else { 254 | for _, d := range ds { 255 | fmt.Printf("%s\t%s\n", d.Serial, d.Status) 256 | } 257 | } 258 | return nil 259 | }, 260 | }, 261 | { 262 | Name: "app", 263 | Usage: "app view and managerment", 264 | Subcommands: []cli.Command{ 265 | { 266 | Name: "list", 267 | Usage: "list app", 268 | Flags: []cli.Flag{ 269 | cli.BoolFlag{ 270 | Name: "f", 271 | Usage: "see their associated file", 272 | }, 273 | cli.BoolFlag{ 274 | Name: "s", 275 | Usage: "filter to only show system packages", 276 | }, 277 | cli.BoolFlag{ 278 | Name: "3", 279 | Usage: "filter to only show third party packages", 280 | }, 281 | }, 282 | Action: func(ctx *cli.Context) error { 283 | args := []string{"shell", "pm", "list", "packages"} 284 | if ctx.Bool("s") { 285 | args = append(args, "-s") 286 | } 287 | if ctx.Bool("f") { 288 | args = append(args, "-f") 289 | } 290 | if ctx.Bool("3") { 291 | args = append(args, "-3") 292 | } 293 | adbWrap(args...) 294 | return nil 295 | }, 296 | }, 297 | }, 298 | }, 299 | { 300 | Name: "screenshot", 301 | Usage: "take screenshot", 302 | Flags: []cli.Flag{ 303 | cli.StringFlag{ 304 | Name: "output, o", 305 | Value: "screenshot.png", 306 | Usage: "output screenshot name", 307 | }, 308 | cli.BoolFlag{ 309 | Name: "open", 310 | Usage: "open file after screenshot", 311 | }, 312 | }, 313 | Action: actScreenshot, 314 | }, 315 | { 316 | Name: "shell", 317 | Usage: "run shell command", 318 | SkipFlagParsing: true, 319 | Action: func(ctx *cli.Context) error { 320 | serial, err := chooseOne() 321 | if err != nil { 322 | return err 323 | } 324 | device := DefaultAdbClient.DeviceWithSerial(serial) 325 | 326 | var cmd string 327 | if len(ctx.Args()) != 0 { 328 | cmd = `PATH="$PATH:/data/local/tmp" ` + shellquote.Join(ctx.Args()...) 329 | } 330 | rwc, err := device.OpenShell(cmd) 331 | if err != nil { 332 | return err 333 | } 334 | defer rwc.Close() 335 | tty, err := tty.Open() 336 | if err != nil { 337 | log.Fatal(err) 338 | } 339 | defer tty.Close() 340 | go io.Copy(rwc, tty.Input()) 341 | _, err = io.Copy(tty.Output(), rwc) 342 | return err 343 | }, 344 | }, 345 | { 346 | Name: "install", 347 | Usage: "install apk", 348 | UsageText: "fa install [ul] ", 349 | // UseShortOptionHandling: true, // not supported in current urfav/cli 350 | Flags: []cli.Flag{ 351 | cli.BoolFlag{ 352 | Name: "force, f", 353 | Usage: "uninstall if already installed", 354 | }, 355 | cli.BoolFlag{ 356 | Name: "launch, l", 357 | Usage: "launch after success installed", 358 | }, 359 | }, 360 | Action: actInstall, 361 | }, 362 | { 363 | Name: "pidcat", 364 | Usage: "logcat filter with package name", 365 | UsageText: "fa pidcat [package-name ...]", 366 | Flags: []cli.Flag{ 367 | cli.BoolFlag{ 368 | Name: "current", 369 | Usage: "filter logcat by current running app", 370 | }, 371 | cli.BoolFlag{ 372 | Name: "clear", 373 | Usage: "clear the entire log before running", 374 | }, 375 | cli.StringFlag{ 376 | Name: "min-level, l", 377 | Usage: "Minimum level to be displayed {V,D,I,W,E,F}", 378 | }, 379 | cli.StringSliceFlag{ 380 | Name: "tag, t", 381 | Usage: "filter output by specified tag(s)", 382 | }, 383 | cli.StringSliceFlag{ 384 | Name: "ignore-tag, i", 385 | Usage: "filter output by ignoring specified tag(s)", 386 | }, 387 | }, 388 | Action: func(ctx *cli.Context) error { 389 | serial, err := chooseOne() 390 | if err != nil { 391 | return err 392 | } 393 | faDir := filepath.Join(os.Getenv("HOME"), ".fa") 394 | if err := os.MkdirAll(faDir, 0755); err != nil { 395 | return err 396 | } 397 | pidcatPath := filepath.Join(faDir, "pidcat.py") 398 | err = ioutil.WriteFile(pidcatPath, []byte(pidcatCode), 0644) 399 | if err != nil { 400 | return err 401 | } 402 | args := []string{pidcatPath, "-s", serial} 403 | if ctx.Bool("current") { 404 | args = append(args, "--current") 405 | } 406 | if ctx.Bool("clear") { 407 | args = append(args, "--clear") 408 | } 409 | if ctx.String("min-level") != "" { 410 | args = append(args, "-l", ctx.String("min-level")) 411 | } 412 | for _, tag := range ctx.StringSlice("tag") { 413 | args = append(args, "-t", tag) 414 | } 415 | for _, ignore := range ctx.StringSlice("ignore-tag") { 416 | args = append(args, "-i", ignore) 417 | } 418 | args = append(args, ctx.Args()...) 419 | return runCommand("python", args...) 420 | }, 421 | }, 422 | { 423 | Name: "get-serialno", 424 | Usage: "print serial-number", 425 | Action: func(ctx *cli.Context) error { 426 | serial, err := chooseOne() 427 | if err != nil { 428 | return err 429 | } 430 | client := NewAdbClient() 431 | device := client.DeviceWithSerial(serial) 432 | realSerial, err := device.SerialNo() 433 | if err != nil { 434 | return err 435 | } 436 | println(realSerial) 437 | return nil 438 | }, 439 | }, 440 | { 441 | Name: "healthcheck", 442 | Usage: "check device health status", 443 | Action: func(ctx *cli.Context) error { 444 | log.Println("check install") 445 | err := runCommand(os.Args[0], "install", "-f", "https://github.com/appium/java-client/raw/master/src/test/java/io/appium/java_client/ApiDemos-debug.apk") 446 | if err != nil { 447 | return err 448 | } 449 | log.Println("OKAY") 450 | return nil 451 | }, 452 | }, 453 | { 454 | Name: "share", 455 | Usage: "provides an USB device over TCP using a translating proxy", 456 | Flags: []cli.Flag{ 457 | cli.BoolFlag{ 458 | Name: "tunnel", 459 | Usage: "share device to public net, using labstack.me tunnel service", 460 | }, 461 | cli.IntFlag{ 462 | Name: "port", 463 | Usage: "listen port", 464 | Value: 6174, 465 | }, 466 | }, 467 | Action: func(ctx *cli.Context) error { 468 | serial, err := chooseOne() 469 | if err != nil { 470 | return err 471 | } 472 | client := adb.NewClient(fmt.Sprintf("%s:%d", defaultHost, defaultPort)) 473 | device := client.DeviceWithSerial(serial) 474 | 475 | if !ctx.Bool("tunnel") { 476 | adbd := adb.NewADBDaemon(device) 477 | fmt.Printf("Connect with: adb connect %s:%d\n", GetLocalIP(), ctx.Int("port")) 478 | return adbd.ListenAndServe(":" + strconv.Itoa(ctx.Int("port"))) 479 | } 480 | 481 | rand.Seed(time.Now().Unix()) 482 | c := &tunnel.Configuration{ 483 | Host: "labstack.me:22", 484 | RemoteHost: "0.0.0.0", 485 | RemotePort: 10000 + rand.Intn(2000), 486 | Channel: make(chan int), 487 | InBoundConnectionHook: func(in net.Conn) error { 488 | log.Println("Accept new connection", in.RemoteAddr().String()) 489 | device.ServeTCP(in) 490 | return nil 491 | }, 492 | } 493 | 494 | CREATE: 495 | go tunnel.Create(c) 496 | event := <-c.Channel 497 | if event == tunnel.EventReconnect { 498 | log.Println("trying to reconnect") 499 | time.Sleep(1 * time.Second) 500 | goto CREATE 501 | } 502 | return nil 503 | }, 504 | }, 505 | { 506 | Name: "watch", 507 | Usage: "show newest state when device state change", 508 | Flags: []cli.Flag{ 509 | cli.StringFlag{ 510 | Name: "online-hook-cmd", 511 | Usage: "run when device came online", 512 | }, 513 | }, 514 | Action: func(ctx *cli.Context) error { 515 | client := NewAdbClient() 516 | eventC, err := client.Watch() 517 | if err != nil { 518 | return err 519 | } 520 | onlineHook := ctx.String("online-hook-cmd") 521 | for ev := range eventC { 522 | fmt.Println(ev) 523 | if ev.CameOnline() { 524 | // log.Println("Online", ev) 525 | var cmd *exec.Cmd 526 | if runtime.GOOS == "windows" { 527 | cmd = exec.Command("cmd", "/c", onlineHook) 528 | } else { 529 | cmd = exec.Command("bash", "-c", onlineHook) 530 | } 531 | cmd.Env = append(os.Environ(), "SERIAL="+ev.Serial) 532 | cmd.Stdout = os.Stdout 533 | cmd.Stderr = os.Stderr 534 | cmd.Run() 535 | } 536 | // if ev.WentOffline { 537 | 538 | // } 539 | } 540 | return nil 541 | }, 542 | }, 543 | } 544 | err := app.Run(os.Args) 545 | if err != nil { 546 | log.Fatal(err) 547 | } 548 | } 549 | -------------------------------------------------------------------------------- /pidcat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // maybe one day I will implemented in go, maybe the day will never come. ^_^ 4 | const pidcatCode = `#!/usr/bin/python -u 5 | 6 | ''' 7 | Copyright 2009, The Android Open Source Project 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ''' 18 | 19 | # Script to highlight adb logcat output for console 20 | # Originally written by Jeff Sharkey, http://jsharkey.org/ 21 | # Piping detection and popen() added by other Android team members 22 | # Package filtering and output improvements by Jake Wharton, http://jakewharton.com 23 | 24 | import argparse 25 | import sys 26 | import re 27 | import subprocess 28 | from subprocess import PIPE 29 | 30 | __version__ = '2.1.0' 31 | 32 | LOG_LEVELS = 'VDIWEF' 33 | LOG_LEVELS_MAP = dict([(LOG_LEVELS[i], i) for i in range(len(LOG_LEVELS))]) 34 | parser = argparse.ArgumentParser(description='Filter logcat by package name') 35 | parser.add_argument('package', nargs='*', help='Application package name(s)') 36 | parser.add_argument('-w', '--tag-width', metavar='N', dest='tag_width', type=int, default=23, help='Width of log tag') 37 | parser.add_argument('-l', '--min-level', dest='min_level', type=str, choices=LOG_LEVELS+LOG_LEVELS.lower(), default='V', help='Minimum level to be displayed') 38 | parser.add_argument('--color-gc', dest='color_gc', action='store_true', help='Color garbage collection') 39 | parser.add_argument('--always-display-tags', dest='always_tags', action='store_true',help='Always display the tag name') 40 | parser.add_argument('--current', dest='current_app', action='store_true',help='Filter logcat by current running app') 41 | parser.add_argument('-s', '--serial', dest='device_serial', help='Device serial number (adb -s option)') 42 | parser.add_argument('-d', '--device', dest='use_device', action='store_true', help='Use first device for log input (adb -d option)') 43 | parser.add_argument('-e', '--emulator', dest='use_emulator', action='store_true', help='Use first emulator for log input (adb -e option)') 44 | parser.add_argument('-c', '--clear', dest='clear_logcat', action='store_true', help='Clear the entire log before running') 45 | parser.add_argument('-t', '--tag', dest='tag', action='append', help='Filter output by specified tag(s)') 46 | parser.add_argument('-i', '--ignore-tag', dest='ignored_tag', action='append', help='Filter output by ignoring specified tag(s)') 47 | parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + __version__, help='Print the version number and exit') 48 | parser.add_argument('-a', '--all', dest='all', action='store_true', default=False, help='Print all log messages') 49 | 50 | args = parser.parse_args() 51 | min_level = LOG_LEVELS_MAP[args.min_level.upper()] 52 | 53 | package = args.package 54 | 55 | base_adb_command = ['adb'] 56 | if args.device_serial: 57 | base_adb_command.extend(['-s', args.device_serial]) 58 | if args.use_device: 59 | base_adb_command.append('-d') 60 | if args.use_emulator: 61 | base_adb_command.append('-e') 62 | 63 | if args.current_app: 64 | system_dump_command = base_adb_command + ["shell", "dumpsys", "activity", "activities"] 65 | system_dump = subprocess.Popen(system_dump_command, stdout=PIPE, stderr=PIPE).communicate()[0] 66 | running_package_name = re.search(".*TaskRecord.*A[= ]([^ ^}]*)", system_dump).group(1) 67 | package.append(running_package_name) 68 | 69 | if len(package) == 0: 70 | args.all = True 71 | 72 | # Store the names of packages for which to match all processes. 73 | catchall_package = filter(lambda package: package.find(":") == -1, package) 74 | # Store the name of processes to match exactly. 75 | named_processes = filter(lambda package: package.find(":") != -1, package) 76 | # Convert default process names from : (cli notation) to (android notation) in the exact names match group. 77 | named_processes = map(lambda package: package if package.find(":") != len(package) - 1 else package[:-1], named_processes) 78 | 79 | header_size = args.tag_width + 1 + 3 + 1 # space, level, space 80 | 81 | width = -1 82 | try: 83 | # Get the current terminal width 84 | import fcntl, termios, struct 85 | h, width = struct.unpack('hh', fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('hh', 0, 0))) 86 | except: 87 | pass 88 | 89 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) 90 | 91 | RESET = '\033[0m' 92 | 93 | def termcolor(fg=None, bg=None): 94 | codes = [] 95 | if fg is not None: codes.append('3%d' % fg) 96 | if bg is not None: codes.append('10%d' % bg) 97 | return '\033[%sm' % ';'.join(codes) if codes else '' 98 | 99 | def colorize(message, fg=None, bg=None): 100 | return termcolor(fg, bg) + message + RESET 101 | 102 | def indent_wrap(message): 103 | if width == -1: 104 | return message 105 | message = message.replace('\t', ' ') 106 | wrap_area = width - header_size 107 | messagebuf = '' 108 | current = 0 109 | while current < len(message): 110 | next = min(current + wrap_area, len(message)) 111 | messagebuf += message[current:next] 112 | if next < len(message): 113 | messagebuf += '\n' 114 | messagebuf += ' ' * header_size 115 | current = next 116 | return messagebuf 117 | 118 | 119 | LAST_USED = [RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN] 120 | KNOWN_TAGS = { 121 | 'dalvikvm': WHITE, 122 | 'Process': WHITE, 123 | 'ActivityManager': WHITE, 124 | 'ActivityThread': WHITE, 125 | 'AndroidRuntime': CYAN, 126 | 'jdwp': WHITE, 127 | 'StrictMode': WHITE, 128 | 'DEBUG': YELLOW, 129 | } 130 | 131 | def allocate_color(tag): 132 | # this will allocate a unique format for the given tag 133 | # since we dont have very many colors, we always keep track of the LRU 134 | if tag not in KNOWN_TAGS: 135 | KNOWN_TAGS[tag] = LAST_USED[0] 136 | color = KNOWN_TAGS[tag] 137 | if color in LAST_USED: 138 | LAST_USED.remove(color) 139 | LAST_USED.append(color) 140 | return color 141 | 142 | 143 | RULES = { 144 | # StrictMode policy violation; ~duration=319 ms: android.os.StrictMode$StrictModeDiskWriteViolation: policy=31 violation=1 145 | re.compile(r'^(StrictMode policy violation)(; ~duration=)(\d+ ms)') 146 | : r'%s\1%s\2%s\3%s' % (termcolor(RED), RESET, termcolor(YELLOW), RESET), 147 | } 148 | 149 | # Only enable GC coloring if the user opted-in 150 | if args.color_gc: 151 | # GC_CONCURRENT freed 3617K, 29% free 20525K/28648K, paused 4ms+5ms, total 85ms 152 | key = re.compile(r'^(GC_(?:CONCURRENT|FOR_M?ALLOC|EXTERNAL_ALLOC|EXPLICIT) )(freed >>>> ([a-zA-Z0-9._:]+) \[ userId:0 \| appId:(\d+) \]$') 171 | PID_KILL = re.compile(r'^Killing (\d+):([a-zA-Z0-9._:]+)/[^:]+: (.*)$') 172 | PID_LEAVE = re.compile(r'^No longer want ([a-zA-Z0-9._:]+) \(pid (\d+)\): .*$') 173 | PID_DEATH = re.compile(r'^Process ([a-zA-Z0-9._:]+) \(pid (\d+)\) has died.?$') 174 | LOG_LINE = re.compile(r'^([A-Z])/(.+?)\( *(\d+)\): (.*?)$') 175 | BUG_LINE = re.compile(r'.*nativeGetEnabledTags.*') 176 | BACKTRACE_LINE = re.compile(r'^#(.*?)pc\s(.*?)$') 177 | 178 | adb_command = base_adb_command[:] 179 | adb_command.append('logcat') 180 | adb_command.extend(['-v', 'brief']) 181 | 182 | # Clear log before starting logcat 183 | if args.clear_logcat: 184 | adb_clear_command = list(adb_command) 185 | adb_clear_command.append('-c') 186 | adb_clear = subprocess.Popen(adb_clear_command) 187 | 188 | while adb_clear.poll() is None: 189 | pass 190 | 191 | # This is a ducktype of the subprocess.Popen object 192 | class FakeStdinProcess(): 193 | def __init__(self): 194 | self.stdout = sys.stdin 195 | def poll(self): 196 | return None 197 | 198 | if sys.stdin.isatty(): 199 | adb = subprocess.Popen(adb_command, stdin=PIPE, stdout=PIPE) 200 | else: 201 | adb = FakeStdinProcess() 202 | pids = set() 203 | last_tag = None 204 | app_pid = None 205 | 206 | def match_packages(token): 207 | if len(package) == 0: 208 | return True 209 | if token in named_processes: 210 | return True 211 | index = token.find(':') 212 | return (token in catchall_package) if index == -1 else (token[:index] in catchall_package) 213 | 214 | def parse_death(tag, message): 215 | if tag != 'ActivityManager': 216 | return None, None 217 | kill = PID_KILL.match(message) 218 | if kill: 219 | pid = kill.group(1) 220 | package_line = kill.group(2) 221 | if match_packages(package_line) and pid in pids: 222 | return pid, package_line 223 | leave = PID_LEAVE.match(message) 224 | if leave: 225 | pid = leave.group(2) 226 | package_line = leave.group(1) 227 | if match_packages(package_line) and pid in pids: 228 | return pid, package_line 229 | death = PID_DEATH.match(message) 230 | if death: 231 | pid = death.group(2) 232 | package_line = death.group(1) 233 | if match_packages(package_line) and pid in pids: 234 | return pid, package_line 235 | return None, None 236 | 237 | def parse_start_proc(line): 238 | start = PID_START_5_1.match(line) 239 | if start is not None: 240 | line_pid, line_package, target = start.groups() 241 | return line_package, target, line_pid, '', '' 242 | start = PID_START.match(line) 243 | if start is not None: 244 | line_package, target, line_pid, line_uid, line_gids = start.groups() 245 | return line_package, target, line_pid, line_uid, line_gids 246 | start = PID_START_DALVIK.match(line) 247 | if start is not None: 248 | line_pid, line_package, line_uid = start.groups() 249 | return line_package, '', line_pid, line_uid, '' 250 | return None 251 | 252 | def tag_in_tags_regex(tag, tags): 253 | return any(re.match(r'^' + t + r'$', tag) for t in map(str.strip, tags)) 254 | 255 | ps_command = base_adb_command + ['shell', 'ps'] 256 | ps_pid = subprocess.Popen(ps_command, stdin=PIPE, stdout=PIPE, stderr=PIPE) 257 | while True: 258 | try: 259 | line = ps_pid.stdout.readline().decode('utf-8', 'replace').strip() 260 | except KeyboardInterrupt: 261 | break 262 | if len(line) == 0: 263 | break 264 | 265 | pid_match = PID_LINE.match(line) 266 | if pid_match is not None: 267 | pid = pid_match.group(1) 268 | proc = pid_match.group(2) 269 | if proc in catchall_package: 270 | seen_pids = True 271 | pids.add(pid) 272 | 273 | while adb.poll() is None: 274 | try: 275 | line = adb.stdout.readline().decode('utf-8', 'replace').strip() 276 | except KeyboardInterrupt: 277 | break 278 | if len(line) == 0: 279 | break 280 | 281 | bug_line = BUG_LINE.match(line) 282 | if bug_line is not None: 283 | continue 284 | 285 | log_line = LOG_LINE.match(line) 286 | if log_line is None: 287 | continue 288 | 289 | level, tag, owner, message = log_line.groups() 290 | tag = tag.strip() 291 | start = parse_start_proc(line) 292 | if start: 293 | line_package, target, line_pid, line_uid, line_gids = start 294 | if match_packages(line_package): 295 | pids.add(line_pid) 296 | 297 | app_pid = line_pid 298 | 299 | linebuf = '\n' 300 | linebuf += colorize(' ' * (header_size - 1), bg=WHITE) 301 | linebuf += indent_wrap(' Process %s created for %s\n' % (line_package, target)) 302 | linebuf += colorize(' ' * (header_size - 1), bg=WHITE) 303 | linebuf += ' PID: %s UID: %s GIDs: %s' % (line_pid, line_uid, line_gids) 304 | linebuf += '\n' 305 | print(linebuf) 306 | last_tag = None # Ensure next log gets a tag printed 307 | 308 | dead_pid, dead_pname = parse_death(tag, message) 309 | if dead_pid: 310 | pids.remove(dead_pid) 311 | linebuf = '\n' 312 | linebuf += colorize(' ' * (header_size - 1), bg=RED) 313 | linebuf += ' Process %s (PID: %s) ended' % (dead_pname, dead_pid) 314 | linebuf += '\n' 315 | print(linebuf) 316 | last_tag = None # Ensure next log gets a tag printed 317 | 318 | # Make sure the backtrace is printed after a native crash 319 | if tag == 'DEBUG': 320 | bt_line = BACKTRACE_LINE.match(message.lstrip()) 321 | if bt_line is not None: 322 | message = message.lstrip() 323 | owner = app_pid 324 | 325 | if not args.all and owner not in pids: 326 | continue 327 | if level in LOG_LEVELS_MAP and LOG_LEVELS_MAP[level] < min_level: 328 | continue 329 | if args.ignored_tag and tag_in_tags_regex(tag, args.ignored_tag): 330 | continue 331 | if args.tag and not tag_in_tags_regex(tag, args.tag): 332 | continue 333 | 334 | linebuf = '' 335 | 336 | if args.tag_width > 0: 337 | # right-align tag title and allocate color if needed 338 | if tag != last_tag or args.always_tags: 339 | last_tag = tag 340 | color = allocate_color(tag) 341 | tag = tag[-args.tag_width:].rjust(args.tag_width) 342 | linebuf += colorize(tag, fg=color) 343 | else: 344 | linebuf += ' ' * args.tag_width 345 | linebuf += ' ' 346 | 347 | # write out level colored edge 348 | if level in TAGTYPES: 349 | linebuf += TAGTYPES[level] 350 | else: 351 | linebuf += ' ' + level + ' ' 352 | linebuf += ' ' 353 | 354 | # format tag message using rules 355 | for matcher in RULES: 356 | replace = RULES[matcher] 357 | message = matcher.sub(replace, message) 358 | 359 | linebuf += indent_wrap(message) 360 | print(linebuf.encode('utf-8'))` 361 | -------------------------------------------------------------------------------- /screenshot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "runtime" 8 | "time" 9 | 10 | "github.com/pkg/browser" 11 | // "github.com/urfave/cli" 12 | cli "gopkg.in/urfave/cli.v1" 13 | ) 14 | 15 | func anyFuncs(funcs ...func() error) error { 16 | var err error 17 | for _, f := range funcs { 18 | if err = f(); err == nil { 19 | return nil 20 | } 21 | } 22 | return err 23 | } 24 | 25 | func takeScreenshot(serial, output string) error { 26 | execOut := func() error { 27 | c := adbCommand(serial, "exec-out", "screencap", "-p", "2>/dev/null") 28 | imgfile, err := os.Create(output) 29 | if err != nil { 30 | return err 31 | } 32 | defer func() { 33 | imgfile.Close() 34 | if err != nil { 35 | os.Remove(output) 36 | } 37 | }() 38 | c.Stdout = imgfile 39 | return c.Run() 40 | } 41 | screencap := func() error { 42 | tmpPath := fmt.Sprintf("/sdcard/fa-screenshot-%d.png", time.Now().UnixNano()) 43 | c := adbCommand(serial, "shell", "screencap", "-p", tmpPath) 44 | if err := c.Run(); err != nil { 45 | return err 46 | } 47 | defer adbCommand(serial, "shell", "rm", tmpPath).Run() 48 | return adbCommand(serial, "pull", tmpPath, output).Run() 49 | } 50 | if runtime.GOOS == "windows" { 51 | return screencap() 52 | } 53 | return anyFuncs(execOut, screencap) 54 | } 55 | 56 | func actScreenshot(ctx *cli.Context) (err error) { 57 | serial, err := chooseOne() 58 | if err != nil { 59 | return err 60 | } 61 | output := ctx.String("output") 62 | err = takeScreenshot(serial, output) 63 | if err == nil { 64 | log.Println("saved to", output) 65 | if ctx.Bool("open") { 66 | browser.OpenFile(output) 67 | } 68 | } 69 | return err 70 | } 71 | -------------------------------------------------------------------------------- /scripts/tcp-proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # This is a simple port-forward / proxy, written using only the default python 3 | # library. If you want to make a suggestion or fix something you can contact-me 4 | # at voorloop_at_gmail.com 5 | # Distributed over IDC(I Don't Care) license 6 | import socket 7 | import select 8 | import time 9 | import sys 10 | 11 | # Changing the buffer_size and delay, you can improve the speed and bandwidth. 12 | # But when buffer get to high or delay go too down, you can broke things 13 | buffer_size = 4096 14 | delay = 0.0001 15 | forward_to = ('127.0.0.1', 5037) 16 | #forward_to = ('10.249.80.122', 57149) 17 | 18 | class Forward: 19 | def __init__(self): 20 | self.forward = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 21 | 22 | def start(self, host, port): 23 | try: 24 | self.forward.connect((host, port)) 25 | return self.forward 26 | except Exception, e: 27 | print e 28 | return False 29 | 30 | class TheServer: 31 | input_list = [] 32 | channel = {} 33 | 34 | def __init__(self, host, port): 35 | self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 36 | self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 37 | self.server.bind((host, port)) 38 | self.server.listen(200) 39 | 40 | def main_loop(self): 41 | self.input_list.append(self.server) 42 | while 1: 43 | time.sleep(delay) 44 | ss = select.select 45 | inputready, outputready, exceptready = ss(self.input_list, [], []) 46 | for self.s in inputready: 47 | if self.s == self.server: 48 | self.on_accept() 49 | break 50 | 51 | self.data = self.s.recv(buffer_size) 52 | if len(self.data) == 0: 53 | self.on_close() 54 | break 55 | else: 56 | self.on_recv() 57 | 58 | def on_accept(self): 59 | forward = Forward().start(forward_to[0], forward_to[1]) 60 | clientsock, clientaddr = self.server.accept() 61 | if forward: 62 | print '[proxy]', clientaddr, "has connected" 63 | self.input_list.append(clientsock) 64 | self.input_list.append(forward) 65 | self.channel[clientsock] = forward 66 | self.channel[forward] = clientsock 67 | else: 68 | print "[proxy] Can't establish connection with remote server.", 69 | print "[proxy] Closing connection with client side", clientaddr 70 | clientsock.close() 71 | 72 | def on_close(self): 73 | print '[proxy]', self.s.getpeername(), "has disconnected" 74 | print '[proxy]', self.channel[self.s].getpeername(), "has disconnected, too" 75 | #remove objects from input_list 76 | self.input_list.remove(self.s) 77 | self.input_list.remove(self.channel[self.s]) 78 | out = self.channel[self.s] 79 | # close the connection with client 80 | self.channel[out].close() # equivalent to do self.s.close() 81 | # close the connection with remote server 82 | self.channel[self.s].close() 83 | # delete both objects from channel dict 84 | del self.channel[out] 85 | del self.channel[self.s] 86 | 87 | def on_recv(self): 88 | data = self.data 89 | # here we can parse and/or modify the data before send forward 90 | #print isinstance(self.s, Forward), self.server 91 | print '[%5s]: %s' % (self.s.getpeername()[1], data) 92 | self.channel[self.s].send(data) 93 | 94 | if __name__ == '__main__': 95 | server = TheServer('', 6037) 96 | try: 97 | server.main_loop() 98 | except KeyboardInterrupt: 99 | print "Ctrl C - Stopping server" 100 | sys.exit(1) -------------------------------------------------------------------------------- /tunnel-test/main.go: -------------------------------------------------------------------------------- 1 | // REFERENCE 2 | // Go generate RSA https://gist.github.com/sdorra/1c95de8cb80da31610d2ad767cd6f251 3 | // Go Resty https://github.com/go-resty/resty 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "time" 10 | 11 | tunnel "github.com/labstack/tunnel-client" 12 | "github.com/qiniu/log" 13 | resty "gopkg.in/resty.v1" 14 | ) 15 | 16 | func main() { 17 | c := &tunnel.Configuration{ 18 | Host: "labstack.me:22", 19 | RemoteHost: "0.0.0.0", 20 | RemotePort: 8000, 21 | Channel: make(chan int), 22 | } 23 | c.TargetHost = "127.0.0.1" 24 | c.TargetPort = 6174 25 | 26 | // Ref: https://github.com/labstack/tunnel-client/blob/master/cmd/root.go 27 | res, err := resty.R(). 28 | SetAuthToken("hello world"). 29 | SetHeader("Content-Type", "application/json"). 30 | SetHeader("User-Agent", "labstack/tunnel"). 31 | Get("http://httpbin.org/get") 32 | if err != nil { 33 | log.Fatalf("request err: %v", err) 34 | } else if res.StatusCode() != http.StatusOK { 35 | log.Fatalf("request status code not 200, receive %d", res.StatusCode()) 36 | } 37 | fmt.Printf("Response Body: %v", res.String()) 38 | 39 | CREATE: 40 | go tunnel.Create(c) 41 | event := <-c.Channel 42 | if event == tunnel.EventReconnect { 43 | log.Info("trying to reconnect") 44 | time.Sleep(1 * time.Second) 45 | goto CREATE 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tunnel/tunnel.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | 12 | "github.com/qiniu/log" 13 | "golang.org/x/crypto/ssh" 14 | ) 15 | 16 | type ( 17 | Configuration struct { 18 | Protocol string `json:"protocol"` 19 | Subdomain string `json:"subdomain"` 20 | Domain string `json:"domain"` 21 | Port int `json:"port"` 22 | Host string `json:"host"` 23 | User string 24 | RemoteHost string 25 | RemotePort int 26 | TargetHost string 27 | TargetPort int 28 | InBoundConnectionHook func(net.Conn) error 29 | Channel chan int 30 | HideBanner bool 31 | } 32 | 33 | Error struct { 34 | Code int `json:"code,omitempty"` 35 | Message string `json:"message"` 36 | } 37 | ) 38 | 39 | const ( 40 | _ = iota 41 | EventReconnect 42 | ) 43 | 44 | var ( 45 | hostBytes = []byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDoSLknvlFrFzroOlh1cqvcIFelHO+Wvj1UZ/p3J9bgsJGiKfh3DmBqEw1DOEwpHJz4zuV375TyjGuHuGZ4I4xztnwauhFplfEvriVHQkIDs6UnGwJVr15XUQX04r0i6mLbJs5KqIZTZuZ9ZGOj7ZWnaA7C07nPHGrERKV2Fm67rPvT6/qFikdWUbCt7KshbzdwwfxUohmv+NI7vw2X6vPU8pDaNEY7vS3YgwD/WlvQx+WDF2+iwLVW8OWWjFuQso6Eg1BSLygfPNhAHoiOWjDkijc8U9LYkUn7qsDCnvJxCoTTNmdECukeHfzrUjTSw72KZoM5KCRV78Wrctai1Qn6yRQz9BOSguxewLfzHtnT43/MLdwFXirJ/Ajquve2NAtYmyGCq5HcvpDAyi7lQ0nFBnrWv5zU3YxrISIpjovVyJjfPx8SCRlYZwVeUq6N2yAxCzJxbElZPtaTSoXBIFtoas2NXnCWPgenBa/2bbLQqfgbN8VQ9RaUISKNuYDIn4+eO72+RxF9THzZeV17pnhTVK88XU4asHot1gXwAt4vEhSjdUBC9KUIkfukI6F4JFxtvuO96octRahdV1Qg0vF+D0+SPy2HxqjgZWgPE2Xh/NmuIXwbE0wkymR2wrgj8Hd4C92keo2NBRh9dD7D2negnVYaYsC+3k/si5HNuCHnHQ== tunnel@labstack.com") 46 | ) 47 | 48 | func Create(c *Configuration) { 49 | hostKey, _, _, _, err := ssh.ParseAuthorizedKey(hostBytes) 50 | if err != nil { 51 | log.Fatalf("failed to parse host key: %v", err) 52 | } 53 | config := &ssh.ClientConfig{ 54 | User: c.User, 55 | Auth: []ssh.AuthMethod{ 56 | ssh.Password("password"), 57 | }, 58 | HostKeyCallback: ssh.FixedHostKey(hostKey), 59 | BannerCallback: func(message string) error { 60 | if !c.HideBanner { 61 | fmt.Print(message) 62 | } 63 | return nil 64 | }, 65 | } 66 | client := new(ssh.Client) 67 | 68 | // Connect 69 | proxy := os.Getenv("http_proxy") 70 | if proxy != "" { 71 | proxyURL, err := url.Parse(proxy) 72 | if err != nil { 73 | log.Fatalf("cannot open new session: %v", err) 74 | } 75 | tcp, err := net.Dial("tcp", proxyURL.Hostname()) 76 | if err != nil { 77 | log.Fatalf("cannot open new session: %v", err) 78 | } 79 | connReq := &http.Request{ 80 | Method: "CONNECT", 81 | URL: &url.URL{Path: c.Host}, 82 | Host: c.Host, 83 | Header: make(http.Header), 84 | } 85 | if proxyURL.User != nil { 86 | if p, ok := proxyURL.User.Password(); ok { 87 | connReq.SetBasicAuth(proxyURL.User.Username(), p) 88 | } 89 | } 90 | connReq.Write(tcp) 91 | resp, err := http.ReadResponse(bufio.NewReader(tcp), connReq) 92 | if err != nil { 93 | log.Fatalf("cannot open new session: %v", err) 94 | } 95 | defer resp.Body.Close() 96 | 97 | conn, chans, reqs, err := ssh.NewClientConn(tcp, c.Host, config) 98 | if err != nil { 99 | log.Fatalf("cannot open new session: %v", err) 100 | } 101 | client = ssh.NewClient(conn, chans, reqs) 102 | } else { 103 | client, err = ssh.Dial("tcp", c.Host, config) 104 | } 105 | if err != nil { 106 | log.Errorf("failed to connect: %v", err) 107 | c.Channel <- EventReconnect 108 | return 109 | } 110 | defer client.Close() 111 | 112 | // Session 113 | sess, err := client.NewSession() 114 | if err != nil { 115 | log.Fatalf("failed to create session: %v", err) 116 | } 117 | defer sess.Close() 118 | r, err := sess.StdoutPipe() 119 | if err != nil { 120 | log.Print(err) 121 | } 122 | br := bufio.NewReader(r) 123 | go func() { 124 | for { 125 | line, _, err := br.ReadLine() 126 | if err != nil { 127 | if err == io.EOF { 128 | c.Channel <- EventReconnect 129 | return 130 | } else { 131 | log.Fatalf("failed to read: %v", err) 132 | } 133 | } 134 | fmt.Printf("%s\n", line) 135 | } 136 | }() 137 | 138 | // Remote listener 139 | ln, err := client.Listen("tcp", fmt.Sprintf("%s:%d", c.RemoteHost, c.RemotePort)) 140 | if err != nil { 141 | log.Fatalf("failed to listen on remote host: %v", err) 142 | } 143 | defer ln.Close() 144 | 145 | for { 146 | // Handle inbound connection 147 | in, err := ln.Accept() 148 | if err != nil { 149 | log.Printf("failed to accept connection: %v", err) 150 | return 151 | } 152 | 153 | if c.InBoundConnectionHook != nil { 154 | go c.InBoundConnectionHook(in) 155 | continue 156 | } 157 | 158 | go func(in net.Conn) { 159 | defer in.Close() 160 | 161 | // Target connection 162 | out, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.TargetHost, c.TargetPort)) 163 | if err != nil { 164 | log.Printf("failed to connect to target: %v", err) 165 | return 166 | } 167 | defer out.Close() 168 | 169 | // Copy 170 | errCh := make(chan error, 2) 171 | cp := func(dst io.Writer, src io.Reader) { 172 | _, err := io.Copy(dst, src) 173 | errCh <- err 174 | } 175 | go cp(in, out) 176 | go cp(out, in) 177 | 178 | // Handle error 179 | err = <-errCh 180 | if err != nil && err != io.EOF { 181 | log.Printf("failed to copy: %v", err) 182 | } 183 | }(in) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net" 4 | 5 | func GetLocalIP() string { 6 | addrs, err := net.InterfaceAddrs() 7 | if err != nil { 8 | return "localhost" 9 | } 10 | for _, address := range addrs { 11 | // check the address type and if it is not a loopback the display it 12 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 13 | if ipnet.IP.To4() != nil { 14 | return ipnet.IP.String() 15 | } 16 | } 17 | } 18 | return "localhost" 19 | } 20 | --------------------------------------------------------------------------------