blob: 45c7c4030769a07b9a4712c5cfe54ae120af03d0 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The following enables go generate to generate the doc.go file.
//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"v.io/x/lib/cmdline"
"v.io/x/lib/gosh"
)
var (
allDevicesFlag bool
allEmulatorsFlag bool
devicesFlag string
)
func init() {
cmdMadb.Flags.BoolVar(&allDevicesFlag, "d", false, `Restrict the command to only run on real devices.`)
cmdMadb.Flags.BoolVar(&allEmulatorsFlag, "e", false, `Restrict the command to only run on emulators.`)
cmdMadb.Flags.StringVar(&devicesFlag, "n", "", `Comma-separated device serials, qualifiers, or nicknames (set by 'madb name'). Command will be run only on specified devices.`)
}
var cmdMadb = &cmdline.Command{
Children: []*cmdline.Command{cmdMadbExec, cmdMadbStart, cmdMadbName},
Name: "madb",
Short: "Multi-device Android Debug Bridge",
Long: `
Multi-device Android Debug Bridge
The madb command wraps Android Debug Bridge (adb) command line tool
and provides various features for controlling multiple Android devices concurrently.
`,
}
func main() {
cmdline.Main(cmdMadb)
}
// Makes sure that adb server is running.
// Intended to be called at the beginning of each subcommand.
func startAdbServer() error {
// TODO(youngseokyoon): search for installed adb tool more rigourously.
if err := exec.Command("adb", "start-server").Run(); err != nil {
return fmt.Errorf("Failed to start adb server. Please make sure that adb is in your PATH: %v", err)
}
return nil
}
type deviceType string
const (
emulator deviceType = "Emulator"
realDevice deviceType = "RealDevice"
)
type device struct {
Serial string
Type deviceType
Qualifiers []string
Nickname string
}
// Returns the display name which is intended to be used as the console output prefix.
// This would be the nickname of the device if there is one; otherwise, the serial number is used.
func (d device) displayName() string {
if d.Nickname != "" {
return d.Nickname
}
return d.Serial
}
// Runs "adb devices -l" command, and parses the result to get all the device serial numbers.
func getDevices(nicknameFile string) ([]device, error) {
sh := gosh.NewShell(gosh.Opts{})
defer sh.Cleanup()
output := sh.Cmd("adb", "devices", "-l").Stdout()
nsm, err := readNicknameSerialMap(nicknameFile)
if err != nil {
fmt.Fprintln(os.Stderr, "Warning: Could not read the nickname file.")
}
return parseDevicesOutput(output, nsm)
}
// Parses the output generated from "adb devices -l" command and return the list of device serial numbers
// Devices that are currently offline are excluded from the returned list.
func parseDevicesOutput(output string, nsm map[string]string) ([]device, error) {
lines := strings.Split(output, "\n")
result := []device{}
// Check the first line of the output
if len(lines) <= 0 || strings.TrimSpace(lines[0]) != "List of devices attached" {
return result, fmt.Errorf("The output from 'adb devices -l' command does not look as expected.")
}
// Iterate over all the device serial numbers, starting from the second line.
for _, line := range lines[1:] {
fields := strings.Fields(line)
if len(fields) <= 1 || fields[1] == "offline" {
continue
}
// Fill in the device serial and all the qualifiers.
d := device{
Serial: fields[0],
Qualifiers: fields[2:],
}
// Determine whether this device is an emulator or a real device.
if strings.HasPrefix(d.Serial, "emulator") {
d.Type = emulator
} else {
d.Type = realDevice
}
// Determine whether there is a nickname defined for this device,
// so that the console output prefix can display the nickname instead of the serial.
NSMLoop:
for nickname, serial := range nsm {
if d.Serial == serial {
d.Nickname = nickname
break
}
for _, qualifier := range d.Qualifiers {
if qualifier == serial {
d.Nickname = nickname
break NSMLoop
}
}
}
result = append(result, d)
}
return result, nil
}
// Gets all the devices specified by the device specifier flags.
// Intended to be used by most of the madb sub-commands except for 'madb name'.
func getSpecifiedDevices() ([]device, error) {
allDevices, err := getDevices(getDefaultNameFilePath())
if err != nil {
return nil, err
}
filtered := filterSpecifiedDevices(allDevices)
if len(filtered) == 0 {
return nil, fmt.Errorf("No devices matching the device specifiers.")
}
return filtered, nil
}
func filterSpecifiedDevices(devices []device) []device {
// If no device specifier flags are set, run on all devices and emulators.
if noDevicesSpecified() {
return devices
}
result := make([]device, 0, len(devices))
for _, d := range devices {
if shouldIncludeDevice(d) {
result = append(result, d)
}
}
return result
}
func noDevicesSpecified() bool {
return allDevicesFlag == false &&
allEmulatorsFlag == false &&
devicesFlag == ""
}
func shouldIncludeDevice(d device) bool {
if allDevicesFlag && d.Type == realDevice {
return true
}
if allEmulatorsFlag && d.Type == emulator {
return true
}
tokens := strings.Split(devicesFlag, ",")
for _, token := range tokens {
// Ignore empty tokens
if token == "" {
continue
}
if d.Serial == token || d.Nickname == token {
return true
}
for _, qualifier := range d.Qualifiers {
if qualifier == token {
return true
}
}
}
return false
}
type subCommandFunc func(env *cmdline.Env, args []string, d device) error
type subCommandRunner struct {
subCmd subCommandFunc
}
var _ cmdline.Runner = (*subCommandRunner)(nil)
// Invokes the sub command on all the devices in parallel.
func (r subCommandRunner) Run(env *cmdline.Env, args []string) error {
if err := startAdbServer(); err != nil {
return err
}
devices, err := getSpecifiedDevices()
if err != nil {
return err
}
wg := sync.WaitGroup{}
var errs []error
var errDevices []device
for _, d := range devices {
// Capture the current value.
deviceCopy := d
wg.Add(1)
go func() {
// Remember the first error returned by the sub command.
if e := r.subCmd(env, args, deviceCopy); err == nil && e != nil {
errs = append(errs, e)
errDevices = append(errDevices, deviceCopy)
}
wg.Done()
}()
}
wg.Wait()
// Report any errors returned from the go-routines.
if errs != nil {
buffer := bytes.Buffer{}
buffer.WriteString("Error occurred while running the command on the following devices:")
for i := 0; i < len(errs); i++ {
buffer.WriteString("\n[" + errDevices[i].displayName() + "]\t" + errs[i].Error())
}
return fmt.Errorf(buffer.String())
}
return nil
}