Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import (
"context"
"fmt"
"log"
"net"
"time"

"github.com/taigrr/adb"
Expand All @@ -74,16 +75,20 @@ import (
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Equivalent to `adb connect 192.168.2.5` with a 10 second timeout
opts := adb.Options{ Address: "192.168.2.5" }
// Equivalent to `adb connect 192.168.2.5:5555` with a 10 second timeout
opts := adb.ConnOptions{ Address: net.IPAddr{IP: net.ParseIP("192.168.2.5")} }
dev, err := adb.Connect(ctx, opts)
if err != nil {
log.Fatalf("unable to connect to device %s: %v", opts.Address, err)
log.Fatalf("unable to connect to device %s: %v", opts.Address.String(), err)
}
defer dev.Disconnect()
stdout, stderr, errCode, err := dev.Shell("ls")
defer func() {
if err := dev.Disconnect(ctx); err != nil {
log.Printf("disconnect failed: %v", err)
}
}()
stdout, stderr, errCode, err := dev.Shell(ctx, "ls")
if err != nil {
log.Fatalf("unable to shell into device %s: %v", opts.Address, err)
log.Fatalf("unable to shell into device %s: %v", opts.Address.String(), err)
}
log.Printf("Stdout: %s\nStderr: %s\n, ErrCode: %d", stdout, stderr, errCode)
}
Expand Down
94 changes: 87 additions & 7 deletions adb.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package adb
import (
"context"
"errors"
"fmt"
"net"
"os"
"strconv"
Expand Down Expand Up @@ -44,18 +45,35 @@ type ConnOptions struct {
// If the connection fails or cannot complete on time, Connect will return an error.
// TODO
func Connect(ctx context.Context, opts ConnOptions) (Device, error) {
if opts.Port == 0 {
opts.Port = 5555
device := Device{
ConnType: Network,
IP: opts.Address,
Port: opts.Port,
SerialNo: opts.SerialNo,
}
return Device{}, nil
if device.Port == 0 {
device.Port = 5555
}

stdout, _, errcode, err := execute(ctx, []string{"connect", device.ConnString()})
if err != nil {
return Device{}, err
}
if errcode != 0 {
return Device{}, ErrUnspecified
}

device.applyConnectedDevice(stdout)

return device, nil
}

func (d Device) ConnString() string {
port := d.Port
if port == 0 {
port = 5555
}
return d.IP.String() + ":" + strconv.Itoa(int(port))
return net.JoinHostPort(d.IP.String(), strconv.Itoa(int(port)))
}

// Connect to a previously discovered device.
Expand All @@ -74,8 +92,8 @@ func (d Device) Reconnect(ctx context.Context) (Device, error) {
if errcode != 0 {
return d, ErrUnspecified
}
_, _ = stdout, stderr
// TODO capture and store serial number into d before returning
_, _ = stderr, stdout
d.applyConnectedDevice(stdout)
return d, nil
}

Expand Down Expand Up @@ -111,20 +129,82 @@ func parseDevices(stdout string) ([]Device, error) {
SerialNo: Serial(words[0]),
IsAuthorized: words[1] == "device",
}
if networkDevice, err := parseNetworkDevice(words[0]); err == nil {
d.ConnType = Network
d.IP = networkDevice.IP
d.Port = networkDevice.Port
} else {
d.ConnType = USB
}
devs = append(devs, d)
}

return devs, nil
}

func (d *Device) applyConnectedDevice(stdout string) {
connectedDevice, err := parseConnectedDevice(stdout)
if err != nil {
return
}
if connectedDevice.SerialNo != "" {
d.SerialNo = connectedDevice.SerialNo
}
d.ConnType = connectedDevice.ConnType
d.IP = connectedDevice.IP
d.Port = connectedDevice.Port
d.IsAuthorized = connectedDevice.IsAuthorized
}

func parseConnectedDevice(stdout string) (Device, error) {
lines := strings.Split(stdout, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if strings.HasPrefix(trimmed, "connected to ") {
serial := strings.TrimPrefix(trimmed, "connected to ")
return parseNetworkDevice(serial)
}
if strings.HasPrefix(trimmed, "already connected to ") {
serial := strings.TrimPrefix(trimmed, "already connected to ")
return parseNetworkDevice(serial)
}
}
return Device{}, fmt.Errorf("unable to parse connected device from %q", stdout)
}

func parseNetworkDevice(serial string) (Device, error) {
host, portStr, err := net.SplitHostPort(serial)
if err != nil {
return Device{}, err
}
ip := net.ParseIP(host)
if ip == nil {
return Device{}, fmt.Errorf("invalid IP address %q", host)
}
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return Device{}, err
}
return Device{
SerialNo: Serial(serial),
IsAuthorized: true,
ConnType: Network,
IP: net.IPAddr{IP: ip},
Port: uint(port),
}, nil
}

// Disconnect from a device.
//
// If a device is already disconnected or otherwise not found, returns an error.
func (d Device) Disconnect(ctx context.Context) error {
if d.ConnType != Network {
return ErrConnUSB
}
_, _, _, err := execute(ctx, []string{"-s", d.ConnString(), "disconnect"})
_, _, _, err := execute(ctx, []string{"disconnect", d.ConnString()})
return err
}

Expand Down
89 changes: 81 additions & 8 deletions adb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ func Test_parseDevices(t *testing.T) {
19291FDEE0023W device`},
wantErr: false,
want: []Device{
{IsAuthorized: true, SerialNo: "19291FDEE0023W"},
{IsAuthorized: true, SerialNo: "19291FDEE0023W", ConnType: USB},
},
},
{
name: "1 unauth dev", args: args{stdout: `List of devices attached
HT75R0202681 unauthorized`},
wantErr: false,
want: []Device{
{IsAuthorized: false, SerialNo: "HT75R0202681"},
{IsAuthorized: false, SerialNo: "HT75R0202681", ConnType: USB},
},
},
{
Expand All @@ -45,9 +45,9 @@ HT75R0202681 unauthorized`},
HT75R0202681 unauthorized`},
wantErr: false,
want: []Device{
{IsAuthorized: true, SerialNo: "19291FDEE0023W"},
{IsAuthorized: true, SerialNo: "9B061FFBA00BC9"},
{IsAuthorized: false, SerialNo: "HT75R0202681"},
{IsAuthorized: true, SerialNo: "19291FDEE0023W", ConnType: USB},
{IsAuthorized: true, SerialNo: "9B061FFBA00BC9", ConnType: USB},
{IsAuthorized: false, SerialNo: "HT75R0202681", ConnType: USB},
},
},
{
Expand All @@ -62,7 +62,16 @@ HT75R0202681 unauthorized`},
ABCD1234 offline`},
wantErr: false,
want: []Device{
{IsAuthorized: false, SerialNo: "ABCD1234"},
{IsAuthorized: false, SerialNo: "ABCD1234", ConnType: USB},
},
},
{
name: "network device",
args: args{stdout: `List of devices attached
192.168.1.10:5555 device`},
wantErr: false,
want: []Device{
{IsAuthorized: true, SerialNo: "192.168.1.10:5555", ConnType: Network, IP: net.IPAddr{IP: net.ParseIP("192.168.1.10")}, Port: 5555},
},
},
{
Expand All @@ -74,7 +83,7 @@ ABCD1234 offline`},
`},
wantErr: false,
want: []Device{
{IsAuthorized: true, SerialNo: "19291FDEE0023W"},
{IsAuthorized: true, SerialNo: "19291FDEE0023W", ConnType: USB},
},
},
}
Expand Down Expand Up @@ -111,7 +120,7 @@ func TestDevice_ConnString(t *testing.T) {
{
name: "ipv6",
dev: Device{IP: net.IPAddr{IP: net.ParseIP("::1")}, Port: 5555},
want: "::1:5555",
want: "[::1]:5555",
},
}
for _, tt := range tests {
Expand All @@ -124,6 +133,70 @@ func TestDevice_ConnString(t *testing.T) {
}
}

func Test_parseConnectedDevice(t *testing.T) {
tests := []struct {
name string
stdout string
want Device
wantErr bool
}{
{
name: "connected",
stdout: "connected to 192.168.1.10:5555\n",
want: Device{SerialNo: "192.168.1.10:5555", IsAuthorized: true, ConnType: Network, IP: net.IPAddr{IP: net.ParseIP("192.168.1.10")}, Port: 5555},
},
{
name: "already connected",
stdout: "already connected to 192.168.1.10:5555\n",
want: Device{SerialNo: "192.168.1.10:5555", IsAuthorized: true, ConnType: Network, IP: net.IPAddr{IP: net.ParseIP("192.168.1.10")}, Port: 5555},
},
{
name: "connected ipv6",
stdout: "connected to [2001:db8::1]:5555\n",
want: Device{SerialNo: "[2001:db8::1]:5555", IsAuthorized: true, ConnType: Network, IP: net.IPAddr{IP: net.ParseIP("2001:db8::1")}, Port: 5555},
},
{
name: "unparseable output",
stdout: "unable to connect to 192.168.1.10:5555\n",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseConnectedDevice(tt.stdout)
if (err != nil) != tt.wantErr {
t.Fatalf("parseConnectedDevice() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseConnectedDevice() = %v, want %v", got, tt.want)
}
})
}
}

func TestDevice_applyConnectedDevice(t *testing.T) {
t.Run("updates network metadata from adb output", func(t *testing.T) {
device := Device{ConnType: Network, IP: net.IPAddr{IP: net.ParseIP("10.0.0.5")}, Port: 5555}
device.applyConnectedDevice("connected to 192.168.1.10:5556\n")

want := Device{SerialNo: "192.168.1.10:5556", IsAuthorized: true, ConnType: Network, IP: net.IPAddr{IP: net.ParseIP("192.168.1.10")}, Port: 5556}
if !reflect.DeepEqual(device, want) {
t.Fatalf("applyConnectedDevice() = %#v, want %#v", device, want)
}
})

t.Run("leaves device unchanged on unparseable output", func(t *testing.T) {
original := Device{SerialNo: "existing", ConnType: Network, IP: net.IPAddr{IP: net.ParseIP("10.0.0.5")}, Port: 5555}
device := original
device.applyConnectedDevice("unable to connect\n")

if !reflect.DeepEqual(device, original) {
t.Fatalf("applyConnectedDevice() mutated device: %#v != %#v", device, original)
}
})
}

func TestTapSequence_ShortenSleep(t *testing.T) {
seq := TapSequence{
Events: []Input{
Expand Down
Loading