From 560c9e4d1db2835f1b228c618a7d66cd0f99eb4a Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Sun, 26 Apr 2026 06:32:44 +0000 Subject: [PATCH 1/2] fix(network): handle adb tcp device metadata --- README.md | 17 +++++++---- adb.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++---- adb_test.go | 62 +++++++++++++++++++++++++++++++++----- 3 files changed, 145 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 17032a0..d13c87f 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ import ( "context" "fmt" "log" + "net" "time" "github.com/taigrr/adb" @@ -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) } diff --git a/adb.go b/adb.go index a7d8136..be6b3bf 100644 --- a/adb.go +++ b/adb.go @@ -3,6 +3,7 @@ package adb import ( "context" "errors" + "fmt" "net" "os" "strconv" @@ -44,10 +45,36 @@ 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 + } + + connectedDevice, parseErr := parseConnectedDevice(stdout) + if parseErr == nil { + if connectedDevice.SerialNo != "" { + device.SerialNo = connectedDevice.SerialNo + } + device.ConnType = connectedDevice.ConnType + device.IP = connectedDevice.IP + device.Port = connectedDevice.Port + device.IsAuthorized = connectedDevice.IsAuthorized + } + + return device, nil } func (d Device) ConnString() string { @@ -55,7 +82,7 @@ func (d Device) ConnString() string { 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. @@ -111,12 +138,60 @@ 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 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. @@ -124,7 +199,7 @@ 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 } diff --git a/adb_test.go b/adb_test.go index a098239..3b2b4d1 100644 --- a/adb_test.go +++ b/adb_test.go @@ -27,7 +27,7 @@ func Test_parseDevices(t *testing.T) { 19291FDEE0023W device`}, wantErr: false, want: []Device{ - {IsAuthorized: true, SerialNo: "19291FDEE0023W"}, + {IsAuthorized: true, SerialNo: "19291FDEE0023W", ConnType: USB}, }, }, { @@ -35,7 +35,7 @@ func Test_parseDevices(t *testing.T) { HT75R0202681 unauthorized`}, wantErr: false, want: []Device{ - {IsAuthorized: false, SerialNo: "HT75R0202681"}, + {IsAuthorized: false, SerialNo: "HT75R0202681", ConnType: USB}, }, }, { @@ -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}, }, }, { @@ -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}, }, }, { @@ -74,7 +83,7 @@ ABCD1234 offline`}, `}, wantErr: false, want: []Device{ - {IsAuthorized: true, SerialNo: "19291FDEE0023W"}, + {IsAuthorized: true, SerialNo: "19291FDEE0023W", ConnType: USB}, }, }, } @@ -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 { @@ -124,6 +133,43 @@ 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: "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 TestTapSequence_ShortenSleep(t *testing.T) { seq := TapSequence{ Events: []Input{ From e04a257dd71ffba045cc625a1b2367a1273373d4 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Mon, 27 Apr 2026 06:32:08 +0000 Subject: [PATCH 2/2] fix(network): refresh reconnect device metadata --- adb.go | 29 +++++++++++++++++------------ adb_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/adb.go b/adb.go index be6b3bf..d9e1358 100644 --- a/adb.go +++ b/adb.go @@ -63,16 +63,7 @@ func Connect(ctx context.Context, opts ConnOptions) (Device, error) { return Device{}, ErrUnspecified } - connectedDevice, parseErr := parseConnectedDevice(stdout) - if parseErr == nil { - if connectedDevice.SerialNo != "" { - device.SerialNo = connectedDevice.SerialNo - } - device.ConnType = connectedDevice.ConnType - device.IP = connectedDevice.IP - device.Port = connectedDevice.Port - device.IsAuthorized = connectedDevice.IsAuthorized - } + device.applyConnectedDevice(stdout) return device, nil } @@ -101,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 } @@ -151,6 +142,20 @@ func parseDevices(stdout string) ([]Device, error) { 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 { diff --git a/adb_test.go b/adb_test.go index 3b2b4d1..1d2c446 100644 --- a/adb_test.go +++ b/adb_test.go @@ -150,6 +150,11 @@ func Test_parseConnectedDevice(t *testing.T) { 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", @@ -170,6 +175,28 @@ func Test_parseConnectedDevice(t *testing.T) { } } +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{