• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2016 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import logging
16import re
17import subprocess
18import threading
19import time
20
21from mobly import utils
22
23# Command to use for running ADB commands.
24ADB = 'adb'
25
26# adb gets confused if we try to manage bound ports in parallel, so anything to
27# do with port forwarding must happen under this lock.
28ADB_PORT_LOCK = threading.Lock()
29
30# Number of attempts to execute "adb root", and seconds for interval time of
31# this commands.
32ADB_ROOT_RETRY_ATTMEPTS = 3
33ADB_ROOT_RETRY_ATTEMPT_INTERVAL_SEC = 10
34
35# Qualified class name of the default instrumentation test runner.
36DEFAULT_INSTRUMENTATION_RUNNER = 'com.android.common.support.test.runner.AndroidJUnitRunner'
37
38# Adb getprop call should never take too long.
39DEFAULT_GETPROP_TIMEOUT_SEC = 5
40DEFAULT_GETPROPS_ATTEMPTS = 3
41DEFAULT_GETPROPS_RETRY_SLEEP_SEC = 1
42
43# The regex pattern indicating the `adb connect` command did not fail.
44PATTERN_ADB_CONNECT_SUCCESS = re.compile(
45    r'^connected to .*|^already connected to .*')
46
47
48class Error(Exception):
49  """Base error type for adb proxy module."""
50
51
52class AdbError(Error):
53  """Raised when an adb command encounters an error.
54
55  Attributes:
56    cmd: list of strings, the adb command executed.
57    stdout: byte string, the raw stdout of the command.
58    stderr: byte string, the raw stderr of the command.
59    ret_code: int, the return code of the command.
60    serial: string, the serial of the device the command is executed on.
61      This is an empty string if the adb command is not specific to a
62      device.
63  """
64
65  def __init__(self, cmd, stdout, stderr, ret_code, serial=''):
66    super().__init__()
67    self.cmd = cmd
68    self.stdout = stdout
69    self.stderr = stderr
70    self.ret_code = ret_code
71    self.serial = serial
72
73  def __str__(self):
74    return ('Error executing adb cmd "%s". ret: %d, stdout: %s, stderr: %s') % (
75        utils.cli_cmd_to_string(
76            self.cmd), self.ret_code, self.stdout, self.stderr)
77
78
79class AdbTimeoutError(Error):
80  """Raised when an command did not complete within expected time.
81
82  Attributes:
83    cmd: list of strings, the adb command that timed out
84    timeout: float, the number of seconds passed before timing out.
85    serial: string, the serial of the device the command is executed on.
86      This is an empty string if the adb command is not specific to a
87      device.
88  """
89
90  def __init__(self, cmd, timeout, serial=''):
91    super().__init__()
92    self.cmd = cmd
93    self.timeout = timeout
94    self.serial = serial
95
96  def __str__(self):
97    return 'Timed out executing command "%s" after %ss.' % (
98        utils.cli_cmd_to_string(self.cmd), self.timeout)
99
100
101def is_adb_available():
102  """Checks if adb is available as a command line tool.
103
104  Returns:
105    True if adb binary is available in console, False otherwise.
106  """
107  ret, out, err = utils.run_command('which adb', shell=True)
108  clean_out = out.decode('utf-8').strip()
109  if clean_out:
110    return True
111  return False
112
113
114def list_occupied_adb_ports():
115  """Lists all the host ports occupied by adb forward.
116
117  This is useful because adb will silently override the binding if an attempt
118  to bind to a port already used by adb was made, instead of throwing binding
119  error. So one should always check what ports adb is using before trying to
120  bind to a port with adb.
121
122  Returns:
123    A list of integers representing occupied host ports.
124  """
125  out = AdbProxy().forward('--list')
126  clean_lines = str(out, 'utf-8').strip().split('\n')
127  used_ports = []
128  for line in clean_lines:
129    tokens = line.split(' tcp:')
130    if len(tokens) != 3:
131      continue
132    used_ports.append(int(tokens[1]))
133  return used_ports
134
135
136class AdbProxy:
137  """Proxy class for ADB.
138
139  For syntactic reasons, the '-' in adb commands need to be replaced with
140  '_'. Can directly execute adb commands on an object:
141  >> adb = AdbProxy(<serial>)
142  >> adb.start_server()
143  >> adb.devices() # will return the console output of "adb devices".
144
145  By default, command args are expected to be an iterable which is passed
146  directly to subprocess.Popen():
147  >> adb.shell(['echo', 'a', 'b'])
148
149  This way of launching commands is recommended by the subprocess
150  documentation to avoid shell injection vulnerabilities and avoid having to
151  deal with multiple layers of shell quoting and different shell environments
152  between different OSes.
153
154  If you really want to run the command through the system shell, this is
155  possible by supplying shell=True, but try to avoid this if possible:
156  >> adb.shell('cat /foo > /tmp/file', shell=True)
157  """
158
159  def __init__(self, serial=''):
160    self.serial = serial
161
162  def _exec_cmd(self, args, shell, timeout, stderr) -> bytes:
163    """Executes adb commands.
164
165    Args:
166      args: string or list of strings, program arguments.
167        See subprocess.Popen() documentation.
168      shell: bool, True to run this command through the system shell,
169        False to invoke it directly. See subprocess.Popen() docs.
170      timeout: float, the number of seconds to wait before timing out.
171        If not specified, no timeout takes effect.
172      stderr: a Byte stream, like io.BytesIO, stderr of the command will
173        be written to this object if provided.
174
175    Returns:
176      The output of the adb command run if exit code is 0.
177
178    Raises:
179      ValueError: timeout value is invalid.
180      AdbError: The adb command exit code is not 0.
181      AdbTimeoutError: The adb command timed out.
182    """
183    if timeout and timeout <= 0:
184      raise ValueError('Timeout is not a positive value: %s' % timeout)
185    try:
186      (ret, out, err) = utils.run_command(args, shell=shell, timeout=timeout)
187    except subprocess.TimeoutExpired:
188      raise AdbTimeoutError(cmd=args, timeout=timeout, serial=self.serial)
189
190    if stderr:
191      stderr.write(err)
192    logging.debug('cmd: %s, stdout: %s, stderr: %s, ret: %s',
193                  utils.cli_cmd_to_string(args), out, err, ret)
194    if ret == 0:
195      return out
196    else:
197      raise AdbError(cmd=args,
198                     stdout=out,
199                     stderr=err,
200                     ret_code=ret,
201                     serial=self.serial)
202
203  def _execute_and_process_stdout(self, args, shell, handler) -> bytes:
204    """Executes adb commands and processes the stdout with a handler.
205
206    Args:
207      args: string or list of strings, program arguments.
208        See subprocess.Popen() documentation.
209      shell: bool, True to run this command through the system shell,
210        False to invoke it directly. See subprocess.Popen() docs.
211      handler: func, a function to handle adb stdout line by line.
212
213    Returns:
214      The stderr of the adb command run if exit code is 0.
215
216    Raises:
217      AdbError: The adb command exit code is not 0.
218    """
219    proc = subprocess.Popen(args,
220                            stdout=subprocess.PIPE,
221                            stderr=subprocess.PIPE,
222                            shell=shell,
223                            bufsize=1)
224    out = '[elided, processed via handler]'
225    try:
226      # Even if the process dies, stdout.readline still works
227      # and will continue until it runs out of stdout to process.
228      while True:
229        line = proc.stdout.readline()
230        if line:
231          handler(line)
232        else:
233          break
234    finally:
235      # Note, communicate will not contain any buffered output.
236      (unexpected_out, err) = proc.communicate()
237      if unexpected_out:
238        out = '[unexpected stdout] %s' % unexpected_out
239        for line in unexpected_out.splitlines():
240          handler(line)
241
242    ret = proc.returncode
243    logging.debug('cmd: %s, stdout: %s, stderr: %s, ret: %s',
244                  utils.cli_cmd_to_string(args), out, err, ret)
245    if ret == 0:
246      return err
247    else:
248      raise AdbError(cmd=args, stdout=out, stderr=err, ret_code=ret)
249
250  def _construct_adb_cmd(self, raw_name, args, shell):
251    """Constructs an adb command with arguments for a subprocess call.
252
253    Args:
254      raw_name: string, the raw unsanitized name of the adb command to
255        format.
256      args: string or list of strings, arguments to the adb command.
257        See subprocess.Proc() documentation.
258      shell: bool, True to run this command through the system shell,
259        False to invoke it directly. See subprocess.Proc() docs.
260
261    Returns:
262      The adb command in a format appropriate for subprocess. If shell is
263        True, then this is a string; otherwise, this is a list of
264        strings.
265    """
266    args = args or ''
267    name = raw_name.replace('_', '-')
268    if shell:
269      args = utils.cli_cmd_to_string(args)
270      # Add quotes around "adb" in case the ADB path contains spaces. This
271      # is pretty common on Windows (e.g. Program Files).
272      if self.serial:
273        adb_cmd = '"%s" -s "%s" %s %s' % (ADB, self.serial, name, args)
274      else:
275        adb_cmd = '"%s" %s %s' % (ADB, name, args)
276    else:
277      adb_cmd = [ADB]
278      if self.serial:
279        adb_cmd.extend(['-s', self.serial])
280      adb_cmd.append(name)
281      if args:
282        if isinstance(args, str):
283          adb_cmd.append(args)
284        else:
285          adb_cmd.extend(args)
286    return adb_cmd
287
288  def _exec_adb_cmd(self, name, args, shell, timeout, stderr) -> bytes:
289    adb_cmd = self._construct_adb_cmd(name, args, shell=shell)
290    out = self._exec_cmd(adb_cmd, shell=shell, timeout=timeout, stderr=stderr)
291    return out
292
293  def _execute_adb_and_process_stdout(self, name, args, shell, handler) -> bytes:
294    adb_cmd = self._construct_adb_cmd(name, args, shell=shell)
295    err = self._execute_and_process_stdout(adb_cmd,
296                                           shell=shell,
297                                           handler=handler)
298    return err
299
300  def _parse_getprop_output(self, output):
301    """Parses the raw output of `adb shell getprop` into a dictionary.
302
303    Args:
304      output: byte str, the raw output of the `adb shell getprop` call.
305
306    Returns:
307      dict, name-value pairs of the properties.
308    """
309    output = output.decode('utf-8', errors='ignore').replace('\r\n', '\n')
310    results = {}
311    for line in output.split(']\n'):
312      if not line:
313        continue
314      try:
315        name, value = line.split(': ', 1)
316      except ValueError:
317        logging.debug('Failed to parse adb getprop line %s', line)
318        continue
319      name = name.strip()[1:-1]
320      # Remove any square bracket from either end of the value string.
321      if value and value[0] == '[':
322        value = value[1:]
323      results[name] = value
324    return results
325
326  @property
327  def current_user_id(self) -> int:
328    """The integer ID of the current Android user.
329
330    Some adb commands require specifying a user ID to work properly. Use
331    this to get the current user ID.
332
333    Note a "user" is not the same as an "account" in Android. See AOSP's
334    documentation for details.
335    https://source.android.com/devices/tech/admin/multi-user
336    """
337    sdk_int = int(self.getprop('ro.build.version.sdk'))
338    if sdk_int >= 24:
339      return int(self.shell(['am', 'get-current-user']))
340    if sdk_int >= 21:
341      user_info_str = self.shell(['dumpsys', 'user']).decode('utf-8')
342      return int(re.findall(r'\{(\d+):', user_info_str)[0])
343    # Multi-user is not supported in SDK < 21, only user 0 exists.
344    return 0
345
346  def connect(self, address) -> bytes:
347    """Executes the `adb connect` command with proper status checking.
348
349    Args:
350      address: string, the address of the Android instance to connect to.
351
352    Returns:
353      The stdout content.
354
355    Raises:
356      AdbError: if the connection failed.
357    """
358    stdout = self._exec_adb_cmd('connect',
359                                address,
360                                shell=False,
361                                timeout=None,
362                                stderr=None)
363    if PATTERN_ADB_CONNECT_SUCCESS.match(stdout.decode('utf-8')) is None:
364      raise AdbError(cmd=f'connect {address}',
365                     stdout=stdout,
366                     stderr='',
367                     ret_code=0)
368    return stdout
369
370  def getprop(self, prop_name):
371    """Get a property of the device.
372
373    This is a convenience wrapper for `adb shell getprop xxx`.
374
375    Args:
376      prop_name: A string that is the name of the property to get.
377
378    Returns:
379      A string that is the value of the property, or None if the property
380      doesn't exist.
381    """
382    return self.shell(
383        ['getprop', prop_name],
384        timeout=DEFAULT_GETPROP_TIMEOUT_SEC).decode('utf-8').strip()
385
386  def getprops(self, prop_names):
387    """Get multiple properties of the device.
388
389    This is a convenience wrapper for `adb shell getprop`. Use this to
390    reduce the number of adb calls when getting multiple properties.
391
392    Args:
393      prop_names: list of strings, the names of the properties to get.
394
395    Returns:
396      A dict containing name-value pairs of the properties requested, if
397      they exist.
398    """
399    attempts = DEFAULT_GETPROPS_ATTEMPTS
400    results = {}
401    for attempt in range(attempts):
402      # The ADB getprop command can randomly return empty string, so try
403      # multiple times. This value should always be non-empty if the device
404      # in a working state.
405      raw_output = self.shell(['getprop'], timeout=DEFAULT_GETPROP_TIMEOUT_SEC)
406      properties = self._parse_getprop_output(raw_output)
407      if properties:
408        for name in prop_names:
409          if name in properties:
410            results[name] = properties[name]
411        break
412      # Don't call sleep on the last attempt.
413      if attempt < attempts - 1:
414        time.sleep(DEFAULT_GETPROPS_RETRY_SLEEP_SEC)
415    return results
416
417  def has_shell_command(self, command) -> bool:
418    """Checks to see if a given check command exists on the device.
419
420    Args:
421      command: A string that is the name of the command to check.
422
423    Returns:
424      A boolean that is True if the command exists and False otherwise.
425    """
426    try:
427      output = self.shell(['command', '-v', command]).decode('utf-8').strip()
428      return command in output
429    except AdbError:
430      # If the command doesn't exist, then 'command -v' can return
431      # an exit code > 1.
432      return False
433
434  def forward(self, args=None, shell=False) -> bytes:
435    with ADB_PORT_LOCK:
436      return self._exec_adb_cmd('forward',
437                                args,
438                                shell,
439                                timeout=None,
440                                stderr=None)
441
442  def instrument(self, package, options=None, runner=None, handler=None) -> bytes:
443    """Runs an instrumentation command on the device.
444
445    This is a convenience wrapper to avoid parameter formatting.
446
447    Example:
448
449    .. code-block:: python
450
451      device.instrument(
452        'com.my.package.test',
453        options = {
454          'class': 'com.my.package.test.TestSuite',
455        },
456      )
457
458    Args:
459      package: string, the package of the instrumentation tests.
460      options: dict, the instrumentation options including the test
461        class.
462      runner: string, the test runner name, which defaults to
463        DEFAULT_INSTRUMENTATION_RUNNER.
464      handler: optional func, when specified the function is used to parse
465        the instrumentation stdout line by line as the output is
466        generated; otherwise, the stdout is simply returned once the
467        instrumentation is finished.
468
469    Returns:
470      The stdout of instrumentation command or the stderr if the handler
471        is set.
472    """
473    if runner is None:
474      runner = DEFAULT_INSTRUMENTATION_RUNNER
475    if options is None:
476      options = {}
477
478    options_list = []
479    for option_key, option_value in options.items():
480      options_list.append('-e %s %s' % (option_key, option_value))
481    options_string = ' '.join(options_list)
482
483    instrumentation_command = 'am instrument -r -w %s %s/%s' % (options_string,
484                                                                package, runner)
485    logging.info('AndroidDevice|%s: Executing adb shell %s', self.serial,
486                 instrumentation_command)
487    if handler is None:
488      return self._exec_adb_cmd('shell',
489                                instrumentation_command,
490                                shell=False,
491                                timeout=None,
492                                stderr=None)
493    else:
494      return self._execute_adb_and_process_stdout('shell',
495                                                  instrumentation_command,
496                                                  shell=False,
497                                                  handler=handler)
498
499  def root(self) -> bytes:
500    """Enables ADB root mode on the device.
501
502    This method will retry to execute the command `adb root` when an
503    AdbError occurs, since sometimes the error `adb: unable to connect
504    for root: closed` is raised when executing `adb root` immediately after
505    the device is booted to OS.
506
507    Returns:
508      A string that is the stdout of root command.
509
510    Raises:
511      AdbError: If the command exit code is not 0.
512    """
513    for attempt in range(ADB_ROOT_RETRY_ATTMEPTS):
514      try:
515        return self._exec_adb_cmd('root',
516                                  args=None,
517                                  shell=False,
518                                  timeout=None,
519                                  stderr=None)
520      except AdbError as e:
521        if attempt + 1 < ADB_ROOT_RETRY_ATTMEPTS:
522          logging.debug('Retry the command "%s" since Error "%s" occurred.' %
523                        (utils.cli_cmd_to_string(
524                            e.cmd), e.stderr.decode('utf-8').strip()))
525          # Buffer between "adb root" commands.
526          time.sleep(ADB_ROOT_RETRY_ATTEMPT_INTERVAL_SEC)
527        else:
528          raise e
529
530  def __getattr__(self, name):
531
532    def adb_call(args=None, shell=False, timeout=None, stderr=None) -> bytes:
533      """Wrapper for an ADB command.
534
535      Args:
536        args: string or list of strings, arguments to the adb command.
537          See subprocess.Proc() documentation.
538        shell: bool, True to run this command through the system shell,
539          False to invoke it directly. See subprocess.Proc() docs.
540        timeout: float, the number of seconds to wait before timing out.
541          If not specified, no timeout takes effect.
542        stderr: a Byte stream, like io.BytesIO, stderr of the command
543          will be written to this object if provided.
544
545      Returns:
546        The output of the adb command run if exit code is 0.
547      """
548      return self._exec_adb_cmd(name,
549                                args,
550                                shell=shell,
551                                timeout=timeout,
552                                stderr=stderr)
553
554    return adb_call
555