Skip to content

Commit 931b58c

Browse files
committed
Support concurrent ADB device selection
1 parent d21f707 commit 931b58c

7 files changed

Lines changed: 460 additions & 16 deletions

File tree

acquisition/acquisition.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"github.com/botherder/go-savetime/hashes"
2020
"github.com/google/uuid"
2121
"github.com/mvt-project/androidqf/adb"
22-
"github.com/mvt-project/androidqf/assets"
2322
"github.com/mvt-project/androidqf/log"
2423
"github.com/mvt-project/androidqf/utils"
2524
)
@@ -178,10 +177,6 @@ func (a *Acquisition) Complete() {
178177
if a.Collector != nil {
179178
a.Collector.Clean()
180179
}
181-
182-
// Stop ADB server before trying to remove extracted assets
183-
adb.Client.KillServer()
184-
assets.CleanAssets()
185180
}
186181

187182
func (a *Acquisition) GetSystemInformation() error {

adb/adb.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ func New() (*ADB, error) {
3232
}
3333
log.Debugf("ADB found at path: %s", adb.ExePath)
3434

35-
log.Debug("Killing existing ADB server if running")
36-
adb.KillServer()
37-
3835
// Confirm that we can call "adb devices" without errors
3936
_, err = adb.Devices()
4037
if err != nil {
@@ -63,11 +60,10 @@ func (a *ADB) SetSerial(serial string) (string, error) {
6360
}
6461
a.Serial = serial
6562
} else {
66-
// Problem if multiple devices
6763
if len(devices) > 1 {
6864
return "", fmt.Errorf("multiple devices connected, please stop AndroidQF and provide a serial number")
6965
}
70-
a.Serial = ""
66+
a.Serial = devices[0]
7167
}
7268
return a.Serial, nil
7369
}

adb/adb_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package adb
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestMain(m *testing.M) {
11+
if os.Getenv("ANDROIDQF_FAKE_ADB") == "1" {
12+
fakeADB()
13+
return
14+
}
15+
os.Exit(m.Run())
16+
}
17+
18+
func fakeADB() {
19+
if len(os.Args) < 2 {
20+
os.Exit(2)
21+
}
22+
23+
switch os.Args[1] {
24+
case "devices":
25+
fmt.Println("List of devices attached")
26+
for _, device := range strings.Split(os.Getenv("ANDROIDQF_FAKE_ADB_DEVICES"), ",") {
27+
device = strings.TrimSpace(device)
28+
if device != "" {
29+
fmt.Printf("%s\tdevice\n", device)
30+
}
31+
}
32+
default:
33+
os.Exit(2)
34+
}
35+
}
36+
37+
func newFakeADB(t *testing.T, devices string) *ADB {
38+
t.Helper()
39+
t.Setenv("ANDROIDQF_FAKE_ADB", "1")
40+
t.Setenv("ANDROIDQF_FAKE_ADB_DEVICES", devices)
41+
return &ADB{ExePath: os.Args[0]}
42+
}
43+
44+
func TestSetSerialSingleDeviceUsesExplicitSerial(t *testing.T) {
45+
client := newFakeADB(t, "device-1")
46+
serial, err := client.SetSerial("")
47+
if err != nil {
48+
t.Fatalf("SetSerial returned error: %v", err)
49+
}
50+
if serial != "device-1" {
51+
t.Fatalf("serial = %q, want device-1", serial)
52+
}
53+
if client.Serial != "device-1" {
54+
t.Fatalf("client.Serial = %q, want device-1", client.Serial)
55+
}
56+
}
57+
58+
func TestSetSerialMultipleDevicesWithoutSerialErrors(t *testing.T) {
59+
client := newFakeADB(t, "device-1,device-2")
60+
_, err := client.SetSerial("")
61+
if err == nil {
62+
t.Fatal("SetSerial returned nil error, want multiple devices error")
63+
}
64+
}
65+
66+
func TestSetSerialExplicitSerial(t *testing.T) {
67+
client := newFakeADB(t, "device-1,device-2")
68+
serial, err := client.SetSerial("device-2")
69+
if err != nil {
70+
t.Fatalf("SetSerial returned error: %v", err)
71+
}
72+
if serial != "device-2" {
73+
t.Fatalf("serial = %q, want device-2", serial)
74+
}
75+
}

