skia2/infra/bots/recipe_modules/flavor/ios.py
Leandro Lovisolo 17fbcd145b ios.py: Bugfix to prevent iOS tasks from failing with "Error: ImageMountFailed".
This CL fixes a bug in the logic that checks whether there's an image mounted on the attached iOS device.

Currently this is determined by checking whether strings "ImagePresent: true" or "ImageSignature:" are present in the output of command "ideviceimagemounter --list". However, these strings do not match the command's actual output:

  # No image mounted.
  $ ideviceimagemounter --list
  Status: Complete

  # Image mounted (fake signature for illustrative purposes).
  $ ideviceimagemounter --list
  ImageSignature[1]:
   0: Rm9yIGlsbHVzdHJhdGl2ZSBwdXJwb3NlcyBvbmx5LiBUaGlzIGlzIG5vdCBhIHJlYWwgc2lnbmF0dXJlLg==
  Status: Complete

As is, the recipe fails to detect that an image is already mounted on the attached iOS device. The recipe then tries to mount one, which makes the "ideviceimagemounter" command fail with "Error: ImageAlreadyMounted". The following example shows how to reproduce this error:

  # Initially no image is mounted.
  $ ideviceimagemounter --list
  Status: Complete

  # Mount an image.
  $ ideviceimagemounter DeveloperDiskImage.dmg DeveloperDiskImage.dmg.signature
  Uploading DeveloperDiskImage.dmg
  done.
  Mounting...
  Done.
  Status: Complete

  # Verify that the image is mounted.
  $ ideviceimagemounter --list
  ImageSignature[1]:
   0: Rm9yIGlsbHVzdHJhdGl2ZSBwdXJwb3NlcyBvbmx5LiBUaGlzIGlzIG5vdCBhIHJlYWwgc2lnbmF0dXJlLg==
  Status: Complete

  # Try to mount an image when one is already mounted.
  $ ideviceimagemounter DeveloperDiskImage.dmg DeveloperDiskImage.dmg.signature
  Uploading DeveloperDiskImage.dmg
  done.
  Mounting...
  Error: ImageMountFailed

Checking for presence of string "ImageSignature" in the ideviceimagemounter command's output seems to solve the problem.

In the process I also discovered that self._run() requires keyword argument stdout=self.m.raw_io.output() in order to return the command's output. As is, the returned stdout is always blank.

Note to reviewer:
- Patchsets 1 to 11 iterate on a potential fix.
- Patchset 12 reproduces the bug. See the failing tryjob.
- Patchset 13 tries the fix. See the successful tryjob.
- Patchsets 14+ clean up the CL.

Change-Id: I3ca1cdbf4bfa450c3e87d6d87b5f615f28c4aaba
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/281058
Reviewed-by: Weston Tracey <westont@google.com>
Commit-Queue: Leandro Lovisolo <lovisolo@google.com>
2020-04-02 19:18:06 +00:00

168 lines
5.9 KiB
Python

# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from . import default
"""iOS flavor, used for running code on iOS."""
class iOSFlavor(default.DefaultFlavor):
def __init__(self, m, app_name):
super(iOSFlavor, self).__init__(m, app_name)
self.device_dirs = default.DeviceDirs(
bin_dir='[unused]',
dm_dir='dm',
perf_data_dir='perf',
resource_dir='resources',
images_dir='images',
lotties_dir='lotties',
skp_dir='skps',
svg_dir='svgs',
mskp_dir='mskp',
tmp_dir='tmp',
texttraces_dir='')
@property
def env(self):
return {
'IOS_BUNDLE_ID': 'com.google.%s' % self.app_name,
'IOS_MOUNT_POINT': self.m.vars.slave_dir.join('mnt_iosdevice'),
}
def context(self):
return self.m.context(env=self.env)
def _run(self, title, *cmd, **kwargs):
def sleep(attempt):
self.m.python.inline('sleep before attempt %d' % attempt, """
import time
time.sleep(2)
""") # pragma: nocover
return self.m.run.with_retry(self.m.step, title, 3, cmd=list(cmd),
between_attempts_fn=sleep, **kwargs)
def install(self):
with self.context():
self._install()
def _install(self):
# We assume a single device is connected.
# Pair the device.
try:
self.m.run(self.m.step, 'check if device is paired',
cmd=['idevicepair', 'validate'],
infra_step=True, abort_on_failure=True,
fail_build_on_failure=False)
except self.m.step.StepFailure: # pragma: nocover
self._run('pair device', 'idevicepair', 'pair') # pragma: nocover
# Mount developer image.
image_info = self._run('list mounted image',
'ideviceimagemounter', '--list',
stdout=self.m.raw_io.output())
image_info_out = image_info.stdout.strip() if image_info.stdout else ''
if 'ImageSignature' not in image_info_out:
image_pkgs = self.m.file.glob_paths('locate ios-dev-image package',
self.m.path['start_dir'],
'ios-dev-image*',
test_data=['ios-dev-image-13.2'])
if len(image_pkgs) != 1:
raise Exception('glob for ios-dev-image* returned %s'
% image_pkgs) # pragma: nocover
image_pkg = image_pkgs[0]
contents = self.m.file.listdir(
'locate image and signature', image_pkg,
test_data=['DeveloperDiskImage.dmg',
'DeveloperDiskImage.dmg.signature'])
image = None
sig = None
for f in contents:
if str(f).endswith('.dmg'):
image = f
if str(f).endswith('.dmg.signature'):
sig = f
if not image or not sig:
raise Exception('%s does not contain *.dmg and *.dmg.signature' %
image_pkg) # pragma: nocover
self._run('mount developer image', 'ideviceimagemounter', image, sig)
# Install app (necessary before copying any resources to the device).
if self.app_name:
app_package = self.host_dirs.bin_dir.join('%s.app' % self.app_name)
def uninstall_app(attempt):
# If app ID changes, upgrade will fail, so try uninstalling.
self.m.run(self.m.step,
'uninstall %s' % self.app_name,
cmd=['ideviceinstaller', '-U',
'com.google.%s' % self.app_name],
infra_step=True,
# App may not be installed.
abort_on_failure=False, fail_build_on_failure=False)
num_attempts = 2
self.m.run.with_retry(self.m.step, 'install %s' % self.app_name,
num_attempts,
cmd=['ideviceinstaller', '-i', app_package],
between_attempts_fn=uninstall_app,
infra_step=True)
def step(self, name, cmd, **kwargs):
app_name = cmd[0]
bundle_id = 'com.google.%s' % app_name
args = [bundle_id] + map(str, cmd[1:])
success = False
with self.context():
try:
self.m.run(self.m.step, name, cmd=['idevicedebug', 'run'] + args)
success = True
finally:
if not success:
self.m.run(self.m.python, '%s with full debug output' % name,
script=self.module.resource('ios_debug_cmd.py'),
args=args)
def _run_ios_script(self, script, first, *rest):
with self.context():
full = self.m.path['start_dir'].join(
'skia', 'platform_tools', 'ios', 'bin', 'ios_' + script)
self.m.run(self.m.step,
name = '%s %s' % (script, first),
cmd = [full, first] + list(rest),
infra_step=True)
def copy_file_to_device(self, host, device):
self._run_ios_script('push_file', host, device)
def copy_directory_contents_to_device(self, host, device):
self._run_ios_script('push_if_needed', host, device)
def copy_directory_contents_to_host(self, device, host):
self._run_ios_script('pull_if_needed', device, host)
def remove_file_on_device(self, path):
self._run_ios_script('rm', path)
def create_clean_device_dir(self, path):
self._run_ios_script('rm', path)
self._run_ios_script('mkdir', path)
def read_file_on_device(self, path, **kwargs):
with self.context():
full = self.m.path['start_dir'].join(
'skia', 'platform_tools', 'ios', 'bin', 'ios_cat_file')
rv = self.m.run(self.m.step,
name = 'cat_file %s' % path,
cmd = [full, path],
stdout=self.m.raw_io.output(),
infra_step=True,
**kwargs)
return rv.stdout.rstrip() if rv and rv.stdout else None