1#!/usr/bin/env python3 2# 3# Copyright 2016 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17from builtins import str 18 19import logging 20import re 21import shlex 22import shutil 23 24from acts.controllers.adb_lib.error import AdbCommandError 25from acts.controllers.adb_lib.error import AdbError 26from acts.libs.proc import job 27from acts.metrics.loggers import usage_metadata_logger 28 29DEFAULT_ADB_TIMEOUT = 60 30DEFAULT_ADB_PULL_TIMEOUT = 180 31 32ADB_REGEX = re.compile('adb:') 33# Uses a regex to be backwards compatible with previous versions of ADB 34# (N and above add the serial to the error msg). 35DEVICE_NOT_FOUND_REGEX = re.compile('error: device (?:\'.*?\' )?not found') 36DEVICE_OFFLINE_REGEX = re.compile('error: device offline') 37# Raised when adb forward commands fail to forward a port. 38CANNOT_BIND_LISTENER_REGEX = re.compile('error: cannot bind listener:') 39# Expected output is "Android Debug Bridge version 1.0.XX 40ADB_VERSION_REGEX = re.compile('Android Debug Bridge version 1.0.(\d+)') 41GREP_REGEX = re.compile('grep(\s+)') 42 43ROOT_USER_ID = '0' 44SHELL_USER_ID = '2000' 45 46 47def parsing_parcel_output(output): 48 """Parsing the adb output in Parcel format. 49 50 Parsing the adb output in format: 51 Result: Parcel( 52 0x00000000: 00000000 00000014 00390038 00340031 '........8.9.1.4.' 53 0x00000010: 00300038 00300030 00300030 00340032 '8.0.0.0.0.0.2.4.' 54 0x00000020: 00350034 00330035 00320038 00310033 '4.5.5.3.8.2.3.1.' 55 0x00000030: 00000000 '.... ') 56 """ 57 output = ''.join(re.findall(r"'(.*)'", output)) 58 return re.sub(r'[.\s]', '', output) 59 60 61class AdbProxy(object): 62 """Proxy class for ADB. 63 64 For syntactic reasons, the '-' in adb commands need to be replaced with 65 '_'. Can directly execute adb commands on an object: 66 >> adb = AdbProxy(<serial>) 67 >> adb.start_server() 68 >> adb.devices() # will return the console output of "adb devices". 69 """ 70 71 def __init__(self, serial="", ssh_connection=None): 72 """Construct an instance of AdbProxy. 73 74 Args: 75 serial: str serial number of Android device from `adb devices` 76 ssh_connection: SshConnection instance if the Android device is 77 connected to a remote host that we can reach via SSH. 78 """ 79 self.serial = serial 80 self._server_local_port = None 81 adb_path = shutil.which('adb') 82 adb_cmd = [shlex.quote(adb_path)] 83 if serial: 84 adb_cmd.append("-s %s" % serial) 85 if ssh_connection is not None: 86 # Kill all existing adb processes on the remote host (if any) 87 # Note that if there are none, then pkill exits with non-zero status 88 ssh_connection.run("pkill adb", ignore_status=True) 89 # Copy over the adb binary to a temp dir 90 temp_dir = ssh_connection.run("mktemp -d").stdout.strip() 91 ssh_connection.send_file(adb_path, temp_dir) 92 # Start up a new adb server running as root from the copied binary. 93 remote_adb_cmd = "%s/adb %s root" % (temp_dir, "-s %s" % serial 94 if serial else "") 95 ssh_connection.run(remote_adb_cmd) 96 # Proxy a local port to the adb server port 97 local_port = ssh_connection.create_ssh_tunnel(5037) 98 self._server_local_port = local_port 99 100 if self._server_local_port: 101 adb_cmd.append("-P %d" % local_port) 102 self.adb_str = " ".join(adb_cmd) 103 self._ssh_connection = ssh_connection 104 105 def get_user_id(self): 106 """Returns the adb user. Either 2000 (shell) or 0 (root).""" 107 return self.shell('id -u') 108 109 def is_root(self, user_id=None): 110 """Checks if the user is root. 111 112 Args: 113 user_id: if supplied, the id to check against. 114 Returns: 115 True if the user is root. False otherwise. 116 """ 117 if not user_id: 118 user_id = self.get_user_id() 119 return user_id == ROOT_USER_ID 120 121 def ensure_root(self): 122 """Ensures the user is root after making this call. 123 124 Note that this will still fail if the device is a user build, as root 125 is not accessible from a user build. 126 127 Returns: 128 False if the device is a user build. True otherwise. 129 """ 130 self.ensure_user(ROOT_USER_ID) 131 return self.is_root() 132 133 def ensure_user(self, user_id=SHELL_USER_ID): 134 """Ensures the user is set to the given user. 135 136 Args: 137 user_id: The id of the user. 138 """ 139 if self.is_root(user_id): 140 self.root() 141 else: 142 self.unroot() 143 self.wait_for_device() 144 return self.get_user_id() == user_id 145 146 def _exec_cmd(self, cmd, ignore_status=False, timeout=DEFAULT_ADB_TIMEOUT): 147 """Executes adb commands in a new shell. 148 149 This is specific to executing adb commands. 150 151 Args: 152 cmd: A string or list that is the adb command to execute. 153 154 Returns: 155 The stdout of the adb command. 156 157 Raises: 158 AdbError for errors in ADB operations. 159 AdbCommandError for errors from commands executed through ADB. 160 """ 161 if isinstance(cmd, list): 162 cmd = ' '.join(cmd) 163 result = job.run(cmd, ignore_status=True, timeout=timeout) 164 ret, out, err = result.exit_status, result.stdout, result.stderr 165 166 if any(pattern.match(err) for pattern in 167 [ADB_REGEX, DEVICE_OFFLINE_REGEX, DEVICE_NOT_FOUND_REGEX, 168 CANNOT_BIND_LISTENER_REGEX]): 169 raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret) 170 if "Result: Parcel" in out: 171 return parsing_parcel_output(out) 172 if ignore_status or (ret == 1 and GREP_REGEX.search(cmd)): 173 return out or err 174 if ret != 0: 175 raise AdbCommandError(cmd=cmd, stdout=out, stderr=err, ret_code=ret) 176 return out 177 178 def _exec_adb_cmd(self, name, arg_str, **kwargs): 179 return self._exec_cmd(' '.join((self.adb_str, name, arg_str)), 180 **kwargs) 181 182 def _exec_cmd_nb(self, cmd, **kwargs): 183 """Executes adb commands in a new shell, non blocking. 184 185 Args: 186 cmds: A string that is the adb command to execute. 187 188 """ 189 return job.run_async(cmd, **kwargs) 190 191 def _exec_adb_cmd_nb(self, name, arg_str, **kwargs): 192 return self._exec_cmd_nb(' '.join((self.adb_str, name, arg_str)), 193 **kwargs) 194 195 def tcp_forward(self, host_port, device_port): 196 """Starts tcp forwarding from localhost to this android device. 197 198 Args: 199 host_port: Port number to use on localhost 200 device_port: Port number to use on the android device. 201 202 Returns: 203 Forwarded port on host as int or command output string on error 204 """ 205 if self._ssh_connection: 206 # We have to hop through a remote host first. 207 # 1) Find some free port on the remote host's localhost 208 # 2) Setup forwarding between that remote port and the requested 209 # device port 210 remote_port = self._ssh_connection.find_free_port() 211 host_port = self._ssh_connection.create_ssh_tunnel( 212 remote_port, local_port=host_port) 213 output = self.forward("tcp:%d tcp:%d" % (host_port, device_port), 214 ignore_status=True) 215 # If hinted_port is 0, the output will be the selected port. 216 # Otherwise, there will be no output upon successfully 217 # forwarding the hinted port. 218 if not output: 219 return host_port 220 try: 221 output_int = int(output) 222 except ValueError: 223 return output 224 return output_int 225 226 def remove_tcp_forward(self, host_port): 227 """Stop tcp forwarding a port from localhost to this android device. 228 229 Args: 230 host_port: Port number to use on localhost 231 """ 232 if self._ssh_connection: 233 remote_port = self._ssh_connection.close_ssh_tunnel(host_port) 234 if remote_port is None: 235 logging.warning("Cannot close unknown forwarded tcp port: %d", 236 host_port) 237 return 238 # The actual port we need to disable via adb is on the remote host. 239 host_port = remote_port 240 self.forward("--remove tcp:%d" % host_port) 241 242 def getprop(self, prop_name): 243 """Get a property of the device. 244 245 This is a convenience wrapper for "adb shell getprop xxx". 246 247 Args: 248 prop_name: A string that is the name of the property to get. 249 250 Returns: 251 A string that is the value of the property, or None if the property 252 doesn't exist. 253 """ 254 return self.shell("getprop %s" % prop_name) 255 256 # TODO: This should be abstracted out into an object like the other shell 257 # command. 258 def shell(self, command, ignore_status=False, timeout=DEFAULT_ADB_TIMEOUT): 259 return self._exec_adb_cmd( 260 'shell', 261 shlex.quote(command), 262 ignore_status=ignore_status, 263 timeout=timeout) 264 265 def shell_nb(self, command): 266 return self._exec_adb_cmd_nb('shell', shlex.quote(command)) 267 268 def __getattr__(self, name): 269 def adb_call(*args, **kwargs): 270 usage_metadata_logger.log_usage(self.__module__, name) 271 clean_name = name.replace('_', '-') 272 if clean_name in ['pull', 'push'] and 'timeout' not in kwargs: 273 kwargs['timeout'] = DEFAULT_ADB_PULL_TIMEOUT 274 arg_str = ' '.join(str(elem) for elem in args) 275 return self._exec_adb_cmd(clean_name, arg_str, **kwargs) 276 277 return adb_call 278 279 def get_version_number(self): 280 """Returns the version number of ADB as an int (XX in 1.0.XX). 281 282 Raises: 283 AdbError if the version number is not found/parsable. 284 """ 285 version_output = self.version() 286 match = re.search(ADB_VERSION_REGEX, version_output) 287 288 if not match: 289 logging.error('Unable to capture ADB version from adb version ' 290 'output: %s' % version_output) 291 raise AdbError('adb version', version_output, '', '') 292 return int(match.group(1)) 293