diff options
author | borenet <borenet@google.com> | 2015-07-31 04:13:38 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-07-31 04:13:38 -0700 |
commit | 850bbf1bb62e07b02e16627e522079f60333d857 (patch) | |
tree | 0be0f7d56a20c715a551d7ed0ee0a635c9d1d942 | |
parent | d94708e71e8611015cdeb4f5874a11816e40ecdc (diff) |
Add android_run_app.go
Launches an app, pipes its output, and exits when the app exits.
Loosely based on the old buildbot code in python:
https://skia.googlesource.com/buildbot/+/ac0663c599a443a4958c8cad5aefd25eb09eff58/slave/skia_slave_scripts/utils/android_utils.py
BUG=skia:4093
Review URL: https://codereview.chromium.org/1256353002
-rw-r--r-- | platform_tools/android/bin/android_run_app.go | 248 |
1 files changed, 248 insertions, 0 deletions
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 0000000000..53ae0fd938 --- /dev/null +++ b/platform_tools/android/bin/android_run_app.go @@ -0,0 +1,248 @@ +// Copyright (c) 2015 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 ( + adbPath = 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.") +) + +// Struct used for running ADB commands. +type ADB struct { + path string + serial string +} + +// ADBFromFlags returns an ADB instance based on flags passed to the program. +func ADBFromFlags() *ADB { + rv := &ADB{ + path: *adbPath, + serial: *serial, + } + if rv.path == "" { + rv.path = "adb" + } + return rv +} + +// Cmd creates an exec.Cmd object for the given ADB command. +func (adb *ADB) Cmd(log bool, args ...string) *exec.Cmd { + cmd := []string{} + if adb.serial != "" { + cmd = append(cmd, "-s", adb.serial) + } + cmd = append(cmd, args...) + if log { + glog.Infof("Exec `%s %s`", adb.path, strings.Join(cmd, " ")) + } + return exec.Command(adb.path, cmd...) +} + +// KillSkia kills any running process. +func (adb *ADB) Kill(proc string) error { + return adb.Cmd(false, "shell", "am", "force-stop", proc).Run() +} + +// PollSkia checks to see if the given process is running. If so, return the pid, otherwise return -1. +func (adb *ADB) Poll(proc string) (int64, error) { + output, err := adb.Cmd(false, "shell", "ps").Output() + 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, proc) { + 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 *ADB, appName string, args []string) error { + // Kill any running instances of the app. + if err := adb.Kill(COM_SKIA); err != nil { + return fmt.Errorf("Failed to kill app: %v", err) + } + + // Clear the ADB log. + if err := adb.Cmd(false, "logcat", "-c").Run(); err != nil { + return fmt.Errorf("Failed to clear ADB log: %v", err) + } + + // Prepare to read the subprocess output. + logProc := adb.Cmd(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 := adb.Cmd(true, cmd...).Output() + 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 := adb.Kill(COM_SKIA); 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 := adb.Poll(COM_SKIA) + 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() + if err := RunApp(ADBFromFlags(), *app, args); err != nil { + glog.Fatal(err) + } +} |