blob: 2a1e6c5bdf0dd2fb75088cafe5bf3f0a4ade087c [file] [log] [blame]
/*
* Copyright 2015 Google Inc. 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 cherry
import (
"os/exec"
"bytes"
"bufio"
"strings"
"errors"
"../rtdb"
"time"
"strconv"
"fmt"
"log"
)
type commandError struct {
cmdName string
cmdArgs []string
subErr error
stdErr string
}
func indent (str string, numSpaces int) string {
indentation := strings.Repeat(" ", numSpaces)
numNewlines := strings.Count(str, "\n")
return indentation + strings.Replace(str, "\n", "\n" + indentation, numNewlines-1)
}
func (err commandError) Error () string {
result := fmt.Sprintf("command '%s' with arguments [", err.cmdName)
for ndx, arg := range err.cmdArgs {
if ndx > 0 {
result += ", "
}
result += "'" + arg + "'"
}
result += fmt.Sprintf("] gave '%v', with the following stderr:\n%s\n", err.subErr, strings.TrimSuffix(indent(err.stdErr, 4), "\n"))
return result
}
func getSystemCommandOutput (command string, args ...string) (string, error) {
cmd := exec.Command(command, args...)
outputBuffer := bytes.Buffer{}
errOutputBuffer := bytes.Buffer{}
cmd.Stdout = &outputBuffer
cmd.Stderr = &errOutputBuffer
err := cmd.Run()
if err != nil {
return "", commandError {
cmdName: command,
cmdArgs: args,
subErr: err,
stdErr: errOutputBuffer.String(),
}
}
return outputBuffer.String(), nil
}
func runCommands (commands ...*exec.Cmd) error {
for _, cmd := range commands {
stdErr := bytes.Buffer{}
cmd.Stderr = &stdErr
err := cmd.Run()
if err != nil {
return commandError {
cmdName: cmd.Args[0],
cmdArgs: cmd.Args[1:],
subErr: err,
stdErr: stdErr.String(),
}
}
}
return nil
}
func lines (str string) []string {
scanner := bufio.NewScanner(strings.NewReader(str))
result := []string{}
for scanner.Scan() {
result = append(result, scanner.Text())
}
return result
}
type adbDeviceInfo struct {
serialNumber string
state string
qualifiers map[string]string
}
func parseADBDeviceInfo (infoStr string) (adbDeviceInfo, error) {
infoParts := strings.Fields(infoStr)
if len(infoParts) < 1 {
return adbDeviceInfo{}, errors.New("No device info present")
}
deviceInfo := adbDeviceInfo {
serialNumber: infoParts[0],
state: "",
qualifiers: map[string]string{},
}
infoRest := infoParts[1:]
for _, infoPart := range infoRest {
keyValue := strings.SplitN(infoPart, ":", 2)
if len(keyValue) == 1 {
if deviceInfo.state != "" {
deviceInfo.state += " "
}
deviceInfo.state += keyValue[0]
} else {
deviceInfo.qualifiers[keyValue[0]] = keyValue[1]
}
}
return deviceInfo, nil
}
func getADBDeviceInfos () ([]adbDeviceInfo, error) {
deviceInfoStr, err := getSystemCommandOutput("adb", "devices", "-l")
if err != nil { return []adbDeviceInfo{}, err }
deviceInfoLines := lines(deviceInfoStr)
deviceInfos := []adbDeviceInfo{}
serialNumbers := map[string]struct{}{} // Keep track to avoid duplicates (sometimes adb gives those).
// There may be irrelevant lines at the beginning of the output,
// like info about the ADB daemon starting. Skip those.
deviceListStartFound := false
for _, line := range deviceInfoLines {
if deviceListStartFound {
info, err := parseADBDeviceInfo(line)
if err == nil {
if _, alreadyIn := serialNumbers[info.serialNumber]; !alreadyIn {
serialNumbers[info.serialNumber] = struct{}{}
deviceInfos = append(deviceInfos, info)
}
}
} else if strings.Trim(line, " ") == "List of devices attached" {
deviceListStartFound = true
}
}
return deviceInfos, nil
}
type adbPortForwardInfo struct {
serialNumber string
localProtocol string
localPort int
remoteProtocol string
remotePort int
}
func parseADBPortSpec (specStr string) (string, int, error) {
parts := strings.SplitN(specStr, ":", 2)
if len(parts) != 2 { return "", 0, errors.New("Expected 2 colon-separated fields") }
port, err := strconv.Atoi(parts[1])
if err != nil { return "", 0, errors.New("Expected second field to be valid integer") }
return parts[0], port, nil
}
func parseADBPortForwardInfo (infoStr string) (adbPortForwardInfo, error) {
infoParts := strings.Fields(infoStr)
if len(infoParts) != 3 {
return adbPortForwardInfo{}, errors.New("Expected 3 whitespace-delimited fields")
}
localProtocol, localPort, err := parseADBPortSpec(infoParts[1])
if err != nil { return adbPortForwardInfo{}, fmt.Errorf("Invalid local port spec; %v", err) }
remoteProtocol, remotePort, err := parseADBPortSpec(infoParts[2])
if err != nil { return adbPortForwardInfo{}, fmt.Errorf("Invalid remote port spec; %v", err) }
return adbPortForwardInfo {
serialNumber: infoParts[0],
localProtocol: localProtocol,
localPort: localPort,
remoteProtocol: remoteProtocol,
remotePort: remotePort,
}, nil
}
func getADBPortForwardInfos () ([]adbPortForwardInfo, error) {
forwardInfoStr, err := getSystemCommandOutput("adb", "forward", "--list")
if err != nil { return []adbPortForwardInfo{}, err }
forwardInfoLines := lines(forwardInfoStr)
forwardInfos := []adbPortForwardInfo{}
for _, line := range forwardInfoLines {
info, err := parseADBPortForwardInfo(line)
if err == nil {
forwardInfos = append(forwardInfos, info)
}
}
return forwardInfos, nil
}
func serialNumberToDeviceId (ser string) string {
return "adb:" + ser
}
func StartADBDeviceListPoller (rtdbServer *rtdb.Server, interval time.Duration) {
go func () {
previousInfos := []adbDeviceInfo{}
previousErrorStr := ""
for {
select {
case <- time.After(interval):
// Get devices from ADB and compare to previous; update connection list according to changes.
currentInfos, err := getADBDeviceInfos()
currentErrorStr := ""
if err != nil { currentErrorStr = err.Error() }
// potentiallyNewlyNamedDevices will contain the devices that might have gotten
// a name ("model" qualifier in adb device list) since the last update. These are
// devices whose state has changed, and newly connected devices.
potentiallyNewlyNamedDevices := []adbDeviceInfo{}
opSet := rtdb.NewOpSet()
// Compare previousInfos and currentInfos to detect removed and added devices. We
// do it this way (instead of just replacing the whole list) because otherwise
// the AngularJS-using GUI will think that the whole list has changed, which will
// result in inconveniences if the user is editing non-changing adb device configs.
// The way this is implemented treats additions in the middle of the list as if
// all items after the added item were removed, then that item appended, and finally
// the removed items re-appended; in such cases, the operation sequence is rather
// suboptimal, but with how adb seems to report the devices (new items are appended
// to the end), these cases shouldn't occur often.
curInfoNdx := 0 // Current index in the device list being updated.
// Detect devices that were present both previously and currently (and may have had their
// states changed), and also devices that were removed since the previous time.
for _, oldInfo := range previousInfos {
if curInfoNdx < len(currentInfos) && oldInfo.serialNumber == currentInfos[curInfoNdx].serialNumber {
// Device is still connected. Check possible state change.
if oldInfo.state != currentInfos[curInfoNdx].state {
opSet.Call(typeADBDeviceConnectionList, "adbDeviceConnectionList", "SetConnectionState", curInfoNdx, currentInfos[curInfoNdx].state)
if currentInfos[curInfoNdx].qualifiers["model"] != "" {
potentiallyNewlyNamedDevices = append(potentiallyNewlyNamedDevices, currentInfos[curInfoNdx])
}
}
curInfoNdx++
} else {
// Device was disconnected since the previous time.
opSet.Call(typeADBDeviceConnectionList, "adbDeviceConnectionList", "Remove", curInfoNdx)
}
}
// Add new devices to the end of the list. Also create configs for previously unseen devices.
for _, newInfo := range currentInfos[curInfoNdx:] {
deviceId := serialNumberToDeviceId(newInfo.serialNumber)
modelName := newInfo.qualifiers["model"]
{
var config DeviceConfig
err := rtdbServer.GetObject(deviceId, &config)
if err != nil {
// Device hasn't been seen previously; create a new config for it.
log.Printf("[adb] detected new device '%s' (model name '%s')\n", newInfo.serialNumber, modelName)
newConfig := DeviceConfig {
IsADBDevice: true,
ADBSerialNumber: newInfo.serialNumber,
Name: modelName,
TargetAddress: "127.0.0.1",
TargetPort: 50016,
CommandLine: "--deqp-watchdog=enable --deqp-crashhandler=enable",
}
opSet.Call(typeDeviceConfig, deviceId, "Init", newConfig)
} else if config.Name == "" && modelName != "" {
// Device has been seen previously, but didn't have a name, and now does.
potentiallyNewlyNamedDevices = append(potentiallyNewlyNamedDevices, newInfo)
}
}
connection := ADBDeviceConnection {
DeviceId: deviceId,
State: newInfo.state,
}
opSet.Call(typeADBDeviceConnectionList, "adbDeviceConnectionList", "Append", connection)
}
// Set device config names for devices that don't have those yet, but do have a model name in the adb output.
for _, info := range potentiallyNewlyNamedDevices {
deviceId := serialNumberToDeviceId(info.serialNumber)
modelName := info.qualifiers["model"]
var config DeviceConfig
err := rtdbServer.GetObject(deviceId, &config)
if err != nil { panic(err) }
if config.Name == "" {
log.Printf("[adb] got model name for previously unnamed device '%s'; setting name to '%s'\n", info.serialNumber, modelName)
opSet.Call(typeDeviceConfig, deviceId, "SetName", modelName)
}
}
if previousErrorStr != currentErrorStr {
opSet.Call(typeADBDeviceConnectionList, "adbDeviceConnectionList", "SetError", currentErrorStr)
}
err = rtdbServer.ExecuteOpSet(opSet)
if err != nil { panic(err) }
previousInfos = currentInfos
previousErrorStr = currentErrorStr
}
}
}()
}
func LaunchAndroidExecServer (adbSerialNumber string, localPort int) error {
return runCommands(
exec.Command("adb", "-s", adbSerialNumber, "forward", "tcp:" + strconv.Itoa(localPort), "tcp:50016"),
exec.Command("adb", "-s", adbSerialNumber, "shell", "setprop", "log.tag.dEQP", "DEBUG"),
exec.Command("adb", "-s", adbSerialNumber, "shell", "am", "start", "-n", "com.drawelements.deqp/.execserver.ServiceStarter"),
)
}
func RemoveADBPortForward (adbSerialNumber string, localPort int) error {
forwardInfos, err := getADBPortForwardInfos()
if err != nil { return err }
anyRemoved := false
for _, info := range forwardInfos {
if info.localProtocol == "tcp" && info.serialNumber == adbSerialNumber && info.localPort == localPort {
err := runCommands(exec.Command("adb", "-s", info.serialNumber, "forward", "--remove", info.localProtocol + ":" + strconv.Itoa(info.localPort)))
if err != nil { return err }
anyRemoved = true
}
}
if !anyRemoved { return errors.New("No matching forwards found") }
return nil
}