devtools/madb: initial commit of the madb tool

This CL includes the very basic functionality of the madb tool.
The proposal document can be found at:  go/madb-proposal

Change-Id: I4b0904d14e312a330c890a7b7a3f71f70ba33d6a
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..d218c7c
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,73 @@
+// 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.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+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.
+
+Usage:
+   madb [flags] <command>
+
+The madb commands are:
+   exec        Run the provided adb command on all the specified devices
+               concurrently
+   help        Display help for commands or topics
+
+The global flags are:
+ -metadata=<just specify -metadata to activate>
+   Displays metadata for the program and exits.
+ -time=false
+   Dump timing information to stderr before exiting the program.
+
+Madb exec - Run the provided adb command on all the specified devices concurrently
+
+Runs the provided adb command on all the specified devices concurrently.
+
+For example, the following line:
+
+    madb -a exec push ./foo.txt /sdcard/foo.txt
+
+copies the ./foo.txt file to /sdcard/foo.txt for all the currently connected
+Android devices (specified by -a flag).
+
+To see the list of available adb commands, type 'adb help'.
+
+Usage:
+   madb exec [flags] <command>
+
+<command> is a normal adb command, which will be executed on all the specified
+devices.
+
+Madb help - Display help for commands or topics
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+Usage:
+   madb help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The madb help flags are:
+ -style=compact
+   The formatting style for help output:
+      compact   - Good for compact cmdline output.
+      full      - Good for cmdline output, shows all global flags.
+      godoc     - Good for godoc processing.
+      shortonly - Only output short description.
+   Override the default by setting the CMDLINE_STYLE environment variable.
+ -width=<terminal width>
+   Format output to this target width in runes, or unlimited if width < 0.
+   Defaults to the terminal width if available.  Override the default by setting
+   the CMDLINE_WIDTH environment variable.
+*/
+package main
diff --git a/exec.go b/exec.go
new file mode 100644
index 0000000..653829b
--- /dev/null
+++ b/exec.go
@@ -0,0 +1,64 @@
+// 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.
+
+package main
+
+import (
+	"os"
+
+	"v.io/x/lib/cmdline"
+	"v.io/x/lib/gosh"
+)
+
+var cmdMadbExec = &cmdline.Command{
+	Runner: cmdline.RunnerFunc(runMadbExec),
+	Name:   "exec",
+	Short:  "Run the provided adb command on all the specified devices concurrently",
+	Long: `
+Runs the provided adb command on all the specified devices concurrently.
+
+For example, the following line:
+
+    madb -a exec push ./foo.txt /sdcard/foo.txt
+
+copies the ./foo.txt file to /sdcard/foo.txt for all the currently connected Android devices (specified by -a flag).
+
+To see the list of available adb commands, type 'adb help'.
+`,
+	ArgsName: "<command>",
+	ArgsLong: `
+<command> is a normal adb command, which will be executed on all the specified devices.
+`,
+}
+
+func runMadbExec(env *cmdline.Env, args []string) error {
+	if err := startAdbServer(); err != nil {
+		return err
+	}
+
+	devices, err := getDevices()
+	if err != nil {
+		return err
+	}
+
+	for _, device := range devices {
+		sh := gosh.NewShell(gosh.Opts{})
+		defer sh.Cleanup()
+
+		cmdArgs := append([]string{"-s", device}, args...)
+		cmd := sh.Cmd("adb", cmdArgs...)
+
+		// TODO(youngseokyoon): use pipes instead to prefix console messages with their device names.
+		// For now, just forward all the messages to stdout/stderr.
+		cmd.AddStdoutWriter(gosh.NopWriteCloser(os.Stdout))
+		cmd.AddStderrWriter(gosh.NopWriteCloser(os.Stderr))
+
+		cmd.Start()
+		defer cmd.Wait()
+
+		// TODO(youngseokyoon): check for exit code of each command
+	}
+
+	return nil
+}
diff --git a/madb.go b/madb.go
new file mode 100644
index 0000000..11020fd
--- /dev/null
+++ b/madb.go
@@ -0,0 +1,80 @@
+// 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 (
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"v.io/x/lib/cmdline"
+	"v.io/x/lib/gosh"
+)
+
+var cmdMadb = &cmdline.Command{
+	Children: []*cmdline.Command{cmdMadbExec},
+	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
+}
+
+// Runs "adb devices" command, and parses the result to get all the device serial numbers.
+func getDevices() ([]string, error) {
+	sh := gosh.NewShell(gosh.Opts{})
+	defer sh.Cleanup()
+
+	output := sh.Cmd("adb", "devices", "-l").Stdout()
+
+	return parseDevicesOutput(output)
+}
+
+// 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) ([]string, error) {
+	lines := strings.Split(output, "\n")
+
+	result := []string{}
+
+	// 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
+		}
+
+		result = append(result, strings.Fields(line)[0])
+	}
+
+	return result, nil
+}
diff --git a/madb_test.go b/madb_test.go
new file mode 100644
index 0000000..6f271f8
--- /dev/null
+++ b/madb_test.go
@@ -0,0 +1,56 @@
+// 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.
+
+package main
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestParseDevicesOutput(t *testing.T) {
+	var output string
+
+	// Normal case
+	output = `List of devices attached
+deviceid01       device usb:3-3.4.3 product:bullhead model:Nexus_5X device:bullhead
+deviceid02       device product:sdk_phone_armv7 model:sdk_phone_armv7 device:generic
+
+`
+
+	result, err := parseDevicesOutput(output)
+	if err != nil {
+		t.Fatalf("failed to parse the output: %v", err)
+	}
+	if got, want := result, []string{"deviceid01", "deviceid02"}; !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+
+	// No devices at all
+	output = `List of devices attached
+
+`
+
+	result, err = parseDevicesOutput(output)
+	if err != nil {
+		t.Fatalf("failed to parse the output: %v", err)
+	}
+	if got, want := result, []string{}; !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+
+	// Offline devices should be excluded
+	output = `List of devices attached
+deviceid01       offline usb:3-3.4.3 product:bullhead model:Nexus_5X device:bullhead
+deviceid02       device product:sdk_phone_armv7 model:sdk_phone_armv7 device:generic
+
+`
+	result, err = parseDevicesOutput(output)
+	if err != nil {
+		t.Fatalf("failed to parse the output: %v", err)
+	}
+	if got, want := result, []string{"deviceid02"}; !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+}