• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
6from recipe_engine import recipe_api
7from recipe_engine import recipe_test_api
8
9from . import default
10import subprocess  # TODO(borenet): No! Remove this.
11
12
13"""Android flavor, used for running code on Android."""
14
15
16class AndroidFlavor(default.DefaultFlavor):
17  def __init__(self, m, app_name):
18    super(AndroidFlavor, self).__init__(m, app_name)
19    self._ever_ran_adb = False
20    self.ADB_BINARY = '/usr/bin/adb.1.0.35'
21    self.ADB_PUB_KEY = '/home/chrome-bot/.android/adbkey'
22    if 'skia' not in self.m.vars.swarming_bot_id:
23      self.ADB_BINARY = '/opt/infra-android/tools/adb'
24      self.ADB_PUB_KEY = ('/home/chrome-bot/.android/'
25                          'chrome_infrastructure_adbkey')
26
27    # Data should go in android_data_dir, which may be preserved across runs.
28    android_data_dir = '/sdcard/revenge_of_the_skiabot/'
29    self.device_dirs = default.DeviceDirs(
30        bin_dir        = '/data/local/tmp/',
31        dm_dir         = android_data_dir + 'dm_out',
32        perf_data_dir  = android_data_dir + 'perf',
33        resource_dir   = android_data_dir + 'resources',
34        images_dir     = android_data_dir + 'images',
35        lotties_dir    = android_data_dir + 'lotties',
36        skp_dir        = android_data_dir + 'skps',
37        svg_dir        = android_data_dir + 'svgs',
38        mskp_dir       = android_data_dir + 'mskp',
39        tmp_dir        = android_data_dir,
40        texttraces_dir = android_data_dir + 'text_blob_traces')
41
42    # A list of devices we can't root.  If rooting fails and a device is not
43    # on the list, we fail the task to avoid perf inconsistencies.
44    self.cant_root = ['GalaxyS7_G930FD', 'GalaxyS9',
45                      'GalaxyS20', 'MotoG4', 'NVIDIA_Shield',
46                      'P30', 'Pixel4','Pixel4XL', 'Pixel5', 'TecnoSpark3Pro', 'JioNext']
47
48    # Maps device type -> CPU ids that should be scaled for nanobench.
49    # Many devices have two (or more) different CPUs (e.g. big.LITTLE
50    # on Nexus5x). The CPUs listed are the biggest cpus on the device.
51    # The CPUs are grouped together, so we only need to scale one of them
52    # (the one listed) in order to scale them all.
53    # E.g. Nexus5x has cpu0-3 as one chip and cpu4-5 as the other. Thus,
54    # if one wants to run a single-threaded application (e.g. nanobench), one
55    # can disable cpu0-3 and scale cpu 4 to have only cpu4 and 5 at the same
56    # frequency.  See also disable_for_nanobench.
57    self.cpus_to_scale = {
58      'Nexus5x': [4],
59      'Pixel': [2],
60      'Pixel2XL': [4]
61    }
62
63    # Maps device type -> CPU ids that should be turned off when running
64    # single-threaded applications like nanobench. The devices listed have
65    # multiple, differnt CPUs. We notice a lot of noise that seems to be
66    # caused by nanobench running on the slow CPU, then the big CPU. By
67    # disabling this, we see less of that noise by forcing the same CPU
68    # to be used for the performance testing every time.
69    self.disable_for_nanobench = {
70      'Nexus5x': range(0, 4),
71      'Pixel': range(0, 2),
72      'Pixel2XL': range(0, 4),
73      'Pixel6': range(4,8), # Only use the 4 small cores.
74      'Pixel7': range(4,8),
75    }
76
77    self.gpu_scaling = {
78      "Nexus5":  450000000,
79      "Nexus5x": 600000000,
80    }
81
82  def _wait_for_device(self, title, attempt):
83    self.m.run(self.m.step,
84                'adb kill-server after failure of \'%s\' (attempt %d)' % (
85                    title, attempt),
86                cmd=[self.ADB_BINARY, 'kill-server'],
87                infra_step=True, timeout=30, abort_on_failure=False,
88                fail_build_on_failure=False)
89    self.m.run(self.m.step,
90                'wait for device after failure of \'%s\' (attempt %d)' % (
91                    title, attempt),
92                cmd=[self.ADB_BINARY, 'wait-for-device'], infra_step=True,
93                timeout=180, abort_on_failure=False,
94                fail_build_on_failure=False)
95    self.m.run(self.m.step,
96                'adb devices -l after failure of \'%s\' (attempt %d)' % (
97                    title, attempt),
98                cmd=[self.ADB_BINARY, 'devices', '-l'],
99                infra_step=True, timeout=30, abort_on_failure=False,
100                fail_build_on_failure=False)
101    self.m.run(self.m.step,
102                'adb reboot device after failure of \'%s\' (attempt %d)' % (
103                    title, attempt),
104                cmd=[self.ADB_BINARY, 'reboot'],
105                infra_step=True, timeout=30, abort_on_failure=False,
106                fail_build_on_failure=False)
107    self.m.run(self.m.step,
108                'wait for device after failure of \'%s\' (attempt %d)' % (
109                    title, attempt),
110                cmd=[
111                    self.ADB_BINARY, 'wait-for-device', 'shell',
112                    # Wait until the boot is actually complete.
113                    # https://android.stackexchange.com/a/164050
114                    'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done',
115                ],
116                timeout=180, abort_on_failure=False,
117                fail_build_on_failure=False)
118    device = self.m.vars.builder_cfg.get('model')
119    if (device in self.cant_root): # pragma: nocover
120      return
121    self.m.run(self.m.step,
122                'adb root',
123                cmd=[
124                  self.ADB_BINARY, 'root'
125                ],
126                timeout=180, abort_on_failure=False,
127                fail_build_on_failure=False)
128
129  def _adb(self, title, *cmd, **kwargs):
130    # The only non-infra adb steps (dm / nanobench) happen to not use _adb().
131    if 'infra_step' not in kwargs:
132      kwargs['infra_step'] = True
133
134    self._ever_ran_adb = True
135    # ADB seems to be occasionally flaky on every device, so always retry.
136    attempts = kwargs.pop('attempts', 3)
137
138    def wait_for_device(attempt):
139      return self._wait_for_device(title, attempt)
140
141    with self.m.context(cwd=self.m.path['start_dir'].join('skia')):
142      with self.m.env({'ADB_VENDOR_KEYS': self.ADB_PUB_KEY}):
143        return self.m.run.with_retry(self.m.step, title, attempts,
144                                     cmd=[self.ADB_BINARY]+list(cmd),
145                                     between_attempts_fn=wait_for_device,
146                                     **kwargs)
147
148  def _scale_for_dm(self):
149    device = self.m.vars.builder_cfg.get('model')
150    if (device in self.cant_root or
151        self.m.vars.internal_hardware_label):
152      return
153
154    # This is paranoia... any CPUs we disabled while running nanobench
155    # ought to be back online now that we've restarted the device.
156    for i in self.disable_for_nanobench.get(device, []):
157      self._set_cpu_online(i, 1) # enable
158
159    scale_up = self.cpus_to_scale.get(device, [0])
160    # For big.LITTLE devices, make sure we scale the LITTLE cores up;
161    # there is a chance they are still in powersave mode from when
162    # swarming slows things down for cooling down and charging.
163    if 0 not in scale_up:
164      scale_up.append(0)
165    for i in scale_up:
166      # AndroidOne doesn't support ondemand governor. hotplug is similar.
167      if device == 'AndroidOne':
168        self._set_governor(i, 'hotplug')
169      elif device in ['Pixel3a', 'Pixel4', 'Pixel4a', 'Wembley', 'Pixel6', 'Pixel7']:
170        # Pixel3a/4/4a have userspace powersave performance schedutil.
171        # performance seems like a reasonable choice.
172        self._set_governor(i, 'performance')
173      else:
174        self._set_governor(i, 'ondemand')
175
176  def _scale_for_nanobench(self):
177    device = self.m.vars.builder_cfg.get('model')
178    if (device in self.cant_root or
179      self.m.vars.internal_hardware_label):
180      return
181
182    # Set to 'powersave' for Pixel6 and Pixel7.
183    for i in self.cpus_to_scale.get(device, [0]):
184      if device in ['Pixel6', 'Pixel7']:
185        self._set_governor(i, 'powersave')
186      else:
187        self._set_governor(i, 'userspace')
188        self._scale_cpu(i, 0.6)
189
190    for i in self.disable_for_nanobench.get(device, []):
191      self._set_cpu_online(i, 0) # disable
192
193    if device in self.gpu_scaling:
194      #https://developer.qualcomm.com/qfile/28823/lm80-p0436-11_adb_commands.pdf
195      # Section 3.2.1 Commands to put the GPU in performance mode
196      # Nexus 5 is  320000000 by default
197      # Nexus 5x is 180000000 by default
198      gpu_freq = self.gpu_scaling[device]
199      self.m.run.with_retry(self.m.python.inline,
200        "Lock GPU to %d (and other perf tweaks)" % gpu_freq,
201        3, # attempts
202        program="""
203import os
204import subprocess
205import sys
206import time
207ADB = sys.argv[1]
208freq = sys.argv[2]
209idle_timer = "10000"
210
211log = subprocess.check_output([ADB, 'root']).decode('utf-8')
212# check for message like 'adbd cannot run as root in production builds'
213print(log)
214if 'cannot' in log:
215  raise Exception('adb root failed')
216
217subprocess.check_output([ADB, 'shell', 'stop', 'thermald']).decode('utf-8')
218
219subprocess.check_output([ADB, 'shell', 'echo "%s" > '
220    '/sys/class/kgsl/kgsl-3d0/gpuclk' % freq]).decode('utf-8')
221
222actual_freq = subprocess.check_output([ADB, 'shell', 'cat '
223    '/sys/class/kgsl/kgsl-3d0/gpuclk']).decode('utf-8').strip()
224if actual_freq != freq:
225  raise Exception('Frequency (actual, expected) (%s, %s)'
226                  % (actual_freq, freq))
227
228subprocess.check_call([ADB, 'shell', 'echo "%s" > '
229    '/sys/class/kgsl/kgsl-3d0/idle_timer' % idle_timer])
230
231actual_timer = subprocess.check_output([ADB, 'shell', 'cat '
232    '/sys/class/kgsl/kgsl-3d0/idle_timer']).decode('utf-8').strip()
233if actual_timer != idle_timer:
234  raise Exception('idle_timer (actual, expected) (%s, %s)'
235                  % (actual_timer, idle_timer))
236
237for s in ['force_bus_on', 'force_rail_on', 'force_clk_on']:
238  subprocess.check_call([ADB, 'shell', 'echo "1" > '
239      '/sys/class/kgsl/kgsl-3d0/%s' % s])
240  actual_set = subprocess.check_output([ADB, 'shell', 'cat '
241      '/sys/class/kgsl/kgsl-3d0/%s' % s]).decode('utf-8').strip()
242  if actual_set != "1":
243    raise Exception('%s (actual, expected) (%s, 1)'
244                    % (s, actual_set))
245""",
246        args = [self.ADB_BINARY, gpu_freq],
247        infra_step=True,
248        timeout=30)
249
250  def _set_governor(self, cpu, gov):
251    self._ever_ran_adb = True
252    self.m.run.with_retry(self.m.python.inline,
253        "Set CPU %d's governor to %s" % (cpu, gov),
254        3, # attempts
255        program="""
256import os
257import subprocess
258import sys
259import time
260ADB = sys.argv[1]
261cpu = int(sys.argv[2])
262gov = sys.argv[3]
263
264log = subprocess.check_output([ADB, 'root']).decode('utf-8')
265# check for message like 'adbd cannot run as root in production builds'
266print(log)
267if 'cannot' in log:
268  raise Exception('adb root failed')
269
270subprocess.check_output([
271    ADB, 'shell',
272    'echo "%s" > /sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor' % (
273        gov, cpu)]).decode('utf-8')
274actual_gov = subprocess.check_output([
275    ADB, 'shell', 'cat /sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor' %
276        cpu]).decode('utf-8').strip()
277if actual_gov != gov:
278  raise Exception('(actual, expected) (%s, %s)'
279                  % (actual_gov, gov))
280""",
281        args = [self.ADB_BINARY, cpu, gov],
282        infra_step=True,
283        timeout=30)
284
285
286  def _set_cpu_online(self, cpu, value):
287    """Set /sys/devices/system/cpu/cpu{N}/online to value (0 or 1)."""
288    self._ever_ran_adb = True
289    msg = 'Disabling'
290    if value:
291      msg = 'Enabling'
292
293    def wait_for_device(attempt):
294      return self._wait_for_device("set cpu online", attempt) # pragma: nocover
295
296    self.m.run.with_retry(self.m.python.inline,
297        '%s CPU %d' % (msg, cpu),
298        3, # attempts
299        program="""
300import os
301import subprocess
302import sys
303import time
304ADB = sys.argv[1]
305cpu = int(sys.argv[2])
306value = int(sys.argv[3])
307
308log = subprocess.check_output([ADB, 'root']).decode('utf-8')
309# check for message like 'adbd cannot run as root in production builds'
310print(log)
311if 'cannot' in log:
312  raise Exception('adb root failed')
313
314# If we try to echo 1 to an already online cpu, adb returns exit code 1.
315# So, check the value before trying to write it.
316prior_status = subprocess.check_output([ADB, 'shell', 'cat '
317    '/sys/devices/system/cpu/cpu%d/online' % cpu]).decode('utf-8').strip()
318if prior_status == str(value):
319  print('CPU %d online already %d' % (cpu, value))
320  sys.exit()
321
322subprocess.check_call([ADB, 'shell', 'echo %s > '
323    '/sys/devices/system/cpu/cpu%d/online' % (value, cpu)])
324actual_status = subprocess.check_output([ADB, 'shell', 'cat '
325    '/sys/devices/system/cpu/cpu%d/online' % cpu]).decode('utf-8').strip()
326if actual_status != str(value):
327  raise Exception('(actual, expected) (%s, %d)'
328                  % (actual_status, value))
329""",
330        args = [self.ADB_BINARY, cpu, value],
331        infra_step=True,
332        between_attempts_fn=wait_for_device,
333        timeout=30)
334
335
336  def _scale_cpu(self, cpu, target_percent):
337    self._ever_ran_adb = True
338
339    def wait_for_device(attempt):
340      return self._wait_for_device("scale cpu", attempt)
341
342    self.m.run.with_retry(self.m.python.inline,
343        'Scale CPU %d to %f' % (cpu, target_percent),
344        3, # attempts
345        program="""
346import os
347import subprocess
348import sys
349import time
350ADB = sys.argv[1]
351target_percent = float(sys.argv[2])
352cpu = int(sys.argv[3])
353log = subprocess.check_output([ADB, 'root']).decode('utf-8')
354# check for message like 'adbd cannot run as root in production builds'
355print(log)
356if 'cannot' in log:
357  raise Exception('adb root failed')
358
359root = '/sys/devices/system/cpu/cpu%d/cpufreq' %cpu
360
361# All devices we test on give a list of their available frequencies.
362available_freqs = subprocess.check_output([ADB, 'shell',
363    'cat %s/scaling_available_frequencies' % root]).decode('utf-8')
364
365# Check for message like '/system/bin/sh: file not found'
366if available_freqs and '/system/bin/sh' not in available_freqs:
367  available_freqs = sorted(
368      int(i) for i in available_freqs.strip().split())
369else:
370  raise Exception('Could not get list of available frequencies: %s' %
371                  available_freqs)
372
373maxfreq = available_freqs[-1]
374target = int(round(maxfreq * target_percent))
375freq = maxfreq
376for f in reversed(available_freqs):
377  if f <= target:
378    freq = f
379    break
380
381print('Setting frequency to %d' % freq)
382
383# If scaling_max_freq is lower than our attempted setting, it won't take.
384# We must set min first, because if we try to set max to be less than min
385# (which sometimes happens after certain devices reboot) it returns a
386# perplexing permissions error.
387subprocess.check_call([ADB, 'shell', 'echo 0 > '
388    '%s/scaling_min_freq' % root])
389subprocess.check_call([ADB, 'shell', 'echo %d > '
390    '%s/scaling_max_freq' % (freq, root)])
391subprocess.check_call([ADB, 'shell', 'echo %d > '
392    '%s/scaling_setspeed' % (freq, root)])
393time.sleep(5)
394actual_freq = subprocess.check_output([ADB, 'shell', 'cat '
395    '%s/scaling_cur_freq' % root]).decode('utf-8').strip()
396if actual_freq != str(freq):
397  raise Exception('(actual, expected) (%s, %d)'
398                  % (actual_freq, freq))
399""",
400        args = [self.ADB_BINARY, str(target_percent), cpu],
401        infra_step=True,
402        between_attempts_fn=wait_for_device,
403        timeout=30)
404
405
406  def _asan_setup_path(self):
407    return self.m.vars.workdir.join(
408        'android_ndk_linux', 'toolchains', 'llvm', 'prebuilt', 'linux-x86_64',
409        'lib64', 'clang', '9.0.8', 'bin', 'asan_device_setup')
410
411
412  def install(self):
413    self._adb('mkdir ' + self.device_dirs.resource_dir,
414              'shell', 'mkdir', '-p', self.device_dirs.resource_dir)
415    if self.m.vars.builder_cfg.get('model') in ['GalaxyS20', 'GalaxyS9']:
416      # See skia:10184, should be moot once upgraded to Android 11?
417      self._adb('cp libGLES_mali.so to ' + self.device_dirs.bin_dir,
418                 'shell', 'cp',
419                '/vendor/lib64/egl/libGLES_mali.so',
420                self.device_dirs.bin_dir + 'libvulkan.so')
421    if 'ASAN' in self.m.vars.extra_tokens:
422      self._ever_ran_adb = True
423      self.m.run(self.m.python.inline, 'Setting up device to run ASAN',
424                 program="""
425import os
426import subprocess
427import sys
428import time
429ADB = sys.argv[1]
430ASAN_SETUP = sys.argv[2]
431
432def wait_for_device():
433  while True:
434    time.sleep(5)
435    print('Waiting for device')
436    subprocess.check_call([ADB, 'wait-for-device'])
437    bit1 = subprocess.check_output([ADB, 'shell', 'getprop',
438                                   'dev.bootcomplete']).decode('utf-8')
439    bit2 = subprocess.check_output([ADB, 'shell', 'getprop',
440                                   'sys.boot_completed']).decode('utf-8')
441    if '1' in bit1 and '1' in bit2:
442      print('Device detected')
443      break
444
445log = subprocess.check_output([ADB, 'root']).decode('utf-8')
446# check for message like 'adbd cannot run as root in production builds'
447print(log)
448if 'cannot' in log:
449  raise Exception('adb root failed')
450
451output = subprocess.check_output([ADB, 'disable-verity']).decode('utf-8')
452print(output)
453
454if 'already disabled' not in output:
455  print('Rebooting device')
456  subprocess.check_call([ADB, 'reboot'])
457  wait_for_device()
458
459def installASAN(revert=False):
460  # ASAN setup script is idempotent, either it installs it or
461  # says it's installed.  Returns True on success, false otherwise.
462  out = subprocess.check_output([ADB, 'wait-for-device']).decode('utf-8')
463  print(out)
464  cmd = [ASAN_SETUP]
465  if revert:
466    cmd = [ASAN_SETUP, '--revert']
467  process = subprocess.Popen(cmd, env={'ADB': ADB},
468                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
469
470  # this also blocks until command finishes
471  (stdout, stderr) = process.communicate()
472  print(stdout.decode('utf-8'))
473  print('Stderr: %s' % stderr.decode('utf-8'))
474  return process.returncode == 0
475
476if not installASAN():
477  print('Trying to revert the ASAN install and then re-install')
478  # ASAN script sometimes has issues if it was interrupted or partially applied
479  # Try reverting it, then re-enabling it
480  if not installASAN(revert=True):
481    raise Exception('reverting ASAN install failed')
482
483  # Sleep because device does not reboot instantly
484  time.sleep(10)
485
486  if not installASAN():
487    raise Exception('Tried twice to setup ASAN and failed.')
488
489# Sleep because device does not reboot instantly
490time.sleep(10)
491wait_for_device()
492# Sleep again to hopefully avoid error "secure_mkdirs failed: No such file or
493# directory" when pushing resources to the device.
494time.sleep(60)
495""",
496                 args = [self.ADB_BINARY, self._asan_setup_path()],
497                 infra_step=True,
498                 timeout=300,
499                 abort_on_failure=True)
500    if self.app_name:
501      if (self.app_name == 'nanobench'):
502        self._scale_for_nanobench()
503      else:
504        self._scale_for_dm()
505      app_path = self.host_dirs.bin_dir.join(self.app_name)
506      self._adb('push %s' % self.app_name,
507                'push', app_path, self.device_dirs.bin_dir)
508
509
510
511  def cleanup_steps(self):
512    self.m.run(self.m.step,
513                'adb reboot device',
514                cmd=[self.ADB_BINARY, 'reboot'],
515                infra_step=True, timeout=30, abort_on_failure=False,
516                fail_build_on_failure=False)
517    self.m.run(self.m.step,
518                'wait for device after rebooting',
519                cmd=[
520                    self.ADB_BINARY, 'wait-for-device', 'shell',
521                    # Wait until the boot is actually complete.
522                    # https://android.stackexchange.com/a/164050
523                    'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done',
524                ],
525                timeout=180, abort_on_failure=False,
526                fail_build_on_failure=False)
527
528    if 'ASAN' in self.m.vars.extra_tokens:
529      self._ever_ran_adb = True
530      # Remove ASAN.
531      self.m.run(self.m.step,
532                 'wait for device before uninstalling ASAN',
533                 cmd=[self.ADB_BINARY, 'wait-for-device', 'shell',
534                 # Wait until the boot is actually complete.
535                 # https://android.stackexchange.com/a/164050
536                 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done',
537                 ], infra_step=True,
538                 timeout=180, abort_on_failure=False,
539                 fail_build_on_failure=False)
540      self.m.run(self.m.step, 'uninstall ASAN',
541                 cmd=[self._asan_setup_path(), '--revert'],
542                 infra_step=True, timeout=300,
543                 abort_on_failure=False, fail_build_on_failure=False)
544
545    if self._ever_ran_adb:
546      self.m.run(self.m.python.inline, 'dump log', program="""
547          import os
548          import subprocess
549          import sys
550          out = sys.argv[1]
551          log = subprocess.check_output([
552              '%s', 'logcat', '-d']).decode('utf-8', errors='ignore')
553          for line in log.split('\\n'):
554            tokens = line.split()
555            if len(tokens) == 11 and tokens[-7] == 'F' and tokens[-3] == 'pc':
556              addr, path = tokens[-2:]
557              local = os.path.join(out, os.path.basename(path))
558              if os.path.exists(local):
559                try:
560                  sym = subprocess.check_output([
561                      'addr2line', '-Cfpe', local, addr]).decode('utf-8')
562                  line = line.replace(addr, addr + ' ' + sym.strip())
563                except subprocess.CalledProcessError:
564                  pass
565            print(line)
566          """ % self.ADB_BINARY,
567          args=[self.host_dirs.bin_dir],
568          infra_step=True,
569          timeout=300,
570          abort_on_failure=False)
571
572    # Only quarantine the bot if the first failed step
573    # is an infra step. If, instead, we did this for any infra failures, we
574    # would do this too much. For example, if a Nexus 10 died during dm
575    # and the following pull step would also fail "device not found" - causing
576    # us to run the shutdown command when the device was probably not in a
577    # broken state; it was just rebooting.
578    if (self.m.run.failed_steps and
579        isinstance(self.m.run.failed_steps[0], recipe_api.InfraFailure)):
580      bot_id = self.m.vars.swarming_bot_id
581      self.m.file.write_text('Quarantining Bot',
582                             '/home/chrome-bot/%s.force_quarantine' % bot_id,
583                             ' ')
584
585    # if self._ever_ran_adb:
586    #   self._adb('kill adb server', 'kill-server')
587
588  def step(self, name, cmd):
589    sh = '%s.sh' % cmd[0]
590    self.m.run.writefile(self.m.vars.tmp_dir.join(sh),
591        'set -x; LD_LIBRARY_PATH=%s %s%s; echo $? >%src' % (
592            self.device_dirs.bin_dir,
593            self.device_dirs.bin_dir, subprocess.list2cmdline(map(str, cmd)),
594            self.device_dirs.bin_dir))
595    self._adb('push %s' % sh,
596              'push', self.m.vars.tmp_dir.join(sh), self.device_dirs.bin_dir)
597
598    self._adb('clear log', 'logcat', '-c')
599    self.m.python.inline('%s' % cmd[0], """
600    import subprocess
601    import sys
602    bin_dir = sys.argv[1]
603    sh      = sys.argv[2]
604    subprocess.check_call(['%s', 'shell', 'sh', bin_dir + sh])
605    try:
606      sys.exit(int(subprocess.check_output([
607          '%s', 'shell', 'cat', bin_dir + 'rc']).decode('utf-8')))
608    except ValueError:
609      print("Couldn't read the return code.  Probably killed for OOM.")
610      sys.exit(1)
611    """ % (self.ADB_BINARY, self.ADB_BINARY),
612      args=[self.device_dirs.bin_dir, sh])
613
614  def copy_file_to_device(self, host, device):
615    self._adb('push %s %s' % (host, device), 'push', host, device)
616
617  def copy_directory_contents_to_device(self, host, device):
618    contents = self.m.file.glob_paths('ls %s/*' % host,
619                                      host, '*',
620                                      test_data=['foo.png', 'bar.jpg'])
621    args = contents + [device]
622    self._adb('push %s/* %s' % (host, device), 'push', *args)
623
624  def copy_directory_contents_to_host(self, device, host):
625    # TODO(borenet): When all of our devices are on Android 6.0 and up, we can
626    # switch to using tar to zip up the results before pulling.
627    with self.m.step.nest('adb pull'):
628      tmp = self.m.path.mkdtemp('adb_pull')
629      self._adb('pull %s' % device, 'pull', device, tmp)
630      paths = self.m.file.glob_paths(
631          'list pulled files',
632          tmp,
633          self.m.path.basename(device) + self.m.path.sep + '*',
634          test_data=['%d.png' % i for i in (1, 2)])
635      for p in paths:
636        self.m.file.copy('copy %s' % self.m.path.basename(p), p, host)
637
638  def read_file_on_device(self, path, **kwargs):
639    testKwargs = {
640      'attempts': 1,
641      'abort_on_failure': False,
642      'fail_build_on_failure': False,
643    }
644    rv = self._adb('check if %s exists' % path,
645                   'shell', 'test', '-f', path, **testKwargs)
646    if not rv: # pragma: nocover
647      return None
648
649    rv = self._adb('read %s' % path,
650                   'shell', 'cat', path, stdout=self.m.raw_io.output(),
651                   **kwargs)
652    return rv.stdout.decode('utf-8').rstrip() if rv and rv.stdout else None
653
654  def remove_file_on_device(self, path):
655    self.m.run.with_retry(self.m.python.inline, 'rm %s' % path, 3, program="""
656        import subprocess
657        import sys
658
659        # Remove the path.
660        adb = sys.argv[1]
661        path = sys.argv[2]
662        print('Removing %s' % path)
663        cmd = [adb, 'shell', 'rm', '-rf', path]
664        print(' '.join(cmd))
665        subprocess.check_call(cmd)
666
667        # Verify that the path was deleted.
668        print('Checking for existence of %s' % path)
669        cmd = [adb, 'shell', 'ls', path]
670        print(' '.join(cmd))
671        try:
672          output = subprocess.check_output(
673              cmd, stderr=subprocess.STDOUT).decode('utf-8')
674        except subprocess.CalledProcessError as e:
675          output = e.output.decode('utf-8')
676        print('Output was:')
677        print('======')
678        print(output)
679        print('======')
680        if 'No such file or directory' not in output:
681          raise Exception('%s exists despite being deleted' % path)
682        """,
683        args=[self.ADB_BINARY, path],
684        infra_step=True)
685
686  def create_clean_device_dir(self, path):
687    self.remove_file_on_device(path)
688    self._adb('mkdir %s' % path, 'shell', 'mkdir', '-p', path)
689