device_selection_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestResolveADBSerialNoDevicesDoesNotPrompt(t *testing.T) {
10+
called := false
11+
serial, prompted, err := resolveADBSerial("", nil, func([]deviceMenuItem) (string, error) {
12+
called = true
13+
return "", nil
14+
}, nil)
15+
if err != nil {
16+
t.Fatalf("resolveADBSerial returned error: %v", err)
17+
}
18+
if serial != "" {
19+
t.Fatalf("serial = %q, want empty", serial)
20+
}
21+
if prompted {
22+
t.Fatal("prompted = true, want false")
23+
}
24+
if called {
25+
t.Fatal("selector was called for zero devices")
26+
}
27+
}
28+
29+
func TestResolveADBSerialSingleDeviceDoesNotPrompt(t *testing.T) {
30+
called := false
31+
serial, prompted, err := resolveADBSerial("", []string{"device-1"}, func([]deviceMenuItem) (string, error) {
32+
called = true
33+
return "", nil
34+
}, nil)
35+
if err != nil {
36+
t.Fatalf("resolveADBSerial returned error: %v", err)
37+
}
38+
if serial != "device-1" {
39+
t.Fatalf("serial = %q, want device-1", serial)
40+
}
41+
if prompted {
42+
t.Fatal("prompted = true, want false")
43+
}
44+
if called {
45+
t.Fatal("selector was called for one device")
46+
}
47+
}
48+
49+
func TestResolveADBSerialMultipleDevicesPromptsWithRunningStatus(t *testing.T) {
50+
started := time.Date(2026, 7, 7, 10, 11, 12, 0, time.UTC)
51+
running := map[string]runningExtraction{
52+
"device-2": {
53+
Serial: "device-2",
54+
PID: 1234,
55+
Started: started,
56+
},
57+
}
58+
59+
var gotItems []deviceMenuItem
60+
serial, prompted, err := resolveADBSerial("", []string{"device-1", "device-2"}, func(items []deviceMenuItem) (string, error) {
61+
gotItems = items
62+
return items[1].Serial, nil
63+
}, running)
64+
if err != nil {
65+
t.Fatalf("resolveADBSerial returned error: %v", err)
66+
}
67+
if serial != "device-2" {
68+
t.Fatalf("serial = %q, want device-2", serial)
69+
}
70+
if !prompted {
71+
t.Fatal("prompted = false, want true")
72+
}
73+
if len(gotItems) != 2 {
74+
t.Fatalf("selector got %d items, want 2", len(gotItems))
75+
}
76+
if gotItems[0].Status != "" {
77+
t.Fatalf("first item status = %q, want empty", gotItems[0].Status)
78+
}
79+
if gotItems[1].Status == "" {
80+
t.Fatal("second item status is empty, want running extraction status")
81+
}
82+
}
83+
84+
func TestResolveADBSerialMultipleDevicesReturnsSelectorError(t *testing.T) {
85+
wantErr := errors.New("selection failed")
86+
serial, prompted, err := resolveADBSerial("", []string{"device-1", "device-2"}, func([]deviceMenuItem) (string, error) {
87+
return "", wantErr
88+
}, nil)
89+
if !errors.Is(err, wantErr) {
90+
t.Fatalf("err = %v, want %v", err, wantErr)
91+
}
92+
if serial != "" {
93+
t.Fatalf("serial = %q, want empty", serial)
94+
}
95+
if !prompted {
96+
t.Fatal("prompted = false, want true")
97+
}
98+
}
99+
100+
func TestResolveADBSerialExplicitSerialDoesNotPrompt(t *testing.T) {
101+
called := false
102+
serial, prompted, err := resolveADBSerial("requested", []string{"device-1", "device-2"}, func([]deviceMenuItem) (string, error) {
103+
called = true
104+
return "", nil
105+
}, nil)
106+
if err != nil {
107+
t.Fatalf("resolveADBSerial returned error: %v", err)
108+
}
109+
if serial != "requested" {
110+
t.Fatalf("serial = %q, want requested", serial)
111+
}
112+
if prompted {
113+
t.Fatal("prompted = true, want false")
114+
}
115+
if called {
116+
t.Fatal("selector was called for explicit serial")
117+
}
118+
}

main.go

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import (
2121
"github.com/mvt-project/androidqf/utils"
2222
)
2323

