| /* |
| * 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 |
| } |