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