1# 2# Copyright 2016 - The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16from builtins import str 17 18import logging 19import random 20import socket 21import subprocess 22import time 23 24from vts.runners.host import const 25from vts.utils.python.common import cmd_utils 26 27 28# Default adb timeout 5 minutes 29DEFAULT_ADB_TIMEOUT = 300 30# Adb long timeout (10 minutes) for adb push/pull/bugreport/bugreportz 31DEFAULT_ADB_LONG_TIMEOUT = 600 32# Adb short timeout (30 seconds) 33DEFAULT_ADB_SHORT_TIMEOUT = 30 34 35class AdbError(Exception): 36 """Raised when there is an error in adb operations.""" 37 38 def __init__(self, cmd, stdout, stderr, ret_code): 39 self.cmd = cmd 40 self.stdout = stdout 41 self.stderr = stderr 42 self.ret_code = ret_code 43 44 def __str__(self): 45 return ("Error executing adb cmd '%s'. ret: %d, stdout: %s, stderr: %s" 46 ) % (self.cmd, self.ret_code, self.stdout, self.stderr) 47 48 49def get_available_host_port(): 50 """Gets a host port number available for adb forward. 51 52 Returns: 53 An integer representing a port number on the host available for adb 54 forward. 55 """ 56 while True: 57 port = random.randint(1024, 9900) 58 if is_port_available(port): 59 return port 60 61 62def is_port_available(port): 63 """Checks if a given port number is available on the system. 64 65 Args: 66 port: An integer which is the port number to check. 67 68 Returns: 69 True if the port is available; False otherwise. 70 """ 71 # Make sure adb is not using this port so we don't accidentally interrupt 72 # ongoing runs by trying to bind to the port. 73 if port in list_occupied_adb_ports(): 74 return False 75 s = None 76 try: 77 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 78 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 79 s.bind(('localhost', port)) 80 return True 81 except socket.error: 82 return False 83 finally: 84 if s: 85 s.close() 86 87 88def list_occupied_adb_ports(): 89 """Lists all the host ports occupied by adb forward. 90 91 This is useful because adb will silently override the binding if an attempt 92 to bind to a port already used by adb was made, instead of throwing binding 93 error. So one should always check what ports adb is using before trying to 94 bind to a port with adb. 95 96 Returns: 97 A list of integers representing occupied host ports. 98 """ 99 out = AdbProxy().forward("--list") 100 clean_lines = str(out, 'utf-8').strip().split('\n') 101 used_ports = [] 102 for line in clean_lines: 103 tokens = line.split(" tcp:") 104 if len(tokens) != 3: 105 continue 106 used_ports.append(int(tokens[1])) 107 return used_ports 108 109 110class AdbProxy(): 111 """Proxy class for ADB. 112 113 For syntactic reasons, the '-' in adb commands need to be replaced with 114 '_'. Can directly execute adb commands on an object: 115 >> adb = AdbProxy(<serial>) 116 >> adb.start_server() 117 >> adb.devices() # will return the console output of "adb devices". 118 """ 119 120 def __init__(self, serial="", log=None): 121 self.serial = serial 122 if serial: 123 self.adb_str = "adb -s {}".format(serial) 124 else: 125 self.adb_str = "adb" 126 self.log = log 127 128 def _exec_cmd(self, cmd, no_except=False, timeout=DEFAULT_ADB_TIMEOUT): 129 """Executes adb commands in a new shell. 130 131 This is specific to executing adb binary because stderr is not a good 132 indicator of cmd execution status. 133 134 Args: 135 cmd: string, the adb command to execute. 136 no_except: bool, controls whether exception can be thrown. 137 timeout: float, timeout in seconds. If the command times out, the 138 exit code is not 0. 139 140 Returns: 141 The output of the adb command run if the exit code is 0 and if 142 exceptions are allowed. Otherwise, returns a dictionary containing 143 stdout, stderr, and exit code. 144 145 Raises: 146 AdbError if the adb command exit code is not 0 and exceptions are 147 allowed. 148 """ 149 out, err, ret = cmd_utils.ExecuteOneShellCommand(cmd, timeout) 150 logging.debug("cmd: %s, stdout: %s, stderr: %s, ret: %s", cmd, out, 151 err, ret) 152 if no_except: 153 return { 154 const.STDOUT: out, 155 const.STDERR: err, 156 const.EXIT_CODE: ret, 157 } 158 else: 159 if ret == 0: 160 return out 161 else: 162 raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret) 163 164 def tcp_forward(self, host_port, device_port): 165 """Starts TCP forwarding. 166 167 Args: 168 host_port: Port number to use on the computer. 169 device_port: Port number to use on the android device. 170 """ 171 self.forward("tcp:{} tcp:{}".format(host_port, device_port)) 172 173 def reverse_tcp_forward(self, device_port, host_port): 174 """Starts reverse TCP forwarding. 175 176 Args: 177 device_port: Port number to use on the android device. 178 host_port: Port number to use on the computer. 179 """ 180 self.reverse("tcp:{} tcp:{}".format(device_port, host_port)) 181 182 def __getattr__(self, name): 183 184 def adb_call(*args, **kwargs): 185 clean_name = name.replace('_', '-') 186 arg_str = ' '.join(str(elem) for elem in args) 187 if clean_name == 'shell': 188 arg_str = self._quote_wrap_shell_command(arg_str) 189 elif "timeout" not in kwargs.keys(): 190 # for non-shell command like adb pull/push/bugreport, set longer default timeout 191 kwargs["timeout"] = DEFAULT_ADB_LONG_TIMEOUT 192 return self._exec_cmd(' '.join((self.adb_str, clean_name, arg_str)), 193 **kwargs) 194 195 return adb_call 196 197 def _quote_wrap_shell_command(self, cmd): 198 """Wraps adb shell command with double quotes. 199 200 Double quotes inside the command will be replaced with \". 201 202 Args: 203 cmd: string, command string. 204 205 Returns: 206 string, quote wrapped command. 207 """ 208 return '"%s"' % cmd.replace('"', '\\"')