1# Copyright (c) 2012 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Provides an interface to communicate with the device via the adb command.
6
7Assumes adb binary is currently on system path.
8"""
9
10import collections
11import datetime
12import logging
13import os
14import re
15import shlex
16import subprocess
17import sys
18import tempfile
19import time
20
21import io_stats_parser
22from pylib import pexpect
23
24CHROME_SRC = os.path.join(
25    os.path.abspath(os.path.dirname(__file__)), '..', '..', '..')
26
27sys.path.append(os.path.join(CHROME_SRC, 'third_party', 'android_testrunner'))
28import adb_interface
29
30import cmd_helper
31import errors  #  is under ../../../third_party/android_testrunner/errors.py
32
33
34# Pattern to search for the next whole line of pexpect output and capture it
35# into a match group. We can't use ^ and $ for line start end with pexpect,
36# see http://www.noah.org/python/pexpect/#doc for explanation why.
37PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r')
38
39# Set the adb shell prompt to be a unique marker that will [hopefully] not
40# appear at the start of any line of a command's output.
41SHELL_PROMPT = '~+~PQ\x17RS~+~'
42
43# Java properties file
44LOCAL_PROPERTIES_PATH = '/data/local.prop'
45
46# Property in /data/local.prop that controls Java assertions.
47JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions'
48
49MEMORY_INFO_RE = re.compile('^(?P<key>\w+):\s+(?P<usage_kb>\d+) kB$')
50NVIDIA_MEMORY_INFO_RE = re.compile('^\s*(?P<user>\S+)\s*(?P<name>\S+)\s*'
51                                   '(?P<pid>\d+)\s*(?P<usage_bytes>\d+)$')
52
53# Keycode "enum" suitable for passing to AndroidCommands.SendKey().
54KEYCODE_HOME = 3
55KEYCODE_BACK = 4
56KEYCODE_DPAD_UP = 19
57KEYCODE_DPAD_DOWN = 20
58KEYCODE_DPAD_RIGHT = 22
59KEYCODE_ENTER = 66
60KEYCODE_MENU = 82
61
62MD5SUM_DEVICE_PATH = '/data/local/tmp/md5sum_bin'
63
64def GetEmulators():
65  """Returns a list of emulators.  Does not filter by status (e.g. offline).
66
67  Both devices starting with 'emulator' will be returned in below output:
68
69    * daemon not running. starting it now on port 5037 *
70    * daemon started successfully *
71    List of devices attached
72    027c10494100b4d7        device
73    emulator-5554   offline
74    emulator-5558   device
75  """
76  re_device = re.compile('^emulator-[0-9]+', re.MULTILINE)
77  devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices']))
78  return devices
79
80
81def GetAVDs():
82  """Returns a list of AVDs."""
83  re_avd = re.compile('^[ ]+Name: ([a-zA-Z0-9_:.-]+)', re.MULTILINE)
84  avds = re_avd.findall(cmd_helper.GetCmdOutput(['android', 'list', 'avd']))
85  return avds
86
87
88def GetAttachedDevices():
89  """Returns a list of attached, online android devices.
90
91  If a preferred device has been set with ANDROID_SERIAL, it will be first in
92  the returned list.
93
94  Example output:
95
96    * daemon not running. starting it now on port 5037 *
97    * daemon started successfully *
98    List of devices attached
99    027c10494100b4d7        device
100    emulator-5554   offline
101  """
102  re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE)
103  devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices']))
104  preferred_device = os.environ.get('ANDROID_SERIAL')
105  if preferred_device in devices:
106    devices.remove(preferred_device)
107    devices.insert(0, preferred_device)
108  return devices
109
110def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None):
111  """Gets a list of files from `ls` command output.
112
113  Python's os.walk isn't used because it doesn't work over adb shell.
114
115  Args:
116    path: The path to list.
117    ls_output: A list of lines returned by an `ls -lR` command.
118    re_file: A compiled regular expression which parses a line into named groups
119        consisting of at minimum "filename", "date", "time", "size" and
120        optionally "timezone".
121    utc_offset: A 5-character string of the form +HHMM or -HHMM, where HH is a
122        2-digit string giving the number of UTC offset hours, and MM is a
123        2-digit string giving the number of UTC offset minutes. If the input
124        utc_offset is None, will try to look for the value of "timezone" if it
125        is specified in re_file.
126
127  Returns:
128    A dict of {"name": (size, lastmod), ...} where:
129      name: The file name relative to |path|'s directory.
130      size: The file size in bytes (0 for directories).
131      lastmod: The file last modification date in UTC.
132  """
133  re_directory = re.compile('^%s/(?P<dir>[^:]+):$' % re.escape(path))
134  path_dir = os.path.dirname(path)
135
136  current_dir = ''
137  files = {}
138  for line in ls_output:
139    directory_match = re_directory.match(line)
140    if directory_match:
141      current_dir = directory_match.group('dir')
142      continue
143    file_match = re_file.match(line)
144    if file_match:
145      filename = os.path.join(current_dir, file_match.group('filename'))
146      if filename.startswith(path_dir):
147        filename = filename[len(path_dir)+1:]
148      lastmod = datetime.datetime.strptime(
149          file_match.group('date') + ' ' + file_match.group('time')[:5],
150          '%Y-%m-%d %H:%M')
151      if not utc_offset and 'timezone' in re_file.groupindex:
152        utc_offset = file_match.group('timezone')
153      if isinstance(utc_offset, str) and len(utc_offset) == 5:
154        utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]),
155                                       minutes=int(utc_offset[3:5]))
156        if utc_offset[0:1] == '-':
157          utc_delta = -utc_delta
158        lastmod -= utc_delta
159      files[filename] = (int(file_match.group('size')), lastmod)
160  return files
161
162def _ComputeFileListHash(md5sum_output):
163  """Returns a list of MD5 strings from the provided md5sum output."""
164  return [line.split('  ')[0] for line in md5sum_output]
165
166def _HasAdbPushSucceeded(command_output):
167  """Returns whether adb push has succeeded from the provided output."""
168  if not command_output:
169    return False
170  # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)"
171  # Errors look like this: "failed to copy  ... "
172  if not re.search('^[0-9]', command_output.splitlines()[-1]):
173    logging.critical('PUSH FAILED: ' + command_output)
174    return False
175  return True
176
177def GetLogTimestamp(log_line, year):
178  """Returns the timestamp of the given |log_line| in the given year."""
179  try:
180    return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]),
181                                      '%Y-%m-%d %H:%M:%S.%f')
182  except (ValueError, IndexError):
183    logging.critical('Error reading timestamp from ' + log_line)
184    return None
185
186
187class AndroidCommands(object):
188  """Helper class for communicating with Android device via adb.
189
190  Args:
191    device: If given, adb commands are only send to the device of this ID.
192        Otherwise commands are sent to all attached devices.
193  """
194
195  def __init__(self, device=None):
196    self._adb = adb_interface.AdbInterface()
197    if device:
198      self._adb.SetTargetSerial(device)
199    self._logcat = None
200    self.logcat_process = None
201    self._pushed_files = []
202    self._device_utc_offset = self.RunShellCommand('date +%z')[0]
203    self._md5sum_path = ''
204    self._external_storage = ''
205
206  def Adb(self):
207    """Returns our AdbInterface to avoid us wrapping all its methods."""
208    return self._adb
209
210  def IsRootEnabled(self):
211    """Checks if root is enabled on the device."""
212    root_test_output = self.RunShellCommand('ls /root') or ['']
213    return not 'Permission denied' in root_test_output[0]
214
215  def EnableAdbRoot(self):
216    """Enables adb root on the device.
217
218    Returns:
219      True: if output from executing adb root was as expected.
220      False: otherwise.
221    """
222    return_value = self._adb.EnableAdbRoot()
223    # EnableAdbRoot inserts a call for wait-for-device only when adb logcat
224    # output matches what is expected. Just to be safe add a call to
225    # wait-for-device.
226    self._adb.SendCommand('wait-for-device')
227    return return_value
228
229  def GetDeviceYear(self):
230    """Returns the year information of the date on device."""
231    return self.RunShellCommand('date +%Y')[0]
232
233  def GetExternalStorage(self):
234    if not self._external_storage:
235      self._external_storage = self.RunShellCommand('echo $EXTERNAL_STORAGE')[0]
236      assert self._external_storage, 'Unable to find $EXTERNAL_STORAGE'
237    return self._external_storage
238
239  def WaitForDevicePm(self):
240    """Blocks until the device's package manager is available.
241
242    To workaround http://b/5201039, we restart the shell and retry if the
243    package manager isn't back after 120 seconds.
244
245    Raises:
246      errors.WaitForResponseTimedOutError after max retries reached.
247    """
248    last_err = None
249    retries = 3
250    while retries:
251      try:
252        self._adb.WaitForDevicePm()
253        return  # Success
254      except errors.WaitForResponseTimedOutError as e:
255        last_err = e
256        logging.warning('Restarting and retrying after timeout: %s', e)
257        retries -= 1
258        self.RestartShell()
259    raise last_err  # Only reached after max retries, re-raise the last error.
260
261  def RestartShell(self):
262    """Restarts the shell on the device. Does not block for it to return."""
263    self.RunShellCommand('stop')
264    self.RunShellCommand('start')
265
266  def Reboot(self, full_reboot=True):
267    """Reboots the device and waits for the package manager to return.
268
269    Args:
270      full_reboot: Whether to fully reboot the device or just restart the shell.
271    """
272    # TODO(torne): hive can't reboot the device either way without breaking the
273    # connection; work out if we can handle this better
274    if os.environ.get('USING_HIVE'):
275      logging.warning('Ignoring reboot request as we are on hive')
276      return
277    if full_reboot or not self.IsRootEnabled():
278      self._adb.SendCommand('reboot')
279      timeout = 300
280    else:
281      self.RestartShell()
282      timeout = 120
283    # To run tests we need at least the package manager and the sd card (or
284    # other external storage) to be ready.
285    self.WaitForDevicePm()
286    self.WaitForSdCardReady(timeout)
287
288  def Uninstall(self, package):
289    """Uninstalls the specified package from the device.
290
291    Args:
292      package: Name of the package to remove.
293
294    Returns:
295      A status string returned by adb uninstall
296    """
297    uninstall_command = 'uninstall %s' % package
298
299    logging.info('>>> $' + uninstall_command)
300    return self._adb.SendCommand(uninstall_command, timeout_time=60)
301
302  def Install(self, package_file_path, reinstall=False):
303    """Installs the specified package to the device.
304
305    Args:
306      package_file_path: Path to .apk file to install.
307      reinstall: Reinstall an existing apk, keeping the data.
308
309    Returns:
310      A status string returned by adb install
311    """
312    assert os.path.isfile(package_file_path), ('<%s> is not file' %
313                                               package_file_path)
314
315    install_cmd = ['install']
316
317    if reinstall:
318      install_cmd.append('-r')
319
320    install_cmd.append(package_file_path)
321    install_cmd = ' '.join(install_cmd)
322
323    logging.info('>>> $' + install_cmd)
324    return self._adb.SendCommand(install_cmd, timeout_time=2*60, retry_count=0)
325
326  def ManagedInstall(self, apk_path, keep_data=False, package_name=None,
327                     reboots_on_failure=2):
328    """Installs specified package and reboots device on timeouts.
329
330    Args:
331      apk_path: Path to .apk file to install.
332      keep_data: Reinstalls instead of uninstalling first, preserving the
333        application data.
334      package_name: Package name (only needed if keep_data=False).
335      reboots_on_failure: number of time to reboot if package manager is frozen.
336
337    Returns:
338      A status string returned by adb install
339    """
340    reboots_left = reboots_on_failure
341    while True:
342      try:
343        if not keep_data:
344          assert package_name
345          self.Uninstall(package_name)
346        install_status = self.Install(apk_path, reinstall=keep_data)
347        if 'Success' in install_status:
348          return install_status
349      except errors.WaitForResponseTimedOutError:
350        print '@@@STEP_WARNINGS@@@'
351        logging.info('Timeout on installing %s' % apk_path)
352
353      if reboots_left <= 0:
354        raise Exception('Install failure')
355
356      # Force a hard reboot on last attempt
357      self.Reboot(full_reboot=(reboots_left == 1))
358      reboots_left -= 1
359
360  def MakeSystemFolderWritable(self):
361    """Remounts the /system folder rw."""
362    out = self._adb.SendCommand('remount')
363    if out.strip() != 'remount succeeded':
364      raise errors.MsgException('Remount failed: %s' % out)
365
366  def RestartAdbServer(self):
367    """Restart the adb server."""
368    self.KillAdbServer()
369    self.StartAdbServer()
370
371  def KillAdbServer(self):
372    """Kill adb server."""
373    adb_cmd = ['adb', 'kill-server']
374    return cmd_helper.RunCmd(adb_cmd)
375
376  def StartAdbServer(self):
377    """Start adb server."""
378    adb_cmd = ['adb', 'start-server']
379    return cmd_helper.RunCmd(adb_cmd)
380
381  def WaitForSystemBootCompleted(self, wait_time):
382    """Waits for targeted system's boot_completed flag to be set.
383
384    Args:
385      wait_time: time in seconds to wait
386
387    Raises:
388      WaitForResponseTimedOutError if wait_time elapses and flag still not
389      set.
390    """
391    logging.info('Waiting for system boot completed...')
392    self._adb.SendCommand('wait-for-device')
393    # Now the device is there, but system not boot completed.
394    # Query the sys.boot_completed flag with a basic command
395    boot_completed = False
396    attempts = 0
397    wait_period = 5
398    while not boot_completed and (attempts * wait_period) < wait_time:
399      output = self._adb.SendShellCommand('getprop sys.boot_completed',
400                                          retry_count=1)
401      output = output.strip()
402      if output == '1':
403        boot_completed = True
404      else:
405        # If 'error: xxx' returned when querying the flag, it means
406        # adb server lost the connection to the emulator, so restart the adb
407        # server.
408        if 'error:' in output:
409          self.RestartAdbServer()
410        time.sleep(wait_period)
411        attempts += 1
412    if not boot_completed:
413      raise errors.WaitForResponseTimedOutError(
414          'sys.boot_completed flag was not set after %s seconds' % wait_time)
415
416  def WaitForSdCardReady(self, timeout_time):
417    """Wait for the SD card ready before pushing data into it."""
418    logging.info('Waiting for SD card ready...')
419    sdcard_ready = False
420    attempts = 0
421    wait_period = 5
422    external_storage = self.GetExternalStorage()
423    while not sdcard_ready and attempts * wait_period < timeout_time:
424      output = self.RunShellCommand('ls ' + external_storage)
425      if output:
426        sdcard_ready = True
427      else:
428        time.sleep(wait_period)
429        attempts += 1
430    if not sdcard_ready:
431      raise errors.WaitForResponseTimedOutError(
432          'SD card not ready after %s seconds' % timeout_time)
433
434  # It is tempting to turn this function into a generator, however this is not
435  # possible without using a private (local) adb_shell instance (to ensure no
436  # other command interleaves usage of it), which would defeat the main aim of
437  # being able to reuse the adb shell instance across commands.
438  def RunShellCommand(self, command, timeout_time=20, log_result=False):
439    """Send a command to the adb shell and return the result.
440
441    Args:
442      command: String containing the shell command to send. Must not include
443               the single quotes as we use them to escape the whole command.
444      timeout_time: Number of seconds to wait for command to respond before
445        retrying, used by AdbInterface.SendShellCommand.
446      log_result: Boolean to indicate whether we should log the result of the
447                  shell command.
448
449    Returns:
450      list containing the lines of output received from running the command
451    """
452    logging.info('>>> $' + command)
453    if "'" in command: logging.warning(command + " contains ' quotes")
454    result = self._adb.SendShellCommand(
455        "'%s'" % command, timeout_time).splitlines()
456    if ['error: device not found'] == result:
457      raise errors.DeviceUnresponsiveError('device not found')
458    if log_result:
459      logging.info('\n>>> '.join(result))
460    return result
461
462  def KillAll(self, process):
463    """Android version of killall, connected via adb.
464
465    Args:
466      process: name of the process to kill off
467
468    Returns:
469      the number of processes killed
470    """
471    pids = self.ExtractPid(process)
472    if pids:
473      self.RunShellCommand('kill ' + ' '.join(pids))
474    return len(pids)
475
476  def KillAllBlocking(self, process, timeout_sec):
477    """Blocking version of killall, connected via adb.
478
479    This waits until no process matching the corresponding name appears in ps'
480    output anymore.
481
482    Args:
483      process: name of the process to kill off
484      timeout_sec: the timeout in seconds
485
486    Returns:
487      the number of processes killed
488    """
489    processes_killed = self.KillAll(process)
490    if processes_killed:
491      elapsed = 0
492      wait_period = 0.1
493      # Note that this doesn't take into account the time spent in ExtractPid().
494      while self.ExtractPid(process) and elapsed < timeout_sec:
495        time.sleep(wait_period)
496        elapsed += wait_period
497      if elapsed >= timeout_sec:
498        return 0
499    return processes_killed
500
501  def StartActivity(self, package, activity, wait_for_completion=False,
502                    action='android.intent.action.VIEW',
503                    category=None, data=None,
504                    extras=None, trace_file_name=None):
505    """Starts |package|'s activity on the device.
506
507    Args:
508      package: Name of package to start (e.g. 'com.google.android.apps.chrome').
509      activity: Name of activity (e.g. '.Main' or
510        'com.google.android.apps.chrome.Main').
511      wait_for_completion: wait for the activity to finish launching (-W flag).
512      action: string (e.g. "android.intent.action.MAIN"). Default is VIEW.
513      category: string (e.g. "android.intent.category.HOME")
514      data: Data string to pass to activity (e.g. 'http://www.example.com/').
515      extras: Dict of extras to pass to activity. Values are significant.
516      trace_file_name: If used, turns on and saves the trace to this file name.
517    """
518    cmd = 'am start -a %s' % action
519    if wait_for_completion:
520      cmd += ' -W'
521    if category:
522      cmd += ' -c %s' % category
523    if package and activity:
524      cmd += ' -n %s/%s' % (package, activity)
525    if data:
526      cmd += ' -d "%s"' % data
527    if extras:
528      for key in extras:
529        value = extras[key]
530        if isinstance(value, str):
531          cmd += ' --es'
532        elif isinstance(value, bool):
533          cmd += ' --ez'
534        elif isinstance(value, int):
535          cmd += ' --ei'
536        else:
537          raise NotImplementedError(
538              'Need to teach StartActivity how to pass %s extras' % type(value))
539        cmd += ' %s %s' % (key, value)
540    if trace_file_name:
541      cmd += ' --start-profiler ' + trace_file_name
542    self.RunShellCommand(cmd)
543
544  def GoHome(self):
545    """Tell the device to return to the home screen. Blocks until completion."""
546    self.RunShellCommand('am start -W '
547        '-a android.intent.action.MAIN -c android.intent.category.HOME')
548
549  def CloseApplication(self, package):
550    """Attempt to close down the application, using increasing violence.
551
552    Args:
553      package: Name of the process to kill off, e.g.
554      com.google.android.apps.chrome
555    """
556    self.RunShellCommand('am force-stop ' + package)
557
558  def ClearApplicationState(self, package):
559    """Closes and clears all state for the given |package|."""
560    self.CloseApplication(package)
561    self.RunShellCommand('rm -r /data/data/%s/app_*' % package)
562    self.RunShellCommand('rm -r /data/data/%s/cache/*' % package)
563    self.RunShellCommand('rm -r /data/data/%s/files/*' % package)
564    self.RunShellCommand('rm -r /data/data/%s/shared_prefs/*' % package)
565
566  def SendKeyEvent(self, keycode):
567    """Sends keycode to the device.
568
569    Args:
570      keycode: Numeric keycode to send (see "enum" at top of file).
571    """
572    self.RunShellCommand('input keyevent %d' % keycode)
573
574  def PushIfNeeded(self, local_path, device_path):
575    """Pushes |local_path| to |device_path|.
576
577    Works for files and directories. This method skips copying any paths in
578    |test_data_paths| that already exist on the device with the same hash.
579
580    All pushed files can be removed by calling RemovePushedFiles().
581    """
582    assert os.path.exists(local_path), 'Local path not found %s' % local_path
583
584    if not self._md5sum_path:
585      default_build_type = os.environ.get('BUILD_TYPE', 'Debug')
586      md5sum_path = '%s/out/%s/md5sum_bin' % (CHROME_SRC, default_build_type)
587      if not os.path.exists(md5sum_path):
588        md5sum_path = '%s/out/Release/md5sum_bin' % (CHROME_SRC)
589        if not os.path.exists(md5sum_path):
590          print >> sys.stderr, 'Please build md5sum.'
591          sys.exit(1)
592      command = 'push %s %s' % (md5sum_path, MD5SUM_DEVICE_PATH)
593      assert _HasAdbPushSucceeded(self._adb.SendCommand(command))
594      self._md5sum_path = md5sum_path
595
596    self._pushed_files.append(device_path)
597    hashes_on_device = _ComputeFileListHash(
598        self.RunShellCommand(MD5SUM_DEVICE_PATH + ' ' + device_path))
599    assert os.path.exists(local_path), 'Local path not found %s' % local_path
600    hashes_on_host = _ComputeFileListHash(
601        subprocess.Popen(
602            '%s_host %s' % (self._md5sum_path, local_path),
603            stdout=subprocess.PIPE, shell=True).stdout)
604    if hashes_on_device == hashes_on_host:
605      return
606
607    # They don't match, so remove everything first and then create it.
608    if os.path.isdir(local_path):
609      self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60)
610      self.RunShellCommand('mkdir -p %s' % device_path)
611
612    # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of
613    # 60 seconds which isn't sufficient for a lot of users of this method.
614    push_command = 'push %s %s' % (local_path, device_path)
615    logging.info('>>> $' + push_command)
616    output = self._adb.SendCommand(push_command, timeout_time=30*60)
617    assert _HasAdbPushSucceeded(output)
618
619
620  def GetFileContents(self, filename, log_result=False):
621    """Gets contents from the file specified by |filename|."""
622    return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' +
623                                filename + '"; fi', log_result=log_result)
624
625  def SetFileContents(self, filename, contents):
626    """Writes |contents| to the file specified by |filename|."""
627    with tempfile.NamedTemporaryFile() as f:
628      f.write(contents)
629      f.flush()
630      self._adb.Push(f.name, filename)
631
632  def RemovePushedFiles(self):
633    """Removes all files pushed with PushIfNeeded() from the device."""
634    for p in self._pushed_files:
635      self.RunShellCommand('rm -r %s' % p, timeout_time=2*60)
636
637  def ListPathContents(self, path):
638    """Lists files in all subdirectories of |path|.
639
640    Args:
641      path: The path to list.
642
643    Returns:
644      A dict of {"name": (size, lastmod), ...}.
645    """
646    # Example output:
647    # /foo/bar:
648    # -rw-r----- 1 user group   102 2011-05-12 12:29:54.131623387 +0100 baz.txt
649    re_file = re.compile('^-(?P<perms>[^\s]+)\s+'
650                         '(?P<user>[^\s]+)\s+'
651                         '(?P<group>[^\s]+)\s+'
652                         '(?P<size>[^\s]+)\s+'
653                         '(?P<date>[^\s]+)\s+'
654                         '(?P<time>[^\s]+)\s+'
655                         '(?P<filename>[^\s]+)$')
656    return _GetFilesFromRecursiveLsOutput(
657        path, self.RunShellCommand('ls -lR %s' % path), re_file,
658        self._device_utc_offset)
659
660
661  def SetJavaAssertsEnabled(self, enable):
662    """Sets or removes the device java assertions property.
663
664    Args:
665      enable: If True the property will be set.
666
667    Returns:
668      True if the file was modified (reboot is required for it to take effect).
669    """
670    # First ensure the desired property is persisted.
671    temp_props_file = tempfile.NamedTemporaryFile()
672    properties = ''
673    if self._adb.Pull(LOCAL_PROPERTIES_PATH, temp_props_file.name):
674      properties = file(temp_props_file.name).read()
675    re_search = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) +
676                           r'\s*=\s*all\s*$', re.MULTILINE)
677    if enable != bool(re.search(re_search, properties)):
678      re_replace = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) +
679                              r'\s*=\s*\w+\s*$', re.MULTILINE)
680      properties = re.sub(re_replace, '', properties)
681      if enable:
682        properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY
683
684      file(temp_props_file.name, 'w').write(properties)
685      self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH)
686
687    # Next, check the current runtime value is what we need, and
688    # if not, set it and report that a reboot is required.
689    was_set = 'all' in self.RunShellCommand('getprop ' + JAVA_ASSERT_PROPERTY)
690    if was_set == enable:
691      return False
692
693    self.RunShellCommand('setprop %s "%s"' % (JAVA_ASSERT_PROPERTY,
694                                              enable and 'all' or ''))
695    return True
696
697  def GetBuildId(self):
698    """Returns the build ID of the system (e.g. JRM79C)."""
699    build_id = self.RunShellCommand('getprop ro.build.id')[0]
700    assert build_id
701    return build_id
702
703  def GetBuildType(self):
704    """Returns the build type of the system (e.g. eng)."""
705    build_type = self.RunShellCommand('getprop ro.build.type')[0]
706    assert build_type
707    return build_type
708
709  def StartMonitoringLogcat(self, clear=True, timeout=10, logfile=None,
710                            filters=None):
711    """Starts monitoring the output of logcat, for use with WaitForLogMatch.
712
713    Args:
714      clear: If True the existing logcat output will be cleared, to avoiding
715             matching historical output lurking in the log.
716      timeout: How long WaitForLogMatch will wait for the given match
717      filters: A list of logcat filters to be used.
718    """
719    if clear:
720      self.RunShellCommand('logcat -c')
721    args = []
722    if self._adb._target_arg:
723      args += shlex.split(self._adb._target_arg)
724    args += ['logcat', '-v', 'threadtime']
725    if filters:
726      args.extend(filters)
727    else:
728      args.append('*:v')
729
730    if logfile:
731      logfile = NewLineNormalizer(logfile)
732
733    # Spawn logcat and syncronize with it.
734    for _ in range(4):
735      self._logcat = pexpect.spawn('adb', args, timeout=timeout,
736                                   logfile=logfile)
737      self.RunShellCommand('log startup_sync')
738      if self._logcat.expect(['startup_sync', pexpect.EOF,
739                              pexpect.TIMEOUT]) == 0:
740        break
741      self._logcat.close(force=True)
742    else:
743      logging.critical('Error reading from logcat: ' + str(self._logcat.match))
744      sys.exit(1)
745
746  def GetMonitoredLogCat(self):
747    """Returns an "adb logcat" command as created by pexpected.spawn."""
748    if not self._logcat:
749      self.StartMonitoringLogcat(clear=False)
750    return self._logcat
751
752  def WaitForLogMatch(self, success_re, error_re, clear=False):
753    """Blocks until a matching line is logged or a timeout occurs.
754
755    Args:
756      success_re: A compiled re to search each line for.
757      error_re: A compiled re which, if found, terminates the search for
758          |success_re|. If None is given, no error condition will be detected.
759      clear: If True the existing logcat output will be cleared, defaults to
760          false.
761
762    Raises:
763      pexpect.TIMEOUT upon the timeout specified by StartMonitoringLogcat().
764
765    Returns:
766      The re match object if |success_re| is matched first or None if |error_re|
767      is matched first.
768    """
769    logging.info('<<< Waiting for logcat:' + str(success_re.pattern))
770    t0 = time.time()
771    while True:
772      if not self._logcat:
773        self.StartMonitoringLogcat(clear)
774      try:
775        while True:
776          # Note this will block for upto the timeout _per log line_, so we need
777          # to calculate the overall timeout remaining since t0.
778          time_remaining = t0 + self._logcat.timeout - time.time()
779          if time_remaining < 0: raise pexpect.TIMEOUT(self._logcat)
780          self._logcat.expect(PEXPECT_LINE_RE, timeout=time_remaining)
781          line = self._logcat.match.group(1)
782          if error_re:
783            error_match = error_re.search(line)
784            if error_match:
785              return None
786          success_match = success_re.search(line)
787          if success_match:
788            return success_match
789          logging.info('<<< Skipped Logcat Line:' + str(line))
790      except pexpect.TIMEOUT:
791        raise pexpect.TIMEOUT(
792            'Timeout (%ds) exceeded waiting for pattern "%s" (tip: use -vv '
793            'to debug)' %
794            (self._logcat.timeout, success_re.pattern))
795      except pexpect.EOF:
796        # It seems that sometimes logcat can end unexpectedly. This seems
797        # to happen during Chrome startup after a reboot followed by a cache
798        # clean. I don't understand why this happens, but this code deals with
799        # getting EOF in logcat.
800        logging.critical('Found EOF in adb logcat. Restarting...')
801        # Rerun spawn with original arguments. Note that self._logcat.args[0] is
802        # the path of adb, so we don't want it in the arguments.
803        self._logcat = pexpect.spawn('adb',
804                                     self._logcat.args[1:],
805                                     timeout=self._logcat.timeout,
806                                     logfile=self._logcat.logfile)
807
808  def StartRecordingLogcat(self, clear=True, filters=['*:v']):
809    """Starts recording logcat output to eventually be saved as a string.
810
811    This call should come before some series of tests are run, with either
812    StopRecordingLogcat or SearchLogcatRecord following the tests.
813
814    Args:
815      clear: True if existing log output should be cleared.
816      filters: A list of logcat filters to be used.
817    """
818    if clear:
819      self._adb.SendCommand('logcat -c')
820    logcat_command = 'adb %s logcat -v threadtime %s' % (self._adb._target_arg,
821                                                         ' '.join(filters))
822    self.logcat_process = subprocess.Popen(logcat_command, shell=True,
823                                           stdout=subprocess.PIPE)
824
825  def StopRecordingLogcat(self):
826    """Stops an existing logcat recording subprocess and returns output.
827
828    Returns:
829      The logcat output as a string or an empty string if logcat was not
830      being recorded at the time.
831    """
832    if not self.logcat_process:
833      return ''
834    # Cannot evaluate directly as 0 is a possible value.
835    # Better to read the self.logcat_process.stdout before killing it,
836    # Otherwise the communicate may return incomplete output due to pipe break.
837    if self.logcat_process.poll() is None:
838      self.logcat_process.kill()
839    (output, _) = self.logcat_process.communicate()
840    self.logcat_process = None
841    return output
842
843  def SearchLogcatRecord(self, record, message, thread_id=None, proc_id=None,
844                         log_level=None, component=None):
845    """Searches the specified logcat output and returns results.
846
847    This method searches through the logcat output specified by record for a
848    certain message, narrowing results by matching them against any other
849    specified criteria.  It returns all matching lines as described below.
850
851    Args:
852      record: A string generated by Start/StopRecordingLogcat to search.
853      message: An output string to search for.
854      thread_id: The thread id that is the origin of the message.
855      proc_id: The process that is the origin of the message.
856      log_level: The log level of the message.
857      component: The name of the component that would create the message.
858
859    Returns:
860      A list of dictionaries represeting matching entries, each containing keys
861      thread_id, proc_id, log_level, component, and message.
862    """
863    if thread_id:
864      thread_id = str(thread_id)
865    if proc_id:
866      proc_id = str(proc_id)
867    results = []
868    reg = re.compile('(\d+)\s+(\d+)\s+([A-Z])\s+([A-Za-z]+)\s*:(.*)$',
869                     re.MULTILINE)
870    log_list = reg.findall(record)
871    for (tid, pid, log_lev, comp, msg) in log_list:
872      if ((not thread_id or thread_id == tid) and
873          (not proc_id or proc_id == pid) and
874          (not log_level or log_level == log_lev) and
875          (not component or component == comp) and msg.find(message) > -1):
876        match = dict({'thread_id': tid, 'proc_id': pid,
877                      'log_level': log_lev, 'component': comp,
878                      'message': msg})
879        results.append(match)
880    return results
881
882  def ExtractPid(self, process_name):
883    """Extracts Process Ids for a given process name from Android Shell.
884
885    Args:
886      process_name: name of the process on the device.
887
888    Returns:
889      List of all the process ids (as strings) that match the given name.
890      If the name of a process exactly matches the given name, the pid of
891      that process will be inserted to the front of the pid list.
892    """
893    pids = []
894    for line in self.RunShellCommand('ps', log_result=False):
895      data = line.split()
896      try:
897        if process_name in data[-1]:  # name is in the last column
898          if process_name == data[-1]:
899            pids.insert(0, data[1])  # PID is in the second column
900          else:
901            pids.append(data[1])
902      except IndexError:
903        pass
904    return pids
905
906  def GetIoStats(self):
907    """Gets cumulative disk IO stats since boot (for all processes).
908
909    Returns:
910      Dict of {num_reads, num_writes, read_ms, write_ms} or None if there
911      was an error.
912    """
913    for line in self.GetFileContents('/proc/diskstats', log_result=False):
914      stats = io_stats_parser.ParseIoStatsLine(line)
915      if stats.device == 'mmcblk0':
916        return {
917            'num_reads': stats.num_reads_issued,
918            'num_writes': stats.num_writes_completed,
919            'read_ms': stats.ms_spent_reading,
920            'write_ms': stats.ms_spent_writing,
921        }
922    logging.warning('Could not find disk IO stats.')
923    return None
924
925  def GetMemoryUsageForPid(self, pid):
926    """Returns the memory usage for given pid.
927
928    Args:
929      pid: The pid number of the specific process running on device.
930
931    Returns:
932      A tuple containg:
933      [0]: Dict of {metric:usage_kb}, for the process which has specified pid.
934      The metric keys which may be included are: Size, Rss, Pss, Shared_Clean,
935      Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap,
936      KernelPageSize, MMUPageSize, Nvidia (tablet only).
937      [1]: Detailed /proc/[PID]/smaps information.
938    """
939    usage_dict = collections.defaultdict(int)
940    smaps = collections.defaultdict(dict)
941    current_smap = ''
942    for line in self.GetFileContents('/proc/%s/smaps' % pid, log_result=False):
943      items = line.split()
944      # See man 5 proc for more details. The format is:
945      # address perms offset dev inode pathname
946      if len(items) > 5:
947        current_smap = ' '.join(items[5:])
948      elif len(items) > 3:
949        current_smap = ' '.join(items[3:])
950      match = re.match(MEMORY_INFO_RE, line)
951      if match:
952        key = match.group('key')
953        usage_kb = int(match.group('usage_kb'))
954        usage_dict[key] += usage_kb
955        if key not in smaps[current_smap]:
956          smaps[current_smap][key] = 0
957        smaps[current_smap][key] += usage_kb
958    if not usage_dict or not any(usage_dict.values()):
959      # Presumably the process died between ps and calling this method.
960      logging.warning('Could not find memory usage for pid ' + str(pid))
961
962    for line in self.GetFileContents('/d/nvmap/generic-0/clients',
963                                     log_result=False):
964      match = re.match(NVIDIA_MEMORY_INFO_RE, line)
965      if match and match.group('pid') == pid:
966        usage_bytes = int(match.group('usage_bytes'))
967        usage_dict['Nvidia'] = int(round(usage_bytes / 1000.0))  # kB
968        break
969
970    return (usage_dict, smaps)
971
972  def GetMemoryUsageForPackage(self, package):
973    """Returns the memory usage for all processes whose name contains |pacakge|.
974
975    Args:
976      package: A string holding process name to lookup pid list for.
977
978    Returns:
979      A tuple containg:
980      [0]: Dict of {metric:usage_kb}, summed over all pids associated with
981           |name|.
982      The metric keys which may be included are: Size, Rss, Pss, Shared_Clean,
983      Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap,
984      KernelPageSize, MMUPageSize, Nvidia (tablet only).
985      [1]: a list with detailed /proc/[PID]/smaps information.
986    """
987    usage_dict = collections.defaultdict(int)
988    pid_list = self.ExtractPid(package)
989    smaps = collections.defaultdict(dict)
990
991    for pid in pid_list:
992      usage_dict_per_pid, smaps_per_pid = self.GetMemoryUsageForPid(pid)
993      smaps[pid] = smaps_per_pid
994      for (key, value) in usage_dict_per_pid.items():
995        usage_dict[key] += value
996
997    return usage_dict, smaps
998
999  def ProcessesUsingDevicePort(self, device_port):
1000    """Lists processes using the specified device port on loopback interface.
1001
1002    Args:
1003      device_port: Port on device we want to check.
1004
1005    Returns:
1006      A list of (pid, process_name) tuples using the specified port.
1007    """
1008    tcp_results = self.RunShellCommand('cat /proc/net/tcp', log_result=False)
1009    tcp_address = '0100007F:%04X' % device_port
1010    pids = []
1011    for single_connect in tcp_results:
1012      connect_results = single_connect.split()
1013      # Column 1 is the TCP port, and Column 9 is the inode of the socket
1014      if connect_results[1] == tcp_address:
1015        socket_inode = connect_results[9]
1016        socket_name = 'socket:[%s]' % socket_inode
1017        lsof_results = self.RunShellCommand('lsof', log_result=False)
1018        for single_process in lsof_results:
1019          process_results = single_process.split()
1020          # Ignore the line if it has less than nine columns in it, which may
1021          # be the case when a process stops while lsof is executing.
1022          if len(process_results) <= 8:
1023            continue
1024          # Column 0 is the executable name
1025          # Column 1 is the pid
1026          # Column 8 is the Inode in use
1027          if process_results[8] == socket_name:
1028            pids.append((int(process_results[1]), process_results[0]))
1029        break
1030    logging.info('PidsUsingDevicePort: %s', pids)
1031    return pids
1032
1033  def FileExistsOnDevice(self, file_name):
1034    """Checks whether the given file exists on the device.
1035
1036    Args:
1037      file_name: Full path of file to check.
1038
1039    Returns:
1040      True if the file exists, False otherwise.
1041    """
1042    assert '"' not in file_name, 'file_name cannot contain double quotes'
1043    status = self._adb.SendShellCommand(
1044        '\'test -e "%s"; echo $?\'' % (file_name))
1045    if 'test: not found' not in status:
1046      return int(status) == 0
1047
1048    status = self._adb.SendShellCommand(
1049        '\'ls "%s" >/dev/null 2>&1; echo $?\'' % (file_name))
1050    return int(status) == 0
1051
1052
1053class NewLineNormalizer(object):
1054  """A file-like object to normalize EOLs to '\n'.
1055
1056  Pexpect runs adb within a pseudo-tty device (see
1057  http://www.noah.org/wiki/pexpect), so any '\n' printed by adb is written
1058  as '\r\n' to the logfile. Since adb already uses '\r\n' to terminate
1059  lines, the log ends up having '\r\r\n' at the end of each line. This
1060  filter replaces the above with a single '\n' in the data stream.
1061  """
1062  def __init__(self, output):
1063    self._output = output
1064
1065  def write(self, data):
1066    data = data.replace('\r\r\n', '\n')
1067    self._output.write(data)
1068
1069  def flush(self):
1070    self._output.flush()
1071
1072