Home | History | Annotate | Download | only in flavor
      1 # Copyright 2016 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 from recipe_engine import recipe_api
      6 
      7 import default_flavor
      8 import re
      9 import subprocess
     10 
     11 
     12 """GN Android flavor utils, used for building Skia for Android with GN."""
     13 class GNAndroidFlavorUtils(default_flavor.DefaultFlavorUtils):
     14   def __init__(self, m):
     15     super(GNAndroidFlavorUtils, self).__init__(m)
     16     self._ever_ran_adb = False
     17     self.ADB_BINARY = '/usr/bin/adb.1.0.35'
     18     self._golo_devices = ['Nexus5x']
     19     if self.m.vars.builder_cfg.get('model') in self._golo_devices:
     20       self.ADB_BINARY = '/opt/infra-android/tools/adb'
     21 
     22     self.device_dirs = default_flavor.DeviceDirs(
     23         dm_dir        = self.m.vars.android_data_dir + 'dm_out',
     24         perf_data_dir = self.m.vars.android_data_dir + 'perf',
     25         resource_dir  = self.m.vars.android_data_dir + 'resources',
     26         images_dir    = self.m.vars.android_data_dir + 'images',
     27         skp_dir       = self.m.vars.android_data_dir + 'skps',
     28         svg_dir       = self.m.vars.android_data_dir + 'svgs',
     29         tmp_dir       = self.m.vars.android_data_dir)
     30 
     31     # A list of devices we can't root.  If rooting fails and a device is not
     32     # on the list, we fail the task to avoid perf inconsistencies.
     33     self.rootable_blacklist = ['GalaxyS6', 'GalaxyS7_G930A', 'GalaxyS7_G930FD',
     34                                'MotoG4', 'NVIDIA_Shield']
     35 
     36     # Maps device type -> CPU ids that should be scaled for nanobench.
     37     # Many devices have two (or more) different CPUs (e.g. big.LITTLE
     38     # on Nexus5x). The CPUs listed are the biggest cpus on the device.
     39     # The CPUs are grouped together, so we only need to scale one of them
     40     # (the one listed) in order to scale them all.
     41     # E.g. Nexus5x has cpu0-3 as one chip and cpu4-5 as the other. Thus,
     42     # if one wants to run a single-threaded application (e.g. nanobench), one
     43     # can disable cpu0-3 and scale cpu 4 to have only cpu4 and 5 at the same
     44     # frequency.  See also disable_for_nanobench.
     45     self.cpus_to_scale = {
     46       'Nexus5x': [4],
     47       'NexusPlayer': [0, 2], # has 2 identical chips, so scale them both.
     48       'Pixel': [2],
     49       'Pixel2XL': [4]
     50     }
     51 
     52     # Maps device type -> CPU ids that should be turned off when running
     53     # single-threaded applications like nanobench. The devices listed have
     54     # multiple, differnt CPUs. We notice a lot of noise that seems to be
     55     # caused by nanobench running on the slow CPU, then the big CPU. By
     56     # disabling this, we see less of that noise by forcing the same CPU
     57     # to be used for the performance testing every time.
     58     self.disable_for_nanobench = {
     59       'Nexus5x': range(0, 4),
     60       'Pixel': range(0, 2),
     61       'Pixel2XL': range(0, 4),
     62       'PixelC': range(0, 2)
     63     }
     64 
     65     self.gpu_scaling = {
     66       "Nexus5":  450000000,
     67       "Nexus5x": 600000000,
     68     }
     69 
     70   def _run(self, title, *cmd, **kwargs):
     71     with self.m.context(cwd=self.m.vars.skia_dir):
     72       return self.m.run(self.m.step, title, cmd=list(cmd), **kwargs)
     73 
     74   def _py(self, title, script, infra_step=True):
     75     with self.m.context(cwd=self.m.vars.skia_dir):
     76       return self.m.run(self.m.python, title, script=script,
     77                         infra_step=infra_step)
     78 
     79   def _adb(self, title, *cmd, **kwargs):
     80     # The only non-infra adb steps (dm / nanobench) happen to not use _adb().
     81     if 'infra_step' not in kwargs:
     82       kwargs['infra_step'] = True
     83 
     84     self._ever_ran_adb = True
     85     attempts = 1
     86     flaky_devices = ['NexusPlayer', 'PixelC']
     87     if self.m.vars.builder_cfg.get('model') in flaky_devices:
     88       attempts = 3
     89 
     90     def wait_for_device(attempt):
     91       self.m.run(self.m.step,
     92                  'kill adb server after failure of \'%s\' (attempt %d)' % (
     93                      title, attempt),
     94                  cmd=[self.ADB_BINARY, 'kill-server'],
     95                  infra_step=True, timeout=30, abort_on_failure=False,
     96                  fail_build_on_failure=False)
     97       self.m.run(self.m.step,
     98                  'wait for device after failure of \'%s\' (attempt %d)' % (
     99                      title, attempt),
    100                  cmd=[self.ADB_BINARY, 'wait-for-device'], infra_step=True,
    101                  timeout=180, abort_on_failure=False,
    102                  fail_build_on_failure=False)
    103 
    104     with self.m.context(cwd=self.m.vars.skia_dir):
    105       return self.m.run.with_retry(self.m.step, title, attempts,
    106                                    cmd=[self.ADB_BINARY]+list(cmd),
    107                                    between_attempts_fn=wait_for_device,
    108                                    **kwargs)
    109 
    110   def _scale_for_dm(self):
    111     device = self.m.vars.builder_cfg.get('model')
    112     if (device in self.rootable_blacklist or
    113         self.m.vars.internal_hardware_label):
    114       return
    115 
    116     # This is paranoia... any CPUs we disabled while running nanobench
    117     # ought to be back online now that we've restarted the device.
    118     for i in self.disable_for_nanobench.get(device, []):
    119       self._set_cpu_online(i, 1) # enable
    120 
    121     scale_up = self.cpus_to_scale.get(device, [0])
    122     # For big.LITTLE devices, make sure we scale the LITTLE cores up;
    123     # there is a chance they are still in powersave mode from when
    124     # swarming slows things down for cooling down and charging.
    125     if 0 not in scale_up:
    126       scale_up.append(0)
    127     for i in scale_up:
    128       # AndroidOne doesn't support ondemand governor. hotplug is similar.
    129       if device == 'AndroidOne':
    130         self._set_governor(i, 'hotplug')
    131       else:
    132         self._set_governor(i, 'ondemand')
    133 
    134   def _scale_for_nanobench(self):
    135     device = self.m.vars.builder_cfg.get('model')
    136     if (device in self.rootable_blacklist or
    137       self.m.vars.internal_hardware_label):
    138       return
    139 
    140     for i in self.cpus_to_scale.get(device, [0]):
    141       self._set_governor(i, 'userspace')
    142       self._scale_cpu(i, 0.6)
    143 
    144     for i in self.disable_for_nanobench.get(device, []):
    145       self._set_cpu_online(i, 0) # disable
    146 
    147     if device in self.gpu_scaling:
    148       #https://developer.qualcomm.com/qfile/28823/lm80-p0436-11_adb_commands.pdf
    149       # Section 3.2.1 Commands to put the GPU in performance mode
    150       # Nexus 5 is  320000000 by default
    151       # Nexus 5x is 180000000 by default
    152       gpu_freq = self.gpu_scaling[device]
    153       self.m.run.with_retry(self.m.python.inline,
    154         "Lock GPU to %d (and other perf tweaks)" % gpu_freq,
    155         3, # attempts
    156         program="""
    157 import os
    158 import subprocess
    159 import sys
    160 import time
    161 ADB = sys.argv[1]
    162 freq = sys.argv[2]
    163 idle_timer = "10000"
    164 
    165 log = subprocess.check_output([ADB, 'root'])
    166 # check for message like 'adbd cannot run as root in production builds'
    167 print log
    168 if 'cannot' in log:
    169   raise Exception('adb root failed')
    170 
    171 subprocess.check_output([ADB, 'shell', 'stop', 'thermald'])
    172 
    173 subprocess.check_output([ADB, 'shell', 'echo "%s" > '
    174     '/sys/class/kgsl/kgsl-3d0/gpuclk' % freq])
    175 
    176 actual_freq = subprocess.check_output([ADB, 'shell', 'cat '
    177     '/sys/class/kgsl/kgsl-3d0/gpuclk']).strip()
    178 if actual_freq != freq:
    179   raise Exception('Frequency (actual, expected) (%s, %s)'
    180                   % (actual_freq, freq))
    181 
    182 subprocess.check_output([ADB, 'shell', 'echo "%s" > '
    183     '/sys/class/kgsl/kgsl-3d0/idle_timer' % idle_timer])
    184 
    185 actual_timer = subprocess.check_output([ADB, 'shell', 'cat '
    186     '/sys/class/kgsl/kgsl-3d0/idle_timer']).strip()
    187 if actual_timer != idle_timer:
    188   raise Exception('idle_timer (actual, expected) (%s, %s)'
    189                   % (actual_timer, idle_timer))
    190 
    191 for s in ['force_bus_on', 'force_rail_on', 'force_clk_on']:
    192   subprocess.check_output([ADB, 'shell', 'echo "1" > '
    193       '/sys/class/kgsl/kgsl-3d0/%s' % s])
    194   actual_set = subprocess.check_output([ADB, 'shell', 'cat '
    195       '/sys/class/kgsl/kgsl-3d0/%s' % s]).strip()
    196   if actual_set != "1":
    197     raise Exception('%s (actual, expected) (%s, 1)'
    198                     % (s, actual_set))
    199 """,
    200         args = [self.ADB_BINARY, gpu_freq],
    201         infra_step=True,
    202         timeout=30)
    203 
    204   def _set_governor(self, cpu, gov):
    205     self._ever_ran_adb = True
    206     self.m.run.with_retry(self.m.python.inline,
    207         "Set CPU %d's governor to %s" % (cpu, gov),
    208         3, # attempts
    209         program="""
    210 import os
    211 import subprocess
    212 import sys
    213 import time
    214 ADB = sys.argv[1]
    215 cpu = int(sys.argv[2])
    216 gov = sys.argv[3]
    217 
    218 log = subprocess.check_output([ADB, 'root'])
    219 # check for message like 'adbd cannot run as root in production builds'
    220 print log
    221 if 'cannot' in log:
    222   raise Exception('adb root failed')
    223 
    224 subprocess.check_output([ADB, 'shell', 'echo "%s" > '
    225     '/sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor' % (gov, cpu)])
    226 actual_gov = subprocess.check_output([ADB, 'shell', 'cat '
    227     '/sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor' % cpu]).strip()
    228 if actual_gov != gov:
    229   raise Exception('(actual, expected) (%s, %s)'
    230                   % (actual_gov, gov))
    231 """,
    232         args = [self.ADB_BINARY, cpu, gov],
    233         infra_step=True,
    234         timeout=30)
    235 
    236 
    237   def _set_cpu_online(self, cpu, value):
    238     """Set /sys/devices/system/cpu/cpu{N}/online to value (0 or 1)."""
    239     self._ever_ran_adb = True
    240     msg = 'Disabling'
    241     if value:
    242       msg = 'Enabling'
    243     self.m.run.with_retry(self.m.python.inline,
    244         '%s CPU %d' % (msg, cpu),
    245         3, # attempts
    246         program="""
    247 import os
    248 import subprocess
    249 import sys
    250 import time
    251 ADB = sys.argv[1]
    252 cpu = int(sys.argv[2])
    253 value = int(sys.argv[3])
    254 
    255 log = subprocess.check_output([ADB, 'root'])
    256 # check for message like 'adbd cannot run as root in production builds'
    257 print log
    258 if 'cannot' in log:
    259   raise Exception('adb root failed')
    260 
    261 # If we try to echo 1 to an already online cpu, adb returns exit code 1.
    262 # So, check the value before trying to write it.
    263 prior_status = subprocess.check_output([ADB, 'shell', 'cat '
    264     '/sys/devices/system/cpu/cpu%d/online' % cpu]).strip()
    265 if prior_status == str(value):
    266   print 'CPU %d online already %d' % (cpu, value)
    267   sys.exit()
    268 
    269 subprocess.check_output([ADB, 'shell', 'echo %s > '
    270     '/sys/devices/system/cpu/cpu%d/online' % (value, cpu)])
    271 actual_status = subprocess.check_output([ADB, 'shell', 'cat '
    272     '/sys/devices/system/cpu/cpu%d/online' % cpu]).strip()
    273 if actual_status != str(value):
    274   raise Exception('(actual, expected) (%s, %d)'
    275                   % (actual_status, value))
    276 """,
    277         args = [self.ADB_BINARY, cpu, value],
    278         infra_step=True,
    279         timeout=30)
    280 
    281 
    282   def _scale_cpu(self, cpu, target_percent):
    283     self._ever_ran_adb = True
    284     self.m.run.with_retry(self.m.python.inline,
    285         'Scale CPU %d to %f' % (cpu, target_percent),
    286         3, # attempts
    287         program="""
    288 import os
    289 import subprocess
    290 import sys
    291 import time
    292 ADB = sys.argv[1]
    293 target_percent = float(sys.argv[2])
    294 cpu = int(sys.argv[3])
    295 log = subprocess.check_output([ADB, 'root'])
    296 # check for message like 'adbd cannot run as root in production builds'
    297 print log
    298 if 'cannot' in log:
    299   raise Exception('adb root failed')
    300 
    301 root = '/sys/devices/system/cpu/cpu%d/cpufreq' %cpu
    302 
    303 # All devices we test on give a list of their available frequencies.
    304 available_freqs = subprocess.check_output([ADB, 'shell',
    305     'cat %s/scaling_available_frequencies' % root])
    306 
    307 # Check for message like '/system/bin/sh: file not found'
    308 if available_freqs and '/system/bin/sh' not in available_freqs:
    309   available_freqs = sorted(
    310       int(i) for i in available_freqs.strip().split())
    311 else:
    312   raise Exception('Could not get list of available frequencies: %s' %
    313                   available_freqs)
    314 
    315 maxfreq = available_freqs[-1]
    316 target = int(round(maxfreq * target_percent))
    317 freq = maxfreq
    318 for f in reversed(available_freqs):
    319   if f <= target:
    320     freq = f
    321     break
    322 
    323 print 'Setting frequency to %d' % freq
    324 
    325 # If scaling_max_freq is lower than our attempted setting, it won't take.
    326 # We must set min first, because if we try to set max to be less than min
    327 # (which sometimes happens after certain devices reboot) it returns a
    328 # perplexing permissions error.
    329 subprocess.check_output([ADB, 'shell', 'echo 0 > '
    330     '%s/scaling_min_freq' % root])
    331 subprocess.check_output([ADB, 'shell', 'echo %d > '
    332     '%s/scaling_max_freq' % (freq, root)])
    333 subprocess.check_output([ADB, 'shell', 'echo %d > '
    334     '%s/scaling_setspeed' % (freq, root)])
    335 time.sleep(5)
    336 actual_freq = subprocess.check_output([ADB, 'shell', 'cat '
    337     '%s/scaling_cur_freq' % root]).strip()
    338 if actual_freq != str(freq):
    339   raise Exception('(actual, expected) (%s, %d)'
    340                   % (actual_freq, freq))
    341 """,
    342         args = [self.ADB_BINARY, str(target_percent), cpu],
    343         infra_step=True,
    344         timeout=30)
    345 
    346   def compile(self, unused_target):
    347     compiler      = self.m.vars.builder_cfg.get('compiler')
    348     configuration = self.m.vars.builder_cfg.get('configuration')
    349     extra_tokens  = self.m.vars.extra_tokens
    350     os            = self.m.vars.builder_cfg.get('os')
    351     target_arch   = self.m.vars.builder_cfg.get('target_arch')
    352 
    353     assert compiler == 'Clang'  # At this rate we might not ever support GCC.
    354 
    355     extra_cflags = []
    356     if configuration == 'Debug':
    357       extra_cflags.append('-O1')
    358 
    359     ndk_asset = 'android_ndk_linux'
    360     if 'Mac' in os:
    361       ndk_asset = 'android_ndk_darwin'
    362     elif 'Win' in os:
    363       ndk_asset = 'n'
    364 
    365     quote = lambda x: '"%s"' % x
    366     args = {
    367         'ndk': quote(self.m.vars.slave_dir.join(ndk_asset)),
    368         'target_cpu': quote(target_arch),
    369     }
    370 
    371     if configuration != 'Debug':
    372       args['is_debug'] = 'false'
    373     if 'Vulkan' in extra_tokens:
    374       args['ndk_api'] = 24
    375       args['skia_enable_vulkan_debug_layers'] = 'false'
    376     if 'ASAN' in extra_tokens:
    377       args['sanitize'] = '"ASAN"'
    378       if target_arch == 'arm' and 'ndk_api' not in args:
    379         args['ndk_api'] = 21
    380 
    381     # If an Android API level is specified, use that.
    382     for t in extra_tokens:
    383       m = re.search(r'API(\d+)', t)
    384       if m and len(m.groups()) == 1:
    385         args['ndk_api'] = m.groups()[0]
    386         break
    387 
    388     if extra_cflags:
    389       args['extra_cflags'] = repr(extra_cflags).replace("'", '"')
    390 
    391     gn_args = ' '.join('%s=%s' % (k,v) for (k,v) in sorted(args.iteritems()))
    392     gn      = 'gn.exe'    if 'Win' in os else 'gn'
    393     ninja   = 'ninja.exe' if 'Win' in os else 'ninja'
    394     gn      = self.m.vars.skia_dir.join('bin', gn)
    395 
    396     self._py('fetch-gn', self.m.vars.skia_dir.join('bin', 'fetch-gn'))
    397     self._run('gn gen', gn, 'gen', self.out_dir, '--args=' + gn_args)
    398     self._run('ninja', ninja, '-k', '0', '-C', self.out_dir)
    399 
    400   def install(self):
    401     self._adb('mkdir ' + self.device_dirs.resource_dir,
    402               'shell', 'mkdir', '-p', self.device_dirs.resource_dir)
    403     if 'ASAN' in self.m.vars.extra_tokens:
    404       asan_setup = self.m.vars.slave_dir.join(
    405             'android_ndk_linux', 'toolchains', 'llvm', 'prebuilt',
    406             'linux-x86_64', 'bin', 'asan_device_setup')
    407       self.m.run(self.m.python.inline, 'Setting up device to run ASAN',
    408         program="""
    409 import os
    410 import subprocess
    411 import sys
    412 import time
    413 ADB = sys.argv[1]
    414 ASAN_SETUP = sys.argv[2]
    415 
    416 def wait_for_device():
    417   while True:
    418     time.sleep(5)
    419     print 'Waiting for device'
    420     subprocess.check_output([ADB, 'wait-for-device'])
    421     bit1 = subprocess.check_output([ADB, 'shell', 'getprop',
    422                                    'dev.bootcomplete'])
    423     bit2 = subprocess.check_output([ADB, 'shell', 'getprop',
    424                                    'sys.boot_completed'])
    425     if '1' in bit1 and '1' in bit2:
    426       print 'Device detected'
    427       break
    428 
    429 log = subprocess.check_output([ADB, 'root'])
    430 # check for message like 'adbd cannot run as root in production builds'
    431 print log
    432 if 'cannot' in log:
    433   raise Exception('adb root failed')
    434 
    435 output = subprocess.check_output([ADB, 'disable-verity'])
    436 print output
    437 
    438 if 'already disabled' not in output:
    439   print 'Rebooting device'
    440   subprocess.check_output([ADB, 'reboot'])
    441   wait_for_device()
    442 
    443 # ASAN setup script is idempotent, either it installs it or says it's installed
    444 output = subprocess.check_output([ADB, 'wait-for-device'])
    445 process = subprocess.Popen([ASAN_SETUP], env={'ADB': ADB},
    446                            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    447 
    448 # this also blocks until command finishes
    449 (stdout, stderr) = process.communicate()
    450 print stdout
    451 print 'Stderr: %s' % stderr
    452 if process.returncode:
    453   raise Exception('setup ASAN returned with non-zero exit code: %d' %
    454                   process.returncode)
    455 
    456 if 'Please wait until the device restarts' in stdout:
    457   # Sleep because device does not reboot instantly
    458   time.sleep(30)
    459 wait_for_device()
    460 """,
    461         args = [self.ADB_BINARY, asan_setup],
    462           infra_step=True,
    463           timeout=300,
    464           abort_on_failure=True)
    465 
    466   def cleanup_steps(self):
    467     if self._ever_ran_adb:
    468       self.m.run(self.m.python.inline, 'dump log', program="""
    469           import os
    470           import subprocess
    471           import sys
    472           out = sys.argv[1]
    473           log = subprocess.check_output(['%s', 'logcat', '-d'])
    474           for line in log.split('\\n'):
    475             tokens = line.split()
    476             if len(tokens) == 11 and tokens[-7] == 'F' and tokens[-3] == 'pc':
    477               addr, path = tokens[-2:]
    478               local = os.path.join(out, os.path.basename(path))
    479               if os.path.exists(local):
    480                 sym = subprocess.check_output(['addr2line', '-Cfpe', local, addr])
    481                 line = line.replace(addr, addr + ' ' + sym.strip())
    482             print line
    483           """ % self.ADB_BINARY,
    484           args=[self.m.vars.skia_out.join(self.m.vars.configuration)],
    485           infra_step=True,
    486           timeout=300,
    487           abort_on_failure=False)
    488 
    489     # Only quarantine the bot if the first failed step
    490     # is an infra step. If, instead, we did this for any infra failures, we
    491     # would do this too much. For example, if a Nexus 10 died during dm
    492     # and the following pull step would also fail "device not found" - causing
    493     # us to run the shutdown command when the device was probably not in a
    494     # broken state; it was just rebooting.
    495     if (self.m.run.failed_steps and
    496         isinstance(self.m.run.failed_steps[0], recipe_api.InfraFailure)):
    497       bot_id = self.m.vars.swarming_bot_id
    498       self.m.file.write_text('Quarantining Bot',
    499                              '/home/chrome-bot/%s.force_quarantine' % bot_id,
    500                              ' ')
    501 
    502     if self._ever_ran_adb:
    503       self._adb('kill adb server', 'kill-server')
    504 
    505   def step(self, name, cmd, **kwargs):
    506     if (cmd[0] == 'nanobench'):
    507       self._scale_for_nanobench()
    508     else:
    509       self._scale_for_dm()
    510     app = self.m.vars.skia_out.join(self.m.vars.configuration, cmd[0])
    511     self._adb('push %s' % cmd[0],
    512               'push', app, self.m.vars.android_bin_dir)
    513 
    514     sh = '%s.sh' % cmd[0]
    515     self.m.run.writefile(self.m.vars.tmp_dir.join(sh),
    516         'set -x; %s%s; echo $? >%src' %
    517         (self.m.vars.android_bin_dir, subprocess.list2cmdline(map(str, cmd)),
    518             self.m.vars.android_bin_dir))
    519     self._adb('push %s' % sh,
    520               'push', self.m.vars.tmp_dir.join(sh), self.m.vars.android_bin_dir)
    521 
    522     self._adb('clear log', 'logcat', '-c')
    523     self.m.python.inline('%s' % cmd[0], """
    524     import subprocess
    525     import sys
    526     bin_dir = sys.argv[1]
    527     sh      = sys.argv[2]
    528     subprocess.check_call(['%s', 'shell', 'sh', bin_dir + sh])
    529     try:
    530       sys.exit(int(subprocess.check_output(['%s', 'shell', 'cat',
    531                                             bin_dir + 'rc'])))
    532     except ValueError:
    533       print "Couldn't read the return code.  Probably killed for OOM."
    534       sys.exit(1)
    535     """ % (self.ADB_BINARY, self.ADB_BINARY),
    536       args=[self.m.vars.android_bin_dir, sh])
    537 
    538   def copy_file_to_device(self, host, device):
    539     self._adb('push %s %s' % (host, device), 'push', host, device)
    540 
    541   def copy_directory_contents_to_device(self, host, device):
    542     # Copy the tree, avoiding hidden directories and resolving symlinks.
    543     self.m.run(self.m.python.inline, 'push %s/* %s' % (host, device),
    544                program="""
    545     import os
    546     import subprocess
    547     import sys
    548     host   = sys.argv[1]
    549     device = sys.argv[2]
    550     for d, _, fs in os.walk(host):
    551       p = os.path.relpath(d, host)
    552       if p != '.' and p.startswith('.'):
    553         continue
    554       for f in fs:
    555         print os.path.join(p,f)
    556         subprocess.check_call(['%s', 'push',
    557                                os.path.realpath(os.path.join(host, p, f)),
    558                                os.path.join(device, p, f)])
    559     """ % self.ADB_BINARY, args=[host, device], infra_step=True)
    560 
    561   def copy_directory_contents_to_host(self, device, host):
    562     self._adb('pull %s %s' % (device, host), 'pull', device, host)
    563 
    564   def read_file_on_device(self, path, **kwargs):
    565     rv = self._adb('read %s' % path,
    566                    'shell', 'cat', path, stdout=self.m.raw_io.output(),
    567                    **kwargs)
    568     return rv.stdout.rstrip() if rv and rv.stdout else None
    569 
    570   def remove_file_on_device(self, path):
    571     self._adb('rm %s' % path, 'shell', 'rm', '-f', path)
    572 
    573   def create_clean_device_dir(self, path):
    574     self._adb('rm %s' % path, 'shell', 'rm', '-rf', path)
    575     self._adb('mkdir %s' % path, 'shell', 'mkdir', '-p', path)
    576