• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2019 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import collections
6import contextlib
7import glob
8import json
9import logging
10import os
11import socket
12import stat
13import subprocess
14import threading
15import time
16
17from google.protobuf import text_format  # pylint: disable=import-error
18
19from devil.android import apk_helper
20from devil.android import device_utils
21from devil.android.sdk import adb_wrapper
22from devil.android.sdk import version_codes
23from devil.android.tools import system_app
24from devil.utils import cmd_helper
25from devil.utils import timeout_retry
26from py_utils import tempfile_ext
27from pylib import constants
28from pylib.local.emulator import ini
29from pylib.local.emulator.proto import avd_pb2
30
31# A common root directory to store the CIPD packages for creating or starting
32# the emulator instance, e.g. emulator binary, system images, AVDs.
33COMMON_CIPD_ROOT = os.path.join(constants.DIR_SOURCE_ROOT, '.android_emulator')
34
35# Packages that are needed for runtime.
36_PACKAGES_RUNTIME = object()
37# Packages that are needed during AVD creation.
38_PACKAGES_CREATION = object()
39# All the packages that could exist in the AVD config file.
40_PACKAGES_ALL = object()
41
42# These files are used as backing files for corresponding qcow2 images.
43_BACKING_FILES = ('system.img', 'vendor.img')
44
45_DEFAULT_AVDMANAGER_PATH = os.path.join(constants.ANDROID_SDK_ROOT,
46                                        'cmdline-tools', 'latest', 'bin',
47                                        'avdmanager')
48# Default to a 480dp mdpi screen (a relatively large phone).
49# See https://developer.android.com/training/multiscreen/screensizes
50# and https://developer.android.com/training/multiscreen/screendensities
51# for more information.
52_DEFAULT_SCREEN_DENSITY = 160
53_DEFAULT_SCREEN_HEIGHT = 960
54_DEFAULT_SCREEN_WIDTH = 480
55
56# Default to swiftshader_indirect since it works for most cases.
57_DEFAULT_GPU_MODE = 'swiftshader_indirect'
58
59# The snapshot name to load/save when writable_system=False.
60# This is the default name used by the emulator binary.
61_DEFAULT_SNAPSHOT_NAME = 'default_boot'
62
63# crbug.com/1275767: Set long press timeout to 1000ms to reduce the flakiness
64# caused by click being incorrectly interpreted as longclick.
65_LONG_PRESS_TIMEOUT = '1000'
66
67# The snapshot name to load/save when writable_system=True
68_SYSTEM_SNAPSHOT_NAME = 'boot_with_system'
69
70_SDCARD_NAME = 'cr-sdcard.img'
71
72
73class AvdException(Exception):
74  """Raised when this module has a problem interacting with an AVD."""
75
76  def __init__(self, summary, command=None, stdout=None, stderr=None):
77    message_parts = [summary]
78    if command:
79      message_parts.append('  command: %s' % ' '.join(command))
80    if stdout:
81      message_parts.append('  stdout:')
82      message_parts.extend('    %s' % line for line in stdout.splitlines())
83    if stderr:
84      message_parts.append('  stderr:')
85      message_parts.extend('    %s' % line for line in stderr.splitlines())
86
87    # avd.py is executed with python2.
88    # pylint: disable=R1725
89    super(AvdException, self).__init__('\n'.join(message_parts))
90
91
92def _Load(avd_proto_path):
93  """Loads an Avd proto from a textpb file at the given path.
94
95  Should not be called outside of this module.
96
97  Args:
98    avd_proto_path: path to a textpb file containing an Avd message.
99  """
100  with open(avd_proto_path) as avd_proto_file:
101    # python generated codes are simplified since Protobuf v3.20.0 and cause
102    # pylint error. See https://github.com/protocolbuffers/protobuf/issues/9730
103    # pylint: disable=no-member
104    return text_format.Merge(avd_proto_file.read(), avd_pb2.Avd())
105
106
107def _FindMinSdkFile(apk_dir, min_sdk):
108  """Finds the apk file associated with the min_sdk file.
109
110  This reads a version.json file located in the apk_dir to find an apk file
111  that is closest without going over the min_sdk.
112
113  Args:
114    apk_dir: The directory to look for apk files.
115    min_sdk: The minimum sdk version supported by the device.
116
117  Returns:
118    The path to the file that suits the minSdkFile or None
119  """
120  json_file = os.path.join(apk_dir, 'version.json')
121  if not os.path.exists(json_file):
122    logging.error('Json version file not found: %s', json_file)
123    return None
124
125  min_sdk_found = None
126  curr_min_sdk_version = 0
127  with open(json_file) as f:
128    data = json.loads(f.read())
129    # Finds the entry that is closest to min_sdk without going over.
130    for entry in data:
131      if (entry['min_sdk'] > curr_min_sdk_version
132          and entry['min_sdk'] <= min_sdk):
133        min_sdk_found = entry
134        curr_min_sdk_version = entry['min_sdk']
135
136    if not min_sdk_found:
137      logging.error('No suitable apk file found that suits the minimum sdk %d.',
138                    min_sdk)
139      return None
140
141    logging.info('Found apk file for mininum sdk %d: %r with version %r',
142                 min_sdk, min_sdk_found['file_name'],
143                 min_sdk_found['version_name'])
144    return os.path.join(apk_dir, min_sdk_found['file_name'])
145
146
147class _AvdManagerAgent:
148  """Private utility for interacting with avdmanager."""
149
150  def __init__(self, avd_home, sdk_root):
151    """Create an _AvdManagerAgent.
152
153    Args:
154      avd_home: path to ANDROID_AVD_HOME directory.
155        Typically something like /path/to/dir/.android/avd
156      sdk_root: path to SDK root directory.
157    """
158    self._avd_home = avd_home
159    self._sdk_root = sdk_root
160
161    self._env = dict(os.environ)
162
163    # The avdmanager from cmdline-tools would look two levels
164    # up from toolsdir to find the SDK root.
165    # Pass avdmanager a fake directory under the directory in which
166    # we install the system images s.t. avdmanager can find the
167    # system images.
168    fake_tools_dir = os.path.join(self._sdk_root, 'non-existent-tools',
169                                  'non-existent-version')
170    self._env.update({
171        'ANDROID_AVD_HOME':
172        self._avd_home,
173        'AVDMANAGER_OPTS':
174        '-Dcom.android.sdkmanager.toolsdir=%s' % fake_tools_dir,
175        'JAVA_HOME':
176        constants.JAVA_HOME,
177    })
178
179  def Create(self, avd_name, system_image, force=False):
180    """Call `avdmanager create`.
181
182    Args:
183      avd_name: name of the AVD to create.
184      system_image: system image to use for the AVD.
185      force: whether to force creation, overwriting any existing
186        AVD with the same name.
187    """
188    create_cmd = [
189        _DEFAULT_AVDMANAGER_PATH,
190        '-v',
191        'create',
192        'avd',
193        '-n',
194        avd_name,
195        '-k',
196        system_image,
197    ]
198    if force:
199      create_cmd += ['--force']
200
201    create_proc = cmd_helper.Popen(create_cmd,
202                                   stdin=subprocess.PIPE,
203                                   stdout=subprocess.PIPE,
204                                   stderr=subprocess.PIPE,
205                                   env=self._env)
206    output, error = create_proc.communicate(input='\n')
207    if create_proc.returncode != 0:
208      raise AvdException('AVD creation failed',
209                         command=create_cmd,
210                         stdout=output,
211                         stderr=error)
212
213    for line in output.splitlines():
214      logging.info('  %s', line)
215
216  def Delete(self, avd_name):
217    """Call `avdmanager delete`.
218
219    Args:
220      avd_name: name of the AVD to delete.
221    """
222    delete_cmd = [
223        _DEFAULT_AVDMANAGER_PATH,
224        '-v',
225        'delete',
226        'avd',
227        '-n',
228        avd_name,
229    ]
230    try:
231      for line in cmd_helper.IterCmdOutputLines(delete_cmd, env=self._env):
232        logging.info('  %s', line)
233    except subprocess.CalledProcessError as e:
234      # avd.py is executed with python2.
235      # pylint: disable=W0707
236      raise AvdException('AVD deletion failed: %s' % str(e), command=delete_cmd)
237
238  def List(self):
239    """List existing AVDs by the name."""
240    list_cmd = [
241        _DEFAULT_AVDMANAGER_PATH,
242        '-v',
243        'list',
244        'avd',
245        '-c',
246    ]
247    output = cmd_helper.GetCmdOutput(list_cmd, env=self._env)
248    return output.splitlines()
249
250  def IsAvailable(self, avd_name):
251    """Check if an AVD exists or not."""
252    return avd_name in self.List()
253
254
255class AvdConfig:
256  """Represents a particular AVD configuration.
257
258  This class supports creation, installation, and execution of an AVD
259  from a given Avd proto message, as defined in
260  //build/android/pylib/local/emulator/proto/avd.proto.
261  """
262
263  def __init__(self, avd_proto_path):
264    """Create an AvdConfig object.
265
266    Args:
267      avd_proto_path: path to a textpb file containing an Avd message.
268    """
269    self.avd_proto_path = avd_proto_path
270    self._config = _Load(avd_proto_path)
271
272    self._initialized = False
273    self._initializer_lock = threading.Lock()
274
275  @property
276  def emulator_home(self):
277    """User-specific emulator configuration directory.
278
279    It corresponds to the environment variable $ANDROID_EMULATOR_HOME.
280    Configs like advancedFeatures.ini are expected to be under this dir.
281    """
282    return os.path.join(COMMON_CIPD_ROOT, self._config.avd_package.dest_path)
283
284  @property
285  def emulator_sdk_root(self):
286    """The path to the SDK installation directory.
287
288    It corresponds to the environment variable $ANDROID_HOME.
289
290    To be a valid sdk root, it requires to have the subdirecotries "platforms"
291    and "platform-tools". See http://bit.ly/2YAkyFE for context.
292
293    Also, it is expected to have subdirecotries "emulator" and "system-images".
294    """
295    emulator_sdk_root = os.path.join(COMMON_CIPD_ROOT,
296                                     self._config.emulator_package.dest_path)
297    # Ensure this is a valid sdk root.
298    required_dirs = [
299        os.path.join(emulator_sdk_root, 'platforms'),
300        os.path.join(emulator_sdk_root, 'platform-tools'),
301    ]
302    for d in required_dirs:
303      if not os.path.exists(d):
304        os.makedirs(d)
305
306    return emulator_sdk_root
307
308  @property
309  def emulator_path(self):
310    """The path to the emulator binary."""
311    return os.path.join(self.emulator_sdk_root, 'emulator', 'emulator')
312
313  @property
314  def qemu_img_path(self):
315    """The path to the qemu-img binary.
316
317    This is used to rebase the paths in qcow2 images.
318    """
319    return os.path.join(self.emulator_sdk_root, 'emulator', 'qemu-img')
320
321  @property
322  def mksdcard_path(self):
323    """The path to the mksdcard binary.
324
325    This is used to create a sdcard image.
326    """
327    return os.path.join(self.emulator_sdk_root, 'emulator', 'mksdcard')
328
329  @property
330  def avd_settings(self):
331    """The AvdSettings in the avd proto file.
332
333    This defines how to configure the AVD at creation.
334    """
335    return self._config.avd_settings
336
337  @property
338  def avd_launch_settings(self):
339    """The AvdLaunchSettings in the avd proto file.
340
341    This defines AVD setting during launch time.
342    """
343    return self._config.avd_launch_settings
344
345  @property
346  def avd_name(self):
347    """The name of the AVD to create or use."""
348    return self._config.avd_name
349
350  @property
351  def avd_home(self):
352    """The path that contains the files of one or multiple AVDs."""
353    avd_home = os.path.join(self.emulator_home, 'avd')
354    if not os.path.exists(avd_home):
355      os.makedirs(avd_home)
356
357    return avd_home
358
359  @property
360  def _avd_dir(self):
361    """The path that contains the files of the given AVD."""
362    return os.path.join(self.avd_home, '%s.avd' % self.avd_name)
363
364  @property
365  def _system_image_dir(self):
366    """The path of the directory that directly contains the system images.
367
368    For example, if the system_image_name is
369    "system-images;android-33;google_apis;x86_64"
370
371    The _system_image_dir will be:
372    <COMMON_CIPD_ROOT>/<dest_path>/system-images/android-33/google_apis/x86_64
373
374    This is used to rebase the paths in qcow2 images.
375    """
376    return os.path.join(COMMON_CIPD_ROOT,
377                        self._config.system_image_package.dest_path,
378                        *self._config.system_image_name.split(';'))
379
380  @property
381  def _root_ini_path(self):
382    """The <avd_name>.ini file of the given AVD."""
383    return os.path.join(self.avd_home, '%s.ini' % self.avd_name)
384
385  @property
386  def _config_ini_path(self):
387    """The config.ini file under _avd_dir."""
388    return os.path.join(self._avd_dir, 'config.ini')
389
390  @property
391  def _features_ini_path(self):
392    return os.path.join(self.emulator_home, 'advancedFeatures.ini')
393
394  @property
395  def xdg_config_dir(self):
396    """The base directory to store qt config file.
397
398    This dir should be added to the env variable $XDG_CONFIG_DIRS so that
399    _qt_config_path can take effect. See https://bit.ly/3HIQRZ3 for context.
400    """
401    config_dir = os.path.join(self.emulator_home, '.config')
402    if not os.path.exists(config_dir):
403      os.makedirs(config_dir)
404
405    return config_dir
406
407  @property
408  def _qt_config_path(self):
409    """The qt config file for emulator."""
410    qt_config_dir = os.path.join(self.xdg_config_dir,
411                                 'Android Open Source Project')
412    if not os.path.exists(qt_config_dir):
413      os.makedirs(qt_config_dir)
414
415    return os.path.join(qt_config_dir, 'Emulator.conf')
416
417  def HasSnapshot(self, snapshot_name):
418    """Check if a given snapshot exists or not."""
419    snapshot_path = os.path.join(self._avd_dir, 'snapshots', snapshot_name)
420    return os.path.exists(snapshot_path)
421
422  def Create(self,
423             force=False,
424             snapshot=False,
425             keep=False,
426             additional_apks=None,
427             privileged_apk_tuples=None,
428             cipd_json_output=None,
429             dry_run=False):
430    """Create an instance of the AVD CIPD package.
431
432    This method:
433     - installs the requisite system image
434     - creates the AVD
435     - modifies the AVD's ini files to support running chromium tests
436       in chromium infrastructure
437     - optionally starts, installs additional apks and/or privileged apks, and
438       stops the AVD for snapshotting (default no)
439     - By default creates and uploads an instance of the AVD CIPD package
440       (can be turned off by dry_run flag).
441     - optionally deletes the AVD (default yes)
442
443    Args:
444      force: bool indicating whether to force create the AVD.
445      snapshot: bool indicating whether to snapshot the AVD before creating
446        the CIPD package.
447      keep: bool indicating whether to keep the AVD after creating
448        the CIPD package.
449      additional_apks: a list of strings contains the paths to the APKs. These
450        APKs will be installed after AVD is started.
451      privileged_apk_tuples: a list of (apk_path, device_partition) tuples where
452        |apk_path| is a string containing the path to the APK, and
453        |device_partition| is a string indicating the system image partition on
454        device that contains "priv-app" directory, e.g. "/system", "/product".
455      cipd_json_output: string path to pass to `cipd create` via -json-output.
456      dry_run: When set to True, it will skip the CIPD package creation
457        after creating the AVD.
458    """
459    logging.info('Installing required packages.')
460    self._InstallCipdPackages(_PACKAGES_CREATION)
461
462    avd_manager = _AvdManagerAgent(avd_home=self.avd_home,
463                                   sdk_root=self.emulator_sdk_root)
464
465    logging.info('Creating AVD.')
466    avd_manager.Create(avd_name=self.avd_name,
467                       system_image=self._config.system_image_name,
468                       force=force)
469
470    try:
471      logging.info('Modifying AVD configuration.')
472
473      # Clear out any previous configuration or state from this AVD.
474      with ini.update_ini_file(self._root_ini_path) as r_ini_contents:
475        r_ini_contents['path.rel'] = 'avd/%s.avd' % self.avd_name
476
477      with ini.update_ini_file(self._features_ini_path) as f_ini_contents:
478        # features_ini file will not be refreshed by avdmanager during
479        # creation. So explicitly clear its content to exclude any leftover
480        # from previous creation.
481        f_ini_contents.clear()
482        f_ini_contents.update(self.avd_settings.advanced_features)
483
484      with ini.update_ini_file(self._config_ini_path) as config_ini_contents:
485        # Update avd_properties first so that they won't override settings
486        # like screen and ram_size
487        config_ini_contents.update(self.avd_settings.avd_properties)
488
489        height = self.avd_settings.screen.height or _DEFAULT_SCREEN_HEIGHT
490        width = self.avd_settings.screen.width or _DEFAULT_SCREEN_WIDTH
491        density = self.avd_settings.screen.density or _DEFAULT_SCREEN_DENSITY
492
493        config_ini_contents.update({
494            'disk.dataPartition.size': '4G',
495            'hw.keyboard': 'yes',
496            'hw.lcd.density': density,
497            'hw.lcd.height': height,
498            'hw.lcd.width': width,
499            'hw.mainKeys': 'no',  # Show nav buttons on screen
500        })
501
502        if self.avd_settings.ram_size:
503          config_ini_contents['hw.ramSize'] = self.avd_settings.ram_size
504
505        config_ini_contents['hw.sdCard'] = 'yes'
506        if self.avd_settings.sdcard.size:
507          sdcard_path = os.path.join(self._avd_dir, _SDCARD_NAME)
508          cmd_helper.RunCmd([
509              self.mksdcard_path,
510              self.avd_settings.sdcard.size,
511              sdcard_path,
512          ])
513          config_ini_contents['hw.sdCard.path'] = sdcard_path
514
515      if not additional_apks:
516        additional_apks = []
517      for pkg in self._config.additional_apk:
518        apk_dir = os.path.join(COMMON_CIPD_ROOT, pkg.dest_path)
519        apk_file = _FindMinSdkFile(apk_dir, self._config.min_sdk)
520        # Some of these files come from chrome internal, so may not be
521        # available to non-internal permissioned users.
522        if os.path.exists(apk_file):
523          logging.info('Adding additional apk for install: %s', apk_file)
524          additional_apks.append(apk_file)
525
526      if not privileged_apk_tuples:
527        privileged_apk_tuples = []
528      for pkg in self._config.privileged_apk:
529        apk_dir = os.path.join(COMMON_CIPD_ROOT, pkg.dest_path)
530        apk_file = _FindMinSdkFile(apk_dir, self._config.min_sdk)
531        # Some of these files come from chrome internal, so may not be
532        # available to non-internal permissioned users.
533        if os.path.exists(apk_file):
534          logging.info('Adding privilege apk for install: %s', apk_file)
535          privileged_apk_tuples.append(
536              (apk_file, self._config.install_privileged_apk_partition))
537
538      # Start & stop the AVD.
539      self._Initialize()
540      instance = _AvdInstance(self)
541      # Enable debug for snapshot when it is set to True
542      debug_tags = 'time,init,snapshot' if snapshot else None
543      # Installing privileged apks requires modifying the system
544      # image.
545      writable_system = bool(privileged_apk_tuples)
546      gpu_mode = self.avd_launch_settings.gpu_mode or _DEFAULT_GPU_MODE
547      instance.Start(ensure_system_settings=False,
548                     read_only=False,
549                     writable_system=writable_system,
550                     gpu_mode=gpu_mode,
551                     debug_tags=debug_tags)
552
553      assert instance.device is not None, '`instance.device` not initialized.'
554      # Android devices with full-disk encryption are encrypted on first boot,
555      # and then get decrypted to continue the boot process (See details in
556      # https://bit.ly/3agmjcM).
557      # Wait for this step to complete since it can take a while for old OSs
558      # like M, otherwise the avd may have "Encryption Unsuccessful" error.
559      instance.device.WaitUntilFullyBooted(decrypt=True, timeout=360, retries=0)
560
561      if additional_apks:
562        for apk in additional_apks:
563          instance.device.Install(apk, allow_downgrade=True, reinstall=True)
564          package_name = apk_helper.GetPackageName(apk)
565          package_version = instance.device.GetApplicationVersion(package_name)
566          logging.info('The version for package %r on the device is %r',
567                       package_name, package_version)
568
569      if privileged_apk_tuples:
570        system_app.InstallPrivilegedApps(instance.device, privileged_apk_tuples)
571        for apk, _ in privileged_apk_tuples:
572          package_name = apk_helper.GetPackageName(apk)
573          package_version = instance.device.GetApplicationVersion(package_name)
574          logging.info('The version for package %r on the device is %r',
575                       package_name, package_version)
576
577      # Skip Marshmallow as svc commands fail on this version.
578      if instance.device.build_version_sdk != 23:
579        # Always disable the network to prevent built-in system apps from
580        # updating themselves, which could take over package manager and
581        # cause shell command timeout.
582        # Use svc as this also works on the images with build type "user", and
583        # does not require a reboot or broadcast compared to setting the
584        # airplane_mode_on in "settings/global".
585        logging.info('Disabling the network.')
586        instance.device.RunShellCommand(['svc', 'wifi', 'disable'],
587                                        as_root=True,
588                                        check_return=True)
589        instance.device.RunShellCommand(['svc', 'data', 'disable'],
590                                        as_root=True,
591                                        check_return=True)
592
593      if snapshot:
594        instance.SaveSnapshot()
595
596      instance.Stop()
597
598      # The multiinstance lock file seems to interfere with the emulator's
599      # operation in some circumstances (beyond the obvious -read-only ones),
600      # and there seems to be no mechanism by which it gets closed or deleted.
601      # See https://bit.ly/2pWQTH7 for context.
602      multiInstanceLockFile = os.path.join(self._avd_dir, 'multiinstance.lock')
603      if os.path.exists(multiInstanceLockFile):
604        os.unlink(multiInstanceLockFile)
605
606      package_def_content = {
607          'package':
608          self._config.avd_package.package_name,
609          'root':
610          self.emulator_home,
611          'install_mode':
612          'copy',
613          'data': [{
614              'dir': os.path.relpath(self._avd_dir, self.emulator_home)
615          }, {
616              'file':
617              os.path.relpath(self._root_ini_path, self.emulator_home)
618          }, {
619              'file':
620              os.path.relpath(self._features_ini_path, self.emulator_home)
621          }],
622      }
623
624      logging.info('Creating AVD CIPD package.')
625      logging.info('ensure file content: %s',
626                   json.dumps(package_def_content, indent=2))
627
628      with tempfile_ext.TemporaryFileName(suffix='.json') as package_def_path:
629        with open(package_def_path, 'w') as package_def_file:
630          json.dump(package_def_content, package_def_file)
631
632        logging.info('  %s', self._config.avd_package.package_name)
633        cipd_create_cmd = [
634            'cipd',
635            'create',
636            '-pkg-def',
637            package_def_path,
638            '-tag',
639            'emulator_version:%s' % self._config.emulator_package.version,
640            '-tag',
641            'system_image_version:%s' %
642            self._config.system_image_package.version,
643        ]
644        if cipd_json_output:
645          cipd_create_cmd.extend([
646              '-json-output',
647              cipd_json_output,
648          ])
649        logging.info('running %r%s', cipd_create_cmd,
650                     ' (dry_run)' if dry_run else '')
651        if not dry_run:
652          try:
653            for line in cmd_helper.IterCmdOutputLines(cipd_create_cmd):
654              logging.info('    %s', line)
655          except subprocess.CalledProcessError as e:
656            # avd.py is executed with python2.
657            # pylint: disable=W0707
658            raise AvdException('CIPD package creation failed: %s' % str(e),
659                               command=cipd_create_cmd)
660
661    finally:
662      if not keep:
663        logging.info('Deleting AVD.')
664        avd_manager.Delete(avd_name=self.avd_name)
665
666  def IsAvailable(self):
667    """Returns whether emulator is up-to-date."""
668    if not os.path.exists(self._config_ini_path):
669      return False
670
671    # Skip when no version exists to prevent "IsAvailable()" returning False
672    # for emualtors set up using Create() (rather than Install()).
673    for cipd_root, pkgs in self._IterCipdPackages(_PACKAGES_RUNTIME,
674                                                  check_version=False):
675      stdout = subprocess.run(['cipd', 'installed', '--root', cipd_root],
676                              capture_output=True,
677                              check=False,
678                              encoding='utf8').stdout
679      # Output looks like:
680      # Packages:
681      #   name1:version1
682      #   name2:version2
683      installed = [l.strip().split(':', 1) for l in stdout.splitlines()[1:]]
684
685      if any([p.package_name, p.version] not in installed for p in pkgs):
686        return False
687    return True
688
689  def Uninstall(self):
690    """Uninstall all the artifacts associated with the given config.
691
692    Artifacts includes:
693     - CIPD packages specified in the avd config.
694     - The local AVD created by `Create`, if present.
695
696    """
697    # Delete any existing local AVD. This must occur before deleting CIPD
698    # packages because a AVD needs system image to be recognized by avdmanager.
699    avd_manager = _AvdManagerAgent(avd_home=self.avd_home,
700                                   sdk_root=self.emulator_sdk_root)
701    if avd_manager.IsAvailable(self.avd_name):
702      logging.info('Deleting local AVD %s', self.avd_name)
703      avd_manager.Delete(self.avd_name)
704
705    # Delete installed CIPD packages.
706    for cipd_root, _ in self._IterCipdPackages(_PACKAGES_ALL,
707                                               check_version=False):
708      logging.info('Uninstalling packages in %s', cipd_root)
709      if not os.path.exists(cipd_root):
710        continue
711      # Create an empty ensure file to removed any installed CIPD packages.
712      ensure_path = os.path.join(cipd_root, '.ensure')
713      with open(ensure_path, 'w') as ensure_file:
714        ensure_file.write('$ParanoidMode CheckIntegrity\n\n')
715      ensure_cmd = [
716          'cipd',
717          'ensure',
718          '-ensure-file',
719          ensure_path,
720          '-root',
721          cipd_root,
722      ]
723      try:
724        for line in cmd_helper.IterCmdOutputLines(ensure_cmd):
725          logging.info('    %s', line)
726      except subprocess.CalledProcessError as e:
727        # avd.py is executed with python2.
728        # pylint: disable=W0707
729        raise AvdException('Failed to uninstall CIPD packages: %s' % str(e),
730                           command=ensure_cmd)
731
732  def Install(self):
733    """Installs the requested CIPD packages and prepares them for use.
734
735    This includes making files writeable and revising some of the
736    emulator's internal config files.
737
738    Returns: None
739    Raises: AvdException on failure to install.
740    """
741    self._InstallCipdPackages(_PACKAGES_RUNTIME)
742    self._MakeWriteable()
743    self._UpdateConfigs()
744    self._RebaseQcow2Images()
745
746  def _RebaseQcow2Images(self):
747    """Rebase the paths in qcow2 images.
748
749    qcow2 files may exists in avd directory which have hard-coded paths to the
750    backing files, e.g., system.img, vendor.img. Such paths need to be rebased
751    if the avd is moved to a different directory in order to boot successfully.
752    """
753    for f in _BACKING_FILES:
754      qcow2_image_path = os.path.join(self._avd_dir, '%s.qcow2' % f)
755      if not os.path.exists(qcow2_image_path):
756        continue
757      backing_file_path = os.path.join(self._system_image_dir, f)
758      logging.info('Rebasing the qcow2 image %r with the backing file %r',
759                   qcow2_image_path, backing_file_path)
760      cmd_helper.RunCmd([
761          self.qemu_img_path,
762          'rebase',
763          '-u',
764          '-f',
765          'qcow2',
766          '-b',
767          # The path to backing file must be relative to the qcow2 image.
768          os.path.relpath(backing_file_path, os.path.dirname(qcow2_image_path)),
769          qcow2_image_path,
770      ])
771
772  def _ListPackages(self, packages):
773    if packages is _PACKAGES_RUNTIME:
774      packages = [
775          self._config.avd_package,
776          self._config.emulator_package,
777          self._config.system_image_package,
778      ]
779    elif packages is _PACKAGES_CREATION:
780      packages = [
781          self._config.emulator_package,
782          self._config.system_image_package,
783          *self._config.privileged_apk,
784          *self._config.additional_apk,
785      ]
786    elif packages is _PACKAGES_ALL:
787      packages = [
788          self._config.avd_package,
789          self._config.emulator_package,
790          self._config.system_image_package,
791          *self._config.privileged_apk,
792          *self._config.additional_apk,
793      ]
794    return packages
795
796  def _IterCipdPackages(self, packages, check_version=True):
797    """Iterate a list of CIPD packages by their CIPD roots.
798
799    Args:
800      packages: a list of packages from an AVD config.
801      check_version: If set, raise Exception when a package has no version.
802    """
803    pkgs_by_dir = collections.defaultdict(list)
804    for pkg in self._ListPackages(packages):
805      if pkg.version:
806        pkgs_by_dir[pkg.dest_path].append(pkg)
807      elif check_version:
808        raise AvdException('Expecting a version for the package %s' %
809                           pkg.package_name)
810
811    for pkg_dir, pkgs in pkgs_by_dir.items():
812      cipd_root = os.path.join(COMMON_CIPD_ROOT, pkg_dir)
813      yield cipd_root, pkgs
814
815  def _InstallCipdPackages(self, packages, check_version=True):
816    for cipd_root, pkgs in self._IterCipdPackages(packages,
817                                                  check_version=check_version):
818      logging.info('Installing packages in %s', cipd_root)
819      if not os.path.exists(cipd_root):
820        os.makedirs(cipd_root)
821      ensure_path = os.path.join(cipd_root, '.ensure')
822      with open(ensure_path, 'w') as ensure_file:
823        # Make CIPD ensure that all files are present and correct,
824        # even if it thinks the package is installed.
825        ensure_file.write('$ParanoidMode CheckIntegrity\n\n')
826        for pkg in pkgs:
827          ensure_file.write('%s %s\n' % (pkg.package_name, pkg.version))
828          logging.info('  %s %s', pkg.package_name, pkg.version)
829      ensure_cmd = [
830          'cipd',
831          'ensure',
832          '-ensure-file',
833          ensure_path,
834          '-root',
835          cipd_root,
836      ]
837      try:
838        for line in cmd_helper.IterCmdOutputLines(ensure_cmd):
839          logging.info('    %s', line)
840      except subprocess.CalledProcessError as e:
841        # avd.py is executed with python2.
842        # pylint: disable=W0707
843        raise AvdException('Failed to install CIPD packages: %s' % str(e),
844                           command=ensure_cmd)
845
846  def _MakeWriteable(self):
847    # The emulator requires that some files are writable.
848    for dirname, _, filenames in os.walk(self.emulator_home):
849      for f in filenames:
850        path = os.path.join(dirname, f)
851        mode = os.lstat(path).st_mode
852        if mode & stat.S_IRUSR:
853          mode = mode | stat.S_IWUSR
854        os.chmod(path, mode)
855
856  def _UpdateConfigs(self):
857    """Update various properties in config files after installation.
858
859    AVD config files contain some properties which can be different between AVD
860    creation and installation, e.g. hw.sdCard.path, which is an absolute path.
861    Update their values so that:
862     * Emulator instance can be booted correctly.
863     * The snapshot can be loaded successfully.
864    """
865    logging.info('Updating AVD configurations.')
866    # Update the absolute avd path in root_ini file
867    with ini.update_ini_file(self._root_ini_path) as r_ini_contents:
868      r_ini_contents['path'] = self._avd_dir
869
870    # Update hardware settings.
871    config_paths = [self._config_ini_path]
872    # The file hardware.ini within each snapshot need to be updated as well.
873    hw_ini_glob_pattern = os.path.join(self._avd_dir, 'snapshots', '*',
874                                       'hardware.ini')
875    config_paths.extend(glob.glob(hw_ini_glob_pattern))
876
877    properties = {}
878    # Update hw.sdCard.path if applicable
879    sdcard_path = os.path.join(self._avd_dir, _SDCARD_NAME)
880    if os.path.exists(sdcard_path):
881      properties['hw.sdCard.path'] = sdcard_path
882
883    for config_path in config_paths:
884      with ini.update_ini_file(config_path) as config_contents:
885        config_contents.update(properties)
886
887    # Create qt config file to disable certain warnings when launched in window.
888    with ini.update_ini_file(self._qt_config_path) as config_contents:
889      # Disable nested virtualization warning.
890      config_contents['General'] = {'showNestedWarning': 'false'}
891      # Disable adb warning.
892      config_contents['set'] = {'autoFindAdb': 'false'}
893
894  def _Initialize(self):
895    if self._initialized:
896      return
897
898    with self._initializer_lock:
899      if self._initialized:
900        return
901
902      # Emulator start-up looks for the adb daemon. Make sure it's running.
903      adb_wrapper.AdbWrapper.StartServer()
904
905      # Emulator start-up requires a valid sdk root.
906      assert self.emulator_sdk_root
907
908  def CreateInstance(self, output_manager=None):
909    """Creates an AVD instance without starting it.
910
911    Returns:
912      An _AvdInstance.
913    """
914    self._Initialize()
915    return _AvdInstance(self, output_manager=output_manager)
916
917  def StartInstance(self):
918    """Starts an AVD instance.
919
920    Returns:
921      An _AvdInstance.
922    """
923    instance = self.CreateInstance()
924    instance.Start()
925    return instance
926
927
928class _AvdInstance:
929  """Represents a single running instance of an AVD.
930
931  This class should only be created directly by AvdConfig.StartInstance,
932  but its other methods can be freely called.
933  """
934
935  def __init__(self, avd_config, output_manager=None):
936    """Create an _AvdInstance object.
937
938    Args:
939      avd_config: an AvdConfig instance.
940      output_manager: a pylib.base.output_manager.OutputManager instance.
941    """
942    self._avd_config = avd_config
943    self._avd_name = avd_config.avd_name
944    self._emulator_home = avd_config.emulator_home
945    self._emulator_path = avd_config.emulator_path
946    self._emulator_proc = None
947    self._emulator_serial = None
948    self._emulator_device = None
949
950    self._output_manager = output_manager
951    self._output_file = None
952
953    self._writable_system = False
954    self._debug_tags = None
955
956  def __str__(self):
957    return '%s|%s' % (self._avd_name, (self._emulator_serial or id(self)))
958
959  def Start(self,
960            ensure_system_settings=True,
961            read_only=True,
962            window=False,
963            writable_system=False,
964            gpu_mode=None,
965            wipe_data=False,
966            debug_tags=None,
967            disk_size=None,
968            enable_network=False,
969            require_fast_start=False):
970    """Starts the emulator running an instance of the given AVD.
971
972    Note when ensure_system_settings is True, the program will wait until the
973    emulator is fully booted, and then update system settings.
974    """
975    is_slow_start = not require_fast_start
976    # Force to load system snapshot if detected.
977    if self.HasSystemSnapshot():
978      if not writable_system:
979        logging.info('System snapshot found. Set "writable_system=True" '
980                     'to load it properly.')
981        writable_system = True
982      if read_only:
983        logging.info('System snapshot found. Set "read_only=False" '
984                     'to load it properly.')
985        read_only = False
986    elif writable_system:
987      is_slow_start = True
988      logging.warning('Emulator will be slow to start, as '
989                      '"writable_system=True" but system snapshot not found.')
990
991    self._writable_system = writable_system
992
993    with tempfile_ext.TemporaryFileName() as socket_path, (contextlib.closing(
994        socket.socket(socket.AF_UNIX))) as sock:
995      sock.bind(socket_path)
996      emulator_cmd = [
997          self._emulator_path,
998          '-avd',
999          self._avd_name,
1000          '-report-console',
1001          'unix:%s' % socket_path,
1002          '-no-boot-anim',
1003          # Explicitly prevent emulator from auto-saving to snapshot on exit.
1004          '-no-snapshot-save',
1005          # Explicitly set the snapshot name for auto-load
1006          '-snapshot',
1007          self.GetSnapshotName(),
1008      ]
1009
1010      if wipe_data:
1011        emulator_cmd.append('-wipe-data')
1012      if disk_size:
1013        emulator_cmd.extend(['-partition-size', str(disk_size)])
1014      if read_only:
1015        emulator_cmd.append('-read-only')
1016      if writable_system:
1017        emulator_cmd.append('-writable-system')
1018      # Note when "--gpu-mode" is set to "host":
1019      #  * It needs a valid DISPLAY env, even if "--emulator-window" is false.
1020      #    Otherwise it may throw errors like "Failed to initialize backend
1021      #    EGL display". See the code in https://bit.ly/3ruiMlB as an example
1022      #    to setup the DISPLAY env with xvfb.
1023      #  * It will not work under remote sessions like chrome remote desktop.
1024      if not gpu_mode:
1025        gpu_mode = (self._avd_config.avd_launch_settings.gpu_mode
1026                    or _DEFAULT_GPU_MODE)
1027      emulator_cmd.extend(['-gpu', gpu_mode])
1028      if debug_tags:
1029        self._debug_tags = set(debug_tags.split(','))
1030        # Always print timestamp when debug tags are set.
1031        self._debug_tags.add('time')
1032        emulator_cmd.extend(['-debug', ','.join(self._debug_tags)])
1033        if 'kernel' in self._debug_tags:
1034          # TODO(crbug.com/1404176): newer API levels need "-virtio-console"
1035          # as well to print kernel log.
1036          emulator_cmd.append('-show-kernel')
1037
1038      emulator_env = {
1039          # kill immediately when emulator hang.
1040          'ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL': '0',
1041          # Sets the emulator configuration directory
1042          'ANDROID_EMULATOR_HOME': self._emulator_home,
1043      }
1044      if 'DISPLAY' in os.environ:
1045        emulator_env['DISPLAY'] = os.environ.get('DISPLAY')
1046      if window:
1047        if 'DISPLAY' not in emulator_env:
1048          raise AvdException('Emulator failed to start: DISPLAY not defined')
1049      else:
1050        emulator_cmd.append('-no-window')
1051
1052      # Need this for the qt config file to take effect.
1053      xdg_config_dirs = [self._avd_config.xdg_config_dir]
1054      if 'XDG_CONFIG_DIRS' in os.environ:
1055        xdg_config_dirs.append(os.environ.get('XDG_CONFIG_DIRS'))
1056      emulator_env['XDG_CONFIG_DIRS'] = ':'.join(xdg_config_dirs)
1057
1058      sock.listen(1)
1059
1060      logging.info('Starting emulator...')
1061      logging.info(
1062          '  With environments: %s',
1063          ' '.join(['%s=%s' % (k, v) for k, v in emulator_env.items()]))
1064      logging.info('  With commands: %s', ' '.join(emulator_cmd))
1065
1066      # Enable the emulator log when debug_tags is set.
1067      if self._debug_tags:
1068        # Write to an ArchivedFile if output manager is set, otherwise stdout.
1069        if self._output_manager:
1070          self._output_file = self._output_manager.CreateArchivedFile(
1071              'emulator_%s' % time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()),
1072              'emulator')
1073      else:
1074        self._output_file = open('/dev/null', 'w')
1075      self._emulator_proc = cmd_helper.Popen(emulator_cmd,
1076                                             stdout=self._output_file,
1077                                             stderr=self._output_file,
1078                                             env=emulator_env)
1079
1080      # Waits for the emulator to report its serial as requested via
1081      # -report-console. See http://bit.ly/2lK3L18 for more.
1082      def listen_for_serial(s):
1083        logging.info('Waiting for connection from emulator.')
1084        with contextlib.closing(s.accept()[0]) as conn:
1085          val = conn.recv(1024)
1086          return 'emulator-%d' % int(val)
1087
1088      try:
1089        self._emulator_serial = timeout_retry.Run(
1090            listen_for_serial,
1091            timeout=120 if is_slow_start else 30,
1092            retries=0,
1093            args=[sock])
1094        logging.info('%s started', self._emulator_serial)
1095      except Exception:
1096        self.Stop(force=True)
1097        raise
1098
1099    # Set the system settings in "Start" here instead of setting in "Create"
1100    # because "Create" is used during AVD creation, and we want to avoid extra
1101    # turn-around on rolling AVD.
1102    if ensure_system_settings:
1103      assert self.device is not None, '`instance.device` not initialized.'
1104      logging.info('Waiting for device to be fully booted.')
1105      self.device.WaitUntilFullyBooted(timeout=360 if is_slow_start else 90,
1106                                       retries=0)
1107      logging.info('Device fully booted, verifying system settings.')
1108      _EnsureSystemSettings(self.device)
1109
1110    if enable_network:
1111      _EnableNetwork(self.device)
1112
1113  def Stop(self, force=False):
1114    """Stops the emulator process.
1115
1116    When "force" is True, we will call "terminate" on the emulator process,
1117    which is recommended when emulator is not responding to adb commands.
1118    """
1119    # Close output file first in case emulator process killing goes wrong.
1120    if self._output_file:
1121      if self._debug_tags:
1122        if self._output_manager:
1123          self._output_manager.ArchiveArchivedFile(self._output_file,
1124                                                   delete=True)
1125          link = self._output_file.Link()
1126          if link:
1127            logging.critical('Emulator logs saved to %s', link)
1128      else:
1129        self._output_file.close()
1130      self._output_file = None
1131
1132    if self._emulator_proc:
1133      if self._emulator_proc.poll() is None:
1134        if force or not self.device:
1135          self._emulator_proc.terminate()
1136        else:
1137          self.device.adb.Emu('kill')
1138        self._emulator_proc.wait()
1139      self._emulator_proc = None
1140      self._emulator_serial = None
1141      self._emulator_device = None
1142
1143  def GetSnapshotName(self):
1144    """Return the snapshot name to load/save.
1145
1146    Emulator has a different snapshot process when '-writable-system' flag is
1147    set (See https://issuetracker.google.com/issues/135857816#comment8).
1148
1149    """
1150    if self._writable_system:
1151      return _SYSTEM_SNAPSHOT_NAME
1152
1153    return _DEFAULT_SNAPSHOT_NAME
1154
1155  def HasSystemSnapshot(self):
1156    """Check if the instance has the snapshot named _SYSTEM_SNAPSHOT_NAME."""
1157    return self._avd_config.HasSnapshot(_SYSTEM_SNAPSHOT_NAME)
1158
1159  def SaveSnapshot(self):
1160    snapshot_name = self.GetSnapshotName()
1161    if self.device:
1162      logging.info('Saving snapshot to %r.', snapshot_name)
1163      self.device.adb.Emu(['avd', 'snapshot', 'save', snapshot_name])
1164
1165  @property
1166  def serial(self):
1167    return self._emulator_serial
1168
1169  @property
1170  def device(self):
1171    if not self._emulator_device and self._emulator_serial:
1172      self._emulator_device = device_utils.DeviceUtils(self._emulator_serial)
1173    return self._emulator_device
1174
1175
1176# TODO(crbug.com/1275767): Refactor it to a dict-based approach.
1177def _EnsureSystemSettings(device):
1178  set_long_press_timeout_cmd = [
1179      'settings', 'put', 'secure', 'long_press_timeout', _LONG_PRESS_TIMEOUT
1180  ]
1181  device.RunShellCommand(set_long_press_timeout_cmd, check_return=True)
1182
1183  # Verify if long_press_timeout is set correctly.
1184  get_long_press_timeout_cmd = [
1185      'settings', 'get', 'secure', 'long_press_timeout'
1186  ]
1187  adb_output = device.RunShellCommand(get_long_press_timeout_cmd,
1188                                      check_return=True)
1189  if _LONG_PRESS_TIMEOUT in adb_output:
1190    logging.info('long_press_timeout set to %r', _LONG_PRESS_TIMEOUT)
1191  else:
1192    logging.warning('long_press_timeout is not set correctly')
1193
1194  # TODO(crbug.com/1488458): Move the date sync function to device_utils.py
1195  if device.IsUserBuild():
1196    logging.warning('Cannot sync the device date on "user" build')
1197    return
1198
1199  logging.info('Sync the device date.')
1200  timezone = device.RunShellCommand(['date', '+"%Z"'],
1201                                    single_line=True,
1202                                    check_return=True)
1203  if timezone != 'UTC':
1204    device.RunShellCommand(['setprop', 'persist.sys.timezone', '"Etc/UTC"'],
1205                           check_return=True,
1206                           as_root=True)
1207  set_date_format = '%Y%m%d.%H%M%S'
1208  set_date_command = ['date', '-s']
1209  if device.build_version_sdk >= version_codes.MARSHMALLOW:
1210    set_date_format = '%m%d%H%M%Y.%S'
1211    set_date_command = ['date']
1212  strgmtime = time.strftime(set_date_format, time.gmtime())
1213  set_date_command.append(strgmtime)
1214  device.RunShellCommand(set_date_command, check_return=True, as_root=True)
1215
1216
1217def _EnableNetwork(device):
1218  logging.info('Enable the network on the emulator.')
1219  # TODO(https://crbug.com/1486376): Remove airplane_mode once all AVD
1220  # are rolled to svc-based version.
1221  device.RunShellCommand(
1222      ['settings', 'put', 'global', 'airplane_mode_on', '0'], as_root=True)
1223  device.RunShellCommand(
1224      ['am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE'],
1225      as_root=True)
1226  device.RunShellCommand(['svc', 'wifi', 'enable'], as_root=True)
1227  device.RunShellCommand(['svc', 'data', 'enable'], as_root=True)
1228