blob: 9182ff2e624cc5e533d96a878eebc45bd25995a9 [file]
// Copyright 2018 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package adb represents the "adb" version of the broker tools, compatible with API 15+.
package adb
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"src/common/golang/pprint"
)
var (
checkFilePath = filepath.Join(os.TempDir(), "adbcheck")
)
const (
defaultADB = "/usr/bin/adb"
adbCheckPeriod = 2 * time.Hour
androidADBVar = "ANDROID_ADB="
adbServerPortVar = "ANDROID_ADB_SERVER_PORT"
pushRetryAttempts = 3
)
type runner func(context.Context, *ADB, ...string) ([]byte, []byte, error)
func cmdRunner(ctx context.Context, adb *ADB, args ...string) ([]byte, []byte, error) {
var stdOut, stdErr bytes.Buffer
cmd := exec.CommandContext(ctx, adb.Path, append(adb.args, args...)...)
cmd.Env = append(os.Environ(), adb.env...)
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
err := cmd.Run()
return stdOut.Bytes(), stdErr.Bytes(), err
}
// ADB is a wrapper around the ADB command line tool. It knows which adb server to connect.
type ADB struct {
Path string
env []string // Snapshot of the environment that ADB runs in.
args []string // Args that are passed to adb before the command, like -P.
runner runner
}
func (a *ADB) run(ctx context.Context, args ...string) ([]byte, []byte, error) {
return a.runner(ctx, a, args...)
}
func (a *ADB) init(ctx context.Context, useADBRoot bool) error {
if o, e, err := a.run(ctx, "start-server"); err != nil {
return fmt.Errorf("failed to start adb %v - %s - %s", err, string(o), string(e))
}
// Attempt to run `adb root`, and ignore any errors. We want to use root
// if available but if not we will continue in non-root mode.
if useADBRoot {
a.run(ctx, "root")
}
return nil
}
func (a *ADB) devices(ctx context.Context) ([]string, []string, error) {
stdOut, stdErr, err := a.run(ctx, "devices")
if err != nil {
return nil, nil, fmt.Errorf("error occurred while finding devices, got: %s\nstdout:\n%s\nstderr:\n%s", err, stdOut, stdErr)
}
// Parse the standard out for list of devices, apply a simple parse.
//
// Following is an example of the standard out from an "adb devices" invocation:
//
// $ adb devices
// List of devices attached
// 127.0.0.1:60001 device
//
// See test for more examples.
var devices, unauthorized []string
for _, l := range strings.Split(string(stdOut), "\n") {
switch {
case strings.HasSuffix(l, "device"):
devices = append(devices, strings.TrimSpace(strings.TrimSuffix(l, "device")))
case strings.HasSuffix(l, "unauthorized"):
unauthorized = append(devices, strings.TrimSpace(strings.TrimSuffix(l, "unauthorized")))
}
}
return devices, unauthorized, nil
}
// Controller is a device.Controller for adb connected devices.
type Controller struct {
*ADB
DeviceSerial string
}
func (a *Controller) run(ctx context.Context, args ...string) ([]byte, []byte, error) {
return a.ADB.run(ctx, append(a.deviceArgs(), args...)...)
}
func (a *Controller) deviceArgs() []string {
if a.DeviceSerial == "e" || a.DeviceSerial == "d" {
return []string{"-" + a.DeviceSerial}
}
return []string{"-s", a.DeviceSerial}
}
// Exec executes the command in the shell of the specified device. The shell param is ignored and always assumed to be true
func (a *Controller) Exec(ctx context.Context, cmd string, args []string, shell bool) (string, string, error) {
fullArgs := []string{"shell", cmd + " " + strings.Join(args, " ") + "; echo ret=$?"}
oB, eB, err := a.run(ctx, fullArgs...)
o := string(oB)
e := string(eB)
if err == nil {
r := o[strings.LastIndex(o, "ret="):]
o = strings.TrimSpace(o[:len(o)-len(r)])
ret, convErr := strconv.Atoi(strings.TrimSpace(r[4:]))
if convErr != nil {
err = fmt.Errorf("error parsing return code %v", r)
} else if ret != 0 {
err = fmt.Errorf("non-zero return code '%d' from %s", ret, "adb shell "+cmd+" "+strings.Join(args, " "))
}
}
return o, e, err
}
// Install installs the the split apk(s) in the device.
func (a *Controller) Install(ctx context.Context, args []string, apks ...string) error {
bytes := int64(0)
for _, p := range apks {
fi, err := os.Stat(p)
if err == nil { // Skip any errors for our logging purposes
bytes += fi.Size()
}
}
pprint.Info("Updating %.1f MB across %d split(s)", float64(bytes)/float64(1024*1024), len(apks))
pushCtx, pushCancel := context.WithCancel(ctx)
defer pushCancel()
go func(ctx context.Context) {
base := "Installing"
nDots := 3
for {
select {
case <-ctx.Done():
return
case <-time.After(5 * time.Second):
pprint.Info(base + strings.Repeat(".", nDots))
nDots = (nDots + 1) % 40
}
}
}(pushCtx)
fullArgs := append([]string{"install-multiple"}, args...)
fullArgs = append(fullArgs, apks...)
oB, eB, err := a.run(ctx, fullArgs...)
o := string(oB)
e := string(eB)
if err != nil {
return fmt.Errorf("got adb error running %s:\n%s\n%s", "adb "+strings.Join(args, " "), o, e)
}
return nil
}
// Push pushes an object from host to the device.
func (a *Controller) Push(ctx context.Context, from, to string) error {
errStr := ""
for i := 0; i < pushRetryAttempts; i++ {
errStr = ""
if o, e, err := a.run(ctx, "push", from, to); err != nil {
errStr = fmt.Sprintf("error pushing %s to %s, got: %s %s %v", from, to, o, e, err)
// Break early if permission issue
if strings.Contains(strings.ToLower(string(o)+string(e)), "not permitted") {
errStr += "\n\nTry running `adb root` then retry this command"
break
}
pprint.Warning("pushing %s attempt %d/%d failed, retrying...", from, i+1, pushRetryAttempts)
continue
}
break
}
if errStr != "" {
return errors.New(errStr)
}
return nil
}
// Pull pulls an object from the device to the host.
func (a *Controller) Pull(ctx context.Context, from, to string) error {
if o, e, err := a.run(ctx, "pull", from, to); err != nil {
return fmt.Errorf("error pulling %s to %s, got: %s %s %v", from, to, o, e, err)
}
return nil
}
// setupEnv determines which adb path to use and sets up the implicit environment settings required
// and returns a prepared environment and the correct adb path.
func setupEnv(ctx context.Context, env []string, adbPath string) (*ADB, error) {
adbSource := "default"
// Find ANDROID_ADB entry from environment vars, if found set it as the path unless overridden.
adbEnv := &ADB{}
for _, entry := range env {
if strings.HasPrefix(entry, androidADBVar) {
adbEnv.Path = strings.TrimPrefix(entry, androidADBVar)
adbSource = "environment variable"
break
}
}
// If ANDROID_ADB not found, try using adb from the user's path
if adbEnv.Path == "" {
path, _ := exec.LookPath("adb")
if path != "" {
adbEnv.Path = path
adbSource = "path"
}
}
// The flag should always win
if adbPath != "" {
adbEnv.Path = adbPath
adbSource = "flag"
}
// Fallback to default pre-installed adb, if it exists, or fail if not found.
if adbEnv.Path == "" {
if _, err := os.Stat(defaultADB); err != nil {
return nil, errors.New("unable to find ADB, please set the ANDROID_ADB environment variable or use the --adb flag")
}
adbEnv.Path = defaultADB
adbSource = "fallback"
}
pprint.Info("Using ADB from %s: %s", adbSource, adbEnv.Path)
return adbEnv, nil
}
// New creates a new adb Controller.
// If more than once device is available, deviceFlag must be specified.
func New(ctx context.Context, env []string, deviceSerial, adbPort, adbPath string, useADBRoot bool) (*Controller, error) {
a, err := setupEnv(ctx, env, adbPath)
if err != nil {
return nil, err
}
// Setup additional adb args.
var args []string
if adbPort != "" {
a.args = append(args, "-P", adbPort)
}
a.runner = cmdRunner
if err := a.init(ctx, useADBRoot); err != nil {
return nil, fmt.Errorf("unable to init adb client: %s", err.Error())
}
if deviceSerial == "" {
devices, unauthorized, err := a.devices(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get device list: %s", err.Error())
}
if len(devices) == 0 {
var errMsg strings.Builder
errMsg.WriteString("no available devices")
if len(unauthorized) > 0 {
errMsg.WriteString(", but found the following unauthorized device(s): ")
fmt.Fprintf(&errMsg, "%v", unauthorized)
}
return nil, fmt.Errorf(errMsg.String())
}
if len(devices) > 1 {
return nil, fmt.Errorf("more than one attached device available. Specify one with --device\n%s", devices)
}
deviceSerial = devices[0]
}
return &Controller{a, deviceSerial}, nil
}