24+
type deviceMenuItem struct {
25+
Serial string
26+
Status string
27+
}
28+
2429
func init() {
2530
cfmt.Print(`
2631
{{ __ _ __ ____ }}::green
@@ -39,18 +44,52 @@ func systemPause() {
3944
os.Stdin.Read(make([]byte, 1))
4045
}
4146

42-
func selectADBDevice(devices []string) (string, error) {
47+
func buildDeviceMenuItems(devices []string, running map[string]runningExtraction) []deviceMenuItem {
48+
items := make([]deviceMenuItem, 0, len(devices))
49+
for _, device := range devices {
50+
item := deviceMenuItem{Serial: device}
51+
if state, ok := running[device]; ok {
52+
item.Status = fmt.Sprintf("(extraction running, pid %d, started %s)", state.PID, state.Started.Local().Format("2006-01-02 15:04:05"))
53+
}
54+
items = append(items, item)
55+
}
56+
return items
57+
}
58+
59+
func selectADBDeviceFromMenu(items []deviceMenuItem) (string, error) {
4360
promptDevice := promptui.Select{
4461
Label: "Multiple Android devices detected. Select the device to acquire",
45-
Items: devices,
62+
Items: items,
63+
Templates: &promptui.SelectTemplates{
64+
Active: "> {{ .Serial | cyan }} {{ .Status | yellow }}",
65+
Inactive: " {{ .Serial }} {{ .Status }}",
66+
Selected: "{{ .Serial }}",
67+
},
4668
}
4769

48-
_, device, err := promptDevice.Run()
70+
index, _, err := promptDevice.Run()
4971
if err != nil {
5072
return "", fmt.Errorf("failed to select ADB device: %v", err)
5173
}
5274

53-
return device, nil
75+
return items[index].Serial, nil
76+
}
77+
78+
func selectADBDevice(devices []string) (string, error) {
79+
return selectADBDeviceFromMenu(buildDeviceMenuItems(devices, activeRunningExtractionsBySerial()))
80+
}
81+
82+
func resolveADBSerial(serial string, devices []string, selectDevice func([]deviceMenuItem) (string, error), running map[string]runningExtraction) (string, bool, error) {
83+
serial = strings.TrimSpace(serial)
84+
if serial != "" || len(devices) == 0 {
85+
return serial, false, nil
86+
}
87+
if len(devices) == 1 {
88+
return devices[0], false, nil
89+
}
90+
91+
selectedSerial, err := selectDevice(buildDeviceMenuItems(devices, running))
92+
return selectedSerial, true, err
5493
}
5594

5695
func main() {
@@ -119,15 +158,16 @@ func main() {
119158
}
120159
}
121160
}
161+
specificDeviceRequested := serial != ""
122162

123163
// Initialization
124164
for {
125165
if serial == "" {
126166
devices, err := adb.Client.Devices()
127167
if err != nil {
128168
log.Error(fmt.Sprintf("Error listing ADB devices: %s", err))
129-
} else if len(devices) > 1 {
130-
serial, err = selectADBDevice(devices)
169+
} else {
170+
serial, _, err = resolveADBSerial(serial, devices, selectADBDeviceFromMenu, activeRunningExtractionsBySerial())
131171
if err != nil {
132172
log.Error(fmt.Sprintf("Error selecting ADB device: %s", err))
133173
time.Sleep(5 * time.Second)
@@ -139,17 +179,35 @@ func main() {
139179
serial, err = adb.Client.SetSerial(serial)
140180
if err != nil {
141181
log.Error(fmt.Sprintf("Error trying to connect over ADB: %s", err))
182+
if !specificDeviceRequested {
183+
serial = ""
184+
}
142185
} else {
143186
_, err = adb.Client.GetState()
144187
if err == nil {
145188
break
146189
}
147190
log.Debug(err)
148191
log.Error("Unable to get device state. Please make sure it is connected and authorized. Trying again in 5 seconds...")
192+
if !specificDeviceRequested {
193+
serial = ""
194+
}
149195
}
150196
time.Sleep(5 * time.Second)
151197
}
152198

199+
releaseRunning, err := registerRunningExtraction(adb.Client.Serial, "")
200+
if err != nil {
201+
log.Warningf("Unable to record running extraction state: %v", err)
202+
releaseRunning = func() {}
203+
}
204+
runningReleased := false
205+
defer func() {
206+
if !runningReleased {
207+
releaseRunning()
208+
}
209+
}()
210+
153211
acq, err := acquisition.New(output_folder)
154212
if err != nil {
155213
log.Debug(err)
@@ -201,6 +259,8 @@ func main() {
201259
}
202260

203261
acq.Complete()
262+
releaseRunning()
263+
runningReleased = true
204264
log.Info("Acquisition completed.")
205265

206266
systemPause()

0 commit comments

Comments
 (0)