// Package adb is a thin, timeout-bounded wrapper around the adb command line // tool. Artemis never talks to the ADB wire protocol directly: it shells out to // the adb binary installed in the user profile on .13. package adb import ( "bytes" "context" "fmt" "os" "os/exec" "strings" "time" ) // DefaultTimeout bounds every single adb invocation. A recipe must never run // forever (FR-11), so no command is allowed to run without a deadline. const DefaultTimeout = 20 * time.Second // Runner executes adb commands against one device. type Runner struct { Adb string Serial string Timeout time.Duration } // New returns a Runner. The adb binary is taken from ARTEMIS_ADB, else PATH. func New(serial string) *Runner { adbPath := os.Getenv("ARTEMIS_ADB") if adbPath == "" { adbPath = "adb" } return &Runner{Adb: adbPath, Serial: serial, Timeout: DefaultTimeout} } func (r *Runner) adbArgs(args ...string) []string { full := make([]string, 0, len(args)+2) if r.Serial != "" { full = append(full, "-s", r.Serial) } return append(full, args...) } func (r *Runner) run(timeout time.Duration, args ...string) ([]byte, error) { if timeout <= 0 { timeout = r.Timeout } if timeout <= 0 { timeout = DefaultTimeout } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() cmd := exec.CommandContext(ctx, r.Adb, r.adbArgs(args...)...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() if ctx.Err() == context.DeadlineExceeded { return stdout.Bytes(), fmt.Errorf("adb %s: timed out after %s", strings.Join(args, " "), timeout) } if err != nil { msg := strings.TrimSpace(stderr.String()) if msg == "" { msg = strings.TrimSpace(stdout.String()) } return stdout.Bytes(), fmt.Errorf("adb %s: %w: %s", strings.Join(args, " "), err, msg) } return stdout.Bytes(), nil } // Shell runs a command in the device shell and returns combined stdout. // // The whole command is passed as one argv element; adb joins argv with spaces // and the device's sh parses the result, exactly as a human at a terminal. func (r *Runner) Shell(command string) (string, error) { out, err := r.run(r.Timeout, "shell", command) return string(out), err } // ExecOut runs adb exec-out, which does not add carriage returns. Use it to // read files and binary data off the device. func (r *Runner) ExecOut(args ...string) ([]byte, error) { return r.run(r.Timeout, append([]string{"exec-out"}, args...)...) } // Device is one row of `adb devices -l`. type Device struct { Serial string State string Product string Model string Device string TransportID string } // Devices lists the devices the adb server knows about. func (r *Runner) Devices() ([]Device, error) { out, err := r.run(10*time.Second, "devices", "-l") if err != nil { return nil, err } var devices []Device for _, line := range strings.Split(string(out), "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "List of devices") { continue } fields := strings.Fields(line) if len(fields) < 2 { continue } d := Device{Serial: fields[0], State: fields[1]} for _, f := range fields[2:] { switch { case strings.HasPrefix(f, "product:"): d.Product = strings.TrimPrefix(f, "product:") case strings.HasPrefix(f, "model:"): d.Model = strings.TrimPrefix(f, "model:") case strings.HasPrefix(f, "device:"): d.Device = strings.TrimPrefix(f, "device:") case strings.HasPrefix(f, "transport_id:"): d.TransportID = strings.TrimPrefix(f, "transport_id:") } } devices = append(devices, d) } return devices, nil }