Index: platform_tools/android/bin/android_run_app.go |
diff --git a/platform_tools/android/bin/android_run_app.go b/platform_tools/android/bin/android_run_app.go |
new file mode 100644 |
index 0000000000000000000000000000000000000000..69cc48a113e2b222f438293c83abef3457d07320 |
--- /dev/null |
+++ b/platform_tools/android/bin/android_run_app.go |
@@ -0,0 +1,240 @@ |
+// Copyright (c) 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+/* |
+ Run a Skia app to completion, piping the log to stdout. |
+*/ |
+ |
+package main |
+ |
+import ( |
+ "flag" |
+ "fmt" |
+ "io" |
+ "os" |
+ "os/exec" |
+ "os/signal" |
+ "strconv" |
+ "strings" |
+ "time" |
+ |
+ "github.com/skia-dev/glog" |
+ "go.skia.org/infra/go/common" |
+) |
+ |
+const ( |
+ COM_SKIA = "com.skia" |
+) |
+ |
+var ( |
+ adb = flag.String("adb", "", "Path to the ADB executable.") |
+ app = flag.String("app", "VisualBench", "Name of the app to run.") |
+ serial = flag.String("s", "", "Serial number for the Android device to use.") |
+) |
+ |
+// ADB returns an os.Cmd for the given ADB command. |
+func ADB(adb, serial string, log bool, args ...string) *exec.Cmd { |
stephana
2015/07/30 13:37:42
Nit: I think "adbCommand" or adbCmd would be more
borenet
2015/07/30 17:37:49
I took the suggestion of making ADB a struct. The
|
+ cmd := []string{} |
+ if serial != "" { |
+ cmd = append(cmd, "-s", serial) |
+ } |
+ cmd = append(cmd, args...) |
+ if log { |
+ glog.Infof("Exec `%s %s`", adb, strings.Join(cmd, " ")) |
+ } |
+ return exec.Command(adb, cmd...) |
+} |
+ |
+// RunADB runs the given command through ADB. |
+func RunADB(adb, serial string, log bool, args ...string) ([]byte, error) { |
+ return ADB(adb, serial, log, args...).Output() |
stephana
2015/07/30 13:37:42
This is an unnecessary level of indirection. Inste
borenet
2015/07/30 17:37:49
Removed.
|
+} |
+ |
+// KillSkia kills any running Skia process. |
+func KillSkia(adb, serial string) error { |
+ _, err := RunADB(adb, serial, false, "shell", "am", "force-stop", COM_SKIA) |
+ return err |
+} |
+ |
+// PollSkia checks to see if any Skia process is running. If so, return the pid, otherwise return -1. |
+func PollSkia(adb, serial string) (int64, error) { |
+ output, err := RunADB(adb, serial, false, "shell", "ps") |
stephana
2015/07/30 13:37:42
It might make sense to create a type 'ADB' instead
borenet
2015/07/30 17:37:48
I like this! Done.
|
+ if err != nil { |
+ return -1, fmt.Errorf("Failed to check the running processes on the device: %v", err) |
+ } |
+ for _, line := range strings.Split(string(output), "\n") { |
+ if strings.Contains(line, COM_SKIA) { |
+ fields := strings.Fields(line) |
+ pid, err := strconv.ParseInt(fields[1], 10, 32) |
+ if err != nil { |
+ return -1, fmt.Errorf("Failed to parse \"%s\" to an integer: %v", fields[1], err) |
+ } |
+ return pid, nil |
+ } |
+ } |
+ return -1, nil |
+} |
+ |
+// ReadLinesFromPipe reads from the given pipe and logs its output. |
+func ReadLinesFromPipe(pipe io.Reader, lines chan string) { |
+ buf := []byte{} |
+ |
+ // writeLine recovers from a panic when writing to the channel. |
+ writeLine := func(s string) { |
+ defer func() { |
+ if r := recover(); r != nil { |
+ glog.Warningf("Panic writing to channel... are we exiting?") |
+ } |
+ }() |
+ lines <- s |
+ } |
+ |
+ // readLines reads output lines from the buffer and pushes them into the channel. |
+ readlines := func() { |
+ readIdx := 0 |
+ // Read lines from buf. |
+ for i, b := range buf { |
+ if b == '\n' { |
+ s := string(buf[readIdx:i]) |
+ writeLine(s) |
+ readIdx = i + 1 |
+ } |
+ } |
+ // Remove the lines we read. |
+ buf = buf[readIdx:] |
+ } |
+ |
+ // Loop forever, reading bytes from the pipe. |
+ readbuf := make([]byte, 4096) |
+ for { |
+ read, err := pipe.Read(readbuf) |
+ if read > 0 { |
+ buf = append(buf, readbuf[:read]...) |
+ |
+ // Read lines from the buffers. |
+ readlines() |
+ } |
+ if err != nil { |
+ if err == io.EOF { |
+ break |
+ } else { |
+ glog.Error(err) |
+ } |
+ } else if read == 0 { |
+ time.Sleep(20 * time.Millisecond) |
+ } |
+ } |
+ // Read any remaining lines from the buffers. |
+ readlines() |
+ // "Flush" any incomplete lines from the buffers. |
+ writeLine(string(buf)) |
+} |
+ |
+// RunApp launches the given app on the device, pipes its log output to stdout, |
+// and returns when the app finishes running, with an error if the app did not |
+// complete successfully. |
+func RunApp(adb, serial, appName string, args []string) error { |
+ // Kill any running instances of the app. |
+ if err := KillSkia(adb, serial); err != nil { |
+ return fmt.Errorf("Failed to kill app: %v", err) |
+ } |
+ |
+ // Clear the ADB log. |
+ if _, err := RunADB(adb, serial, false, "logcat", "-c"); err != nil { |
+ return fmt.Errorf("Failed to clear ADB log: %v", err) |
+ } |
+ |
+ // Prepare to read the subprocess output. |
+ logProc := ADB(adb, serial, false, "logcat") |
+ defer func() { |
+ // Cleanup. |
+ if err := logProc.Process.Kill(); err != nil { |
+ glog.Errorf("Failed to kill logcat process: %v", err) |
+ } |
+ }() |
+ |
+ stdout, err := logProc.StdoutPipe() |
+ if err != nil { |
+ return fmt.Errorf("Failed to obtain stdout pipe: %v", err) |
+ } |
+ stdoutLines := make(chan string) |
+ stderr, err := logProc.StderrPipe() |
+ if err != nil { |
+ return fmt.Errorf("Failed to obtain stderr pipe: %v", err) |
+ } |
+ stderrLines := make(chan string) |
+ |
+ go ReadLinesFromPipe(stdout, stdoutLines) |
+ go ReadLinesFromPipe(stderr, stderrLines) |
+ if err := logProc.Start(); err != nil { |
+ return fmt.Errorf("Failed to start logcat process.") |
+ } |
+ |
+ // Launch the app. |
+ stop := make(chan bool, 1) |
+ activity := fmt.Sprintf("%s.%s/%s.%sActivity", COM_SKIA, strings.ToLower(appName), COM_SKIA, appName) |
+ flags := strings.Join(args, " ") |
+ cmd := []string{"shell", "am", "start", "-S", "-n", activity, "--es", "cmdLineFlags", flags} |
+ output, err := RunADB(adb, serial, true, cmd...) |
+ if err != nil { |
+ return fmt.Errorf("Failed to launch app: %v", err) |
+ } |
+ glog.Info(string(output)) |
+ // Make a handler for SIGINT so that we can kill the app if this script is interrupted. |
+ go func() { |
+ interrupt := make(chan os.Signal, 1) |
+ signal.Notify(interrupt, os.Interrupt) |
+ for _ = range interrupt { |
+ glog.Infof("Received SIGINT; killing app.") |
+ stop <- true |
+ close(stdoutLines) |
+ close(stderrLines) |
+ if err := KillSkia(adb, serial); err != nil { |
+ glog.Errorf("Failed to kill app: %v", err) |
+ } |
+ if err := logProc.Process.Kill(); err != nil { |
+ glog.Errorf("Failed to kill logcat process: %v", err) |
+ } |
+ } |
+ }() |
+ |
+ // Loop until the app finishes or we're interrupted, writing output as appropriate. |
+ // TODO(borenet): VisualBench should print its exit code. This script should exit |
+ // with that code, or a non-zero code if the process ended without printing any code. |
+ second := time.Tick(time.Second) |
+PollLoop: |
+ for { |
+ select { |
+ case <-second: |
+ // Poll the Skia process every second to make sure it's still running. |
+ pid, err := PollSkia(adb, serial) |
+ if err != nil { |
+ glog.Errorf("Failed to poll Skia process: %v", err) |
+ } else if pid < 0 { |
+ glog.Infof("Skia process is no longer running!") |
+ break PollLoop |
+ } |
+ case <-stop: |
+ break PollLoop |
+ case line := <-stdoutLines: |
+ glog.Info(line) |
+ case line := <-stderrLines: |
+ glog.Error(line) |
+ } |
+ } |
+ |
+ return nil |
+} |
+ |
+func main() { |
+ common.Init() |
+ args := flag.Args() |
+ adbPath := *adb |
+ if adbPath == "" { |
+ adbPath = "adb" |
+ } |
+ if err := RunApp(adbPath, *serial, *app, args); err != nil { |
+ glog.Fatal(err) |
+ } |
+} |